類中靜態回撥函式應用

pamxy發表於2013-03-18

轉自: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); 


這樣就大功告成。   

相關文章