`
lfzhs
  • 浏览: 76275 次
  • 性别: Icon_minigender_1
  • 来自: 福州
社区版块
存档分类
最新评论

Delphi按名字调用方法高级解决方案

阅读更多

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

  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 分别为待调用方法的名字和参数。

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

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

  procedure AProc(Param: array of const);

即可这样调用:

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

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

  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 了。

  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;

  ...

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

 

原地址:http://www.delphibbs.com/delphibbs/dispq.asp?lid=1289838

分享到:
评论

相关推荐

    解决Delphi7数据执行保护

    在开发过程中,开发者可能会遇到一个问题:当尝试使用Delphi 7编译的程序调用WebService时,由于Windows的数据执行保护(DEP,Data Execution Prevention)机制,程序可能会遭遇运行错误。DEP是一种安全特性,旨在...

    DELPHI PDF文件打印

    在DELPHI编程环境中,处理PDF文件打印是一个常见的需求,特别是在开发桌面应用程序时。...同时,用户反馈和评价也是衡量软件质量的重要标准,因此,如果解决方案有效且用户满意,别忘了鼓励他们给予正面的评价。

    Excel通用打印(delphi)

    本资源“Excel通用打印(Delphi)”提供了一种解决方案,它可能包含了一个用户友好的界面,使得用户能够方便地打印Excel文件中的数据。在Delphi中,实现这一功能通常涉及到对Microsoft Office自动化接口的利用,或者是...

    delphi截图文字识别.rar

    总之,Delphi结合Tesseract 4.1进行截图文字识别,不仅提供了强大的OCR功能,还允许开发者通过自定义DLL进行图像捕获,实现了从图像获取到文字识别的一站式解决方案。通过熟练掌握这些知识点,开发者可以创建出高效...

    Asp与Delphi互通的AES加解密程序源码

    本源码提供了一种解决方案,通过AES(Advanced Encryption Standard)加密算法实现了Asp与Delphi之间的安全互通。 AES是一种对称加密算法,以其高效和强大的安全性而闻名,被广泛应用于各种数据保护场景。它使用...

    delphi 蓝牙VCL——BlueToothFramewor,有demo

    总结来说,Delphi的蓝牙VCL——BlueToothFramework是一个强大且灵活的蓝牙通信解决方案,它简化了Delphi开发者在蓝牙编程中的复杂性,提供了全面的组件和功能。通过深入研究和实践,开发者可以利用这个框架构建出...

    大漠不注册调用dm.dll的方法

    在IT行业中,动态链接库(DLL)是一种非常重要的编程元素...最后,学习和使用大漠SDK或者任何其他第三方库时,建议始终参考官方文档,参加相关的技术论坛,获取最新的更新和解决方案,以确保你的项目稳定、高效且合规。

    delphi调用webservice总结(c#,delphi,java).doc

    ### Delphi调用WebService知识点详解 #### 一、Delphi调用C#编写的WebService 在实际项目开发中,Delphi与其他语言(如C#)编写的WebService进行交互是一种常见的需求。本文档主要介绍了如何使用Delphi 7来调用C#...

    Delphi-OpenCV.zip

    通过深入理解和熟练运用这两者的结合,开发者可以创造出更具创新性和实用性的软件解决方案。尽管存在一定的技术挑战,但随着OpenCV库的不断更新和完善,以及Delphi社区对它的持续支持,这种结合将会变得越来越容易且...

    《Delphi中的DLL封装和调用对象技术》及配书源码

    《Delphi中的DLL封装和调用对象技术》是一本深入探讨如何在Delphi开发环境中利用动态链接库...通过阅读本书和实践源码,你将能够熟练掌握如何在Delphi中创建高效、稳定的DLL解决方案,同时优化你的软件设计和资源管理。

    RemObjects PascalScript(delphi7-delphi10.3)

    综上所述,RemObjects PascalScript为Delphi开发者提供了一个高效、灵活的Pascal脚本解决方案,使得他们能够在各种应用场景中利用Pascal的强大功能。通过熟练掌握PascalScript,开发者可以构建更强大、更具有定制性...

    PythonForDelphi分词.zip

    通过Delphi调用Python的Jieba库,开发者可以轻松地在Delphi程序中实现这些高级功能。 这个项目展示了如何利用现有工具和库,如Python和Jieba,来增强Delphi程序的自然语言处理能力,为开发涉及中文文本处理的Delphi...

    VCLZipPro451_1含delphi调用案例delphi7下编译通过

    这对于需要特殊功能或高度定制的压缩解决方案的项目非常有用。 7. **安全性和隐私**: 该组件支持对压缩文件进行密码保护,以增强数据的安全性。开发者可以设置密码策略,确保敏感数据不被未经授权的用户访问。 ...

    Delphi_SQLLite.rar_DELPHI 数据库_Delphi SQLLite_delphi sqll_sqlite

    在IT行业中,数据库管理系统是软件开发中的重要组成部分,特别是在Delphi这样的高级编程环境中。...通过实践这些示例,你将能够更好地理解Delphi和SQLite的结合,为你的项目构建稳健的数据存储解决方案。

    Delphi多层开发方案比较

    .NET Framework提供了另一种解决方案,它可以创建Web服务,实现跨语言、跨平台的通信。它与Delphi的兼容性主要通过.NET接口或者Web Service来实现。.NET的性能取决于具体的实现方式,如SOAP消息的传输效率较低,而二...

    python for delphi7

    在处理国际化文本时,需要注意可能存在的编码问题,并采取相应的解决方案,比如手动转换字符串编码。 通过以上知识点的学习和实践,你将能够充分利用Python的丰富库资源,结合Delphi强大的GUI设计能力,创建出功能...

    Pb调用 c#编写的DLL

    虽然PB因其简单性和低成本而受到青睐,但其技术限制使得开发者寻求更强大的解决方案,比如利用C#编写的DLL扩展其功能。 首先,在Visual Studio 2008 SP1中创建一个新的C#项目。为了使PB能够调用C# DLL,我们需要...

    DELPHI做LED数字显示的效果

    这种技术不仅适用于传统的桌面软件,也可以应用于移动平台或嵌入式系统,为各种类型的显示屏提供灵活的显示解决方案。熟练掌握此类技术,将极大地提升你在IT行业的专业能力,特别是在可视化界面设计领域。

    Delphi串口及语音传真高级编程

    在Delphi中实现语音传真功能可能涉及到第三方库或API的使用,例如书中可能介绍的APRO 2.x库,它提供了一套完整的传真解决方案,包括发送、接收、管理和存储传真。开发者需要学习如何集成这些库,配置参数,以及调用...

Global site tag (gtag.js) - Google Analytics