在 C++ 開發中,許多庫要求提供回調函數指標,這給面向對象編程(OOP)帶來了麻煩:普通 C 函數缺少成員函數所需的 this 指標。Thunk 技術是一種快速但平台相關的解決方案。作者在研究多篇 Thunk 相關文章後發現,現有方案多針對特定問題,因此設計了一組通用類別,提供統一的解決途徑。
源碼提供了 5 個類別(實際 4 種),均位於 Thunk 命名空間。每個對象包含「對象」與「方法」兩個屬性,能動態生成機器碼,執行時邏輯等同於 Obj.Method(...)。以窗口子類化為例,使用步驟如下:
ThisToStd,將 __thiscall 成員函數轉為 __stdcall 回調)。Attach(this) 綁定目標對象。AttachMethod(&CSubClassing::SubProc) 綁定成員函數。MakeCallback<LONG>() 取得回調函數指標,傳遞給 API(如 SetWindowLong)。class CSubClassing {
private:
Thunk::ThisToStd m_thunk;
public:
CSubClassing() {
m_thunk.Attach(this);
m_thunk.AttachMethod(&CSubClassing::SubProc);
}
void Attach(HWND hWnd) {
m_oldProc = (WNDPROC)SetWindowLong(hWnd, GWL_WNDPROC, m_thunk.MakeCallback<LONG>());
}
private:
LRESULT SubProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
if (msg != WE_NEEDED)
return CallWindowProc(m_oldProc, hWnd, msg, wParam, lParam);
// 處理特定消息...
}
WNDPROC m_oldProc;
};
五個類別介面一致,選定後即可用於 WNDPROC、THREADPROC、Hooking 等場景。示例工程包含 5 個程序,分別測試 ThisToStd、ThisToCdecl、StdToStd、StdToCdecl、CdeclToCdecl,僅靠預處理器定義區分。
關鍵在於理解呼叫者(普通 C 函數)與被呼叫者(成員函數)在參數傳遞、this 指標獲取、堆疊平衡責任上的差異。以 void func(int) 與 void C::func(int) 為例:
CALL 壓返回地址,被呼叫者用 RET N 平衡堆疊。__stdcall,但 this 放在 ECX,被呼叫者同樣用 RET N。因此 ThisToStd 只需在跳轉前將 this 裝入 ECX,參數與返回地址已就位,堆疊平衡自動匹配。
結構僅含兩條指令:MOV ECX, this 與 JMP member_func。利用 #pragma pack(1) 確保緊湊佈局,構造函數初始化操作碼(0xB9、0xE9),Attach 方法分別設定 this 與目標函數偏移。
__stdcall 成員函數期望堆疊頂為 this,故需在返回地址與參數 1 之間插入 this。作者設計巨集 STD_TO_STD_CODES() 產生指令序列:POP EAX; PUSH this; PUSH EAX; JMP member_func。透過巨集區分宣告與定義,確保初始化列表合法。
__cdecl 由呼叫者以 ADD ESP, N 清理堆疊,且 N 不一定等於參數數×4。初版 36 字節,優化後採用「Hook 函數」策略:Thunk 僅含 CALL Hook、this、member_func、恢復 ESP、JMP 返回地址,縮減至 23 字節。Hook 函數以 __declspec(naked) 手寫組合語言,完成保存 ESP/返回地址、設定 ECX、跳轉成員函數。
CdeclToCdecl 類似 ThisToCdecl,差別在 Hook 中將 this 插入堆疊而非 ECX。StdToCdecl 與 CdeclToCdecl 行為一致,故直接 typedef CdeclToCdecl StdToCdecl。
__stdcall 呼叫者期望被呼叫者清理堆疊,但 __cdecl 成員函數用 RET 返回,導致 ESP 資訊丟失,無法設計通用 Thunk。
ECX/EDX 傳遞,與參數個數相關,通用 Thunk 難以實現,但特定場景可專門處理。ThunkBase.cpp 定義 THUNK_FLUSHINSTRUCTIONCACHE。本文提出的一組通用 Thunk 類別,透過動態生成機器碼,優雅地解決了 C++ 成員函數作為 C 風格回調的難題。原理雖涉及平台相關細節,但封裝良好、介面統一,適用於窗口子類化、執行緒啟動、Hook 等多種場景。原始碼可自由使用,作者不承擔責任。