类中静态回调函数应用

转自:http://blog.csdn.net/yyzsyx/article/details/6185525

提出问题: 
回调函数是基于C编程的Windows SDK的技术,不是针对C++的,程序员可以将一个C函数直接作为回调函数,但是如果试图直接使用C++的成员函数作为回调函数将发生错误,甚至编译就不能通过。
分析原因:
普通的C++成员函数都隐含了一个传递函数作为参数,亦即“this”指针,C++通过传递一个指向自身的指针给其成员函数从而实现程序函数可以访问C++的数据成员。这也可以理解为什么C++类的多个实例可以共享成员函数但是确有不同的数据成员。由于this指针的作用,使得将一个CALLBACK型的成员函数作为回调函数安装时就会因为隐含的this指针使得函数参数个数不匹配,从而导致回调函数安装失败
解决方案:
一,不使用成员函数,直接使用普通C函数,为了实现在C函数中可以访问类的成员变量,可以使用友元操作符(friend),在C++中将该C函数说明为类的友元即可。这种处理机制与普通的C编程中使用回调函数一样。
二,使用静态成员函数,静态成员函数不使用this指针作为隐含参数,这样就可以作为回调函数了。静态成员函数具有两大特点:其一,可以在没有类实例的情况下使用;其二,只能访问静态成员变量和静态成员函数,不能访问非静态成员变量和非静态成员函数。由于在C++中使用类成员函数作为回调函数的目的就是为了访问所有的成员变量和成员函数,如果作不到这一点将不具有实际意义。
我们通过使用静态成员函数非静态成员函数包装的办法来解决问题。类实例可以通过附加参数全局变量的方式的方式传递到静态成员函数中。分别举例如下:
1,参数传递的方式
   #include <iostream.h>   
   class TClassA
   {
   public:

      void Display(const char* text) { cout << text << endl; };
      static void Wrapper_To_Call_Display(void* pt2Object, char* text);
      // more....
   };

   //
静态包装函数,能够调用成员函数Display(),本身做为回调函数来使用
   void TClassA::Wrapper_To_Call_Display(void* pt2Object, char* string)
   {
      // 显式类型转换
       TClassA* mySelf = (TClassA*) pt2Object;

       // 调用普通成员函数
       mySelf->Display(string);
   }

   // 回调函数的宿主,在这里回调用函数被使用
   void DoItA(void* pt2Object, void (*pt2Function)(void* pt2Object, char* text))
   {
      // 使用回调函数
      pt2Function(pt2Object, "hi, i'm calling back using a argument ;-)"); 
   }

// 执行示例
   void Callback_Using_Argument()
   {
      TClassA objA;
      DoItA((void*) &objA, TClassA::Wrapper_To_Call_Display);
   }

2,全局变量的方式
   #include <iostream.h>   
   void* pt2Object;       // 全局变量,可以指向任意对象
   class TClassB
   {
   public:

      void Display(const char* text) { cout << text << endl; };
      static void Wrapper_To_Call_Display(char* text);

   };

   // 静态的包装函数
   void TClassB::Wrapper_To_Call_Display(char* string)
   {
       //需要保证全局变量值的正确性
       TClassB* mySelf = (TClassB*) pt2Object;
       mySelf->Display(string);
   }

// 回调用函数的宿主,在这里回调用函数被使用
   void DoItB(void (*pt2Function)(char* text))
   {
   
      pt2Function("hi, i'm calling back using a global ;-)");   // make callback
   }

   // 执行示例
   void Callback_Using_Global()
   {
      TClassB objB;  
      pt2Object = (void*) &objB;
      DoItB(TClassB::Wrapper_To_Call_Display);
   }

注意:通过上面两种方法的比较可以看出,第2种方法中静态包装函数可以和普通成员函数保持签名一致,当回调函数的宿主接口不能改变时,这种方法特别有用。但因为使用了全局变量,也不是一个好的设计。

注: 自己的话

以上的方法是转载的 大致的想法也就是在回调函数中传入类本身的指针,这样回调函数就能使用类中的非静态成员函数了。

多数情况下回调函数是在构造了一个类的对象之后使用的,但是非要将回调函数写成一个静态函数,可以在类之外使用 并且不能调用类内部的非成员函数。好在这种情况下 我使用的类都是只能有唯一一个对象的,所以加入了singleton的模型

加入GetInstance的函数 获取当前对象指针 再在静态回调函数中使用类的该对象的非静态函数 具体方法如下:

class singleton

static singleton* GetInstance()   //port = local port
{
if(!psin)
   psin= new singleton();
return psin;
};
static callback();
void fun();
private:
    singleton *psin;
};
//singalton.cpp
callback()
{
singalton *p;
p = singalton::GetInstance();
p->fun();
}
   
貌似看上去也有些奇怪,希望能找到更好的解决办法!

 

有几种解决办法: 

一种解决方法是用窗口列表,开一个结构数组,窗口类对象创建窗口的时候把窗口HWND和this指针放入数组,全局消息处理函数遍历数组,利用HWND找出this指针,然后定位到对象内部的消息处理函数。这种方法查找对象的时间会随着窗口个数的增多而增长。 

另一种方法比较聪明一点,WNDCLASS里面有个成员数据cbWndExtra一般是不用的,利用这点,注册类时给该成员数据赋值,这样窗口创建时系统会根据该值开辟一块内存与窗口绑定,这时把创建的窗口类的指针放到该块内存,那么在静态的窗口消息循环函数就能利用GetWindowLong(hWnd,GWL_USERDATA)取出该指针,return (CMyWnd*)->WindowProc(...),这样就不用遍历窗口了。但是这样一来就有个致命弱点,对窗口不能调用SetWindowLong(hWnd,GWL_USERDATA,数据),否则就会导致程序崩溃。幸好这个函数(特定这几个参数)是调用几率极低的,对于窗口,由于创建窗口都是调用窗口类的Create函数,不用手工注册WNDCLASS类,也就不会调用SetWindowLong函数。但是毕竟缺乏安全性,而且当一秒钟内处理的窗口消息很多时,这种查找速度也可能不够快。 

还有一种就是比较完美的解决办法,称之为thunk技术。thunk是一组动态生成的ASM指令,它记录了窗口类对象的this指针,并且这组指令可以当作函数,既也可以是窗口过程来使用。thunk先把窗口对象this指针记录下来,然后转向到静态stdProc回调函数,转向之前先记录HWND,然后把堆栈里HWND的内容替换为this指针,这样在stdProc里就可以从HWND取回对象指针,定位到WindowProc了。 

我们先来看看窗口过程函数定义: 

LRESULT WINAPI WindowProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam) 

其实当我们的窗口类CMyWnd创建窗口的时候,窗口句柄是可以得到并且作为成员数据保存,如此一来,第一个参数hWnd是可以不要的,因为可以通过this->m_hWnd得到,我们可以在这里做手脚,hWnd其实质是一个指针,如果把这个参数替换为窗口类对象的this指针,那么我们不就可以通过(CMyWnd*)hWnd->WindowProc转到窗口类内部的窗口过程了吗?但是窗口过程是系统调用的,怎么能把hWnd替换掉呢?我们先来看看系统调用这个函数时的堆栈情况: 

系统调用m_thunk时的堆栈: 
ret HWND MSG WPARAM LPARAM 
------------------------------------------- 
栈顶 栈底 

系统把参数从右到左依次压栈,最后把返回地址压栈,我们只要在系统调用窗口过程时修改堆栈,把其中的hWnd参数替换掉就行了。这时thunk技术就有用武之地了,我们先定义一个结构: 

#pragma pack(push,1) //该结构必须以字节对齐 
struct Thunk { 
BYTE Call; 
int Offset; 
WNDPROC Proc; 
BYTE Code[5]; 
CMyWnd* Window; 
BYTE Jmp; 
BYTE ECX; 
}; 
#pragma pack(pop) 

类定义: 
class CMyWnd 

public: 
BOOL Create(...); 
LRESULT WINAPI WindowProc(UINT,WPARAM,LPARAM); 
static LRESULT WINAPI InitProc(HWND,UINT,WPARAM,LPARAM); 
static LRESULT WINAPI stdProc(HWND,UINT,WPARAM,LPARAM); 
WNDPROC CreateThunk(); 
WNDPROC GetThunk(){return m_thunk} 
... 

private: 
WNDPROC m_thunk 

在创建窗口的时候把窗口过程设定为this->m_thunk,m_thunk的类型是WNDPROC,因此是完全合法的,当然这个m_thunk还没有初始化,在创建窗口前必须初始化: 

WNDPROC CMyWnd::CreateThunk() 

Thunk* thunk = new Thunk; 

/////////////////////////////////////////////// 
// 
//系统调用m_thunk时的堆栈: 
//ret HWND MSG WPARAM LPARAM 
//------------------------------------------- 
//栈顶 栈底 
/////////////////////////////////////////////// 

//call Offset 
//调用code[0],call执行时会把下一条指令压栈,即把Proc压栈 
thunk->Call = 0xE8; // call [rel]32 
thunk->Offset = (size_t)&(((Thunk*)0)->Code)-(size_t)&(((Thunk*)0)->Proc); // 偏移量,跳过Proc到Code[0] 
thunk->Proc = CMyWnd::stdProc; //静态窗口过程 

//pop ecx,Proc已压栈,弹出Proc到ecx 
thunk->Code[0] = 0x59; //pop ecx 

//mov dword ptr [esp+0x4],this 
//Proc已弹出,栈顶是返回地址,紧接着就是HWND了。 
//[esp+0x4]就是HWND 
thunk->Code[1] = 0xC7; // mov 
thunk->Code[2] = 0x44; // dword ptr 
thunk->Code[3] = 0x24; // disp8[esp] 
thunk->Code[4] = 0x04; // +4 
thunk->Window = this; 

//偷梁换柱成功!跳转到Proc 
//jmp [ecx] 
thunk->Jmp = 0xFF; // jmp [r/m]32 
thunk->ECX = 0x21; // [ecx] 

m_thunk = (WNDPROC)thunk; 
return m_thunk; 

这样m_thunk虽然是一个结构,但其数据是一段可执行的代码,而其类型又是WNDPROC,系统就会忠实地按窗口过程规则调用这段代码,m_thunk就把Window字段里记录的this指针替换掉堆栈中的hWnd参数,然后跳转到静态的stdProc: 

//本回调函数的HWND调用之前已由m_thunk替换为对象指针 
LRESULT WINAPI CMyWnd::stdProc(HWND hWnd,UINT uMsg,UINT wParam,LONG lParam) 

CMyWnd* w = (CMyWnd*)hWnd; 

return w->WindowProc(uMsg,wParam,lParam); 

这样就把窗口过程转向到了类成员函数WindowProc,当然这样还有一个问题,就是窗口句柄hWnd还没来得及记录,因此一开始的窗口过程应该先定位到静态的InitProc,CreateWindow的时候给最后一个参数,即初始化参数赋值为this指针: 

CreateWindowEx( 
dwExStyle, 
szClass, 
szTitle, 
dwStyle, 
x, 
y, 
width, 
height, 
hParentWnd, 
hMenu, 
hInst, 
this //初始化参数 
);, 

在InitProc里面取出该指针: 

LRESULT WINAPI CMyWnd::InitProc(HWND hWnd,UINT uMsg,UINT wParam,LONG lParam) 

if(uMsg == WM_NCCREATE) 

CMyWnd *w = NULL; 
w = (CMyWnd*)((LPCREATESTRUCT)lParam)->lpCreateParams; 
if(w) 

//记录hWnd 
w->m_hWnd = hWnd; 

//改变窗口过程为m_thunk 
SetWindowLong(hWnd,GWL_WNDPROC,(LONG)w-CreateThunk()); 
return (*(WNDPROC)(w->GetThunk()))(hWnd,uMsg,wParam,lParam); 


return DefWindowProc(hWnd,uMsg,wParam,lParam); 

这样就大功告成。   

原文链接: https://www.cnblogs.com/pamxy/archive/2013/03/18/2991468.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    类中静态回调函数应用

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/81121

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月9日 下午7:54
下一篇 2023年2月9日 下午7:55

相关推荐