首先我想出了一个最明显的解决方法,我们可以很容易地实现。方法是创建一个全局的结构,这个结构存储HWND和相应的派生类。但是,这个方法有两个主要的问题。第一,这个结构会在窗口逐渐加入程序的过程中越变越大;第二,在结构变得很大之后,在这个结构中进行搜索肯定也会花费大笔时间。
而ATL的最主要目的就是使程序尽可能地小和快。并且,上述技术对于这两个标准都达不到。这个方法不单单是慢,还会在程序中包含大量窗口的情况下占用大量内存。
另一个可能的解决方案是使用WNDCLASS或WNDCLASSEX结构的cbWndExtra域。还有一个问题是,为什么不用cbClsExtra,而要用cbWndExtra呢?答案很简单,cbClsExtra为每个窗口类存储额外的字节,而cbWndExtra为每个窗口存储额外的字节。并且,你可能会从一个窗口类创建多个窗口,这样,如果你使用了cbClsExtra的话,那么你就不能通过cbClsExtra区别不同的回调函数了,因为对于这些相同窗口类产生的窗口来说这个值是一样的。然后,将相应的派生类地址存储到cbWndExtra中。
这个和方法看起来比第一个要好,但是它仍然有两个问题。第一,如果用户希望使用cbWndExtra,那么他/她就可能会覆盖着一技术所使用的数据,这样客户就需要在使用cbWndExtra的时候十分注意了,以防丢失信息。那么好了,你可以在文档中写明在使用你的库时不要使用cbWndExtra,但是仍然会有一个问题:这个方法并不是很快,又一次违背了ATL的规则——ATL应该尽可能地小和快。
ATL没有使用这两个方法中的任何一个,它使用的方法被称作Thunk。Thunk是一个小系列的代码,并且这一术语被用在不同的地方。你可能曾经听过两种Thunking:
Universal Thunking
Universal Thunking允许在16位代码中调用32位的函数,在Win 9x和Win NT/2000/XP下都可以使用,也被称作Generic Thunking。
General Thunking
General Thunking允许在32位代码中调用16位的函数,它只能用在Win 9x中,因为Win NT/2000/XP是纯32位操作系统,所以在32位代码中调用16位的函数不合乎逻辑。General Thunking也被称作Flat Thunking。
ATL没有使用这两种方法,因为你不会在ATL中将16位和32位的代码混合。事实上,ATL插入了一小段代码来调用正确的窗口过程。
在研究ATL的Thunking之前,让我们先从一些基础概念开始。请看下面的简单程序。
程序72.
#include <iostream> using namespace std;
struct S { char ch; int i; };
int main() { cout << "Size of character = " << sizeof(char) << endl; cout << "Size of integer = " << sizeof(int) << endl; cout << "Size of structure = " << sizeof(S) << endl; return 0; } |
程序的输出为:
Size of character = 1 Size of integer = 4 Size of structure = 8 |
一个整型和一个字符的尺寸之和应该是5而不是8。那么让我们略微修改一下程序,再添加一个成员变量,看看会发生什么。
程序73.
#include <iostream> using namespace std;
struct S { char ch1; char ch2; int i; };
int main() { cout << "Size of character = " << sizeof(char) << endl; cout << "Size of integer = " << sizeof(int) << endl; cout << "Size of structure = " << sizeof(S) << endl; return 0; } |
程序的输出和前一个一样。那么这里发生了什么?再修改一下程序,看看布幔之下发生了什么吧。
程序74.
#include <iostream> using namespace std;
struct S { char ch1; char ch2; int i; }s;
int main() { cout << "Address of ch1 = " << (int)&s.ch1 << endl; cout << "Address of ch2 = " << (int)&s.ch2 << endl; cout << "Address of int = " << (int)&s.i << endl; return 0; } |
程序的输出为:
Address of ch1 = 4683576 Address of ch2 = 4683577 Address of int = 4683580 |
这是由于结构和联合成员的字对齐的缘故。如果你注意观察的话,你就能推断出来这个结构外的每个变量都存储在能被4整除的地址上,这是为了提高处理器的性能。所以,这里的结构分配了4的整数倍的内存空间,也就是4683576,ch1和它有相同的地址。ch2成员存储在这个位置之后,而int i存储在4683580的位置上。这个位置不是4683578的原因是它不能被4整除。现在的问题是,4683578和4683579的位置上是什么呢?答案是如果变量是本地变量,那么这里是垃圾值;如果是static或全局变量,那么是0。让我们看看下面这个程序来更好地理解这一点。
程序75.
#include <iostream> using namespace std;
struct S { char ch1; char ch2; int i; };
int main() { S s = { ''A'', ''B'', 10};
void* pVoid = (void*)&s; char* pChar = (char*)pVoid;
cout << (char)*(pChar + 0) << endl; cout << (char)*(pChar + 1) << endl; cout << (char)*(pChar + 2) << endl; cout << (char)*(pChar + 3) << endl; cout << (int)*(pChar + 4) << endl; return 0; } | 程序的输出为:
程序的输出清楚地表明,那些空间中是垃圾值,就像下表一样。
现在,如果我们不想浪费那些空间的话应该怎么做呢?有两个选择:或者使用编译器开关/Zp,或者在声明结构之前使用#pragma语句。
程序76.
#include <iostream> using namespace std;
#pragma pack(push, 1) struct S { char ch; int i; }; #pragma pack(pop)
int main() { cout << "Size of structure = " << sizeof(S) << endl; return 0; } |
程序的输出为:Size of structure = 5
这就意味着现在已经没有字对齐了。事实上,ATL使用这一技术来制作thunk。ATL使用了一个结构,这个结构没有使用字对齐,并且这个结构中直接储存了微处理器的机器代码。
#pragma pack(push,1) // 存储机器代码的结构 struct Thunk { BYTE m_jmp; // jmp指令的操作码 DWORD m_relproc; // 相对jmp }; #pragma pack(pop) |
这种类型的结构保存了thunk代码,它可以在不工作的时候执行。让我们来看看下面这种简单的情况,我们将要使用thunk来执行我们想要执行的函数。
程序77.
#include <iostream> #include <windows.h> using namespace std;
class C;
C* g_pC = NULL;
typedef void(*pFUN)();
#pragma pack(push,1) // 存储机器代码的结构 struct Thunk { BYTE m_jmp; // jmp指令的操作码 DWORD m_relproc; // 相对jmp }; #pragma pack(pop)
class C { public: Thunk m_thunk;
void Init(pFUN pFun, void* pThis) { // 跳转指令的操作码 m_thunk.m_jmp = 0xe9; // 相应函数的地址 m_thunk.m_relproc = (int)pFun - ((int)this+sizeof(Thunk));
FlushInstructionCache(GetCurrentProcess(), &m_thunk, sizeof(m_thunk)); }
// 这是回调函数 static void CallBackFun() { C* pC = g_pC;
// 初始化thunk pC->Init(StaticFun, pC);
// 获得thunk代码地址 pFUN pFun = (pFUN)&(pC->m_thunk);
// 开始执行thunk代码,调用StaticFun pFun();
cout << "C::CallBackFun" << endl; }
static void StaticFun() { cout << "C::StaticFun" << endl; } };
int main() { C objC; g_pC = &objC; C::CallBackFun(); return 0; } |
程序的输出为:
C::StaticFun C::CallBackFun |
在这里,StaticFun是通过thunk调用的,而thunk是在Init成员函数中初始化的。程序的执行是类似这个样子
·CallBackFun ·Init(初始化thunk) ·获得thunk地址 ·执行thunk ·Thunk代码调用StaticFun
ATL也使用了相同的技术来调用正确的回调函数,但是它在调用函数之前还做了一件事情。现在ZWindow又有了一个虚函数ProcessWindowMessage,它在这个类中什么也不做。但是,ZWindow的每个派生类都需要重写它来处理自己的消息。整个的处理过程和我们将ZWindow的派生类地址存入一个指针并调用派生类的虚函数是相同的,但是现在WindowProc的名字是StartWndProc。在这里,ATL使用了这一技术来将HWND参数替换为了this指针。但是,HWND怎么样了,我们就这么失去它了吗?事实上,我们已经将HWND存入了ZWindow类的成员变量中了。
要达到这一点,ATL使用了一个较前一个程序大一点的结构。
#pragma pack(push,1) struct _WndProcThunk { DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd) DWORD m_this; BYTE m_jmp; // jmp WndProc DWORD m_relproc; // 相对jmp }; #pragma pack(pop) |
并且,在初始化的时刻,写入操作码“mov dword ptr [esp +4], pThis”。是类似这个样子:
void Init(WNDPROC proc, void* pThis) { thunk.m_mov = 0x042444C7; //C7 44 24 04 thunk.m_this = (DWORD)pThis; thunk.m_jmp = 0xe9; thunk.m_relproc = (int)proc - ((int)this+sizeof(_WndProcThunk));
FlushInstructionCache(GetCurrentProcess(), &thunk, sizeof(thunk)); } |
并且,在初始化thunk代码之后,获得thunk的地址并向thunk代码设置新的回调函数。然后,thunk代码会调用WindowProc,但是现在第一个参数就不是HWND了,事实上它是this指针。所以我们可以将它安全的转换为ZWindow*,并调用ProcessWindowMessage函数。
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { ZWindow* pThis = (ZWindow*)hWnd;
if (uMsg == WM_NCDESTROY) PostQuitMessage(0);
if (!pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam)) return ::DefWindowProc(pThis->m_hWnd, uMsg, wParam, lParam); else return 0; } |
现在,每个窗口正确的窗口过程就可以被调用了。整个的过程如下图所示:
由于代码长度所限,程序的完整代码将随本文配套提供。我希望能在本系列中之后的文章中继续探究ATL的其它秘密。 |