轉帖於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; ...