Why the exception is not caught by the try… except end;?

隐身守侯 提交于 2019-12-03 23:28:33

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;
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!