I have this code (that runs under iOS with Delphi Tokyo):
procedure TMainForm.Button1Click(Sender: TObject);
var aData: NSData;
begin
try
try
aData := nil;
finally
// this line triggers an exception
aData.release;
end;
except
on E: Exception do begin
exit;
end;
end;
end;
Normally the exception should be caught in the except end
block, but in this case it is not caught by the handler and it is propagated to the Application.OnException
handler.
Access violation at address 0000000100EE9A8C, accessing address
0000000000000000
Did I miss something?
This is a bug (actually, a feature) on iOS and Android platforms (possibly on others with LLVM backend - though they are not explicitly documented).
Core issue is that exception caused by virtual method call on nil
reference constitutes hardware exception that is not captured by nearest exception handler and it is propagated to the next exception handler (in this case to Application exception handler).
Use a Function Call in a try-except Block to Prevent Uncaught Hardware Exceptions
With compilers for iOS devices, except blocks can catch a hardware
exception only if the try block contains a method or function call.
This is a difference related to the LLVM backend of the compiler,
which cannot return if no method/function is called in the try block.
The simplest code that exhibits the issue on iOS and Android platform is:
var
aData: IInterface;
begin
try
aData._Release;
except
end;
end;
Executing above code on Windows platform works as expected and the exception is caught by exception handler. There is no nil
assignment in above code, because aData
is interface reference and they are automatically nilled by compiler on all platforms. Adding nil
assignment is redundant and does not change the outcome.
To show that exceptions are caused by virtual method calls
type
IFoo = interface
procedure Foo;
end;
TFoo = class(TInterfacedObject, IFoo)
public
procedure Foo; virtual;
end;
procedure TFoo.Foo;
var
x, y: integer;
begin
y := 0;
// division by zero causes exception here
x := 5 div y;
end;
In all following code variants, exception escapes exception handler.
var
aData: IFoo;
begin
try
aData.Foo;
except
end;
end;
var
aData: TFoo;
begin
try
aData.Foo;
except
end;
end;
Even if we change Foo
method implementation and remove all code from it, it will still cause escaping exception.
If we change Foo
declaration from virtual to static, exception caused by division to zero will be properly caught because call to static methods on nil
references is allowed and call itself does not throw any exceptions - thus constitutes function call mentioned in documentation.
type
TFoo = class(TInterfacedObject, IFoo)
public
procedure Foo;
end;
TFoo = class(TObject)
public
procedure Foo;
end;
Another static method variant that also causes exception that is properly handled is declaring x
as TFoo
class field and accessing that field in Foo
method.
TFoo = class(TObject)
public
x: Integer;
procedure Foo;
end;
procedure TFoo.Foo;
var
x: integer;
begin
x := 5;
end;
Back to the original question that involved NSData
reference. NSData
is Objective-C class and those are represented as interfaces in Delphi.
// root interface declaration for all Objective-C classes and protocols
IObjectiveC = interface(IInterface)
[IID_IObjectiveC_Name]
end;
Since calling methods on interface reference is always virtual call that goes through VMT table, in this case behaves in similar manner (exhibits same issue) as virtual method call invoked directly on object reference. The call itself throws an exception and is not caught by nearest exception handler.
Workarounds:
One of the workarounds in code where reference might be nil
is checking it for nil
before calling virtual method on it. If needed, in case of nil
reference we can also raise regular exception that will be properly caught by enclosing exception handler.
var
aData: NSData;
begin
try
if Assigned(aData) then
aData.release
else
raise Exception.Create('NSData is nil');
except
end;
end;
Another workaround as mentioned in documentation is to put code in additional function (method)
procedure SafeCall(const aData: NSData);
begin
aData.release;
end;
var
aData: NSData;
begin
try
SafeCall(aData);
except
end;
end;