Catch Me If You Can
It is common knowledge that exceptions raised in some piece of Delphi code can be caught and
handled with
So you think you have all possible exceptions neatly wrapped up and appropriately handled.
Not really... there is this teeny-weeny thingy... your exception-catching stronghold is not so strong on Delphi compilers with the LLVM backend.
Running the above code on Android, iOS or Linux will not catch the exception as expected, and it will bubble up to the next exception handler level. Your perfect application suddenly starts misbehaving.
Even if you do read it, like I did (more than once), you might have been focused on other mobile compiler features, like ARC or the lack of 8-bit strings. And that exception thingy can easily go unnoticed. I certainly managed to miss it.
Use a Function Call in a try-except Block to Prevent Uncaught Hardware Exceptions
To make a long story short, the LLVM backend cannot return from a hardware exception (like AV) if the hardware exception is raised directly within a
To work around the escaping exception problem, all you have to do is wrap your code in functions (methods) and you are back in business.
If a function or method is inlined, then there is no actual function - all code will be inserted in place - and there will be no function call to return from.
Even if not inlined, not all method calls are simple function calls behind the scenes. If the exception is raised in code before the actual function call is made, the exception will not be caught by the immediate exception handler.
And that is what happened in our example. There is a method call inside the
Changing the method signature from virtual to static, changes the outcome - static methods can be called on
So, you have two options when it comes to capturing ALL exceptions inside a particular exception handling block - you either have to wrap your code in functions (methods) (following all of the above mentioned function rules), or you have to prevent a hardware exception from happening in the first place.
In the case of virtual calls on a
try...except
blocks.No matter what.
try
// here goes some horrible or not so horrible code
// that can raise the most horrible exceptions
except
// no matter how horrible exceptions are
// you will always land here where you can handle them,
// eat them up and pretend they never happened,
// or do something and re-raise them
// YOU ARE IN CONTROL
end;
For example:
type
TFoo = class(TObject)
public
procedure Foo; virtual;
end;
procedure TFoo.Foo;
begin
end;
var
Foo: TFoo;
begin
Foo := nil;
try
// calling a virtual method on nil reference raises an Access Violation exception
Foo.Foo;
except
Log.d('CAUGHT');
end;
end;
Capturing and handling exceptions within try...except
blocks is one of the cornerstones of
Delphi coding practices. Millions of lines of code depend on that exception trap, from which
there is no escape unless you explicitly let it out by re-raising.So you think you have all possible exceptions neatly wrapped up and appropriately handled.
Not really... there is this teeny-weeny thingy... your exception-catching stronghold is not so strong on Delphi compilers with the LLVM backend.
Running the above code on Android, iOS or Linux will not catch the exception as expected, and it will bubble up to the next exception handler level. Your perfect application suddenly starts misbehaving.
Wait.... WHAT.... What a bug!!!?
Actually, it is not a bug, it is a feature. Well, not an actual feature... but rather a documented behavior. But who reads the documentation, right?Even if you do read it, like I did (more than once), you might have been focused on other mobile compiler features, like ARC or the lack of 8-bit strings. And that exception thingy can easily go unnoticed. I certainly managed to miss it.
Use a Function Call in a try-except Block to Prevent Uncaught Hardware Exceptions
To make a long story short, the LLVM backend cannot return from a hardware exception (like AV) if the hardware exception is raised directly within a
try...except
block. It can only safely return
if there is a function (method) call within the try...except
block.To work around the escaping exception problem, all you have to do is wrap your code in functions (methods) and you are back in business.
Not so fast... There are some exceptions...
What may seem like a simple function call to us, may not be a simple function call to the compiler...If a function or method is inlined, then there is no actual function - all code will be inserted in place - and there will be no function call to return from.
Even if not inlined, not all method calls are simple function calls behind the scenes. If the exception is raised in code before the actual function call is made, the exception will not be caught by the immediate exception handler.
And that is what happened in our example. There is a method call inside the
try...except
block.
According to the function rule, that exception should have been caught. But, calling a virtual
method goes through the object instance's Virtual Method Table (VMT), and if it runs into a nil
reference,
that process itself triggers an exception. You cannot find the right method to call if the
place where you are looking for it does not exist.Changing the method signature from virtual to static, changes the outcome - static methods can be called on
nil
references. Even if the code within the static method raises an exception, that
exception will be successfully caught because it happened inside the function, not during the call.
type
TFoo = class(TObject)
public
procedure Foo;
end;
When it comes to interface references, all method calls are virtual. Calling any method (even
if it is implemented as static) on a nil
interface reference will throw an exception during the
call and such an exception will not be caught by a try...except
block.So, you have two options when it comes to capturing ALL exceptions inside a particular exception handling block - you either have to wrap your code in functions (methods) (following all of the above mentioned function rules), or you have to prevent a hardware exception from happening in the first place.
In the case of virtual calls on a
nil
reference, that would consist of checking for nil
before
making the call. If you like (or need), if you find a nil
reference you can safely raise a Delphi
exception (software exception), as this one can be properly captured by the try...except
block on all platforms.
var
Foo: TFoo;
begin
try
if Assigned(Foo) then
Foo.Foo
else
raise Exception.Create('Foo is nil');
except
Log.d('CAUGHT');
end;
end;
To be continued... All Hell Breaks Loose
try...finally
and implicit method finalization blocks are also broken...
Comments
Post a Comment