2

この質問は、この質問から生じまし
問題は、システムからの多くのコールバック コマンドを保持できる非ビジュアル コンポーネントを作成することです。ユーザーは、IDE で無制限の数のコールバックを定義できます。コールバックは TCollection で TCollectionItem として定義されます。

これは非常にうまく機能するパターンですが、いくつかの欠点があります。(後で説明します)したがって、もっとうまくできるのではないかと思います;-)

これはメインコンポーネントであり、ユーザーは CommandsTable コレクションを介して IDE で無制限の数のコールバック関数を定義できます

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 は、システムから呼び出されるコールバックです。(stdcall 呼び出し規約)

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() はサンク手法を使用しています。データ実行防止 (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;    
4

2 に答える 2

5

ポインターの配列ではなくレコードの配列を受け入れるように DLL を変更できる場合は、コールバック ポインターとオブジェクト ポインターの両方を含むようにレコードを定義し、コールバック シグネチャに追加のポインター パラメーターを与えることができます。次に、DLL がオブジェクト ポインターをパラメーターとして呼び出すことができる単純なプロキシ関数を定義します。プロキシは、そのポインターを介して実際のオブジェクト メソッドを呼び出すことができます。サンクや下位レベルのアセンブリは不要で、特別なコーディングなしで 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()andを使用するだけで、割り当てられたメモリを実行可能コードを含むものとしてマークできます。これが、VCL 独自のサンク (たとえば、およびで使用される) が DEP 干渉を回避する方法です。VirtualProtect()New()TWinControlTTimer

于 2012-05-16T10:45:32.880 に答える
0

DLL コードを変更することはできないため、質問のコードのスタイルでサンクを使用する以外に方法はありません。インスタンス情報をコールバック関数に取得する方法は他にありません。

于 2012-05-16T13:13:17.067 に答える