Raymond Chen 2007年09月10日
如果控件特定消息属于WM_USER范围,为什么像BM_SETCHECK这样的消息在系统消息范围内?
简要
文章讨论了为何一些控制特定消息如
BM_SETCHECK
属于系统消息范围,而通常这些应属于WM_USER
范围。原因是为了避免在32位Windows中因地址空间隔离导致跨进程通信时的崩溃问题,所有内置控件的遗留消息被移到系统消息类别,窗口管理器可以正确处理这些消息。
正文
当我讨论消息编号属于谁时, 你可能已经注意到,即使是针对编辑框、按钮、列表框、组合框、滚动条和静态控件的控制特定消息, 它们也属于系统范围,尽管它们是控制特定的。 这些消息是如何最终出现在那儿的呢?
它们一开始并不在那里。
在16位Windows中,这些控制特定消息位于控制特定消息范围, 正如你所期望的那样。
#define LB_ADDSTRING (WM_USER + 1) #define LB_INSERTSTRING (WM_USER + 2) #define LB_DELETESTRING (WM_USER + 3) #define LB_RESETCONTENT (WM_USER + 5) #define LB_SETSEL (WM_USER + 6) #define LB_SETCURSEL (WM_USER + 7) #define LB_GETSEL (WM_USER + 8) #define LB_GETCURSEL (WM_USER + 9) #define LB_GETTEXT (WM_USER + 10) ...
想象一下,如果在过渡到Win32时保留了这些消息编号, (给你时间发挥你的想象力。)
这里有一个提示。 由于16位Windows让所有程序在同一个地址空间运行, 程序可以这样做:
char buffer[100];
HWND hwndLB = <a list box that belongs to another process>
SendMessage(hwndLB, LB_GETTEXT, 0, (LPARAM)(LPSTR)buffer);
这读取了属于另一个进程的列表框中的项文本。 由于进程在同一个地址空间运行,发送进程中的缓冲区地址在接收进程中也是有效的, 所以当接收的列表框将结果复制到缓冲区时,一切都可以正常工作。
现在回想一下,如果在过渡到Win32时保留了这些消息编号, (再次给你时间发挥你的想象力。)
考虑一个32位程序,它执行的代码片段与上面的代码片段完全相同。 当程序从16位移植到32位代码时,代码可能没有做任何改变, 因为它没有产生任何编译器警告, 因此没有引起任何需要特别处理的注意。
但由于在Win32中,进程在不同的地址空间运行, 程序现在崩溃了。 更准确地说,它崩溃了那个其他程序, 因为它试图将文本复制到它认为是有效缓冲区的指针, 但实际上是指向错误地址空间的指针。
这正是你想要的。 一个完全合法的程序因为别人的bug而崩溃。 如果你幸运的话,程序员会在测试中发现这个bug, 但他们怎么知道问题出在哪里,因为他们的程序没有崩溃; 崩溃的是另一个程序!
如果你不幸运,这个bug会在测试中溜过去, (例如,它可能在一个很少执行的代码路径中) 而最终用户的体验是 “Microsoft Word随机崩溃。真是垃圾。” (实际上,崩溃是由另一个完全不同的程序引起的。)
为了避免这个问题,所有内置于窗口管理器的“遗留”消息都被移动到了系统消息类别。 这样,当你发送消息0x0189时,窗口管理器知道它是LB_GETTEXT
并且可以为你进行参数封送。 如果它留在WM_USER
范围, 窗口管理器在收到消息0x040A
时就不会知道该怎么做, 因为那可能是LB_GETTEXT
, 也可能是TTM_HITTESTA
或TBM_SETSEL
, 或者是其他一些控制特定消息。
理论上,这种移动只需要对遗留消息进行; 即,在16位Windows中存在窗口消息。 (注意Windows 95增加了一些新的16位消息, 所以这种重新映射至少需要持续到Windows NT 4的shell更新发布。) 尽管如此,窗口管理器团队甚至在系统消息范围内添加了*_GET*INFO
消息, 尽管从兼容性角度来看没有必要将它们放在那里。 我的猜测是,这样做是为了使辅助工具的工作更容易。
但请注意,将新消息放在系统消息范围是例外而不是规则, 对于编辑框和其他“核心”控件来说。 例如,新消息EM_SETCUEBANNER
的数值为0x1501
, 这已经很好地进入了WM_USER
范围。 如果你尝试在不采取必要预防措施的情况下跨进程发送此消息, 你将崩溃目标进程。
注: 标准免责声明适用。 我将不再在未来的文章中重复这个免责声明。