首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> 關於VC++

ATL 布幔之下的秘密 (5):視窗類別、Thunk 機制與訊息路由的深度解析

     編輯:關於VC++

前言:ATL 不只是 COM,更是輕量級視窗框架

許多開發者認為 ATL 僅用於撰寫 COM 元件,實則 ATL 的視窗類別 (CWindow, CWindowImpl) 可用於建立完整的視窗應用程式。雖然缺乏 MFC 的文件/視圖架構,但 ATL 以極小的程式碼體積、極快的執行速度著稱。本文深入探討 ATL 如何解決「成員函數無法作為視窗程序」的核心難題——Thunk 機制,並重現其演進邏輯。

從 SDK 風格到 C++ 封裝的嘗試

經典 Hello World (SDK)

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        case WM_PAINT: { /* ... */ break; }
        case WM_DESTROY: PostQuitMessage(0); break;
    }
    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

int WINAPI WinMain(...) {
    WNDCLASS wc = {0};
    wc.lpfnWndProc = WndProc; // 全域函數
    RegisterClass(&wc);
    HWND hWnd = CreateWindow(...);
    // 訊息迴圈...
}

初步封裝:ZWindow 類別

class ZWindow {
public:
    HWND m_hWnd;
    ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) {}
    BOOL ShowWindow(int nCmdShow) { return ::ShowWindow(m_hWnd, nCmdShow); }
    // ... 其他 API 封裝 ...

    // 嘗試用成員函數作視窗程序 → 編譯錯誤!
    static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
        // 無法存取非靜態成員
    }
};

問題:非靜態成員函數隱含 this 指標,無法轉換為 WNDPROC。靜態成員函數雖可轉換,但無法存取實例資料。

全域指標法:簡單但無法支援多視窗

ZWindow* g_pWnd = nullptr;

static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    ZWindow* pThis = g_pWnd; // 單一全域指標
    pThis->OnPaint(...);     // 呼叫虛函數
    // ...
}

缺點:建立第二個視窗時,g_pWnd 被覆蓋,第一個視窗的訊息會錯誤路由到第二個視窗的處理函數。

ATL 的解決方案:Thunk (代碼存根)

什麼是 Thunk?

Thunk 是一小段機器碼,動態產生於執行期,用於調整呼叫慣例或參數傳遞。ATL 利用 Thunk 將 HWND 參數替換為 this 指標,並跳轉至統一的靜態視窗程序 WindowProc。

Thunk 結構定義 (無位元組對齊)

#pragma pack(push, 1)
struct _WndProcThunk {
    DWORD m_mov;      // 機器碼: mov dword ptr [esp+4], pThis  (esp+4 為 HWND 參數)
    DWORD m_this;     // this 指標
    BYTE  m_jmp;      // jmp 指令操作碼 (0xE9)
    DWORD m_relproc;  // 相對跳轉位移
};
#pragma pack(pop)

Thunk 初始化

void Init(WNDPROC proc, void* pThis) {
    m_mov = 0x042444C7;                 // C7 44 24 04 : mov [esp+4], imm32
    m_this = (DWORD)pThis;              // 立即數 = this 指標
    m_jmp = 0xE9;                       // JMP rel32
    m_relproc = (int)proc - ((int)this + sizeof(_WndProcThunk)); // 相對位移
    FlushInstructionCache(GetCurrentProcess(), this, sizeof(_WndProcThunk));
}

初始化後,&m_thunk 即為可直接傳遞給 CreateWindow 的 WNDPROC。

統一視窗程序 WindowProc

static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    // 此時 hWnd 參數已被 Thunk 替換為 this 指標!
    CWindow* pThis = (CWindow*)hWnd;
    if (uMsg == WM_NCDESTROY) PostQuitMessage(0);
    if (!pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam))
        return ::DefWindowProc(pThis->m_hWnd, uMsg, wParam, lParam);
    return 0;
}

虛函數分派:ProcessWindowMessage

CWindowImpl 定義虛函數 ProcessWindowMessage,衍生類別透過訊息對應巨集 (BEGIN_MSG_MAP) 實作具體處理邏輯,實現多型行為,且無虛函數表開銷(靜態多型)。

完整流程圖解

  1. 建立衍生視窗類別實例 CMyWnd wnd。
  2. 呼叫 wnd.Create(...) 內部:
    a. 配置 Thunk 結構。
    b. thunk.Init(WindowProc, this) 寫入機器碼。
    c. CreateWindow(..., (WNDPROC)&thunk, ...) 註冊類別並建立視窗。
    d. 視窗建立成功,m_hWnd 存入實例。
  3. 系統發送訊息 → 呼叫 Thunk 程式碼 → mov [esp+4], this → jmp WindowProc。
  4. WindowProc 以 this 呼叫 ProcessWindowMessage → 衍生類別的訊息對應表處理訊息。

優勢總結

  • 零額外查表:不需全域 Map 查找 HWND→this,Thunk 直接在堆疊修改參數。
  • 支援無限視窗:每個視窗實例擁有獨立 Thunk,互不干擾。
  • 極小程式碼:Thunk 僅 12 位元組 (x86),無虛函數表、無執行期查找開銷。
  • 靈活擴充:衍生類別可自由覆寫 ProcessWindowMessage,實現 MFC 風格的訊息對應。

後記

ATL 的 Thunk 技術是其「小、快」核心哲學的典型體現。理解此機制不僅有助於深入掌握 ATL/WTL,更能啟發對執行期程式碼生成、呼叫慣例調整等底層技術的思考。下一篇將探討 ATL 的物件對應表、模組狀態管理等進階主題。

本文配套源碼
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved