Шаблон компонента с системой обратного вызова с соглашением о вызовах stdcall

Этот вопрос возник из-за этого one.
Проблема в том, визуальный компонент, который может содержать множество команд обратного вызова из системы. Пользователь может определить неограниченное количество обратных вызовов в среде IDE. Обратные вызовы будут определены в TCollection как TCollectionItem.

Этот шаблон работает очень хорошо, но имеет некоторые недостатки. (описано позже) Поэтому мне интересно, можно ли было бы сделать это лучше ;-)

Это основной компонент, пользователь может определить в IDE неограниченное количество функций обратного вызова через коллекцию CommandsTable

TMainComp = class(TComponent)  
private
   CallbacksArray: array [0..x] of pointer;
   procedure BuildCallbacksArray;    
public 
   procedure Start;
published
   property CommandsTable: TCommandCollection read FCommandsTable write SetCommandsTable;
end;


Каждый элемент коллекции выглядит так, InternalCommandFunction — это обратный вызов, который вызывается из системы. (Соглашение о стандартном вызове)

TCommandCollectionItem = class(TCollectionItem)
public
   function InternalCommandFunction(ASomeNotUsefullPointer:pointer; ASomeInteger: integer): Word; stdcall;
published
   property OnEventCommand: TComandFunc read FOnEventCommand write FOnEventCommand;
end; 


TComandFunc = function(AParam1: integer; AParam2: integer): Word of Object;


А вот и реализация. Весь процесс можно было запустить с помощью процедуры "Старт"

procedure TMainComp.Start;
begin  
  // fill CallBackPointers array with pointers to CallbackFunction
  BuildCallbacksArray;

  // function AddThread is from EXTERNAL dll. This function creates a new thread, 
  // and parameter is a pointer to an array of pointers (callback functions).
  // New created thread in system should call our defined callbacks (commands) 
  AddThread(@CallbacksArray);
end;   

И это проблемный код. Я думаю, что единственный способ получить указатель на функцию «InternalEventFunction» — использовать функцию MethodToProcedure().

procedure TMainComp.BuildCallbacksArray;
begin
   for i := 0 to FCommandsTable.Count - 1 do begin
      // it will not compile
      //CallbacksArray[i] := @FCommandsTable.Items[i].InternalEventFunctionWork;

      // compiles, but not work
      //CallbacksArray[i] := @TCommandCollectionItem.InternalCommandFunction;

      // works pretty good
      CallbacksArray[i] := MethodToProcedure(FCommandsTable.Items[i], @TCommandCollectionItem.InternalCommandFunction);

   end;         
end;


function TEventCollectionItem.InternalEventFunction(ASomeNotUsefullPointer:pointer; ASomeInteger: integer): Word; stdcall;
begin
  // some important preprocessing stuff
  // ...


  if Assigned(FOnEventCommand) then begin
     FOnEventCommand(Param1, Param2);
  end;
end;


Как я уже говорил, это работает нормально, но функция MethodToProcedure() использует технику Thunk. Мне нравится избегать этого, потому что программа не будет работать в системах, где включено предотвращение выполнения данных (DEP), а также в 64-битных архитектурах, вероятно, потребуется совершенно новая функция MethodToProcedure().
Знаете ли вы какой-нибудь лучший шаблон для этого?


просто для завершения, вот метод MethodToProcedure(). (Я не знаю, кто является первоначальным автором).

TMethodToProc = packed record
    popEax: Byte;
    pushSelf: record
      opcode: Byte;
      Self: Pointer;
    end;
    pushEax: Byte;
    jump: record
      opcode: Byte;
      modRm: Byte;
      pTarget: ^Pointer;
      target: Pointer;
    end;
  end;    

function MethodToProcedure(self: TObject; methodAddr: Pointer): Pointer;
var
  mtp: ^TMethodToProc absolute Result;
begin
  New(mtp);
  with mtp^ do
  begin
    popEax := $58;
    pushSelf.opcode := $68;
    pushSelf.Self := Self;
    pushEax := $50;
    jump.opcode := $FF;
    jump.modRm := $25;
    jump.pTarget := @jump.target;
    jump.target := methodAddr;
  end;
end;    

person Peter    schedule 16.05.2012    source источник


Ответы (2)


Если вы можете изменить DLL так, чтобы она принимала массив записей вместо массива указателей, то вы можете определить запись, содержащую как указатель обратного вызова, так и указатель объекта, и присвоить сигнатуре обратного вызова дополнительный параметр указателя. Затем определите простую прокси-функцию, которую DLL может вызывать с указателем объекта в качестве параметра, а прокси-сервер может вызывать метод реального объекта через этот указатель. Не требуется thunking или сборка более низкого уровня, и он будет работать как в 32-разрядной, так и в 64-разрядной версии без специального кодирования. Что-то вроде следующего:

type
  TCallback = function(AUserData: Pointer; AParam1, AParam2: Integer): Word; stdcall;

  TCallbackRec = packed record
    Callback: TCallback;
    UserData: Pointer; 
  end;

  TCommandFunc = function(AParam1, AParam2: integer): Word of object; 

  TCommandCollectionItem = class(TCollectionItem) 
  private
    FOnEventCommand: TCommandFunc;
    function InternalCommandFunction(APara1, AParam2: Integer): Word; 
  published 
    property OnEventCommand: TCommandFunc read FOnEventCommand write FOnEventCommand; 
  end;  

  TMainComp = class(TComponent)  
  private
    CallbacksArray: array of TCallbackRec;
  public 
    procedure Start;
  published
    property CommandsTable: TCommandCollection read FCommandsTable write SetCommandsTable;
  end;

.

function CallbackProxy(AUSerData: Pointer; AParam1, AParam2: Integer): Word; stdcall;
begin
  Result := TEventCollectionItem(AUserData).InternalEventFunction(AParam1, AParam2);
end;

procedure TMainComp.Start; 
var
  i: Integer;
begin 
  SetLength(CallbacksArray, FCommandsTable.Count);
  for i := 0 to FCommandsTable.Count - 1 do begin 
    CallbacksArray[i].Callback := @CallbackProxy; 
    CallbacksArray[i].UserData := FCommandsTable.Items[i]; 
  end;          
  AddThread(@CallbacksArray[0]);
end;    

function TEventCollectionItem.InternalEventFunction(AParam1, AParam2: Integer): Word;
begin 
  // ... 
  if Assigned(FOnEventCommand) then begin 
    Result := FOnEventCommand(Param1, Param2); 
  end; 
end; 

Если это не вариант, то использование переходников является единственным решением, учитывая показанный вами дизайн, и вам потребуются отдельные 32-битные и 64-битные преобразователи. Однако не беспокойтесь о DEP. Просто используйте VirtualAlloc() и VirtualProtect() вместо New(), чтобы пометить выделенную память как содержащую исполняемый код. Именно так собственные преобразователи VCL (используемые, например, TWinControl и TTimer) избегают помех DEP.

person Remy Lebeau    schedule 16.05.2012
comment
Спасибо за ответ. К сожалению, я не могу изменить dll. (Это вообще неправильно спроектировано, но мне с этим жить). Таким образом, преобразователи, вероятно, единственное решение. - person Peter; 16.05.2012

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

person David Heffernan    schedule 16.05.2012