2012-05-16 3 views
2

이 질문은 one에서 발생합니다.
문제는 시스템에서 많은 콜백 명령을 저장할 수있는 비 시각적 구성 요소를 작성하는 것입니다. 사용자는 IDE에서 무제한으로 콜백을 정의 할 수 있습니다. 콜백은 TCollection에서 TCollectionItem으로 정의됩니다.

이것은 꽤 잘 작동하는 패턴이지만 몇 가지 단점이 있습니다. 더 나은 할 수 있다면, 따라서 궁금 후술 ;-)

이, 사용자가 CommandsTable 컬렉션을 콜백 함수의 IDE 무제한의 주요 구성 요소를 정의 할 수있다

stdcall 호출 규칙을 사용하는 콜백 시스템이있는 구성 요소의 패턴

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; 


을 (STDCALL이 협약 호출) 그리고 것은 구현입니다. 전체 프로세스는 "시작"절차

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() 함수를 사용하는 것입니다.

function TEventCollectionItem.InternalEventFunction(ASomeNotUsefullPointer:pointer; ASomeInteger: integer): Word; stdcall; begin // some important preprocessing stuff // ... if Assigned(FOnEventCommand) then begin FOnEventCommand(Param1, Param2); end; end; 



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; 
는 내가 전에 한 바와 같이, 괜찮 작동하지만 기능 MethodToProcedure는() 썽크 기술을 사용합니다. 데이터 실행 방지 (DEP)가 및 64 비트 아키텍처에서 활성화 된 시스템에서 프로그램이 작동하지 않기 때문에 새로운 MethodToProcedure() 함수가 필요합니다.
더 좋은 패턴을 알고 있습니까?


그냥 완료, 여기 MethodToProcedure()입니다. (나는 누가 원저자인지 모릅니다). 대신 포인터의 배열의 기록의 배열을 허용하도록 DLL을 변경할 수있는 경우

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;  

답변

5

, 당신은 콜백 포인터와 객체 포인터를 모두 포함 할 레코드를 정의하고, 콜백 서명을 제공 할 수 있습니다 여분의 포인터 매개 변수. 그런 다음 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에 대해 걱정하지 마십시오. 할당 된 메모리를 실행 코드가 포함 된 것으로 표시 할 수 있도록 New() 대신 VirtualAlloc()VirtualProtect()을 사용하기 만하면됩니다. 이것이 VCL 자신의 썽크 (예 : TWinControlTTimer)가 DEP 간섭을 피하는 방법입니다.

+0

답변 해 주셔서 감사합니다. Unfrotuantely, 나는 DLL을 변경할 수 없습니다. (그것은 전혀 잘못 설계되었지만 함께 살아야합니다.) 그래서 썽크가 유일한 해결책 일 것입니다. – Peter

0

DLL 코드를 수정할 수 없기 때문에 질문에있는 코드 스타일로 썽크를 사용하지 않고 선택할 수 있습니다. 인스턴스 정보를 콜백 함수에 전달할 수있는 다른 방법은 없습니다.