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.
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With