构造一个通用的回调Thunk.(把回调函数指向对象的方法的办法)

原创 2008年01月29日 14:25:00

构造一个通用的回调Thunk.(把回调函数指向对象的方法)
最近又看到了VCL代码中的MakeObjectInstance函数,实际上是一段WndProc的Thunk代码.再一次感叹VCL设计之精巧,效率之高.
不喜欢MFC的消息映射方式,MFC的消息映射虽然好理解,但是是采用查表方式效率实在是太低了.VCL的MakeObjectInstance可以
说是VCL Windows系统的灵魂所在,效率极高.
不禁想可不可以实现一个通用的回调函数Thunk呢,可以把所有回调函数都变成对象的方法.
但是MakeObjectInstance实际上是为WndProc特化的.
分析一下回调函数
1.回调函数不过是一个函数指针.
2.尽管回调函数可以是任何调用约定,但绝大多数Win32API的回调函数都是stdcall.(VC中WINAPI,PASCAL,CALLBACK不过是stdcall的宏).
  我们完全可以不考虑其他的调用约定,只考虑stdcall的.
想一下,如果我们对象的方法也是一个stdcall调用约定的方法,那么和回调函数还差什么呢?
只差一个参数,第一个参数对象实例的指针,在Delphi,Pascal,Ada中叫Self,C++,java,C#中叫this.VB中叫ME.
那么我们只要塞给它这个对象的地址不就行了吗.好在stdcall约定参数是由右向左传递的,也就是说第一个参数是最后传递的,又由于stdccall约定
参数全部是由栈传递的.所以我们只要把对象指针直接压入栈中就行了.
但别忽略了一点,
call指令相当于
     Push 返回地址
     Jmp  函数
ret指令相当于
     pop  返回地址
     Jmp  返回地址     
也就是说实际上在调用函数的时候栈顶保留的是返回地址,如果我们直接压入实例指针的话原来,当跳到函数体中,函数会把返回地址当Self,而Self则
会被当成返回地址,具体会有什么样的后果大家自己去想像一下
所以我们做的事情就是弹出返回地址,压入实例地址,压入返回地址,跳到对象方法去执行.
实际上我们就是要构造这样一段代码当回调用,这段代码插入对象实例参数到第一个参数,然后跳到对象方法:
     pop    eax            //弹出返回地址到eax
     push   对象实例       //压入对象实例
     push   eax            //压入返回地址
     jmp    对应的对象方法 //跳转到相应的对象方法
具体实现如下    
    
//构造出一段Thunk代码
//构造出一段Thunk代码
Function CreateThunk(Obj : TObject; CallBackProc: Pointer):Pointer;
const
  PageSize = 4096;
  SizeOfJmpCode = 5;
type
  TCode = packed record
    Int3: Byte;          //想调试的的时候填Int 3($CC),不想调试的时候填nop($90)
    PopEAX : Byte;       //把返回地址从栈中弹出
    Push: Byte;          //压栈指令
    AddrOfSelf: TObject; //压入Self地址,把Self作为第一个参数
    PushEAX : Byte;      //重新压入返回地址
    Jmp: Byte;           //相对跳转指令
    AddrOfJmp: Cardinal; //要跳转到的地址,
  end;
var
  LCode : ^TCode;
begin
  //分配一段可以执行,可读写的内存
  Result := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  LCode := Result;
  LCode^.Int3 := $90; //nop
  //LCode^.Int3 := $CC; //Int 3
  LCode^.PopEAX := $58;
  LCode^.Push := $68;
  LCode^.AddrOfSelf := Obj;
  LCode^.PushEAX := $50;
  LCode^.Jmp := $E9;
  LCode^.AddrOfJmp := DWORD(CallBackProc) - (DWORD(@LCode^.Jmp) + SizeOfJmpCode);//计算相对地址
end;

//销毁thunk代码
procedure ReleaseThunk(Thunk: Pointer);
begin
  VirtualFree(Thunk, 0, MEM_RELEASE);
end;  

任何Stdcall调用约定的回调都可以用这个Thunk,只要你构造出一个参数一样的对象方法.
具体举个例子:如SetTimer这个API最后一个参数就是一个回调函数.我们可以拿他试试.
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    FThunk : Pointer; //Thunk代码的指针
    FTimerId : Cardinal;
  public
    //构造一个和SetTimer回调参数一样的方法,就等着被调用吧
    procedure TimeProc(hwnd: HWND; uMsg: UINT; var idEvent: UINT; dwTime: DWORD); stdcall;
  end;

var
  Form1             : TForm1;

implementation

{$R *.dfm}

//构造出一段Thunk代码
Function CreateThunk(Obj : TObject; CallBackProc: Pointer):Pointer;
const
  PageSize = 4096;
  SizeOfJmpCode = 5;
type
  TCode = packed record
    Int3: Byte;          //想调试的的时候填Int 3($CC),不想调试的时候填nop($90)
    PopEAX : Byte;       //把返回地址从栈中弹出
    Push: Byte;          //压栈指令
    AddrOfSelf: TObject; //压入Self地址,把Self作为第一个参数
    PushEAX : Byte;      //重新压入返回地址
    Jmp: Byte;           //相对跳转指令
    AddrOfJmp: Cardinal; //要跳转到的地址,
  end;
var
  LCode : ^TCode;
begin
  //分配一段可以执行,可读写的内存
  Result := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  LCode := Result;
  LCode^.Int3 := $90; //nop
  //LCode^.Int3 := $CC; //Int 3
  LCode^.PopEAX := $58;
  LCode^.Push := $68;
  LCode^.AddrOfSelf := Obj;
  LCode^.PushEAX := $50;
  LCode^.Jmp := $E9;
  LCode^.AddrOfJmp := DWORD(CallBackProc) - (DWORD(@LCode^.Jmp) + SizeOfJmpCode);//计算相对地址
end;

//销毁thunk代码
procedure ReleaseThunk(Thunk: Pointer);
begin
  VirtualFree(Thunk, 0, MEM_RELEASE);
end;


procedure TForm1.FormCreate(Sender: TObject);
begin
  //构造Thunk
  FThunk := CreateThunk(Self, @TForm1.TimeProc);
  //把Thunk当作回调函数传递给SetTimer,1000毫秒(1秒)被调用一次
  FTimerId := SetTimer(0, 0, 1000, FThunk);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  //停止Timer
  KillTimer(0, FTimerId);
  //释放Thunk
  ReleaseThunk(FThunk);
end;

procedure TForm1.TimeProc(hwnd: HWND; uMsg: UINT; var idEvent: UINT;
  dwTime: DWORD);
begin
  Caption := Format('我被调用了,GetTickCount=%d',[dwTime]);
end;

end.    

new一个对象,构造函数会执行可以理解,为什么除了构造函数以外的函数也可以执行?

如将相关内容输出到一个word文件中,代码如下(jfinal): public static void main(String args[]) throws JsonProcessingExcepti...
  • Ideality_hunter
  • Ideality_hunter
  • 2016年11月17日 10:00
  • 1121

C++返回值为对象时复制构造函数不执行怎么破

先说点背景知识,调用复制构造函数的三种情况:  1.当用类一个对象去初始化另一个对象时。  2.如果函数形参是类对象。  3.如果函数返回值是类对象,函数执行完成返回调用时。  在辅导学生上机时,有同...
  • sxhelijian
  • sxhelijian
  • 2016年03月25日 11:42
  • 3445

当析构函数遇到多线程──C++ 中线程安全的对象回调

原文地址:http://blog.csdn.net/Solstice/article/details/5238671 当析构函数遇到多线程 ── C++ 中线程安全的对象回调   ...
  • yinxin2745154
  • yinxin2745154
  • 2015年01月31日 20:04
  • 2077

Thunk 回调函数实现面向对象

回调函数实现面向对象 什么是回调函数? 通常,Windows均要求我们将消息处理函数定义为一个全局函数,或者是一个类中的静态成员函数。并且该函数必须采用__stdcall的调用约定(Calling C...
  • yingzheng1983
  • yingzheng1983
  • 2012年05月20日 18:37
  • 1620

利用Thunk让C++成员函数变回调函数

Windows API经常需要回调函数,而在C++开发中面向对象当行其道,若能让C++类的成员函数成为回调函数,简直就是大善!但是C++成员函数都隐含了一个this指针用于指向当前的对象。要实现回调确...
  • zbzoujianfa
  • zbzoujianfa
  • 2013年01月25日 10:19
  • 523

利用Thunk让C++成员函数变回调函数

windows API经常需要回调函数,而在C++开发中面向对象当行其道,若能让C++类的成员函数成为回调函数,简直就是大善!但是C++成员函数都隐含了一个this指针用于指向当前的对象。要实现回调确...
  • weiqubo
  • weiqubo
  • 2012年08月24日 17:50
  • 1589

【C语言】使用回调函数实现一个通用的冒泡排序,可以排序不同的数据类型。

使用回调函数实现一个通用的冒泡排序,可以排序不同的数据类型
  • yaotengjian
  • yaotengjian
  • 2017年07月03日 22:38
  • 254

使用回调函数实现一个通用的冒泡排序,可以排序不同的数据类型。

使用回调函数实现一个通用的冒泡排序,可以排序不同的数据类型。
  • lu_1079776757
  • lu_1079776757
  • 2017年07月18日 15:02
  • 240

swift 类回调方法/回调函数

// A类代码 class A{ func huidiao(){ let tag:String = "XunDianGuanLiControlle" // ...
  • qq_29755359
  • qq_29755359
  • 2017年11月09日 18:46
  • 151

python 回调函数和回调方法的实现

回调与事件驱动 回调函数有比较重要的意义:它在是事件驱动的体现 我们试想一个场景,如果我们触发了某个事件,比如点击事件 那么只要给这个点击事件绑定一个或多个处理事件,也就是回调函数 我们就...
  • zhangshou99111
  • zhangshou99111
  • 2014年11月30日 14:57
  • 4318
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:构造一个通用的回调Thunk.(把回调函数指向对象的方法的办法)
举报原因:
原因补充:

(最多只允许输入30个字)