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

前两天听了组里的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");
}
};

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

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

  • OwnWaterloo says:

    权限atl老版本是有在virtualalloc基础上实现一个池, 新版本这个池接口直接使用heapcreate。
    可能win32 heap以前不支持权限设置吧。
    不同机器这段代码肯定不相同的, atl里面一大堆条件包含…… windows支持的机器都有实现。
    全局链表? thunk需要这个?

    • 哇您是怎么翻到这篇的。。我都忘记还写过这文章了orz|||
      全局链表的话,他大概是这样的。创建窗口之后,他必须等待第一个消息,才能将那段thunk设置到他该去的位置。假如另外一个线程也创建了窗口,并且它的消息更早到达,结果就会给窗口回调函数设置了错误的this指针。
      现在他的做法是查找该链表线程id相等的第一个窗口指针,也就是说假设一个线程里面先创建的窗口一定会先发生第一个事件。这个假设应该还是靠谱的。但是,就像文中说的一样,还是可能会有问题的。
      当然这是我1年前写的文章了,不知道ATL现在的做法是不是有改变。

  • OwnWaterloo says:

    窗口有”创建线程”属性的吧? x线程创建的窗口的消息, 只能被x的GetMessage循环获得?
    我主要是看ATL的thunk代码是如何实现的, 以及对DEP的处理; 至于WINDOWCLASS与某个C++ class是如何关联的到没怎么注意。
    只是觉得, 既然都用了thunk, 应该是不需要全局的东西才对……

  • OwnWaterloo says:

    哦, 想起来了, 消息处理函数是:
    1. 设置WINDOWCLASS中的lpWndProc
    2. 设置WINDOWCLASS中的classname
    3. 注册该WINDOWCLASS
    4. 创建window时通过classname选择被注册的WINDOWCLASS

    那么, 联系WINDOWCLASS与C++ class的策略:
    1. per instance WINDOWCLASS: 每个instance可构造出一段thunk, 作为一个WINDOWCLASS的lnWndProc注册
    2. per class WINDOWCLASS: 每个class共用一个WINDOWCLASS, 它的lpWndProc指向一个与所有instance无关的普通函数, 该函数为每个instance设置thunk —— 用SetWindowLongPtr将WNDPROC改为一个被thunk的代码。

    ATL估计避免注册过多的WINDOWCLASS, 所以选择的 per class WINDOWCLASS?
    但我记得依然不需要全局的吧?
    有几个消息是创建时被处理的(在CreateWindowEx返回之前), 且CreateWindowEx还有一个用户定义的param参数, 不需要通过全局数据来传递啊?
    ATL怎么想的……

    • 唔可能我搞错了吧…我那时候只是听人家的讲座.可能他们说的是他们自己的实现来着吧..记不清了..

Comments are closed.