Catch Me If You Can

It is common knowledge that exceptions raised in some piece of Delphi code can be caught and handled with 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

Popular posts from this blog

Delphi 12.1 & New Quality Portal Released

Coming in Delphi 12: Disabled Floating-Point Exceptions

Assigning result to a function from asynchronous code