How to check if a reference to procedure is nil?
Asked Answered
G

1

15

In the following example code the call to AssertTestObj() causes an access violation.

Project InvokeTest2.exe raised exception class $C0000005 with message 'access violation at 0x00000000: read of address 0x00000000'.

when debugging I can see that the Assigned(NotifyProc) test in TSafeCall<T>.Invoke() does not work as expected - so that Invoke() tries to execute NotifyProc which is nil and thus causes the access violation.

Any ideas why this fails and how to solve it?

program InvokeTest2;

{$APPTYPE CONSOLE}

uses
  System.SysUtils;

type
  TSafeCall<T> = class
  public
    type
      TNotifyProc = reference to procedure (Item: T);
    class procedure Invoke(NotifyProc: TNotifyProc; Item: T); overload;
  end;

  TOnObj = procedure (Value: String) of object;

{ TSafeCall<T> }

class procedure TSafeCall<T>.Invoke(NotifyProc: TNotifyProc; Item: T);
begin
  if Assigned(NotifyProc) then
    NotifyProc(Item);
end;

procedure AssertTestObj(OnExceptionObj_: TOnObj; Value_: String);
begin
  TSafeCall<String>.Invoke(OnExceptionObj_, Value_);
end;

begin
  try
    TSafeCall<String>.Invoke(nil, 'works as expected');

    AssertTestObj(nil, 'this causes an access violation!');
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
Gerardgerardo answered 16/3, 2015 at 9:55 Comment(0)
A
21

This is a compiler bug. Here's my simplified reproduction:

{$APPTYPE CONSOLE}

type
  TProc = reference to procedure;
  TOnObject = procedure of object;

procedure Invoke(Proc: TProc);
begin
  if Assigned(Proc) then
    Proc();
end;

procedure CallInvokeOnObject(OnObject: TOnObject);
begin
  Invoke(OnObject);
end;

begin
  Invoke(nil); // succeeds
  CallInvokeOnObject(nil); // results in AV
end.

You might wonder why I simplified. Your code was a superb reproduction of the problem. However, I wanted to make it absolutely as simple as possible so that I really could be sure that the problem was what I believe it to be. So I removed the generics and the classes.

Now, the test using Assigned is correct. You are right to expect that it will behave as you intend. The problem is that when the compiler generates code to call Invoke from CallInvokeOnObject, it needs to wrap the method of object in a reference procedure interface. In order to do this correctly it would need to test whether or not the method of object is assigned. If not then no wrapper interface should be created and Invoke should be passed nil.

The compiler fails to do that. It unconditionally wraps the method of object in a reference procedure interface. You can see this in the code emitted for CallInvokeOnObject.

Project1.dpr.16: begin // this is the beginning of CallInvokeOnObject
004064D8 55               push ebp
004064D9 8BEC             mov ebp,esp
004064DB 6A00             push $00
004064DD 53               push ebx
004064DE 33C0             xor eax,eax
004064E0 55               push ebp
004064E1 683B654000       push $0040653b
004064E6 64FF30           push dword ptr fs:[eax]
004064E9 648920           mov fs:[eax],esp
004064EC B201             mov dl,$01
004064EE A1F4634000       mov eax,[$004063f4]
004064F3 E8DCDAFFFF       call TObject.Create
004064F8 8BD8             mov ebx,eax
004064FA 8D45FC           lea eax,[ebp-$04]
004064FD 8BD3             mov edx,ebx
004064FF 85D2             test edx,edx
00406501 7403             jz $00406506
00406503 83EAF8           sub edx,-$08
00406506 E881F2FFFF       call @IntfCopy
0040650B 8B4508           mov eax,[ebp+$08]
0040650E 894310           mov [ebx+$10],eax
00406511 8B450C           mov eax,[ebp+$0c]
00406514 894314           mov [ebx+$14],eax
Project18.dpr.17: Invoke(OnObject);
00406517 8BC3             mov eax,ebx
00406519 85C0             test eax,eax
0040651B 7403             jz $00406520
0040651D 83E8E8           sub eax,-$18
00406520 E8DFFDFFFF       call Invoke

That call to TObject.Create is what wraps the method of object in a reference procedure interface. Note that the interface is created unconditionally and then passed to Invoke.

There's no way for you work around this from inside Invoke. By the time the code reaches there it's too late. You cannot detect that the method is not assigned. This should be reported to Embarcadero as a bug.

Your only viable workaround is to add an extra assigned check in CallInvokeOnObject.

Awad answered 16/3, 2015 at 10:28 Comment(1)
@Martin You could usefully change the version to be XE7 update 1, with version number 21.0.17707.5020. That's the latest, and where I did my testing.Awad

© 2022 - 2024 — McMap. All rights reserved.