转帖于https://lfzhs.iteye.com/blog/980200

按名字调用方法似乎一直以来都是大家比较关注的技术,在论坛上有一个经典的答复:

  1. type
  2. TProcedure = procedure(Test: string) of object;
  3.  
  4. procedure ExecuteRoutine(Obj: TObject; Name, Param: string);
  5. var
  6. PMethod: TMethod;
  7. AProcedure: TProcedure;
  8. begin
  9. PMethod.Data := Pointer(Obj);
  10. PMethod.Code := Obj.MethodAddress(Name);
  11. if Assigned(PMethod.Code) then
  12. begin
  13. AProcedure := TProcedure(PMethod);
  14. AProcedure(Param);
  15. end;
  16. end;

使用:待调用方法声明为某个类的 published 方法,Obj 为拥有待调用方法的类的
实例,Name 和 Param 分别为待调用方法的名字和参数。

但是这个办法有一个很大的局限性:一旦 TProcedure 声明定了下来,待调用方法的参
数表也就一定了。要是我定义了多个待调用方法,且参数个数、类型、返回值均不同,
则这个办法也就无能为力了。另:用 GetProcAddress 代替 MethodAddress 也可以实
现类似的效果,不过我们今天讨论的是调用类的“方法”,而它所返回的不是“方法”,
因为 GetProcAddress 仅能取得应用程序的输出(exports)了的过程或函数,这些过
程或函数不属于任何类,也就称不上“方法”。当然,效果类似,但是局限也类似 :-(

那么要是参数的个数、类型均可变就无法解决了吗?(要是这样就不会有本文了)通过
研究,我发现了一种行之有效的办法:Format 函数(注意,不是 DOS 命令,呵呵)相
信大家都不陌生吧,传入它的参数的个数和类型不都是可变的吗?只要声明如下:

  1. procedure AProc(Param: array of const);

即可这样调用:

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

有朋友可能要说了,那不就简单了,这样不就可以了:

  1. type
  2. TProcedure = procedure(Params: array of const) of object;
  3.  
  4. procedure ExecuteRoutine(Obj: TObject; Name: string; Params: array of const);
  5. var
  6. ...
  7. begin
  8. ...
  9. AProcedure(Params);
  10. ...
  11. 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 了。

  1. function ExecuteRoutine(AObj: TObject; AName: string;
  2. Params: array of const): DWord;
  3. const
  4. RecSize = SizeOf(TVarRec); // 循环处理参数列表时递增的字节数
  5. var
  6. PFunc: Pointer;
  7. ParCount: DWord;
  8. begin
  9. if not Assigned(AObj) then
  10. raise Exception.Create ('你确定传进来的是一个对象?');
  11. PFunc := AObj.MethodAddress(AName); // 获取方法地址
  12. if not Assigned(PFunc) then
  13. raise Exception.CreateFmt('找不到 %s 的 Method: %s', [AObj.ClassName,
  14. AName]);
  15.  
  16. ParCount := High(Params) + 1; // 获取参数个数
  17.  
  18. asm
  19. PUSH ESI // 保存 ESI,我们待会儿要用到它
  20.  
  21. MOV ESI, Params // ESI 指向参数表首址
  22. CMP ParCount, 1 // 判断参数个数
  23. JB @NoParam
  24. JE @OneParam
  25. CMP ParCount, 2
  26. JE @TwoParams
  27.  
  28. @ManyParams: // 超过两个参数
  29. CLD // 清空方向标志
  30. MOV ECX, ParCount
  31. SUB ECX, 2 // 循环 ParCount - 2 次
  32. MOV EDX, RecSize // EDX 依次指向每个参数的首址,每次递增 8 Bytes
  33. ADD EDX, RecSize // 跳过前两个参数
  34. @ParamLoop:
  35. MOV EAX, [ESI][EDX] // 用基址变址寻址方式取得一个参数
  36. PUSH EAX // 参数进栈
  37. ADD EDX, RecSize // EDX 指向下一个参数首址
  38. LOOP @ParamLoop
  39.  
  40. @TwoParams: // 两个参数
  41. MOV ECX, [ESI] + RecSize
  42.  
  43. @OneParam: // 一个参数
  44. MOV EDX, [ESI]
  45.  
  46. @NoParam:
  47. MOV EAX, AObj // 传入实例地址(即,隐藏参数 Self)
  48. CALL PFunc // 调用方法
  49. MOV Result, EAX // 返回值放入 Result
  50.  
  51. POP ESI // 记得还原
  52. end;
  53. end;

前面已经说过了,任何类型都可以塞进 4 Bytes,因此将返回值定义为 DWord,你可以
根据自己的需要进行类型转换。这个办法最大限度地保护了待调方法,但也不是完全不
用修改,只有一个地方需要作出适当调整:与 DLL 中的函数返回值一样(别告诉我引用
 ShareMem,那不属于本文讨论的范畴),如果要返回一个长 string,请改为 PChar,
并注意申请必要的空间。

以下是一个使用的例子(再次提醒一下,待调方法必须是某个类的 published 方法):

  1. TForm1 = class(TForm)
  2. Button1: TButton;
  3. procedure Button1Click(Sender: TObject);
  4. private
  5. { Private declarations }
  6. public
  7. { Public declarations }
  8. published // 几个待调方法
  9. function TowInt(I, J: Integer): Integer;
  10. function ThreeInt(I, J, K: Integer): Integer;
  11. function FiveInt(X1, X2, X3, X4, X5: Integer): Integer;
  12. function ThreeChar(I, J, K: Char): PChar;
  13. function TwoStr(X, Y: string): PChar;
  14. function IntNBool(I: Integer; B: Boolean): Boolean;
  15. end;
  16.  
  17. ...
  18.  
  19. function ExecuteRoutine(AObj: TObject; AName: string;
  20. Params: array of const): DWord;
  21. ...
  22.  
  23. function TForm1.TowInt(I, J: Integer): Integer;
  24. begin
  25. ShowMessage(Format('%d + %d', [I, J]));
  26. Result := I + J;
  27. end;
  28.  
  29. function TForm1.ThreeInt(I, J, K: Integer): Integer;
  30. begin
  31. ShowMessage(Format('%d + %d + %d', [I, J, K]));
  32. Result := I + J + K;
  33. end;
  34.  
  35. function TForm1.FiveInt(X1, X2, X3, X4, X5: Integer): Integer;
  36. begin
  37. ShowMessage(Format('%d + %d + %d + %d + %d', [X1, X2, X3, X4, X5]));
  38. Result := X1 + X2 + X3 + X4 + X5;
  39. end;
  40.  
  41. function TForm1.ThreeChar(I, J, K: Char): PChar;
  42. var
  43. Res: string;
  44. begin
  45. ShowMessage(Format('%s + %s + %s', [I, J, K]));
  46. Res := I + J + K;
  47. Result := AllocMem(Length(Res) + 1);
  48. StrPCopy(Result, Res);
  49. end;
  50.  
  51. function TForm1.TwoStr(X, Y: string): PChar;
  52. var
  53. Res: string;
  54. begin
  55. ShowMessage(Format('%s + %s', [X, Y]));
  56. Res := X + Y;
  57. Result := AllocMem(Length(Res) + 1);
  58. StrPCopy(Result, Res);
  59. end;
  60.  
  61. function TForm1.IntNBool(I: Integer; B: Boolean): Boolean;
  62. begin
  63. if B then
  64. ShowMessage(IntToStr(I) + ' and True')
  65. else
  66. ShowMessage(IntToStr(I) + ' and False');
  67.  
  68. Result := B;
  69. end;
  70.  
  71. procedure TForm1.Button1Click(Sender: TObject);
  72. var
  73. i: Integer;
  74. b: Boolean;
  75. s: string;
  76. begin
  77. i := ExecuteRoutine(Self, 'ThreeInt', [10, 23, 17]);
  78. ShowMessage('Result: ' + IntToStr(i));
  79.  
  80. i := ExecuteRoutine(Self, 'FiveInt', [1, 2, 3, 4, 5]);
  81. ShowMessage('Result: ' + IntToStr(i));
  82.  
  83. b := Boolean(ExecuteRoutine(Self, 'IntNBool', [10, False]));
  84. if b then
  85. ShowMessage('Result: True')
  86. else
  87. ShowMessage('Result: False');
  88.  
  89. s := PChar(ExecuteRoutine(Self, 'ThreeChar', ['a', 'b', 'c']));
  90. ShowMessage('Result: ' + s);
  91.  
  92. s := PChar(ExecuteRoutine(Self, 'TwoStr', ['hello', ' world']));
  93. ShowMessage('Result: ' + s);
  94. end;
  95.  
  96. ...

我之所以称该办法为高级解决方案,而非终极,因为它仍然有一个问题没有解决:变参
问题。但是这不是什么大问题,因为完全可以用函数返回值代替变参。啊?你要返回多
个值?那建议返回一个指向结构体的指针,或一个最简单的对象。

  1. TForm1 = class(TForm)
  2. Button1: TButton;
  3. procedure Button1Click(Sender: TObject);
  4. private
  5. { Private declarations }
  6. public
  7. { Public declarations }
  8. published // 几个待调方法
  9. function TowInt(I, J: Integer): Integer;
  10. function ThreeInt(I, J, K: Integer): Integer;
  11. function FiveInt(X1, X2, X3, X4, X5: Integer): Integer;
  12. function ThreeChar(I, J, K: Char): PChar;
  13. function TwoStr(X, Y: string): PChar;
  14. function IntNBool(I: Integer; B: Boolean): Boolean;
  15. end;
  16.  
  17. ...
  18.  
  19. function ExecuteRoutine(AObj: TObject; AName: string;
  20. Params: array of const): DWord;
  21. ...
  22.  
  23. function TForm1.TowInt(I, J: Integer): Integer;
  24. begin
  25. ShowMessage(Format('%d + %d', [I, J]));
  26. Result := I + J;
  27. end;
  28.  
  29. function TForm1.ThreeInt(I, J, K: Integer): Integer;
  30. begin
  31. ShowMessage(Format('%d + %d + %d', [I, J, K]));
  32. Result := I + J + K;
  33. end;
  34.  
  35. function TForm1.FiveInt(X1, X2, X3, X4, X5: Integer): Integer;
  36. begin
  37. ShowMessage(Format('%d + %d + %d + %d + %d', [X1, X2, X3, X4, X5]));
  38. Result := X1 + X2 + X3 + X4 + X5;
  39. end;
  40.  
  41. function TForm1.ThreeChar(I, J, K: Char): PChar;
  42. var
  43. Res: string;
  44. begin
  45. ShowMessage(Format('%s + %s + %s', [I, J, K]));
  46. Res := I + J + K;
  47. Result := AllocMem(Length(Res) + 1);
  48. StrPCopy(Result, Res);
  49. end;
  50.  
  51. function TForm1.TwoStr(X, Y: string): PChar;
  52. var
  53. Res: string;
  54. begin
  55. ShowMessage(Format('%s + %s', [X, Y]));
  56. Res := X + Y;
  57. Result := AllocMem(Length(Res) + 1);
  58. StrPCopy(Result, Res);
  59. end;
  60.  
  61. function TForm1.IntNBool(I: Integer; B: Boolean): Boolean;
  62. begin
  63. if B then
  64. ShowMessage(IntToStr(I) + ' and True')
  65. else
  66. ShowMessage(IntToStr(I) + ' and False');
  67.  
  68. Result := B;
  69. end;
  70.  
  71. procedure TForm1.Button1Click(Sender: TObject);
  72. var
  73. i: Integer;
  74. b: Boolean;
  75. s: string;
  76. begin
  77. i := ExecuteRoutine(Self, 'ThreeInt', [10, 23, 17]);
  78. ShowMessage('Result: ' + IntToStr(i));
  79.  
  80. i := ExecuteRoutine(Self, 'FiveInt', [1, 2, 3, 4, 5]);
  81. ShowMessage('Result: ' + IntToStr(i));
  82.  
  83. b := Boolean(ExecuteRoutine(Self, 'IntNBool', [10, False]));
  84. if b then
  85. ShowMessage('Result: True')
  86. else
  87. ShowMessage('Result: False');
  88.  
  89. s := PChar(ExecuteRoutine(Self, 'ThreeChar', ['a', 'b', 'c']));
  90. ShowMessage('Result: ' + s);
  91.  
  92. s := PChar(ExecuteRoutine(Self, 'TwoStr', ['hello', ' world']));
  93. ShowMessage('Result: ' + s);
  94. end;
  95.  
  96. ...

  

Delphi按名字调用方法高级解决方案的更多相关文章

  1. python_如何通过实例方法名字调用方法?

    案例: 某项目中,我们的代码使用的2个不同库中的图形类: Circle,Triangle 这两个类中都有一个获取面积的方法接口,但是接口的名字不一样 需求: 统一这些接口,不关心具体的接口,只要我调用 ...

  2. 【事务】<查询不到同一调用方法其它事务提交的更新>解决方案

    最近遇到一个很棘手的问题,至今也解释不清楚原因,不过已经找到了解决方案. 先来看看Propagation属性的值含义,@Transactional中Propagation属性有7个选项可供选择: Pr ...

  3. Delphi Dll 动态调用例子(3)-仔细看一下

    http://blog.163.com/bxf_0011/blog/static/35420330200952075114318/ Delphi 动态链接库的动态和静态调用 为了让人能快速的理解 静态 ...

  4. 在Delphi中静态调用DLL

    在Delphi中静态调用DLL top 调用一个DLL比写一个DLL要容易一些.首先给大家介绍的是静态调用方法,稍后将介绍动态调用方法,并就两种方法做一个比较.同样的,我们先举一个静态调用的例子. u ...

  5. Delphi之静态方法,虚方法virtual,动态dynamic,抽象abstract,消息

    Delphi之静态方法,虚方法virtual,动态dynamic,抽象abstract,消息 http://www.cnblogs.com/zhwx/archive/2012/08/28/266055 ...

  6. 织梦dedecms首页、列表页、文章页文章点击浏览次数实时调用方法

    首先呢,先在根目录 /plus 目录下找到count.php  复制一份然后命名为viewclick.php(你也可以命名为你容易理解的名字)用编辑器将viewclick.php打开然后删除以下几行代 ...

  7. phpcms 的实用相关接口,函数,调用方法

    常用函数 , 打开include/global.func.php,下面存放一些公共函数view plaincopy to clipboardprint? strip_tags() 调用内容过滤html ...

  8. C# 程序开始主要是写类和方法 的基本步骤和调用方法

    主程序的使用方式以及调用方法字段.属性.方法 using System; using System.Collections.Generic; using System.Linq; using Syst ...

  9. js 函数闭包内部返回函数体调用方法难点解答

    今天在网上,看到一篇关于js函数难点的文章,js函数的一些难点.在那上面提了一下,关于js函数返回另一个函数的问题,并附上了一道面试题: var add = function(x){ var sum ...

随机推荐

  1. csv注入漏洞原理&&实战

    前言  为了找工作,巩固巩固知识.本文会介绍 csv 注入漏洞的原理,最后给出一个示例.  正文 在 csv 文件 和 xlsx 文件中的每一项的值如果是 =, @, +, - 就会被 excel 识 ...

  2. 自定义RatingBar评分控件

    1.介绍 实现类似美团外卖评分供能,系统提供了RatingBar,今天来自定义一波,当做自定义view的一个学习,效果如下,能够滑动或者点击变化星星数量 2.自定义属性 在values目录下的attr ...

  3. Pig load 用法举例

    users = load '/users.data' using PigStorage() as (name:chararray, age:int, address:chararray);   loa ...

  4. zabbix系列之二——安装

    1Getting zabbix Four ways of getting: Index Option note 1 Install it from the distribution packages ...

  5. hibernate的延迟加载和抓取策略

    一,延迟加载 1.实体类延迟加载 通过代理机制完成,由javassist类库实现运行时代理,修改实体类的字节码实现了运行时代理     <class lazy="true|false& ...

  6. 关于Oracle中sys、system和Scott用户下的数据库连接问题

    system默认:manager sys默认:change_on_install 使用SQL Plus登录数据库时,system使用密码manager可直接登录. 由于为自己的密码时更改过的,所以我的 ...

  7. [翻译] BAFluidView

    BAFluidView https://github.com/antiguab/BAFluidView This view and it's layer create a 2D fluid anima ...

  8. [翻译] UIGlossyButton

    UIGlossyButton https://github.com/waterlou/UIGlossyButton Feature create standard iPhone buttons wit ...

  9. KInect AR沙盒制作的一点小经验

    最近在微博上看到这样一条 微博  >点这看< 看起来非常有意思,就去Google了一下如何制作. 没想到这是一个开源项目,而且还告诉你如何安装 OK,接下来就说说我的制作过程. 首先,先放 ...

  10. 51nod 1275 连续子段的差异

    题目看这里 若[i,j]符合要求,那么[i,j]内的任何连续的子段都是符合要求的.我们可以枚举i,找到能合格的最远的j,然后ans+=(j-i+1). 那么问题就转换成了:在固定i的情况下,如何判断j ...