16

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?

Dalija Prasnikar
  • 27,212
  • 44
  • 82
  • 159
zeus
  • 12,173
  • 9
  • 63
  • 184
  • 2
    Which was the last line that executed before the AV occurred? – Freddie Bell Oct 04 '18 at 17:59
  • @nolaspeaker no understand what you mean ? – zeus Oct 04 '18 at 18:08
  • 1
    He meant (I think) on what exactly line in the code you have provided execution had been stopped? – Josef Švejk Oct 04 '18 at 18:13
  • @Dima it's normally must crash on aData.release; and the exception must be capted in except ... end but it's not the case :( – zeus Oct 04 '18 at 19:18
  • @nolaspeaker the last line is aData.release – zeus Oct 04 '18 at 19:19
  • Why can't we have a [mcve]? – David Heffernan Oct 04 '18 at 20:55
  • 3
    @DavidHeffernan This is the proper example. If you try running that code on iOS exception is not caught in exception handler. – Dalija Prasnikar Oct 04 '18 at 21:15
  • @loki I changed method signature to `Button1Click` because it is less confusing, and issue is not in any way related to `AssetsLibraryAssetForURLResultBlock` Also, while in this example it is obvious that exception is raised by `aData.release` called on nil reference, pointing out line where it happens does not hurt. It may not be obvious to everyone. – Dalija Prasnikar Oct 04 '18 at 21:28
  • Hmmm... should it really cause an exception? What exactly does `Data.release` do? Could it be that it just sends a `release` *message* to `nil`, and messages to `nil` are simply ignored, AFAIK. No Mac here to test this, sorry. – Rudy Velthuis Oct 04 '18 at 21:39
  • 2
    @RudyVelthuis But would that really account for the `Application.OnException` event catching an AV at address 0? That would suggest that `release()` is triggering an *asynchronous* message that gets processed (and consequently crashes) AFTER `Button1Click` has exited – Remy Lebeau Oct 04 '18 at 21:51
  • 3
    FWIW, @TheBitman: Release in Delphi for Windows (and the VCL) is not nearly the same as release on an NSObject derivate class like NSData in iOS or macOS. Totally different things altogether. – Rudy Velthuis Oct 04 '18 at 22:07
  • 2
    @TheBitman: **if you want all people to see your deleted answer, then undelete it**. But you might well leave it deleted and stop amending it. NSData is an Objective-C object (not nearly the same as a Delphi object) wrapped in a Delphi class that implements the interface NSData (so Delphi can use it). Obviously you have no idea about iOS, and TCustomForm.Release has absolutely nothing to do with NSObject.release. The latter is more like release for interfaces, and is used for manual reference counting. Even the NSObject messaging to invoke a method is not nearly the same as that in Windows. – Rudy Velthuis Oct 04 '18 at 22:47
  • 2
    @TheBitman: No, you can't test that on Windows and certainly not in the VCL. Something **tooooooooooootally different**. Not nearly the same. – Rudy Velthuis Oct 04 '18 at 22:50
  • [Delphi Brings ARC to Android](http://blog.marcocantu.com/blog/delphi_arc_android.html) – Freddie Bell Oct 05 '18 at 04:33
  • @DalijaPrasnikar thanks yes Button1Click is more understandable thanks ! – zeus Oct 05 '18 at 08:05
  • @RudyVelthuis : but the exeption "Access violation at address 0000000100EE9A8C, accessing address 0000000000000000" its a delphi exception that say we try to access a nil object (what we actually does). the problem is why the exception get out of the try ... except end ! in the sample you can not see but the address "0000000100EE9A8C" is well inside the button1Click – zeus Oct 05 '18 at 08:08
  • 1
    This is a bug. Delphi on both iOS and Android is not capable of catching exceptions caught by virtual method calls on nil references. NSData is an interface and calling release goes through VMT table. I will need some time to write up answer. In the meantime, the only workaround is to avoid calling anything on nil reference. – Dalija Prasnikar Oct 05 '18 at 09:01
  • 1
    @DalijaPrasnikar , ok thanks i open a bug report here: https://quality.embarcadero.com/browse/RSP-21371 – zeus Oct 05 '18 at 09:08

1 Answers1

19

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;
Dalija Prasnikar
  • 27,212
  • 44
  • 82
  • 159
  • very good article @dalijaPrasnikar! I m curious to know why emb decide to make this like this as it's seam they do it deliberately .. – zeus Oct 06 '18 at 07:47
  • @loki If you read the lines I quoted from documentation - it is because LLVM backend cannot return to the except block if there is no function inside try block. That means Delphi compiler would have to fake function call which would introduce performance penalties both for compile time and runtime. (How much, I cannot tell) But I also seriously don't like this "bug/feature" behavior. – Dalija Prasnikar Oct 06 '18 at 11:38
  • :I think when we write try .. except end we are already aware that their will be maybe some performance penalty, and nowadays with newer CPU i think this "jump" penalty will be very little that we will not even notice it – zeus Oct 06 '18 at 12:39
  • There is also a question of how easy is to implement such exception handling on top of LLVM backend. Solving the problem of the LLVM side is probably not going to happen (at least not anytime soon) This bug/feature report about issue in question sits in their bug tracker since 2007 [Add support for asynchronous "non-call" exceptions, eliminate invoke/call dichotomy](https://bugs.llvm.org/show_bug.cgi?id=1269) – Dalija Prasnikar Oct 07 '18 at 12:23