消息派发转为对象方法-用对象处理窗口消息

前两天听了组里的TechTalk,正好讲到ATL里面怎样做窗口消息的派发的很有意思。
一般来讲,用对象来窗口消息有个通病,就是很难解决HWND和对象指针之间的mapping问题。通常的做法可能是使用一个全局的哈希或其它数据结构来存储所有HWND和对象指针之间的一一对应关系。与其凡是要做窗口的对象封装的时候都去重新发明轮子,不如直接使用ATL。

ATL的做法比较有趣,他没有维护这样一个数据结构来存储HWND和对象指针之间的一一对应关系,而是使用了一些trick,把一小段代码使用setwindowlong设置成那个窗口的winproc。在这一小段代码里面,他会把这个winproc的参数栈做一个修改,用两句汇编指令,把第一个参数HWND hWnd替换成this指针并跳转到相应的winproc中去。而那个winproc会首先将第一个参数static_cast成自己窗口类型的指针,并根据需要调用相应的消息处理函数。

至于实现这些的具体细节,我记忆力不好没有完全记住,不过网上有很多相关文章也无需我多述。大体上说他在createwindow的时候将该窗口对象的指针和创建它的线程编号放入一个全局数组(以此来防止不同的线程同时创建窗口而可能造成的冲突),并把一个叫做StartWindowProc的初始化函数用SetWindowLong设置成该窗口的消息处理函数(这个函数其实只会进入一次)。这个初始化函数在第一个窗口消息发生的时候(即该函数首次进入),会去计算窗口指针和他的成员消息处理函数的位移,并生成那一段汇编代码的thunk,并将他的地址插入到消息处理函数处。此后再进入消息处理函数就会经过这段thunk从而使第一个参数变成了相应对象的this指针并跳转到相应的真正的消息处理函数。

不过这个方案也有一些问题。比如,那段thunk是当作窗口对象的成员存储在数据段的。有些系统会禁止在数据段运行code。在这类系统上需要另外开一片有执行权限的内存,然后把这段thunk放到那段内存中去。还有就是不同的CPU指令是不同的,必须为每一个目标系统写对应的thunk指令段。有比如他在createwindow的时候把窗口指针放在全局链表的头部,之后再首次得到消息的时候从头部开始找属于该线程的第一个指针进行配对。虽然加上了线程id来保证不会有同时创建两个窗口造成冲突误判的问题,但这里面仍然有安全性的concern。(线程ID和窗口句柄的长度都是4字节,为何存储的是线程指针而不是窗口句柄呢?我在talk中没有想到要问这个,这个也许有其他方面的concern吧。。)

另外就是talk中提到一个细节比较有趣,就是如果使用模板的话,多态可以不使用虚函数实现,而使用模板。


template <class T>
class base
{
public:
void print()
{
static_cast<T*>(this)->print();
}
};

class derived:public base<derived>
{
public:
void print()
{
print("hello\n");
}
};

这样做因为转型都是编译期完成的,可以减少继承带来的运行时负担。