Delphi按名字調用方法高級解決方案

轉帖於https://lfzhs.iteye.com/blog/980200數組

按名字調用方法彷佛一直以來都是你們比較關注的技術,在論壇上有一個經典的答覆:函數

type
    TProcedure = procedure(Test: string) of object;

  procedure ExecuteRoutine(Obj: TObject; Name, Param: string);
  var
    PMethod: TMethod;
    AProcedure: TProcedure;
  begin
    PMethod.Data := Pointer(Obj);
    PMethod.Code := Obj.MethodAddress(Name);
    if Assigned(PMethod.Code) then
    begin
      AProcedure := TProcedure(PMethod);
      AProcedure(Param);
    end;
  end;

使用:待調用方法聲明爲某個類的 published 方法,Obj 爲擁有待調用方法的類的
實例,Name 和 Param 分別爲待調用方法的名字和參數。oop


可是這個辦法有一個很大的侷限性:一旦 TProcedure 聲明定了下來,待調用方法的參
數表也就必定了。要是我定義了多個待調用方法,且參數個數、類型、返回值均不一樣,
則這個辦法也就無能爲力了。另:用 GetProcAddress 代替 MethodAddress 也能夠實
現相似的效果,不過咱們今天討論的是調用類的「方法」,而它所返回的不是「方法」,
由於 GetProcAddress 僅能取得應用程序的輸出(exports)了的過程或函數,這些過
程或函數不屬於任何類,也就稱不上「方法」。固然,效果相似,可是侷限也相似 :-(

那麼要是參數的個數、類型都可變就沒法解決了嗎?(要是這樣就不會有本文了)經過
研究,我發現了一種行之有效的辦法:Format 函數(注意,不是 DOS 命令,呵呵)相
信你們都不陌生吧,傳入它的參數的個數和類型不都是可變的嗎?只要聲明以下:spa

procedure AProc(Param: array of const);

便可這樣調用:指針

AProc([123, 'X', True, 'hello'...]);

有朋友可能要說了,那不就簡單了,這樣不就能夠了:code

type
    TProcedure = procedure(Params: array of const) of object;

  procedure ExecuteRoutine(Obj: TObject; Name: string; Params: array of const);
  var
    ...
  begin
    ...
      AProcedure(Params);
    ...
  end;

別急,問題纔剛剛出現呢,你運行試一試?出問題了吧。(爲方便起見,暫時稱咱們的
 ExecuteRoutine 函數,爲控制函數;待調用方法簡稱爲待調方法)這個形參表的聲明
辦法的確適合咱們的控制函數,可是不適合待調方法。爲何?由於待調方法的形參表
的確不是這樣(array of const)的啊。固然了,你說你把全部待調方法的形參表都改
成這個樣子不就能夠了?且不說你須要改動多少東西(包括待調函數的參數表和內部實
現,關鍵是內部實現部分),就看看你改了事後的待調方法的形參表,所有都成了一個
模樣。說不定到時候你本身都不知道到底應該傳什麼參數進去了。所以,咱們應該儘可能
保持待調方法的形參表。

如今問題轉化爲了在控制函數中已知待調方法的地址及其參數列表(存放在一個
 TVarRec 的數組中),如何在調用它的時候將參數傳進去。這須要幾點預備知識:

  1. 首先咱們來看看傳進來的這個參數表:Params。它的類型被 Delphi 稱做可變開
放數組(Variant open array),等價於 array of TVarRec,也就是說 Params 是一
個成員爲 TVarRec 的數組。換句話說,在參數被傳進 Params 的時候,各類類型都被
 Delphi 自動轉化爲了 TVarRec(參見幫助中的 Variant open array parameters 一
節)。看一下 TVarRec 的定義可知,它實際儲存的數據域爲 4 Bytes,超過 4 Bytes 
的只存指針,須要注意的是 TVarRec 的大小是 8 Bytes(經研究發現前 4 Bytes 存放
數據,第 5 Byte 爲類型)。

  2. 調用函數時的參數傳遞的通常狀況(未使用 stdcall 的狀況)。對於通常的函數
或過程,前三個參數分別放在 EAX、EDX、ECX,後面若是還有更多參數的話,就在堆棧
裏面;對於類的方法,EAX 固定用於存放類實例的地址,EDX、ECX 分別存放前兩個參
數,其他參數進棧。在堆棧中每一個元素佔用 4 Bytes,而前面說了,TVarRec 中儲存的
數據也是 4 Bytes,恰好一個參數在堆棧裏面佔一個位子,處理方便。另外,結果返回
到 EAX 中。

  3. 對於調用類的方法,其實有一個默認的隱藏參數 Self 做爲第一個參數傳入,放
入 EAX 寄存器。所以咱們看到的第一參數實際上是第二個,所以咱們處理的時候要注意。

  4. 用 ObjectPascal 語法調用方法,Delphi 會自動幫咱們處理參數的傳遞問題,而
在彙編裏面調用任何函數以前都須要先手動設置各參數。

所以,我決定用內嵌彙編的辦法來解決參數傳遞問題:若是是一個參數,放入 EDX;若
爲兩個參數,分別放入 EDX,ECX;對多於兩個參數的狀況,用 參數個數 - 2 個循環依
次將後續參數進棧。而後將擁有待調方法的實例地址傳入 EAX,就能夠 CALL 了。orm

function ExecuteRoutine(AObj: TObject; AName: string;
    Params: array of const): DWord;
  const
    RecSize = SizeOf(TVarRec); // 循環處理參數列表時遞增的字節數
  var
    PFunc: Pointer;
    ParCount: DWord;
  begin
    if not Assigned(AObj) then
      raise Exception.Create ('你肯定傳進來的是一個對象?');
    PFunc := AObj.MethodAddress(AName); // 獲取方法地址
    if not Assigned(PFunc) then
      raise Exception.CreateFmt('找不到 %s 的 Method: %s', [AObj.ClassName,
        AName]);
      
    ParCount := High(Params) + 1; // 獲取參數個數

    asm
      PUSH        ESI                 // 保存 ESI,咱們待會兒要用到它

      MOV         ESI, Params         // ESI 指向參數表首址
      CMP         ParCount, 1         // 判斷參數個數
      JB          @NoParam
      JE          @OneParam
      CMP         ParCount, 2
      JE          @TwoParams

    @ManyParams: // 超過兩個參數
      CLD                             // 清空方向標誌
      MOV         ECX, ParCount
      SUB         ECX, 2              // 循環 ParCount - 2 次
      MOV         EDX, RecSize        // EDX 依次指向每一個參數的首址,每次遞增 8 Bytes
      ADD         EDX, RecSize        // 跳過前兩個參數
    @ParamLoop:
      MOV         EAX, [ESI][EDX]     // 用基址變址尋址方式取得一個參數
      PUSH        EAX                 // 參數進棧
      ADD         EDX, RecSize        // EDX 指向下一個參數首址
      LOOP        @ParamLoop

    @TwoParams: // 兩個參數
      MOV         ECX, [ESI] + RecSize

    @OneParam: // 一個參數
      MOV         EDX, [ESI]

    @NoParam:
      MOV         EAX, AObj           // 傳入實例地址(即,隱藏參數 Self)
      CALL        PFunc               // 調用方法
      MOV         Result, EAX         // 返回值放入 Result

      POP         ESI                 // 記得還原
    end;
  end;

前面已經說過了,任何類型均可以塞進 4 Bytes,所以將返回值定義爲 DWord,你能夠
根據本身的須要進行類型轉換。這個辦法最大限度地保護了待調方法,但也不是徹底不
用修改,只有一個地方須要做出適當調整:與 DLL 中的函數返回值同樣(別告訴我引用
 ShareMem,那不屬於本文討論的範疇),若是要返回一個長 string,請改成 PChar,
並注意申請必要的空間。

如下是一個使用的例子(再次提醒一下,待調方法必須是某個類的 published 方法):對象

TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  published // 幾個待調方法
    function TowInt(I, J: Integer): Integer;
    function ThreeInt(I, J, K: Integer): Integer;
    function FiveInt(X1, X2, X3, X4, X5: Integer): Integer;
    function ThreeChar(I, J, K: Char): PChar;
    function TwoStr(X, Y: string): PChar;
    function IntNBool(I: Integer; B: Boolean): Boolean;
  end;

  ...

  function ExecuteRoutine(AObj: TObject; AName: string;
    Params: array of const): DWord;
  ...

  function TForm1.TowInt(I, J: Integer): Integer;
  begin
    ShowMessage(Format('%d + %d', [I, J]));
    Result := I + J;
  end;

  function TForm1.ThreeInt(I, J, K: Integer): Integer;
  begin
    ShowMessage(Format('%d + %d + %d', [I, J, K]));
    Result := I + J + K;
  end;

  function TForm1.FiveInt(X1, X2, X3, X4, X5: Integer): Integer;
  begin
    ShowMessage(Format('%d + %d + %d + %d + %d', [X1, X2, X3, X4, X5]));
    Result := X1 + X2 + X3 + X4 + X5;
  end;

  function TForm1.ThreeChar(I, J, K: Char): PChar;
  var
    Res: string;
  begin
    ShowMessage(Format('%s + %s + %s', [I, J, K]));
    Res := I + J + K;
    Result := AllocMem(Length(Res) + 1);
    StrPCopy(Result, Res);
  end;

  function TForm1.TwoStr(X, Y: string): PChar;
  var
    Res: string;
  begin
    ShowMessage(Format('%s + %s', [X, Y]));
    Res := X + Y;
    Result := AllocMem(Length(Res) + 1);
    StrPCopy(Result, Res);
  end;

  function TForm1.IntNBool(I: Integer; B: Boolean): Boolean;
  begin
    if B then
      ShowMessage(IntToStr(I) + ' and True')
    else
      ShowMessage(IntToStr(I) + ' and False');

    Result := B;
  end;

  procedure TForm1.Button1Click(Sender: TObject);
  var
    i: Integer;
    b: Boolean;
    s: string;
  begin
    i := ExecuteRoutine(Self, 'ThreeInt', [10, 23, 17]);
    ShowMessage('Result: ' + IntToStr(i));

    i := ExecuteRoutine(Self, 'FiveInt', [1, 2, 3, 4, 5]);
    ShowMessage('Result: ' + IntToStr(i));

    b := Boolean(ExecuteRoutine(Self, 'IntNBool', [10, False]));
    if b then
      ShowMessage('Result: True')
    else
      ShowMessage('Result: False');

    s := PChar(ExecuteRoutine(Self, 'ThreeChar', ['a', 'b', 'c']));
    ShowMessage('Result: ' + s);

    s := PChar(ExecuteRoutine(Self, 'TwoStr', ['hello', ' world']));
    ShowMessage('Result: ' + s);
  end;

  ...

我之因此稱該辦法爲高級解決方案,而非終極,由於它仍然有一個問題沒有解決:變參
問題。可是這不是什麼大問題,由於徹底能夠用函數返回值代替變參。啊?你要返回多
個值?那建議返回一個指向結構體的指針,或一個最簡單的對象。   blog

TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  published // 幾個待調方法
    function TowInt(I, J: Integer): Integer;
    function ThreeInt(I, J, K: Integer): Integer;
    function FiveInt(X1, X2, X3, X4, X5: Integer): Integer;
    function ThreeChar(I, J, K: Char): PChar;
    function TwoStr(X, Y: string): PChar;
    function IntNBool(I: Integer; B: Boolean): Boolean;
  end;

  ...

  function ExecuteRoutine(AObj: TObject; AName: string;
    Params: array of const): DWord;
  ...

  function TForm1.TowInt(I, J: Integer): Integer;
  begin
    ShowMessage(Format('%d + %d', [I, J]));
    Result := I + J;
  end;

  function TForm1.ThreeInt(I, J, K: Integer): Integer;
  begin
    ShowMessage(Format('%d + %d + %d', [I, J, K]));
    Result := I + J + K;
  end;

  function TForm1.FiveInt(X1, X2, X3, X4, X5: Integer): Integer;
  begin
    ShowMessage(Format('%d + %d + %d + %d + %d', [X1, X2, X3, X4, X5]));
    Result := X1 + X2 + X3 + X4 + X5;
  end;

  function TForm1.ThreeChar(I, J, K: Char): PChar;
  var
    Res: string;
  begin
    ShowMessage(Format('%s + %s + %s', [I, J, K]));
    Res := I + J + K;
    Result := AllocMem(Length(Res) + 1);
    StrPCopy(Result, Res);
  end;

  function TForm1.TwoStr(X, Y: string): PChar;
  var
    Res: string;
  begin
    ShowMessage(Format('%s + %s', [X, Y]));
    Res := X + Y;
    Result := AllocMem(Length(Res) + 1);
    StrPCopy(Result, Res);
  end;

  function TForm1.IntNBool(I: Integer; B: Boolean): Boolean;
  begin
    if B then
      ShowMessage(IntToStr(I) + ' and True')
    else
      ShowMessage(IntToStr(I) + ' and False');

    Result := B;
  end;

  procedure TForm1.Button1Click(Sender: TObject);
  var
    i: Integer;
    b: Boolean;
    s: string;
  begin
    i := ExecuteRoutine(Self, 'ThreeInt', [10, 23, 17]);
    ShowMessage('Result: ' + IntToStr(i));

    i := ExecuteRoutine(Self, 'FiveInt', [1, 2, 3, 4, 5]);
    ShowMessage('Result: ' + IntToStr(i));

    b := Boolean(ExecuteRoutine(Self, 'IntNBool', [10, False]));
    if b then
      ShowMessage('Result: True')
    else
      ShowMessage('Result: False');

    s := PChar(ExecuteRoutine(Self, 'ThreeChar', ['a', 'b', 'c']));
    ShowMessage('Result: ' + s);

    s := PChar(ExecuteRoutine(Self, 'TwoStr', ['hello', ' world']));
    ShowMessage('Result: ' + s);
  end;

  ...
相關文章
相關標籤/搜索