Why the exception is not caught by the try… except

2020-07-03 03:11发布

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?

1条回答
在下西门庆
2楼-- · 2020-07-03 03:51

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;
查看更多
登录 后发表回答