Как проверить, нет ли ссылки на процедуру?

В следующем примере кода вызов AssertTestObj() вызывает нарушение прав доступа.

Project InvokeTest2.exe повышает класс исключений $C0000005 с сообщением "Нарушение доступа при 0x00000000: чтение адреса 0x00000000".

При отладке я вижу, что тест Assigned(NotifyProc) в TSafeCall<T>.Invoke() работает не так, как ожидалось, так что Invoke() пытается выполнить NotifyProc, который является nil и, следовательно, нарушает прав доступа.

Любые идеи, почему это не удается и как его решить?

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.

Ответы

Ответ 1

Это ошибка компилятора. Здесь мое упрощенное воспроизведение:

{$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.

Вы можете удивиться, почему я упростил. Ваш код был превосходным воспроизведением проблемы. Тем не менее, я хотел сделать это максимально простым, чтобы я действительно мог быть уверен, что проблема в том, что я считаю. Поэтому я удалил дженерики и классы.

Теперь проверка с использованием Assigned верна. Вы правы ожидать, что он будет вести себя так, как вы намереваетесь. Проблема заключается в том, что когда компилятор генерирует код для вызова Invoke из CallInvokeOnObject, ему необходимо обернуть метод объекта в интерфейс ссылочной процедуры. Чтобы сделать это правильно, необходимо проверить, назначен ли метод объекта. Если нет, тогда не должен быть создан интерфейс оболочки и Invoke должен быть передан nil.

Компилятор не может этого сделать. Он безоговорочно обертывает метод объекта в интерфейсе эталонной процедуры. Вы можете видеть это в коде, испускаемом для 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

Этот вызов TObject.Create - это то, что обертывает метод объекта в интерфейсе эталонной процедуры. Обратите внимание, что интерфейс создается безоговорочно, а затем передается в Invoke.

Нет никакого способа обойти это изнутри Invoke. К тому времени, когда код дойдет до него, это слишком поздно. Вы не можете обнаружить, что метод не назначен. Это должно быть сообщено Embarcadero как ошибка.

Единственным возможным решением является добавление дополнительной назначенной проверки в CallInvokeOnObject.