您正在查看: win32 分类下的文章

第27章 硬件输入模型和局部输入状态


第27章 硬件输入模型和局部输入状态

本章将讨论系统的硬件输入模型。重点将考察按键和鼠标事件是如何进入系统并发送给适当的窗口过程的。微软设计输入模型的一个主要目标就是为了保证一个线程的动作不要对其他线程的动作产生不好的影响。这里是一个1 6位Wi n d o w s中的例子:如果一个任务引起一个死循环,所有的任务都被挂起,不能再响应用户。用户只能重新启动计算机。这样就给一个单个的任务太多的控制。强壮的操作系统,例如Windows 2000和Windows 98,不会使一个挂起的线程妨碍系统中其他线程接收硬件输入。


27.1 原始输入线程

图2 7 - 1概括描述了系统的硬件输入模型。当系统初始化时,要建立一个特殊的线程,即原始输入线程(raw input thread,R I T)。此外,系统还要建立一个队列,称为系统硬件输入队列(System hardware input queue, SHIQ)。R I T和S H I Q构成系统硬件输入模型的核心。


图27-1 系统的硬件输入模型

那么R I T怎么才能知道要向哪一个线程的虚拟输入队列里增加硬件输入消息?对鼠标消息,R I T只是确定是哪一个窗口在鼠标光标之下。利用这个窗口, R I T调用G e t Wi n d o w T h r e a dP r o c e s s I d来确定是哪个线程建立了这个窗口。返回的线程I D指出哪一个线程应该得到这个鼠标消息。

对按键硬件事件的处理稍有不同。在任何给定的时刻,只有一个线程同R I T“连接”。这个线程称为前景线程(foreground thread),因为它建立了正在与用户交互的窗口,并且这个线程的窗口相对于其他线程所建立的窗口来说处在画面中的前景。

当一个用户在系统上登录时, Windows Explorer 进程让一个线程建立相应的任务栏(t a s k b a r)和桌面。这个线程连接到R I T。如果你又要产生C a l c u l a t o r,那么就又有一个线程来建立一个窗口,并且这个线程变成连接到R I T的线程。注意现在Windows Explorer的线程不再与R I T连接,因为在一个时刻只能有一个线程同R I T连接。当一个按键消息进入S H I Q时,R I T就被唤醒,将这个事件转换成适当的按键消息,并将消息放入与R I T连接的线程的虚拟输入队列。

不同的线程是如何连接到R I T的呢?我们已经说过,当产生一个进程时,这个进程的线程可以建立一个窗口。这个窗口处于前景,其建立窗口的线程同R I T相连接。另外, R I T还要负责处理特殊的键组合,如A l t + Ta b、A l t + E s c和C t r l + A l t + D e l等。因为R I T在内部处理这些键组合,就可以保证用户总能用键盘激活窗口。应用程序不能够拦截和废弃这些键组合。当用户按动了某个特殊的键组合时,R I T激活选定的窗口,并将窗口的线程连接到R I T。Wi n d o w s也提供激活窗口的功能,使窗口的线程连接到R I T。这些功能在本章后面讨论。

从图2 7 - 1中可以看到如何保护线程,避免相互影响的。如果R I T向窗口B1或窗口B2发送一个消息,消息到达线程B的虚拟输入队列。在处理消息时,线程B在与五个内核对象同步时可能会进入死循环或死锁。如果发生这种情况,线程仍然同R I T连接在一起,并且可能有更多的消息要增加到线程的虚拟输入队列中。

这种情况下,用户会发现窗口B 1和B 2都没有反应,可能想切换到窗口A1。为了做这种切换,用户按A l t + Ta b。因为是R I T处理A l t + Ta b按键组合,所以用户总能切换到另外的窗口,不会有什么问题。在选定窗口A1之后,线程A就连接到R I T。这个时候,用户就可以对窗口A1进入输入,尽管线程及其窗口都没有响应。



27.2 局部输入状态


• 哪一个窗口有鼠标捕获。

• 鼠标光标的形状。

• 鼠标光标的可见性。

由于每个线程都有自己的输入状态变量,每个线程都有不同的焦点窗口、鼠标捕获窗口等概念。从一个线程的角度来看,或者它的某个窗口拥有键盘焦点,或者系统中没有窗口拥有键盘焦点;或者它的某个窗口拥有鼠标捕获,或者系统中没有窗口拥有鼠标捕获,等等。读者会想到,这种隔离应该有一些细节,对此我们将在后面讨论。


27.2.1 键盘输入与焦点


我们已经知道,R I T使用户的键盘输入流向一个线程的虚拟输入队列,而不是流向一个窗口。R I T将键盘事件放入线程的虚拟输入队列时不用涉及具体的窗口。当这个线程调用G e t M e s s a g e时,键盘事件从队列中移出并分派给当前有输入焦点的窗口。(由该线程所建立)。图2 7 - 2说明了这个处理过程。


图27-2 RIT将用户的键盘输入导向一个线程的虚拟输入队列

线程1当前正在从R I T接收输入,用窗口A、窗口B或窗口C的句柄作参数调用S e t F o c u s会引起焦点改变。失去焦点的窗口除去它的焦点矩形或隐藏它的插入符号,获得焦点的窗口画出焦点矩形或显示它的插入符号。

假定线程1仍然从R I T接收输入,并用窗口E的句柄作为参数调用S e t F o c u s。这种情况下,系统阻止执行这个调用,因为想要设置焦点的窗口不使用当前连接R I T的虚拟输入队列。在线的线程不一样,那么,对于建立失去焦点窗口的线程,要更新它的局部输入状态变量,说明它没有窗口拥有焦点。这时调用G e t F o c u s将返回N U L L,这会使线程知道当前没有窗口拥有焦点。

函数S e t A c t i v e Wi n d o w激活系统中一个最高层( t o p - l e v e l)的窗口,并对这个窗口设定焦点:

HWND SetActiveWindow(HWND hwnd);
同S e t F o c u s函数一样,如果调用线程没有创建作为函数参数的窗口,则这个函数什么也不做。

与S e t A c t i v e Wi n d o w相配合的函数是G e t A c t i v e Wi n d o w函数:

HWND GetActiveWindow();
这个函数的功能同G e t F o c u s函数差不多,不同之处是它返回由调用线程的局部输入状态变量所指出的活动窗口的句柄。当活动窗口属于另外的线程时, G e t A c t i v e Wi n d o w返回N U L L。

其他可以改变窗口的Z序(Z - o r d e r)、活动状态和焦点状态的函数还包括B r i n g Wi n d o w ToTo p和S e t Wi n d o w P o s:

BOOL BringWindowToTop(HWND hwnd);

BOOL SetWindowPos(
HWND hwnd,
HWND hwndInsertAfter,
int x,
int y,
int cx,
int cy,
UINT fuFlags);


这两个函数功能相同(实际上, B r i n g Wi n d o w To To p函数在内部调用S e t Wi n d o w P o s,以H W N D _ TO P作为第二个参数)。如果调用这两个函数的线程没有连接到R I T,则函数什么也不做。如果调用这些函数的线程同R I T相连接,系统就会激活相应的窗口。注意即使调用线程不是建立这个窗口的线程,也同样有效。这意味着,这个窗口变成活动的,并且建立这个窗口的线程被连接到R I T。这也引起调用线程和新连接到R I T的线程的局部输入状态变量被更新。

有时候,一个线程想让它的窗口成为屏幕的前景。例如,有可能会利用Microsoft Qutlook安排一个会议。在会议开始前的半小时, O u t l o o k弹出一个对话框提醒用户会议将要开始。如果Q u t l o o k的线程没有连接到R I T,这个对话框就会藏在其他窗口的后面,有可能看不见它。因为了制止这种现象,微软对S e t F o r e g r o u n d Wi n d o w函数增加了更多的智能。特别规定,仅当调用一个函数的线程已经连接到R I T或者当前与R I T相连接的线程在一定的时间内(这个时间量由S y s t e m P a r a m e t e r s I n f o函数和S P I _ S E T F O R E G R O U N D _ L O C K T I M E O U T值来控制)没有收到任何输入,这个函数才有效。另外,如果有一个菜单是活动的,这个函数就失效。

如果不允许S e t F o r e g r o u n d Wi n d o w将窗口移到前景,它会闪烁该窗口的标题栏和任务条上该窗口的按钮。用户看到任务条按钮闪烁,就知道该窗口想得到用户的注意。用户应该手工激活这个窗口,看一看要报告什么信息。还可以用S y s t e m P a r a m e t e r s I n f o函数和S P I _ S E T F O R E G R O U N D -F L A S H C O U N T值来控制闪烁。

由于这些新的内容,系统又提供了另外一些函数。如果调用A l l o w S e t F o r e g r o u n d Wi n d o w的线程能够成功调用S e t F o r e g r o u n d Wi n d o w,第一个函数(见下面所列)可使指定进程的一个线程成功调用S e t F o r e g r o u n d Wi n d o w。为了使任何进程都可以在你的线程的窗口上弹出一个窗口,指定A S F W _ A N Y (定义为-1 )作为d w P r o c e s s I d参数:

BOOL AllowSetForegroundWindow(DWORD dwProcessId);
此外,线程可以锁定S e t F o r e g r o u n d Wi n d o w函数,使它总是失效的。方法是调用L o c kS e t F o r e g r o u n d Wi n d o w。

BOOL LockSetForegroundWindow(UINT uLockCode);
对u L o c k C o d e参数可以指定L S F W _ L O C K或者L S F W _ U N L O C K。当一个菜单被激活时,系统在内部调用这个函数,这样一个试图跳到前景的窗口就不能关闭这个菜单。Wi n d o w sE x p l o r e r在显示S t a r t菜单时,需要明确地调用这些函数,因为S t a r t菜单不是一个内置菜单。当用户按了A l t键或者将一个窗口拉到前景时,系统自动解锁S e t F o r e g r o u n d Wi n d o w函数。这可以防止一个程序一直对S e t F o r e g r o u n d Wi n d o w函数封锁。

关于键盘管理和局部输入状态,其他的内容是同步键状态数组。每个线程的局部输入状态变量都包含一个同步键状态数组,但所有的线程要共享一个同步键状态数组。这些数组反映了在任何给定时刻键盘所有键的状态。利用G e t A s y n c K e y S t a t e函数可以确定用户当前是否按下了键盘上的一个键:

SHORT GetAsyncKeyState(int nVirtKey);
参数n Vi r t K e y指出要检查键的虚键代码。结果的高位指出该键当前是否被按下(是为1,否为0)。笔者在处理一个消息时,常用这个函数来检查用户是否释放了鼠标主按钮。为函数参数赋一个虚键值V K _ L B U T TO N,并等待返回值的高位成为0。注意,如果调用函数的线程不是建立的窗口上,鼠标光标就可见了。

鼠标光标管理的另一个方面是使用C l i p C u r s o r函数将鼠标光标剪贴到一个矩形区域。

BOOL ClipCursor(CONST RECT *prc);
这个函数使鼠标被限制在一个由p r c参数指定的矩形区域内。当一个程序调用C l i p C u r s o r函数时,系统该做些什么呢?允许剪贴鼠标光标可能会对其他线程产生不利影响,而不允许剪贴鼠标光标又会影响调用线程。微软实现了一种折衷的方案。当一个线程调用这个函数时,系统将鼠标光标剪贴到指定的矩形区域。但是,如果同步激活事件发生(当用户点击了其他程序的窗口,调用了S e t F o r e g r o u n d Wi n d o w,或按了C t r l + E s c组合键),系统停止剪贴鼠标光标的移动,允许鼠标光标在整个屏幕上自由移动。

现在我们再讨论鼠标捕获。当一个窗口“捕获”鼠标(通过调用S e t C a p t u r e)时,它要求所有的鼠标消息从R I T发到调用线程的虚拟输入队列,并且所有的鼠标消息从虚拟输入队列发到设置捕获的窗口。在程序调用R e l e a s e C a p t u r e之前,要一直继续这种鼠标消息的捕获。

鼠标的捕获必须同系统的强壮性折衷,也只能是一种折衷。当一个程序调用S e t C a p t u r e时,R I T将所有鼠标消息放入线程的虚拟输入队列。S e t C a p t u r e还要为调用S e t C a p t u r e的线程设置局部输入状态变量。

通常一个程序在用户按一个鼠标按钮时调用S e t C a p t u r e。但是即使鼠标按钮没有被按下,也没有理由说一个线程不能调用S e t C a p t u r e。如果当一个鼠标按下时调用S e t C a p t u r e,捕获在全系统范围内执行。但当系统检测出没有鼠标按钮按下时, R I T不再将鼠标消息只发往线程的虚拟输入队列,而是将鼠标消息发往与鼠标光标所在的窗口相联系的输入队列。这是不做鼠标捕获时的正常行为。

但是,最初调用S e t C a p t u r e的线程仍然认为鼠标捕获有效。因此,每当鼠标处于有捕获设置的线程所建立的窗口时,鼠标消息将发往这个线程的捕获窗口。换言之,当用户释放了所有的鼠标按钮时,鼠标捕获不再在全系统范围内执行,而是在一个线程的局部范围内执行。

此外,如果用户想激活一个其他线程所建立的窗口,系统自动向设置捕获的线程发送鼠标按钮按下和鼠标按钮放开的消息。然后系统更新线程的局部输入状态变量,指出该线程不再具有鼠标捕获。很明显,通过这种实现方式,微软希望鼠标点击和拖动是使用鼠标捕获的最常见理由。



27.3 将虚拟输入队列同局部输入状态挂接在一起


从上面的讨论我们可以看出这个输入模型是强壮的,因为每个线程都有自己的局部输入状态环境,并且在必要时每个线程可以连接到R I T或从R I T断开。有时候,我们可能想让两个或多个线程共享一组局部输入状态变量及一个虚拟输入队列。

可以利用A t t a c h T h r e a d I n p u t函数来强制两个或多个线程共享同一个虚拟输入队列和一组局部输入状态变量:

BOOL AttachThreadInput(
DWORD idAttach,
DWORD idAttachTo,
BOOL fAttach);
函数的第一个参数i d A t t a c h,是一个线程的I D,该线程所包含的虚拟输入队列(以及局部输入状态变量)是你不想再使用的。第二个参数i d A t t a c h To,是另一个线程的I D,这个线程所包含的虚拟输入队列(和局部输入状态变量)是想让两个线程共享的。第三个参数f A t t a c h,当想让共享发生时,被设置为T R U E,当想把两个线程的虚拟输入队列和局部输入状态变量分开时,设定为FA L S E。可以通过多次调用A t t a c h T h r e a d I n p u t函数让多个线程共享同一个虚拟输入队列和局部输入状态变量。

我们再考虑前面的例子,假定线程A调用A t t a c h T h r e a d I n p u t,传递线程A的I D作为第一个参数,线程B的I D作为第二个参数,T R U E作为最后一个参数:

SHORT GetKeyState(int nVirtKey);
如图2 7 - 3所示,现在每个发往窗口A 1、窗口B 1或窗口B 2的硬件输入事件都将添加到线程B的虚拟输入队列中。线程A的虚拟输入队列将不再接收输入事件,除非再一次调用A t t a c h T h r e a d I n p u t并传递FA L S E作为最后一个参数,将两个线程的输入队列分开。


图2 7 - 3

当将两个线程的输入都挂接在一起时,就使线程共享单一的虚拟输入队列和同一组局部输入状态变量。但线程仍然使用自己的登记消息队列、发送消息队列、应答消息队列和唤醒标志(见第2 6章的讨论)。

如果让所有的线程都共享一个输入队列,就会严重削弱系统的强壮性。如果某一个线程接收一个按键消息并且挂起,其他的线程就不能接收任何输入了。所以应该尽量避免使用A t t a c h T h r e a d I n p u t函数。

在某些情况下,系统隐式地将两个线程挂接在一起。第一种情况是当一个线程安装一个日志记录挂钩(journal record hook)或日志播放挂钩(journal playback hook)的时候。当挂钩被卸载时,系统自动恢复所有线程,这样线程就可以使用挂钩安装前它们所使用的相同输入队列。

当一个线程安装一个日志记录挂钩时,它是让系统将用户输入的所有硬件事件都通知它。这个线程通常将这些信息保存或记录在一个文件上。因用户的输入必须按进入的次序来记录,所以系统中每个线程要共享一个虚拟输入队列,使所有的输入处理同步。

还有一些情况,系统会代替你隐式地调用A t t a c h T h r e a d I n p u t。假定你的程序建立了两个线程。第一个线程建立了一个对话框。在这个对话框建立之后,第二个线程调用G r e a t Wi n d o w,使用W S _ C H I L D风格,并向这个子窗口的双亲传递对话框的句柄。系统用子窗口的线程调用A t t a c h T h r e a d I n p u t,让子窗口的线程使用对话框线程所使用的输入队列。这样就使对话框的所有子窗口之间对输入强制同步。


27.3.1 LISLab 示例程序


L I S L a b程序(“2 7 L I S L a b . e x e”)清单列在清单2 7 - 1上。这是一个实验室,可以用它来实验局部输入状态。这个程序的源代码和资源文件在本书所附光盘的2 7 - L I S L a b目录下。

为了用局部输入状态做实验,需要两个线程作为实验品。L I S L a b进程有一个线程,这里选择N o t e p a d的线程作为另一个。如果当L I S L a b启动时N o t e p a d没有在运行,L I S L a b将启动N o t e p a d。在L I S L a b初始化之后,就会见到图2 7 - 4所示的对话框。这个窗口的左上角是Wi n d o w s编组框。


图2 7 - 4

LISLab 示例程序清单2 7 - 1

/******************************************************************************
Module:  LISLab.cpp
Notices: Copyright (c) 2000 Jeffrey Richter
******************************************************************************/#include "..\CmnHdr.h"  /* See Appendix A. */
#include <windowsx.h>
#include <tchar.h>
#include <string.h>
#include "Resource.h"///////////////////////////////////////////////////////////////////////////////#define TIMER_DELAY (500)  // Half a second

UINT_PTR g_uTimerId = 1;
intg_nEventId = 0;
DWORD g_dwEventTime = 0;
HWND g_hwndSubject = NULL;
HWND g_hwndNotepad = NULL;///////////////////////////////////////////////////////////////////////////////void CalcWndText(HWND hwnd, PTSTR szBuf, int nLen) {

TCHAR szClass[50], szCaption[50], szBufT[150];

if (hwnd == (HWND) NULL) {
_tcscpy(szBuf, TEXT("(no window)"));
return;
}if (!IsWindow(hwnd)) {
_tcscpy(szBuf, TEXT("(invalid window)"));
return;
}

GetClassName(hwnd, szClass, chDIMOF(szClass));
GetWindowText(hwnd, szCaption, chDIMOF(szCaption));

wsprintf(szBufT, TEXT("[%s] %s"), (PTSTR) szClass,
(*szCaption == 0) ? (PTSTR) TEXT("(no caption)") : (PTSTR) szCaption);
_tcsncpy(szBuf, szBufT, nLen - 1);
szBuf[nLen - 1] = 0; // Force zero-terminated string
}///////////////////////////////////////////////////////////////////////////////// To minimize stack use, one instance of WALKWINDOWTREEDATA
// is created as a local variable in WalkWindowTree() and a
// pointer to it is passed to WalkWindowTreeRecurse.

// Data used by WalkWindowTreeRecurse
typedef struct {
HWND hwndLB; // Handle to the output list box
HWND hwndParent; // Handle to the parent
intnLevel; // Nesting depth
intnIndex; // List box item index
TCHAR szBuf[100]; // Output buffer
intiBuf; // Index into szBuf
} WALKWINDOWTREEDATA, *PWALKWINDOWTREEDATA;void WalkWindowTreeRecurse(PWALKWINDOWTREEDATA pWWT) {

if (!IsWindow(pWWT->hwndParent))
return;

pWWT->nLevel++;
const int nIndexAmount = 2;

for (pWWT->iBuf = 0; pWWT->iBuf < pWWT->nLevel * nIndexAmount; pWWT->iBuf++)
pWWT->szBuf[pWWT->iBuf] = TEXT(' ');

CalcWndText(pWWT->hwndParent, &pWWT->szBuf[pWWT->iBuf],
chDIMOF(pWWT->szBuf) - pWWT->iBuf);
pWWT->nIndex = ListBox_AddString(pWWT->hwndLB, pWWT->szBuf);
ListBox_SetItemData(pWWT->hwndLB, pWWT->nIndex, pWWT->hwndParent);

HWND hwndChild = GetFirstChild(pWWT->hwndParent);
while (hwndChild != NULL) {
pWWT->hwndParent = hwndChild;
WalkWindowTreeRecurse(pWWT);
hwndChild = GetNextSibling(hwndChild);
}

pWWT->nLevel--;
}///////////////////////////////////////////////////////////////////////////////void WalkWindowTree(HWND hwndLB, HWND hwndParent) {

WALKWINDOWTREEDATA WWT;

WWT.hwndLB = hwndLB;
WWT.hwndParent = hwndParent;
WWT.nLevel = -1;

WalkWindowTreeRecurse(&WWT);
}///////////////////////////////////////////////////////////////////////////////BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) {

chSETDLGICONS(hwnd, IDI_LISLAB);

// Associate the Up arrow cursor with the dialog box's client area
SetClassLongPtr(hwnd, GCLP_HCURSOR,
(LONG_PTR) LoadCursor(NULL, IDC_UPARROW));

g_uTimerId = SetTimer(hwnd, g_uTimerId, TIMER_DELAY, NULL);

HWND hwndT = GetDlgitem(hwnd, IDC_WNDFUNC);
ComboBox_AddString(hwndT, TEXT("SetFocus"));
ComboBox_AddString(hwndT, TEXT("SetActiveWindow"));
ComboBox_AddString(hwndT, TEXT("SetForegroundWnd"));
ComboBox_AddString(hwndT, TEXT("BringWindowToTop"));
ComboBox_AddString(hwndT, TEXT("SetWindowPos-TOP"));
ComboBox_AddString(hwndT, TEXT("SetWindowPos-BTM"));
ComboBox_SetCurSel(hwndT, 0);

// Fill the windows list box with our window
hwndT = GetDlgitem(hwnd, IDC_WNDS);
ListBox_AddString(hwndT, TEXT("---> This dialog box

第七部分 附录


第七部分 附录


附录A 建立环境

读者要想建立本书中的示例程序,必须要对编译程序和链接程序的开关选项进行设置。笔者试图将这些设置方面的细节从示例程序中隔离出来,把所有这些设置放在一个头文件里。这个头文件就是C m m H d r. h,它包含在所有示例程序的源代码文件中。

因为无法将所有的设置都放在这个头文件里,我们对每个示例程序的项目设置做了一些改变。对每个项目,我们显示Project Settings对话框,然后做下面所说的改变。

• 在G e n e r a l栏,设定Output Files目录,这样所有最终的. e x e和. d l l文件都在一个目录之下。

• 在C / C + +栏,选择Code Generation 条目,并对Use Run-Time Library 字段选择Multithreaded DLL。

这样就可以了。我只明确改变了两个设置,而接受了其他所有的默认设置。注意要对每个项目的D e b u g建立和R e l e a s e建立都做上述两个改变。我可以在源代码中设定所有其他的编译程序和链接程序的设置,当你在你的项目中使用这里的任何源代码模块时,这些设置都将起作用。


A.1 CmmHdr.h头文件

所有的示例程序都要包含C m m H d r. h头文件,并且要在其他头文件之前包含。笔者编写的C m m H d r. h列在清单A - 1里。这个文件给笔者带来不少便利。这个文件包含宏、链接程序指令、还有一些其他所有示例程序公用的内容。当我想做某些实验时,我只需修改并重建( r e b u i l d)所有的示例程序。C m m H d r. h在所附光盘的根目录下。

这个附录的其余部分将分别讨论C m m H d r. h文件的每一节,解释每一节的基本原理,并描述在重建所有示例程序之前,如何及为什么要对这个文件进行修改。

A.1.1 Windows版本建立选项

因为有些示例程序调用了Microsoft Windows 2000中提供的新函数,本节定义_ W I N 3 2 _W I N N T符号如下:

#define _win32_WINNT 0x0500
这样做是因为新的Windows 2000函数在Wi n d o w s头文件中被定义成下面这样的原型:

#if (_win32_WINNT >= 0x0500)

WINBASEAPI
BOOL
WINAPI
AssignProcessToJobObject(
IN HANDLE hJob,
IN HANDLE hProcess
);

...

#endif /* _WIN32_WINNT >= 0x0500 */


除非像我这样专门定义_ W I N 3 2 _ W I N N T(在包含Wi n d o w s . h之前),否则这些新函数的原型就没有被声明,当试图调用这些函数时,编译程序将产生错误。微软用_ W I N 3 2 _ W I N N T符号来保护这些函数,以使程序员开发的应用程序能够运行在Windows 98及Windows NT的多个版本上。


A.1.2 Unicode建立选项


笔者编写的所有这些示例程序既可按A N S I来编译,也可按U n i c o d e来编译。当针对x 8 6 C P U体系结构来编译这些程序时, A N S I为默认选择,这样程序可以在Windows 98上执行。但对其他C P U体系结构建立程序就要用U n i c o d e,这样程序可以占用较少的内存,并且执行得更快。

为了对x 8 6体系结构建立U n i c o d e版本,只需将定义U N I C O D E的那一行代码的注释符去掉,并重建程序。通过在C m m H d r. h定义U N I C O D E宏,可以很容易地控制如何建立示例程序。关于U n i c o d e的详细内容,可参见第2章。


A.1.3 窗口定义和第4级警告


笔者在开发软件时,总是想保证代码的编译不受错误和警告的限制。我还喜欢在可能最高警告级上进行编译,这样编译程序可以替我做大多数工作,甚至为我检查很小的细节。对于Microsoft C/C++编译程序,这将意味着我要使用第4级警告来建立示例程序。

遗憾的是,微软的操作系统开发部在关于使用第4级警告做编译方面,与我没有共同的思想。其结果,当我使用第4级警告编译示例程序时,Wi n d o w s头文件中的许多行引起编译器产生警告。幸好,这些警告并不表示代码中有问题。大多数情况是由于c语言中非传统的用法所引起的,这些用法依赖编译程序的扩展,几乎所有与Wi n d o w s兼容的编译程序厂商都实现了这些扩展。

本节我确保警告级设定为3,而且C m m H d r. h包含标准的Wi n d o w s . h头文件。当包含了Wi n d o w s . h时,在我编译其余代码时就设置第4级警告。在第4级警告上,编译程序对那些我不认为有问题的内容发出“警告”,这样我通过使用#pragma warning指令显式地告诉编译程序忽略某些良性的警告错。


A.1.4 Pragma消息帮助宏


在我编写代码时,我喜欢让代码的某些部分能够立即运行起来,然后再完善它。为了提醒自己要特别注意某些代码,我习惯于加入下面这样一行代码:

#pragma message("Fix this later")
当编译程序对这一行进行编译时,它会输出一个字符串提醒我还需要再做一些工作。但这条消息不怎么有用。我决定寻找一种办法,让编译程序输出源代码文件的名字,以及p r a g m a出现的行号。这样,我不光知道要做一些工作,而且能够立刻确定在什么地方做。

为了达到这个目的,需要使用一系列宏来修饰pragma message指令。可以这样使用c h M S G宏。

#pragma chMSG(Fix this later)
当编译程序编译上面这一行代码时,会产生这样一行内容:

使用Microsoft Visual Developer Studio,在输出窗口上双击这一行,将会自动定位到相应文件的确切位置上。

C:\CD\CmnHdr.h(82):Fix this later
还有一个方便之处, c h M S G宏不要求对文本串使用引号。

A.1.5 chINRANGE和chDIMOF宏

我时常在编写程序时使用这两个方便有用的宏。第一个宏c h I N R A N G E,用来查看一个数值是否在另外两个数值之间。第二个宏c h D I M O F,只是返回一个数组中元素的数目。这个宏是用s i z e o f操作符先计算整个数组的字节数,然后再用这个数除以数组中一个数据项所占的字节数,从而得出结果。

A.1.6 chBEGINTHREADEX宏

本书中的所有多线程示例程序都使用了微软的C/C + +运行时函数库中的_ b e g i n t h r e a d e x函数,而不是操作系统的C r e a t e T h r e a d函数。我使用这个函数是因为_ b e g i n t h r e a d e x函数为新线程做好了准备,使新线程能够使用C / C + +运行时函数库中的函数,而且还因为它保证在线程返回时清除每个线程的C / C + +运行时库信息(见第6章有关细节)。但遗憾的是_ b e g i n t h r e a d e x函数的原型是这样的。

tunately, the _beginthreadex function is proto

unsigned long __cdecl _beginthreadex(
void *,
unsigned,
unsigned (__stdcall *)(void *),
void *,
unsigned,
unsigned *);


尽管_ b e g i n t h r e a d e x函数用的参数值同C r e a t e T h r e a d函数用的参数值是一样的,但二者的参数的数据类型都不相匹配。C r e a t e T h r e a d函数的原型是这样的:

typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(PVOID pvParam);

HANDLE CreateThread(
PSECURITY_ATTRIBUTES psa,
DWORD cbStack,
PTHREAD_START_ROUTINE pfnStartAddr,
PVOID pvParam,
DWORD fdwCreate,
PDWORD pdwThreadId);


微软在建立_ b e g i n t h r e a d e x函数的原型时没有使用Wi n d o w s数据类型。这是因为微软的C / C + +运行时库开发组不想对操作系统开发组有任何依赖。这使得_ b e g i n t h r e a d e x函数的使用更加困难。

微软定义_ b e g i n t h r e a d e x函数原型的方式实际上存在着两个问题。首先,用于这个函数的一些数据类型同用于C r e a t e T h r e a d函数的原始类型不相匹配。例如Wi n d o w s数据类型D W O R D的定义是这样的:

typedef unsigned long DWORD;
这个数据类型用于C r e a t e T h r e a d函数的c b S t a c k参数以及f d w C r e a t e参数。问题是函数_ b e g i n t h r e a d e x将这两个参数的原型定义为u n s i g n e d,实际意思是unsigned int。编译程序将unsigned int看成是与unsigned long不同的东西,并且产生一个警告。_ b e g i n t h r e a d e x函数不属于标准的C / C + +运行时函数库,只是作为调用C r e a t e T h r e a d函数的替代手段而存在,所以微软应该按下面的形式来定义_ b e g i n t h r e a d e x的原型,这样就不会产生警告了:

unsigned long __cdecl _beginthreadex(
void *psa,
unsigned long cbStack,
unsigned (__stdcall *) (void *pvParam),
void *pvParam,
unsigned long fdwCreate,
unsigned long *pdwThreadId);
第二个问题是第一个问题的一个小变种。_ b e g i n t h r e a d e x函数返回一个unsigned long型的值,代表新建立线程的句柄。程序中通常用H A N D L E型数据变量来保存这个返回值:

HANDLE hThread = _beginthreadex(...);
上面这行代码又使编译程序产生另一个警告错。为了避免编译程序警告,必须改写这一行代码,引入一个转换(c a s t):

HANDLE hThread = (HANDLE) _beginthreadex(...);
这又是一个不方便之处。为了方便起见,我在C m n H d r. h中定义了一个c h B E G I N T H RE A D E X宏,替我执行所有这些转换:

typedef unsigned (__stdcall *PTHREAD_START) (void *);

#define chBEGINTHREADEX(psa, cbStack, pfnStartAddr,
pvParam, fdwCreate, pdwThreadId)
((HANDLE)_beginthreadex(
(void *) (psa),
(unsigned)(cbStack),
(PTHREAD_START) (pfnStartAddr),
(void *) (pvParam),
(unsigned)(fdwCreate),
(unsigned?) (pdwThreadId)))

A.1.7 对x86平台的调试断点改进


即使进程没有在一个调试程序下运行,有时候我也想在我的程序代码中强制一个断点。在Wi n d o w s中要做这件事,可以让线程调用D e b u g B r e a k函数。这个函数在k e r n e l 3 2 . d l l中,可以使一个调试程序同进程挂接。当调试程序被挂接上时,指令指针就定位在引起断点的C P U指令上。这个指令包含在k e r n e l 3 2 . d l l中的D e b u g B r e a k函数里,所以为了看到我的源代码,我必须在D e b u g B r e a k函数之外单步执行。

在x 8 6体系结构上,通过执行“i n t 3”C P U指令来做一个断点。所以,在x 8 6平台之上,我定义D e b u g B r e a k作为这个内联的汇编语言指令。当我的D e b u g B r e a k执行时,我不是在k e r n e l 3 2 . d l l中调用。断点发生在我的代码中,指令指针定位在下一个C/C++语句中。这样就方便多了。


A.1.8 建立软件异常代码


当处理软件异常时,必须建立你自己的3 2位异常代码。这些代码遵循特定的格式(见第2 4章的讨论)。为了更容易地建立这些代码,我使用M A K E S O F T WA R E E X C E P T I O N宏。


A.1.9 chMB宏


c h M B宏只是显示一个消息框。消息框的标题是调用进程可执行代码的全路径名。


A.1.10 chASSERT和chVERIFY宏


在我开发这些示例程序时,为了查找潜在的问题,我在整个代码中多处使用c h A S S E RT宏。这个宏测试由x所标识的表达式是否为T R U E,如果不是,则显示一个消息框指出失败的文件、行和表达式。在程序的发行建立中,这个宏什么也不做。c h V E R I F Y宏与c h A S S E RT宏差不多,区别在于不论是调试建立(debug build)还是发行建立(release build),c h V E R I F Y都要对表达式进行测试。


A . 1 . 11 chHANDLE_DLGMSG宏


当你通过对话框使用消息分流器时,不应该使用微软的Wi n d o w s X . h 头文件中的H A N D L E _ M S G宏,因为这个宏并不能返回T R U E或FA L S E来指出消息是否由对话框的过程来处理。我定义的c h H A N D L E _ D L G M S G宏会通知窗口消息的返回值,适当地处理返回值,以便在一个对话框过程中使用。


A.1.12 chSETDLGICONS宏


由于多数示例程序使用一个对话框作为主窗口,你必须手工改变对话框图标,以便让它正确地显示在Ta s k b a r(任务条)、任务切换窗口和程序本身的标题上。当对话框接收到一个W M _ I N I T D I A L O G消息时,总要调用c h S E T D L G I C O N S宏,以正确设置图标。


A.1.13 OS版本检查内联函数


本书的大多数示例程序可运行在所有平台上,但也有一些程序要求一些Windows 95和Windows 98所不支持的特性,有些程序要求一些只在Windows 2000中提供的特性。每个程序在初始化时要检查宿主系统的版本,如果要求更适用的操作系统时,就显示一个通知。

对那些不能在Windows 95和Windows 98上运行的程序,你会看到,在程序的_ t Wi n M a i n函数中有一个对Wi n d o w s 9 x N o t A l l o w e d函数的调用。对于要求Windows 2000的示例程序,你会看到在程序的_ t Wi n M a i n函中有一个对c h Wi n d o w s 2 0 0 0 R e q u i r e d函数的调用。


A.1.14 确认宿主系统是否支持Unicode


Windows 98不能像Windows 2000那样完全支持U n i c o d e。实际上,调用U n i c o d e函数的程序不能在Windows 98上运行。但遗憾的是,如果调用一个为U n i c o d e编译的程序,Wi n d o w s 9 8不会给出任何通知信息。对本书中的程序,这意味着这些程序从开始到结束,都不会有它们想执行的提示信息。

这确实是一个难题。我需要有一种办法能够知道我的程序是对U n i c o d e建立的,但可能在Windows 98系统上运行。所以我建立了一个CUnicodeSupported C++类。这个类的构造函数只是检查宿主系统是不是对U n i c o d e有良好的支持,如果不是,就显示一个消息框,并且进程结束。

读者会看到在C m n H d r. h中,我建立了这个类的一个全局的静态实例。当我的程序启动时,C / C + +运行时库启动代码调用这个对象的构造函数。如果这个构造函数检测到操作系统完全支持U n i c o d e,构造函数返回而程序继续执行。通过建立这个类的全局实例,我不需要在每个示例程序的源代码模块中再增加特殊的代码。对于非U n i c o d e的程序建立,不需要声明或实例化上述的C + +类。让程序只管运行就是。


A.1.15 强制链接程序寻找(w)WinMain进入点函数


本书以前版本的一些读者,将书中我的源代码模块添加到他们自己的Vi s u a l C + +项目中,但在建立项目时出现链接错误。问题的原因是他们创建了Win32 Console Application项目,导致链接程序去寻找( w ) m a i n进入点函数。因为本书中所有示例程序都是G U I程序,所以我的代码都有一个_ t Wi n M a i n进入点函数。这就是链接程序为什么要报错。

我的回答是,他们应该删除原来的项目,用Visual C++建立新的Win32 Application项目(注意在项目类型中不能出现“ C o n s o l e”一词),再将我的源代码加进去。链接程序寻找一个( w ) Wi n M a i n进入点函数,而这在我的代码中已提供,项目应该能够建立。

为了减少我收到的有关这个问题的电子邮件的数量,我在C m n H d r. h中加入了一个p r a g m a,强制链接程序去寻找( w ) Wi n M a i n进入点函数,即使是用Visual C++建立了一个Win32 ConsoleA p p l i c a t i o n项目。

在第4章,我详细说明了Visual C++项目类型的有关内容,链接程序如何选择进入点函数,及如何重载链接程序的默认动作等。下面的清单A - 1是Cmn Hdr. h 头文件。

清单A-1 CmnHdr. h头文件

/******************************************************************************
Module:  CmnHdr.h
Notices: Copyright (c) 2000 Jeffrey Richter
Purpose: Common header file containing handy macros and definitions
used throughout all the applications in the book.
See Appendix A.
******************************************************************************/#pragma once// Include this header file once per compilation unit//////////////////////// Windows Version Build Option /////////////////////////#define _WIN32_WINNT 0x0500
//#define WINVER 0x0500//////////////////////////// Unicode Build Option /////////////////////////////// If we are not compiling for an x86 CPU, we always compile using Unicode.
#ifndef _M_IX86
#define UNICODE
#endif

// To compile using Unicode on the x86 CPU, uncomment the line below.
//#define UNICODE

// When using Unicode Windows functions, use Unicode C-Runtime functions too.
#ifdef UNICODE
#define _UNICODE
#endif///////////////////////// Include Windows Definitions /////////////////////////#pragma warning(push, 3)
#include <Windows.h>
#pragma warning(pop)
#pragma warning(push, 4)///////////// Verify that the proper header files are being used //////////////#ifndef WT_EXECUTEINPERSISTENTIOTHREAD
#pragma message("You are not using the latest Platform SDK header/library ")
#pragma message("files. This may prevent the project from building correctly.")
#endif////////////// Allow code to compile cleanly at warning level 4 ////////////////* nonstandard extension 'single line comment' was used */
#pragma warning(disable:4001)

// unreferenced formal parameter
#pragma warning(disable:4100)

// Note: Creating precompiled header
#pragma warning(disable:4699)

// function not inlined
#pragma warning(disable:4710)

// unreferenced inline function has been removed
#pragma warning(disable:4514)

// assignment operator could not be generated
#pragma warning(disable:4512)///////////////////////// Pragma message helper macro //////////////////////////*
When the compiler sees a line like this:
#pragma chMSG(Fix this later)

it outputs a line like this: c:\CD\CmnHdr.h(82):Fix this later

You can easily jump directly to this line and examine the surrounding code.
*/

#define chSTR2(x) #x
#define chSTR(x) chSTR2(x)
#define chMSG(desc) message(FILE "(" chSTR(LINE) "):" #desc)////////////////////////////// chINRANGE Macro ////////////////////////////////// This macro returns TRUE if a number is between two others
#define chINRANGE(low, Num, High) (((low) <= (Num)) && ((Num) <= (High)))//////////////////////////////// chDIMOF Macro ////////////////////////////////// This macro evaluates to the number of elements in an array.
#define chDIMOF(Array) (sizeof(Array) / sizeof(Array[

#define chBEGINTHREADEX(psa, cbStack, pfnStartAddr,
pvParam, fdwCreate, pdwThreadId)
((HANDLE)_beginthreadex(
(void *) (psa),
(unsigned)(cbStack),
(PTHREAD_START) (pfnStartAddr),
(void *) (pvParam),
(unsigned)(fdwCreate),
(unsigned *) (pdwThreadId)))////////////////// DebugBreak Improvement for x86 platforms ///////////////////#ifdef X86
#define DebugBreak() _asm { int 3 }
#endif/////////////////////////// Software Exception Macro //////////////////////////// Useful macro for creating your own software exception codes
#define MAKESOFTWAREEXCEPTION(Severity, Facility, Exception)
((DWORD) (
/* Severity code / (Severity ) |
/
MS(0) or Cust(1) / (1<< 29) |
/
Reserved(0)/ (0<< 28) |
/
Facility code / (Facility << 16) |
/
Exception code*/ (Exception << 0)))/////////////////////////// Quick MessageBox Macro ////////////////////////////inline void chMB(PCSTR s) {
char szTMP[128];
GetModuleFileNameA(NULL, szTMP, chDIMOF(szTMP));
MessageBoxA(GetActiveWindow(), s, szTMP, MB_OK);
}//////////////////////////// Assert/Verify Macros /////////////////////////////inline void chFAIL(PSTR szMsg) {
chMB(szMsg);
DebugBreak();
}// Put up an assertion failure message box.
inline void chASSERTFAIL(LPCSTR file, int line, PCSTR expr) {
char sz[128];
wsprintfA(sz, "File %s, line %d : %s", file, line, expr);
chFAIL(sz);
}// Put up a message box if an assertion fails in a debug build.
#ifdef _DEBUG
#define chASSERT(x) if (!(x)) chASSERTFAIL(FILE, LINE, #x)
#else
#define chASSERT(x)
#endif// Assert in debug builds, but don't remove the code in retail builds.
#ifdef DEBUG
#define chVERIFY(x) chASSERT(x)
#else
#define chVERIFY(x) (x)
#endif/////////////////////////// chHANDLE_DLGMSG Macro /////////////////////////////// The normal HANDLE_MSG macro in WindowsX.h does not work properly for dialog
// boxes because DlgProc return a BOOL instead of an LRESULT (like
// WndProcs). This chHANDLE_DLGMSG macro corrects the problem:
#define chHANDLE_DLGMSG(hwnd, message, fn)
case (message): return (SetDlgMsgResult(hwnd, uMsg,
HANDLE
##message((hwnd), (wParam), (lParam), (fn))))//////////////////////// Dialog Box Icon Setting Macro ////////////////////////// Sets the dialog box icons
inline void chSETDLGICONS(HWND hwnd, int idi) {
SendMessage(hwnd, WM_SETICON, TRUE, (LPARAM)
LoadIcon((HINSTANCE) GetWindowLongPtr(hwnd, GWLP_HINSTANCE),
MAKEINTRESOURCE(idi)));
SendMessage(hwnd, WM_SETICON, FALSE, (LPARAM)
LoadIcon((HINSTANCE) GetWindowLongPtr(hwnd, GWLP_HINSTANCE),
MAKEINTRESOURCE(idi)));
}
/////////////////////////// OS Version Check Macros ///////////////////////////inline void chWindows9xNotAllowed() {
OSVERSIONINFO vi = { sizeof(vi) };
GetVersionEx(&vi);
if (vi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS) {
chMB("This application requires features not present in Windows 9x.");
ExitProcess(0);
}
}inline void chWindows2000Required() {
OSVERSIONINFO vi = { sizeof(vi) };
GetVersionEx(&vi);
if ((vi.dwPlatformId != VER_PLATFORM_WIN32_NT) && (vi.dwMajorVersion < 5)) {
chMB("This application requires features present in Windows 2000.");
ExitProcess(0);
}
}///////////////////////////// UNICODE Check Macro /////////////////////////////// Since Windows 98 does not support Unicode, issue an error and terminate
// the process if this is a native Unicode build running on Windows 98

// This is accomplished by creating a global C++ object. Its constructor is
// executed before WinMain.

#ifdef UNICODE

class CUnicodeSupported {
public:
CUnicodeSupported() {
if (GetWindowsDirectoryW(NULL, 0) <= 0) {
chMB("This application requires an OS that supports Unicode.");
ExitProcess(0);
}
}
};

// "static" stops the linker from complaining that multiple instances of the
// object exist when a single project contains multiple source files.
static CUnicodeSupported g_UnicodeSupported;

#endif/////////////////////////// Force Windows subsystem ///////////////////////////#pragma comment(linker, "/subsystem:Windows")///////////////////////////////// End of File /////////////////////////////////


出品,一剑[  

附录B 消息分流器、子控件宏和API宏


附录B 消息分流器、子控件宏和API宏

每当我参加一些会议时,常问一些人是不是使用消息分流器,而回答通常是“ N o”。我再进一步深究这件事,发现很多人不知道消息分流器是干什么用的,甚至没有听说过它。在本书中,通过使用带有消息分流器的C / C + +编写示例代码,我想向大家介绍这种不大为人所知但很有用的宏。

消息分流器定义在Microsoft Visual C++中提供的Wi n d o w s X . h文件里。通常在Wi n d o w s . h文件之后紧接着包含这个文件。Wi n d o w s X . . h文件就是一组# d e f i n e指令,建立了一组供我们使用的宏。Wi n d o w s X . h的宏实际上分为三组:消息分流器、子控件宏和A P I宏。这些宏以下述的方式为我们提供帮助:

• 利用这些宏可以减少程序中要做的转换( c a s t i n g)的数量,并可使所要求的转换是无错误的。使用C / C + +的Wi n d o w s编程中一个大的问题是所要求的转换数量。你很难看到一个不要求某种转换的Wi n d o w s函数调用。但应该尽量避免使用转换,因为转换阻碍编译器发现代码中的潜在错误。一个转换是在告诉编译程序:“我知道我在这里传递了错误的转换,但就要这样做。我知道我在干什么。”当你做了许多转换时,就很容易出错。编译程序应该尽可能对此提供帮助。

• 使代码的可读性更好。

• 可简化1 6位Wi n d o w s、3 2位Wi n d o w s和6 4位Wi n d o w s之间的代码移植工作。

• 易于理解(只是一些宏)

• 这些宏容易结合到已有的代码中。可以不管老的代码而立即在新的代码中使用这些宏。不必修改整个程序。

• 在C和C + +代码中都可以使用这些宏,尽管当使用C + +类时它们不是必需的。

• 如果需要某一个特性,而这些宏不直接支持这个特性,可以根据这个头文件中的宏,很容易地编写自己的宏。

• 不需要参照或记住费解的Wi n d o w s构造。例如,许多Wi n d o w s中的函数,要求一个l o n g型参数,其中这个长参数的高字( h i g h - w o r d)的值代表一个东西,而其低字( l o w - w o r d)又代表另一个东西。在调用这个函数之前,你必须用两个单独的值构造一个l o n g型值。通常利用Wi n D e f . h中的M A K E L O N G宏来做这种事。我简直记不清有多少次把两个值的次序给弄反了,造成对函数传递了一个错误的值。而Wi n d o w s X . h中的宏可以帮我们的忙。


B.1 消息分流器

消息分流器(message cracker)使窗口过程的编写更加容易。通常,窗口过程是用一个大的s w i t c h语句实现的。在我的经验中,我见过有的窗口过程的s w i t c h语句包含5百多行代码。我们都知道按这种方式实现窗口过程是一种坏的习惯,但我们都这么做过。而利用消息分流器可将s w i t c h语句分成小的函数,每个窗口消息对应一个函数。这样使代码更容易管理。

有关窗口过程的另一个问题是每个消息都有w P a r a m和l P a r a m参数,并且根据消息的不同,这些参数的意思也不同。在某些情况下,如对W M _ C O M M A N D消息,w P a r a m包含两个不同的值。w P a r a m参数的高字是通知码,而低字是控件的I D。或者是反过来?我总是忘了次序。如果使用消息分流器,就不用记住或查阅这些内容。消息分流器之所以这样命名,是因为它们对任何给定的消息进行分流。为了处理W M _ C O M M A N D消息,你只需编写这样一个函数:

void Cls_OnCommand(HWND hwnd, int id, HWND hwndCtl,
UINT codeNotify) 
{
switch(id) 
{
case ID_SOMELISTBOX:
if(codeNotify != LBN_SELCHANGE)
break;

// Do LBN_SELCHANGE processing.
break;

case ID_SOMEBUTTON:
break;
}
}


这是多么容易!分流器查看消息的w P a r a m和l P a r a m参数,将参数分开,并调用你的函数。

为了使用消息分流器,必须对你的窗口过程的s w i t c h语句做一些修改。看一看下面的窗口过程:

LRESULT WndProc(HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam) 
{
switch(uMsg) 
{
HANDLE_MSG(hwnd, WM_COMMAND, Cls_OnCommand);
HANDLE_MSG(hwnd, WM_PAINT,Cls_OnPaint);
HANDLE_MSG(hwnd, WM_DESTROY, Cls_OnDestroy);
default:
return(DefWindowProc(hwnd, uMsg, wParam, lParam));
}
}
H A N D L E _ M S G宏在Wi n d o w s X . h中是这样定义的:

#define HANDLE_MSG(hwnd, message, fn) \
case(message): \
return HANDLE_##message((hwnd), (wParam), (lParam), (fn));
对于W M _ C O M M A N D消息,预处理程序把这一行代码扩展成下面的代码:

case(WM_COMMAND):
return HANDLE_WM_COMMAND((hwnd),(wParam), (lParam),
(Cls_OnCommand));
定义在WindowsX.h 中的各H A N D L E _ W M _ *宏是实际的消息分流器。它们分流w P a r a m参数和l P a r a m参数,执行所有必要的转换,并调用适当的消息函数,如前面例举过的C l s _O n C o m m a n d函数。H A N D L E _ W M _ C O M M A N D宏的定义如下:

#define HANDLE_WM_COMMAND(hwnd, wParam, lParam, fn) \
( (fn) ((hwnd), (int) (LOWORD(wParam)), (HWND)(lParam),
(UINT) HIWORD(wParam)), 0L)
当预处理程序扩展这个宏时,其结果是用w P a r a m和l P a r a m参数的内容分流成各自的部分并经适当转换,来调用C l s _ O n C o m m a n d函数。

在使用消息分流器来处理一个消息之前,应该打开Wi n d o w s X . h文件并搜索要处理的消息。例如,如果搜索W M _ C O M M A N D,将会找到文件中包含下面代码行的部分:

/* void Cls_OnCommand(HWND hwnd, int id, HWND hwndCtl,
UINT codeNotify); */
#define HANDLE_WM_COMMAND(hwnd, wParam, lParam, fn) \
((fn)((hwnd), (int)(LOWORD(wParam)), (HWND)(lParam), \
(UINT)HIWORD(wParam)), 0L)
#define FORWARD_WM_COMMAND(hwnd, id, hwndCtl, codeNotify, fn) \
(void)(fn)((hwnd), WM_COMMAND, \
MAKEWPARAM((UINT)(id),(UINT)(codeNotify)), \
(LPARAM)(HWND)(hwndCtl))
第一行是注释行,展示要编写的函数原型。下一行是H A N D L E _ W M _ *宏,我们已经讨论过。最后一行是消息转发器( f o r w a r d e r)。假定在你处理W M _ C O M M A N D消息时,你想调用默认的窗口过程,并让它为你做事。这个函数应该是这个样子:

void Cls_OnCommand (HWND hwnd, int id, HWND hwndCtl,
UINT codeNotify) 
{
// Do some normal processing.

// Do default processing.
FORWARD_WM_COMMAND(hwnd, id, hwndCtl, codeNotify,
DefWindowProc);
}


F O RWA R D _ W M _ *宏将分流开的消息参数重新构造成等价的w P a r a m和l P a r a m。然后这个宏再调用你提供的函数。在上面的例子中,宏调用D e f Wi n d o w P r o c函数,但你可以简单地使用S e n d M e s s a g e或P o s t M e s s a g e。实际上,如果你想发送(或登记)一个消息到系统中的任何窗口,可以使用一个F O RWA R D _ W M _ *宏来帮助合并各个参数。


B.2 子控件宏

子控件宏(Child Control Macro)使发送消息到子控件变得更加容易。这些宏同F O RWA R O _W M _ *宏很相似。每个宏的定义以一个控件类型开始(这个控件是要对它发送消息的控件),后面跟一个下横线和消息名。例如,向一个列表框发送一个L B _ G E T C O U N T消息,就使用Wi n d o w s X . h中的这个宏:

#define ListBox_GetCount(hwndCtl) 
((int)(DWORD)SendMessage((hwndCtl), LB_GETCOUNT, 0, 0L))

关于这个宏我想说两件事。第一,它只用一个参数h w n d C t l,这是列表框的窗口句柄。因为L B _ G E T C O U N T消息忽略了w P a r a m和l P a r a m参数,你不必再管这些参数。你可以看到,宏只传递了零。第二,当S e n d M e s s a g e返回时,结果被转换成i n t,所以你不必提供你自己的转换。


关于子控件宏,有一件事我不喜欢,就是这些宏要用控件窗口的句柄。许多时候,你要发送消息到一个控件,而这个控件是一个对话框的子控件。所以最终你总要调用G e t D l g I t e m,产生这样的代码:

int n = ListBox_GetCount(GetDlgitem(hDlg, ID_LISTBOX));

比起使用S e n d D l g I t e m M e s s a g e,这个代码的运行虽然不慢,但你的程序会包含一些额外的代码。这是由于对G e t D l g I t e m的额外调用。如果需要对同一控件发送几个消息,你可能想调用一次G e t D l g I t e m,保存子窗口的句柄,然后调用你需要的所有的宏,见下面的代码:

HWND hwndCtl = GetDlgitem(hDlg, ID_LISTBOX);
int n = ListBox_GetCount(hwndCtl);
ListBox_AddString(hwndCtl, "Another string");

如果按这种方式设计你的代码,你的程序会运行得更快,因为这样就不会反复地调用G e t D l g I t e m。如果你的对话框有许多控件并且你要寻找的控件在Z序的结尾,则G e t D l g I t e m可能是个很慢的函数。


B.3 API宏

A P I宏可以简化某些常用的操作,如建立一种新字体,选择字体到设备环境,保存原来字体的句柄。代码的形式如下:

HFONT hfontOrig = (HFONT) SelectObject(hdc, (HGDIOBJ) hfontNew);

这个语句要求两个转换以得到没有编译警告错误的编译。在Wi n d o w s X . h中有一个宏,正是为了这个用途而设计:

#define SelectFont(hdc, hfont) 
((HFONT) SelectObject( (hdc), (HGDIOBJ) (HFONT) (hfont)))

如果你使用这个宏,你的程序中的代码行就变成:

HFONT hfontOrig = SelectFont(hdc, hfontNew);
这行代码更容易读,也不容易出错。

在Wi n d o w s X . h中还有其他一些A P I宏,有助于常用的Wi n d o w s任务。建议读者了解并使用这些宏。



出品,一剑[  

第六部分 窗口


第六部分 窗口


第26章 窗口消息

本章介绍Microsoft Wi n d o w s的消息系统是如何支持带有图形用户界面的应用程序的。在设计Windows 2000或Windows 98所使用的窗口系统时,微软有两个主要目标:

• 尽可能保持与过去1 6位Wi n d o w s的兼容性,便于开发人员移植他们已有的1 6位Wi n d o w s程序。

• 使窗口系统强壮,一个线程不会对系统中其他线程产生不利影响。

但是,这两个目标是直接相互冲突的。在1 6位Wi n d o w s系统中,向窗口发送一个消息总是按同步方式执行的:发送程序要在接受消息的窗口完全处理完消息之后才能继续运行。这通常是一个所期望的特性。但是,如果接收消息的窗口花很长的时间来处理消息或者出现挂起,则发送程序就不能再执行。这意味着系统是不强壮的。

这种冲突给微软的设计人员带来了一定的困难。他们的解决方案是两个相互冲突目标之间的出色折衷方案。如果在阅读本章时记住这两个目标,你就会更多地理解微软为什么会做出这样的设计。

我们从一些基本原则开始讨论。Wi n d o w s允许一个进程至多建立10 000个不同类型的用户对象(User object):图符、光标、窗口类、菜单、加速键表等等。当一个线程调用一个函数来建立某个对象时,则该对象就归这个线程的进程所拥有。这样,当进程结束时,如果没有明确删除这个对象,则操作系统会自动删除这个对象。对窗口和挂钩( h o o k )这两种U s e r对象,它们分别由建立窗口和安装挂钩的线程所拥有。如果一个线程建立一个窗口或安装一个挂钩,然后线程结束,操作系统会自动删除窗口或卸载挂钩。

这种线程拥有关系的概念对窗口有重要的意义:建立窗口的线程必须是为窗口处理所有消息的线程。为了使这个概念更加明确具体,可以想像一个线程建立了一个窗口,然后就结束了。在这种情况下,窗口不会收到一个W M _ D E S T R O Y或W M _ N C D E S T R O Y消息,因为线程已经结束,不可能被用来使窗口接收和处理这些消息。

这也意味着每个线程,如果它至少建立了一个窗口,都由系统对它分配一个消息队列。这个队列用于窗口消息的派送( d i s p a t c h)。为了使窗口接收这些消息,线程必须有它自己的消息循环。本章要考查每个线程的消息队列。特别是要看看消息是如何被放置在队列中的,以及线程如何从队列中取出消息并处理它们。



26.1 线程的消息队列


前面已经说过,Wi n d o w s的一个主要目标是为程序的运行提供一个强壮的环境。为实现这个目标,要保证每个线程运行在一个环境中,在这个环境中每个线程都相信自己是唯一运行的线程。更确切地说,每个线程必须有完全不受其他线程影响的消息队列。而且,每个线程必须有一个模拟环境,使线程可以维持它自己的键盘焦点(keyboard focus)、窗口激活、鼠标捕获等概念。

当一个线程第一次被建立时,系统假定线程不会被用于任何与用户相关的任务。这样可以减少线程对系统资源的要求。但是,一旦这个线程调用一个与图形用户界面有关的函数(例如检查它的消息队列或建立一个窗口),系统就会为该线程分配一些另外的资源,以便它能够执行与用户界面有关的任务。特别是,系统分配一个T H R E A D I N F O结构,并将这个数据结构与线程联系起来。

这个T H R E A D I N F O结构包含一组成员变量,利用这组成员,线程可以认为它是在自己独占的环境中运行。T H R E A D I N F O是一个内部的、未公开的数据结构,用来指定线程的登记消息队列(posted-message queue)、发送消息队列( send-message queue)、应答消息队列( r e p l y -message queue)、虚拟输入队列(virtualized-input queue)、唤醒标志(wake flag)、以及用来描述线程局部输入状态的若干变量。图2 6 - 1描述了T H R E A D I N F O结构和与之相联系的三个线程。


图26-1 三个线程及相应的T H R E A D I N F O结构

这个T H R E A D I N F O结构是窗口消息系统的基础,在阅读下面各节内容时,应该参考该图。


26.2 将消息发送到线程的消息队列中

当线程有了与之相联系的T H R E A D I N F O结构时,线程就有了自己的消息队列集合。如果一个进程建立了三个线程,并且所有这些线程都调用C r e a t e Wi n d o w,则将有三个消息队列集合。消息被放置在线程的登记消息队列中,这要通过调用P o s t M e s s a g e函数来完成:

BOOL PostMessage(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
当一个线程调用这个函数时,系统要确定是哪一个线程建立了用h w n d参数标识的窗口。然后系统分配一块内存,将这个消息参数存储在这块内存中,并将这块内存增加到相应线程的登记消息队列中。并且,这个函数还设置Q S _ P O S T M E S S A G E唤醒位(后面会简单讨论)。函数P o s t M e s s a g e在登记了消息之后立即返回,调用该函数的线程不知道登记的消息是否被指定窗口的窗口过程所处理。实际上,有可能这个指定的窗口永远不会收到登记的消息。如果建立这个特定窗口的线程在处理完它的消息队列中的所有消息之前就结束了,就会发生这种事。

还可以通过调用P o s t T h r e a d M e s s a g e将消息放置在线程的登记消息队列中。

BOOL PostThreadMessage(
DWORD dwThreadId,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
注意可以通过调用G e t Wi n d o w s T h r e a d P r o c e s s I d来确定是哪个线程建立了一个窗口。

DWORD GetWindowThreadProcessId(
HWND hwnd,
PDWORD pdwProcessId);
这个函数返回线程的I D,这个线程建立了h w n d参数所标识的窗口。线程I D在全系统范围内是唯一的。还可以通过对p d w P r o c e s s I d参数传递一个D W O R D地址来获取拥有该线程的进程I D,这个进程I D在全系统范围内也是唯一的。通常,我们不需要进程I D,只须对这个参数传递一个N U L L。

P o s t T h r e a d M e s s a g e函数所期望的线程由第一个参数d w T h r e a d I d所标记。当消息被设置到队列中时,M S G结构的h w n d成员将设置成N U L L。当程序要在主消息循环中执行一些特殊处理时要调用这个函数。

要对线程编写主消息循环以便在G e t M e s s a g e或P e e k M e s s a g e取出一个消息时,主消息循环代码检查h w n d是否为N U L L,并检查M S G结构的m s g成员来执行特殊的处理。如果线程确定了该消息不被指派给一个窗口,则不调用D i s p a t c h M e s s a g e,消息循环继续取下一个消息。

像P o s t M e s s a g e函数一样,P o s t T h r e a d M e s s a g e在向线程的队列登记了消息之后就立即返回。调用该函数的线程不知道消息是否被处理。

向线程的队列发送消息的函数还有P o s t Q u i t M e s s a g e:

VOID PostQuitMessage(int nExitCode);
为了终止线程的消息循环,可以调用这个函数。调用P o s t Q u i t M e s s a g e类似于调用:

PostThreadMessage(GetCurrentThreadId(), WM_QUIT, nExitCode, 0);
但是,P o s t Q u i t M e s s a g e并不实际登记一个消息到任何一个T H R E A D I N F O结构的队列。只是在内部, P o s t Q u i t M e s s a g e设定Q S_Q U I T唤醒标志(后面将要讨论),并设置T H R E A D I N F O结构的n E x i t C o d e成员。因为这些操作永远不会失败,所以P o s t Q u i t M e s s a g e的原型被定义成返回V O I D。


26.3 向窗口发送消息

使用S e n d M e s s a g e函数可以将窗口消息直接发送给一个窗口过程:

LRESULT SendMessage(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
窗口过程将处理这个消息。只有当消息被处理之后, S e n d M e s s a g e才能返回到调用程序。由于具有这种同步特性,比之P o s t M e s s a g e或P o s t T h r e a d M e s s a g e,S e n d M e s s a g e用得更频繁。调用这个函数的线程在下一行代码执行之前就知道窗口消息已经被完全处理。

S e n d M e s s a g e是如何工作的呢?如果调用S e n d M e s s a g e的线程向该线程所建立的一个窗口发送一个消息,S e n d M e s s a g e就很简单:它只是调用指定窗口的窗口过程,将其作为一个子例程。当窗口过程完成对消息的处理时,它向S e n d M e s s a g e返回一个值。S e n d M e s s a g e再将这个值返回给调用线程。

但是,当一个线程向其他线程所建立的窗口发送消息时, S e n d M e s s a g e的内部工作就复杂得多(即使两个线程在同一进程中也是如此)。Wi n d o w s要求建立窗口的线程处理窗口的消息。所以当一个线程调用S e n d M e s s a g e向一个由其他进程所建立的窗口发送一个消息,也就是向其他的线程发送消息,发送线程不可能处理窗口消息,因为发送线程不是运行在接收进程的地址空间中,因此不能访问相应窗口过程的代码和数据。实际上,发送线程要挂起,而由另外的线程处理消息。所以为了向其他线程建立的窗口发送一个窗口消息,系统必须执行下面将讨论的动作。

首先,发送的消息要追加到接收线程的发送消息队列,同时还为这个线程设定Q S _ S E N D M E S S A G E标志(后面将讨论)。其次,如果接收线程已经在执行代码并且没有等待消息(如调用G e t M e s s a g e、P e e k M e s s a g e或Wa i t M e s s a g e),发送的消息不会被处理,系统不能中断线程来立即处理消息。当接收进程在等待消息时,系统首先检查Q S _ S E N D M E S S A G E唤醒标志是否被设定,如果是,系统扫描发送消息队列中消息的列表,并找到第一个发送的消息。有可能在这个队列中有几个发送的消息。例如,几个线程可以同时向一个窗口分别发送消息。当发生这样的事时,系统只是将这些消息追加到接收线程的发送消息队列中。

当接收线程等待消息时,系统从发送消息队列中取出第一个消息并调用适当的窗口过程来处理消息。如果在发送消息队列中再没有消息了,则Q S _ S E N D M E S S A G E唤醒标志被关闭。当接收线程处理消息的时候,调用S e n d M e s s a g e的线程被设置成空闲状态( i d l e),等待一个消息出现在它的应答消息队列中。在发送的消息处理之后,窗口过程的返回值被登记到发送线程的应答消息队列中。发送线程现在被唤醒,取出包含在应答消息队列中的返回值。这个返回值就是调用S e n d M e s s a g e的返回值。这时,发送线程继续正常执行。

当一个线程等待S e n d M e s s a g e返回时,它基本上是处于空闲状态。但它可以执行一个任务:如果系统中另外一个线程向一个窗口发送消息,这个窗口是由这个等待S e n d M e s s a g e返回的线程所建立的,则系统要立即处理发送的消息。在这种情况下,系统不必等待线程去调用G e t M e s s a g e、Peek Message或Wa i t M e s s a g e。

由于Wi n d o w s使用上述方法处理线程之间发送的消息,所以有可能造成线程挂起( h a n g)。例如,当处理发送消息的线程含有错误时,会导致进入死循环。那么对于调用S e n d M e s s a g e的线程会发生什么事呢?它会恢复执行吗?这是否意味着一个程序中的b u g会导致另一个程序挂起?答案是确实有这种可能。

利用4个函数—— S e n d M e s s a g e Ti m e o u t、S e n d M e s s a g e C a l l b a c k、S e n d N o t i f y M e s s a g e和R e p l y M e s s a g e,可以编写保护性代码防止出现这种情况。第一个函数是S e n d M e s s a g e Ti m e o u t:

LRESULT SendMessageTimeout(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam,
UINT fuFlags,
UINT uTimeout,
PDWORD_PTR pdwResult);
利用S e n d M e s s a g e Ti m e o u t函数,可以规定等待其他线程答回你消息的时间最大值。前4个参数与传递给S e n d M e s s a g e的参数相同。对f u F l a g s参数,可以传递值S M TO _ N O R M A L (定义为0 )、S M TO _ A B O RT I F H U N G、S M TO _ B L O C K、S M TO _ N O T I M E O U T I F N O T H U N G或这些标志的组合。

S M TO _ A B O RT I F H U N G标志是告诉S e n d M e s s a g e Ti m e o u t去查看接收消息的线程是否处于挂起状态,如果是,就立即返回。S M TO _ N O T I M E O U T I F N O T H U N G标志使函数在接收消息的线程没有挂起时不考虑等待时间限定值。S M T O _ B L O C K 标志使调用线程在S e n d M e s s a g e Ti m e o u t返回之前,不再处理任何其他发送来的消息。S M TO _ N O R M A L标志在Wi n u s e r. h中定义成0,如果不想指定任何其他标志及组合,就使用这个标志。

前面说过,一个线程在等待发送的消息返回时可以被中断,以便处理另一个发送来的消息。使用S M TO _ B L O C K标志阻止系统允许这种中断。仅当线程在等待处理发送的消息的时候(不能处理别的发送消息),才使用这个标志。使用S M TO _ B L O C K可能会产生死锁情况,直到等待时间期满。例如,如果你的线程向另外一个线程发送一个消息,而这个线程又需要向你的线程发送消息。在这种情况下,两个线程都不能继续执行,并且都将永远挂起。

S e n d M e s s a g e Ti m e o u t函数中的u Ti m e o u t参数指定等待应答消息时间的毫秒数。如果这个函数执行成功,返回T R U E,消息的结果复制到一个缓冲区中,该缓冲区的地址由p d w R e s u l t参数指定。

顺便提一下,这个函数在Wi n U s e r. h头文件中的原型是不正确的。这个函数的原型应该被定义成返回一个B O O L型值,因为L R E S U LT实际是通过函数的一个参数返回的。这会引起一些问题,因为如果对函数传递一个无效的窗口句柄或者等待超时, S e n d M e s s a g e Ti m e o u t都会返回FA L S E。要知道函数失败详细情况的唯一办法是调用G e t L a s t E r r o r。如果函数是由于等待超时而失败,则G e t L a s t E r r o r为0(E R R O R _ S U C C E S S)。如果对参数传递了一个无效句柄,G e t L a s t E r r o r为1 4 0 0(E R R O R _ I N VA L I D _ W I N D O W _ H A N D L E)。

如果调用S e n d M e s s a g e Ti m e o u t向调用线程所建立的窗口发送一个消息,系统只是调用这个窗口的窗口过程,并将返回值赋给p d w R e s u l t。因为所有的处理都必须发生在一个线程里,调用S e n d M e s s a g e Ti m e o u t函数之后出现的代码要等消息被处理完之后才能开始执行。

用来在线程间发送消息的第二个函数是S e n d M e s s a g e C a l l b a c k:

BOOL SendMessageCallback(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam,
SENDASYNCPROC pfnResultCallBack,
ULONG_PTR dwData);
同样,前4个参数同S e n d M e s s a g e中使用的一样。当一个线程调用S e n d M e s s a g e C a l l b a c k时,该函数发送消息到接收线程的发送消息队列,并立即返回使发送线程可以继续执行。当接收线程完成对消息的处理时,一个消息被登记到发送线程的应答消息队列中。然后,系统通过调用一个函数将这个应答通知给发送线程,该函数是使用下面的原型编写的。

VOID CALLBACK ResultCallBack(
HWND hwnd,
UINT uMsg,
ULONG_PTR dwData,
LRESULT lResult);
必须将这个函数的地址传递给S e n d M e s s a g e C a l l b a c k函数作为p f n R e s u l t C a l l b a c k参数值。当调用这个函数时,要把完成消息处理的窗口的句柄传递到第一个参数,将消息值传递给第二个参数。第三个参数d w D a t a,总是取传递到S e n d M e s s a g e C a l l b a c k函数的d w D a t a参数的值。系统只是取出对S e n d M e s s a g e C a l l b a c k函数指定的参数值,再直接传递到R e s u l t C a l l b a c k函数。R e s u l t C a l l b a c k函数的最后一个参数是处理消息的窗口过程返回的结果。

因为S e n d M e s s a g e C a l l b a c k在执行线程间发送时会立即返回,所以在接收线程完成对消息的处理时不是立即调用这个回调函数。而是由接收线程先将一个消息登记到发送线程的应答消息队列。发送线程在下一次调用G e t M e s s a g e、P e e k M e s s a g e、Wa i t M e s s a g e或某个S e n d M e s s a g e*函数时,消息从应答消息队列中取出,并执行R e s u l t C a l l B a c k函数。

S e n d M e s s a g e C a l l b a c k函数还有另外一个用处。Wi n d o w s提供了一种广播消息的方法,用这种方法你可以向系统中所有现存的重叠( o v e r l a p p e d)窗口广播一个消息。这可以通过调用S e n d M e s s a g e函数,对参数h w n d传递H W N D _ B R O A D C A S T(定义为- 1)。使用这种方法广播的消息,其返回值我们并不感兴趣,因为S e n d M e s s a g e函数只能返回一个L R E S U LT。但使用S e n d M e s s a g e C a l l b a c k,就可以向每一个重叠窗口广播消息,并查看每一个返回结果。对每一个处理消息的窗口的返回结果都要调用R e s u l t C a l l b a c k函数。

如果调用S e n d M e s s a g e C a l l b a c k向一个由调用线程所建立的窗口发送一个消息,系统立即调用窗口过程,并且在消息被处理之后,系统调用R e s u l t C a l l B a c k函数。在R e s u l t C a l l B a c k函数返回之后,系统从调用S e n d M e s s a g e C a l l b a c k的后面的代码行开始执行。

线程间发送消息的第三个函数是S e n d N o t i f y M e s s a g e:

BOOL SendNotifyMessage(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
S e n d N o t i f y M e s s a g e将一个消息置于接收线程的发送消息队列中,并立即返回到调用线程。这一点与P o s t M e s s a g e函数一样,但S e n d N o t i f y M e s s a g e在两方面与P o s t M e s s a g e不同。

首先,S e n d N o t i f y M e s s a g e是向另外的线程所建立的窗口发送消息,发送的消息比起接收线程消息队列中存放的登记消息有更高的优先级。换句话说,由S e n d N o t i f y M e s s a g e函数存放在队列中的消息总是在P o s t M e s s a g e函数登记到队列中的消息之前取出。

其次,当向一个由调用进程建立的窗口发送消息时, S e n d N o t i f y M e s s a g e同S e n d M e s s a g e函数完全一样:S e n d N o t i f y M e s s a g e在消息被处理完之后才能返回。

我们已经知道,发送给窗口的大多数消息是用于通知的目的。也就是,发送消息是因为窗口需要知道某个状态已经发生变化,在程序能够继续执行之前,窗口要做某种处理。例如,W M _ A C T I VAT E、W M _ D E S T R O Y、W M _ E N A B L E、W M _ S I Z E、W M _ S E T F O C U S和W M _ M O V E等都是系统发送给窗口的通知,而不是登记的消息。这些消息是系统对窗口的通知,因此系统不需要停止运行以等待窗口过程处理这些消息。与此相对应,如果系统向一个窗口发送一个W M _ C R E AT E消息,则在窗口处理完这个消息之前,系统必须等待。如果返回值是-1,则不再建立窗口。

第四个用于线程发送消息的函数是R e p l y M e s s a g e:

BOOL ReplyMessage(LRESULT lResult);
这个函数与前面讨论过的三个函数不同。线程使用S e n d M e s s a g e Ti m e o u t、S e n d M e s s a g eC a l l b a c k和S e n d N o t i f y M e s s a g e发送消息,是为了保护自己以免被挂起。而线程调用R e p l yM e s s a g e是为了接收窗口消息。当一个线程调用R e p l y M e s s a g e时,它是要告诉系统,为了知道消息结果,它已经完成了足够的工作,结果应该包装起来并登记到发送线程的应答消息队列中。这将使发送线程醒来,获得结果,并继续执行。

调用R e p l y M e s s a g e的线程在l R e s u l t参数中指出消息处理的结果。在调用R e p l y M e s s a g e之后,发送消息的线程恢复执行,而处理消息的线程继续处理消息。两个线程都不会被挂起,都可以正常执行。当处理消息的线程从它的窗口过程返回时,它返回的任何值都被忽略。

这里的问题是, R e p l y M e s s a g e必须在接收消息的窗口过程中调用,而不是由调用某个S e n d*函数的线程调用。为了编写保护性代码,最好不要用前面讨论过的三个S e n d*函数中的一个代替对S e n d M e s s a g e的调用,而是依靠窗口过程的实现者来调用R e p l y M e s s a g e。

还应该知道,如果在处理一个由同一线程发送来的消息时调用R e p l y M e s s a g e,则该函数什么也不做。实际上,这就是R e p l y M e s s a g e的返回值所指出的。如果你在处理线程间的消息发送时调用了R e p l y M e s s a g e ,则它返回T R U E ,如果你在处理线程内的消息发送时调用R e p l y M e s s a g e,它返回FA L S E。

有时候,你可能想知道究竟是在处理线程间的消息发送,还是在处理线程内的消息发送。为了搞清楚这一点,可以调用I n S e n d M e s s a g e:

BOOL InSendMessage();
这个函数的名字不能够确切说明它究竟做什么事。初看时,你会以为,当线程在处理一个发送的消息时,该函数返回T R U E,而在处理一个登记的消息时,它返回FA L S E。如果这样想你就错了。这个函数在线程处理线程间发送的消息时,返回T R U E,而在线程处理线程内发送的或登记的消息时,返回FA L S E。I n S e n d M e s s a g e和R e p l y M e s s a g e的返回值是一样的。

还可以调用另外一个函数来确定窗口过程正在处理的消息类型:

DWORD InSendMessageEx(PVOID pvReserved);
当调用这个函数时,必须对p v R e s e r v e d参数传递N U L L。这个函数的返回值指出正在处理的消息的类型。如果返回值是I S M E X _ N O S E N D(定义为0),表示线程正在处理一个线程内发送的或登记的消息。如果返回值不是I S M E X _ N O S E N D,就是表2 6 - 1中描述的位标志的组合。

表26-1 位标志的组合

标志描述
I S M E X _ S E N D线程在处理一个线程间发送的消息,该消息是用S e n d M e s s a g e或Send Message Ti m e o u t函数发送的。如果没有设定I S M E X _ R E P L I E D标志,发送线程被阻塞,等待应答
I S M E X _ N O T I F Y线程在处理一个线程间发送的消息,该消息是用SendNotifyM e s s a g e函数发送的。发送线程不等待应答,也不会阻塞
I S M E X _ C A L L B A C K线程在处理线程间发送的消息,该消息是用S e n d M e s s a g eC a l l b a c k发送的。发送线程不等待应答,也不会被阻塞
I S M E X _ R E P L I E D线程在处理线程间发送的消息,并已经调用R e p l y M e s s a g e。发送线程不会被阻塞


26.4 唤醒一个线程

当一个线程调用G e t M e s s a g e或Wa i t M e s s a g e,但没有对这个线程或这个线程所建立窗口的消息时,系统可以挂起这个线程,这样就不再给它分配C P U时间。当有一个消息登记或发送到这个线程,系统要设置一个唤醒标志,指出现在要给这个线程分配C P U时间,以便处理消息。正常情况下,如果用户不按键或移动鼠标,就没有消息发送给任何窗口。这意味着系统中大多数线程没有被分配给C P U时间。


26.4.1 队列状态标志


当一个线程正在运行时,它可以通过调用G e t Q u e u e S t a t u s函数来查询队列的状态:

DWORD GetQueueStatus(UINT fuFlags);
参数f u F l a g s是一个标志或一组由O R连接起来的标志,可用来测试特定的唤醒位。表2 6 - 2给出了各个标志取值及含义。

表26-2 标志取值及含义

标志队列中的消息
Q S _ K E YW M _ K E Y U P、W M _ K E Y D O W N、W M _ S Y S K E Y U P或W M _ S Y S K E Y D O W N
Q S _ M O U S E M O V EW M _ M O U S E M O V E
Q S _ M O U S E B U T TO NW M _ ? B U T TO N *(其中?代表L、M或R、*代表D O W N、U P或DBLCLK )
Q S _ M O U S E同Q S _ M O U S E M O V E | Q S _ M O U S E B U T TO N
Q S _ I N P U T同Q S _ M O U S E | Q S _ K E Y
Q S _ PA I N TW M _ PA I N T
Q S _ T I M E RW M _ T I M E R
Q S _ H O T K E YW M _ H O T K E Y
Q S _ P O S T M E S S A G E登记的消息(不同于硬件输入事件)。当队列在期望的消息过滤器范围内没有登记的消息时,这个标志要消除。除此之外,这个标志与Q S _ A L L P O S T M E S S A G E相同
Q S _ A L L P O S T M E S S A G E登记的消息(不同于硬件输入事件)。当队列完全没有登记的消息时(在任何消息过滤器范围),该标志被清除。除此之外,该标志与Q S _ P O S T M E S S A G E相同
Q S _ A L L E V E N T S同Q S _ I N P U T | Q S _ P O S T M E S S A G E | Q S _ T I M E R | Q S _ PA I N T | Q S _ H O T K E Y
Q S _ Q U I T已调用P o s t Q u i t M e s s a g e。注意这个标志没有公开,所以在Wi n U s e r.h 文件中没有。它由系统在内部使用
Q S _ S E N D M E S S A G E由另一个线程发送的消息
Q S _ A L L I N P U T同QS_ALLEVENTS|QS_SENDMESSAGE

当调用G e t Q u e u e S t a t u s函数时,f u F l a g s将队列中要检查的消息的类型告诉G e t Q u e u e S t a t u s。用O R连接的Q S _ *标识符的数量越少,调用执行的就越快。当G e t Q u e u e S t a t u s返回时,线程的队列中当前消息的类型在返回值的高字(两字节)中。这个返回的标志的集合总是所想要的标志集的子集。例如,对下面的调用:

BOOL fPaintMsgWaiting = HIWORD(GetQueueStatus(QS_TIMER)) & QS_PAINT;
f P a i n t M s g Wa i t i n g的值总是FA L S E,不论队列中是否有一个W M _ PA I N T消息在等待,因为G e t Q u e u e S t a t u s的参数中没有将Q S _ PA I N T指定为一个标志。

G e t Q u e u e S t a t u s返回值的低字指出已经添加到队列中,并且在上一次对函数G e t Q u e u eS t a t u s、G e t M e s s a g e或P e e k M e s s a g e调用以来还没有处理的消息的类型。

不是所有的唤醒标志都由系统平等对待。对于Q S _ M O U S E M O V E标志,只要队列中存在一个未处理的W M _ M O U S E M O V E消息,这个标志就要被设置。当G e t M e s s a g e或PeekMessage (利用P M _ R E M O V E )从队列中放入新的W M _ M O U S E M O V E消息之前,这个标志被关闭。Q S _ K E Y、Q S _ M O U S E B U T TO N和QS_HOTKEY 标志都根据相应的消息按与此相同的方式处理。

Q S _ PA I N T标志的处理与此不同。如果线程建立的一个窗口有无效的区域, Q S _ PA I N T标志被设置。当这个线程建立的所有窗口所占据的区域变成无效时(通常由于对Va l i d a t e R e c t、Va l i d a t e R e g i o n或B e g i n P a i n t的调用而引起),Q S _ PA I N T标志就被关闭。只有当线程建立的所有窗口都无效时,这个标志才关闭。调用G e t M e s s a g e或P e e k M e s s a g e对这个唤醒标志没有影响。

当线程的登记消息队列中至少有一个消息时, Q S _ P O S T M E S S A G E标志就被设置。这不包括线程的虚拟输入队列中的硬件事件消息。当线程的登记消息队列中的所有消息都已经处理,队列变空时,这个标志被复位。

每当一个定时器(由线程所建立)报时(go off),Q S _ T I M E R标志就被设置。在Get Message或P e e k M e s s a g e返回W M _ T I M E R事件之后,Q S _ T I M E R标志被复位,直到定时器再次报时。

Q S _ S E N D M E S S A G E标志指出有一个消息在线程的发送消息队中。系统在内部使用这个标志,用来确认和处理线程之间发送的消息。对于一个线程向自身发送的消息,不设置这个标志。虽然可以使用Q S _ S E N D M E S S A G E标志,但很少需要这样做。笔者还从未见到一个程序使用这个标志。

还有一个未公开的队列状态标志Q S _ Q U I T。当一个线程调用P o s t Q u i t M e s s a g e时, Q S _Q U I T标志就被设置。系统并不实际向线程的消息队列追加一个W M _ Q U I T消息。G e t Q u e u eS t a t u s函数也不返回这个标志的状态。


26.4.2 从线程的队列中提取消息的算法


当一个线程调用G e t M e s s a g e或P e e k M e s s a g e时,系统必须检查线程的队列状态标志的情况,并确定应该处理哪个消息。图2 6 - 2和下面叙述的步骤说明了系统是如何确定线程应该处理的下一个消息的情况。

  1. 如果Q S _ S E N D M E S S A G E标志被设置,系统向相应的窗口过程发送消息。G e t M e s s a g e或P e e k M e s s a g e函数在内部进行这种处理,并且在窗口过程处理完消息之后不返回到线程,这些函数要等待其他要处理的消息。

  2. 如果消息在线程的登记消息队列中,函数G e t M e s s a g e或P e e k M e s s a g e填充传递给它们的M S G结构,然后函数返回。这时,线程的消息循环通常调用D i s p a t c h M e s s a g e,让相应的窗口过程来处理消息。

  3. 如果Q S _ Q U I T标志被设置。G e t M e s s a g e或P e e k M e s s a g e返回一个W M _ Q U I T消息(其中w P a r a m参数是规定的退出代码)并复位Q S _ Q U I T标志。

  4. 如果消息在线程的虚拟输入队列,函数G e t M e s s a g e或P e e k M e s s a g e返回硬件输入消息。

  5. 如果Q S _ PA I N T标志被设置, G e t M e s s a g e或P e e k M e s s a g e为相应的窗口返回一个W M -PA I N T消息。

  6. 如果Q S _ T I M E R标志被设置,G e t M e s s a g e或P e e k M e s s a g e返回一个W M _ T I M E R消息。


图26-2 从线程队列中提取消息的算法

尽管很难令人相信,但确有理由这样做。微软在设计这个算法时有一个大前提,就是应用程序应该是用户驱动的,用户通过建立硬件输入事件(键盘和鼠标操作)来驱动应用程序。在使用应用程序时,用户可能按一个鼠标按钮,引起一系列要发生的事件。应用程序通过向线程的消息队列中登记消息使每个个别的事件发生。

所以如果按鼠标按钮,处理W M _ L B U T TO N D O W N消息的窗口可能向不同的窗口投送三个消息。由于是硬件事件引发三个软件事件,所以系统要在读取用户的下一个硬件事件之前,先处理这些软件事件。这也说明了为什么登记消息队列要在虚拟输入队列之前检查。

这种事件序列的一个很好的例子是调用Tr a n s l a t e M e s s a g e函数。这个函数检查是否有一个W M _ K E Y D O W N或一个W M _ S Y S K E Y D O W N消息从输入队列中取出。如果有一个这样的消息被取出,系统检查虚键(virtual key)信息是否能转换成等价的字符。如果虚键信息能够转换,Tr a n s l a t e M e s s a g e调用P o s t M e s s a g e将一个W M _ C H A R消息或一个W M _ S Y S C H A R消息放置在登记消息队列中。下次调用G e t M e s s a g e时,系统首先检查登记消息队列中的内容,如果其中有消息存在,从队列中取出消息并将其返回。返回的消息将是W M _ C H A R消息或W M _ S Y S C H A R消息。再下一次调用G e t M e s s a g e时,系统检查登记消息队列,发现队列已空。系统再检查输入队列,在其中找到W M _(S Y S)K E Y U P消息。G e t M e s s a g e返回这个消息。

由于系统是按这种方式工作,下面的硬件事件序列W M _ K E Y D O W N、W M _ K E Y U P生成下面的到窗口过程的消息序列(假定虚键信息可以转换成等价的字符):

WM_KEYDOWN
WM_CHAR
WM_KEYUP
现在我们再回过头来讨论系统如何确定从G e t M e s s a g e或P e e k M e s s a g e返回的消息。在系统检查了登记消息队列之后,但尚未检查虚拟输入队列时,它要检查Q S _ Q U I T标志。我们知道,当线程调用P o s t Q u i t M e s s a g e时设置Q S _ Q U I T标志。调用P o s t Q u i t M e s s a g e类似于(但不相同)调用P o s t T h r e a d M e s s a g e。P o s t T h r e a d M e s s a g e将消息放置在消息队列的尾端,并使消息在检查输入队列之前被处理。为什么P o s t Q u i t M e s s a g e设置一个标志,而不是将W M _ Q U I T消息放入消息队列中?有两个理由。

第一,在低内存(low memory)情况下,登记一个消息有可能失败。如果一个程序想退出,它应该被允许退出,即使是在低内存的情况下。第二个理由是使用标志可使线程在线程的消息循环结束前完成对所有其他登记消息的处理。例如对下面的代码段, W M _ U S E R消息将先于W M _ Q U I T消息从队列中取出,尽管W M _ U S E R消息是在调用P o s t Q u i t M e s s a g e之后登记到队列中的。

case WM_CLOSE:
PostQuitMessage(0);
PostMessage(hwnd, WM_USER, 0, 0);
最后两个消息是W M _ PA I N T和W M _ T I M E R。因为画屏幕是一个慢过程,所以W M _ PA I N T消息的优先级低。如果每当窗口变得无效时就发送一个W M _ PA I N T消息,系统运行就会太慢。在键盘输入之后放置W M _ PA I N T消息,系统会运行得很快。例如,选择一个调用对话框的菜单项,从框中选定一个项,在对话框出现在屏幕上之前一直按E n t e r。如果你的按键速度足够快,按键消息总是先于任何W M _ PA I N T消息从队列中取出。当按E n t e r接受对话框的选项,对话框窗口被清除,系统复位Q S _ PA I N T标志。

最后一个消息W M _ T I M E R,比W M _ PA I N T的优先级还低。为理解这一点,想一想有一个程序用每个W M _ T I M E R消息来更新它的显示画屏。如果定时器消息来的太快,则显示画屏就没有机会重画自己。在W M _ T I M E R消息之前先处理W M _ PA I N T消息,就可以避免这个问题,程序总能更新它的显示画屏。

注意要记住G e t M e s s a g e或P e e k M e s s a g e函数只检查唤醒标志和调用线程的消息队列。这意味着一个线程不能从与其他线程挂接的队列中取得消息,包括同一进程内其他线程的消息。

26.4.3 利用内核对象或队列状态标志唤醒线程


G e t M e s s a g e或P e e k M e s s a g e函数导致一个线程睡眠,直到该线程需要处理一个与用户界面(U I)相关的任务。有时候,若能让线程被唤醒去处理一个与U I有关的任务或其他任务,就会带来许多方便。例如,一个线程可能启动一个长时间运行的操作,并可以让用户取消这个操作。这个线程需要知道何时操作结束(与U I无关的任务),或用户是否按了C a n c e l按钮(与U I相关的任务)来结束操作。

一个线程可以调用M s g Wa i t F o r M u l t i p l e O b j e c t s或M s g Wa i t F o r M u l t i p l e O b j e c t s E x函数,使线程等待它自已的消息。

DWORD MsgWaitForMultipleObjects(
DWORD nCount,
PHANDLE phObjects,
BOOL fWaitAll,
DWORD dwMilliseconds,
DWORD dwWakeMask);

DWORD MsgWaitForMultipleObjectsEx(
DWORD nCount,
PHANDLE phObjects,
DWORD dwMilliseconds,
DWORD dwWakeMask,
DWORD dwFlags);


这两个函数类似于Wa i t F o r M u l t i p l e O b j e c t s函数(在第9章讨论过)。不同之处是,当一个内核对象变成有信号状态( s i g n a l e d)或当一个窗口消息需要派送到调用线程建立的窗口时,这两个函数用于线程调度。

在内部,系统只是向内核句柄的数组添加一个事件内核对象。d w Wa k e M a s k参数告诉系统何时让事件成为有信号状态。d w Wa k e M a s k参数的可能取值的合法范围与可传递到G e t Q u e u eS t a t u s函数的参数值一样。

正常情况下,当Wa i t F o r M u l t i p l e O b j e c t s函数返回时,它返回变成有信号状态的对象的索引以满足调用(WA I T _ O B J E C T _ O到WA I T _ O B J E C T _ O + n C o u n t-1)。增加d w Wa k e M a s k参数就如同向调用增加又一个句柄。如果由于唤醒掩码, M s g Wa i t F o r M u l t i p l e O b j e c t s ( E x )被满足,返回值将是WA I T _ O B J E C T _ O+n C o u n t。

这里是一个例子,说明如何调用M s g Wa i t F o r _ M u l t i p l e O b j e c t s:

MsgWaitForMultipleObjects(0, NULL, TRUE, INFINITE, QS_INPUT);
这条语句的意思是没有传递任何同步对象的句柄,因为n C o u n t和p h O b j e c t s参数设定了O和N U L L。这里让函数等待所有要变成有信号状态的对象,但只指定了一个要等待的对象,参数f Wa i t A l l可以变成FA L S E,而不会改变这个调用的作用。这里还告诉系统,程序将等待,不论等多长时间,直到有键盘消息或鼠标消息出现在调用线程的输入队列中。

当你要用M s g Wa i t F o r M u l t i p l e O b j e c t s函数做某些事的时候,就会发现这个函数缺少许多重要的特性。因此微软不得不又开发了M s g Wa i t F o r M u l t i p l e O b j e c t s E x函数。M s g Wa i t F o rM u l t i p l e O b j e c t s E x是M s g Wa i t F o r M u l t i p l e O b j e c t s的一个超集( s u p e r s e t)。新的特性是通过d w F l a g s参数引进的。对这个参数,可以指定下面标志的任意组合(见表2 6 - 3)。

表26-3 dwFlags 参数的标志

标志描述
M W M O _ WA I TA L L函数等待所有要变成有信号状态的内核对象及要出现在线程队列中的特定消息。如果没有这个标志,函数等待直到有一个内核对象变成s i g n a l e d,或指定的消息出现在线程的队列中
M W M O _ A L E RTA B L E函数在一个可报警状态等待
M W M O _ I N P U TAVA I L A B L E当任何指定的消息在线程的队列中时,函数醒来(本节后面详细解释)

如果不想要任何这些附加的特性,可对参数d w F l a g s传递零(0)。

下面是有关M s g Wa i t F o r M u l t i p l e O b j e c t s(E x)的一些重要内容:

• 由于这个函数只是向内核句柄的数组增加一个内部事件内核对象, n C o u n t参数的最大值是M A X I M U M _ WA I T _ O B J E C T减1或6 3。

• 当对f Wa i t A l l参数传递FA L S E时,那么当一个内核对象是有信号的( s i g n a l e d),或当指定的消息类型出现在线程的队列时,函数返回。

• 当对f Wa i t A l l参数传递T R U E时,那么当所有内核对象成为有信号状态,并且指定的消息类型出现在线程的队列中时,函数返回。这种行为似乎使许多开发人员感到惊讶。开发人员希望有一种办法,当所有内核对象变成有信号的或者当指定的消息类型出现在线程的队列中时,可以唤醒线程。但没有函数能够这样。

• 当调用这两个函数时,实际是查看是否有指定类型的新消息被放入调用线程的队列。

注意,上述最后一条会使许多开发人员吃惊。这里有一个例子。假定一个线程的队列目前包含有两个按键消息。如果这个线程现在要调用M s g Wa i t F o r M u l t i p l e O b j e c t s(E x),其中d w Wa k e M a s k参数设置成Q S _ I N P U T,线程将被唤醒,从队列中取出第一个按键消息,并处理这个消息。现在,如果这个线程要再调用M s g Wa i t F o r M u l t i p l e O b j e c t s(E x),线程将不会被唤醒,因为线程的队列中没有“新”的消息。

对开发人员来说,这已变成了一个主要问题,为此微软增加了M W M O _ I N P U TAVA I LA B L E标志,这只用于M s g Wa i t F o r M u l t i p l e O b j e c t s E x,而不用于M s g Wa i t F o r M u l t i p l e O b j e c t s。

这里是一个例子,讲述如何适当地编码一个使用M s g Wa i t F o r M u l t i p l e O b j e c t s E x的消息循环:

BOOL  fQuit = FALSE; // Should the loop terminate?

while(!fQuit)
{
//Wake when the kernel object is signaled OR
//if we have to process a UI message.
DWORD dwResult = MsgWaitForMultipleObjectsEx(1, &hEvent,
INFINITE, QS_ALLEVENTS, MWMO_INPUTAVAILABLE);

switch(dwResult)
{
case WAIT_OBJECT_0: // The event became signaled.
break;

case WAIT_OBJECT_0 + 1: // A message is in our queue.

//Dispatch all of the messages.
MSG msg;
while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if(msg.message == WM_QUIT)
{
// A WM_QUIT message, exit the loop
fQuit = TRUE;
}
else
{
//Translate and dispatch the message.
TranslateMessage(&msg);
DispatchMessage(&msg);
}
} // Our queue is empty.
break;
}
} // End of while loop


26.5 通过消息发送数据

本节将讨论系统如何利用窗口消息在进程之间传送数据。一些窗口消息在其l P a r a m参数中指出了一个内存块的地址。例如, W M _ S E T T E X T消息使用l P a r a m参数作为指向一个以零结尾的字符串的指针,这个字符串为窗口规定了新的文本标题串。考虑下面的调用:

SendMessage(FindWindow(NULL, "Calculator"), WM_SETTEXT,
0, (LPARAM) "A Test Caption");
这个调用看起来不会有害。它确定C a l c u l a t o r程序窗口的窗口句柄,并试图将窗口的标题改成“A Test Caption”。但我们要仔细看一看究竟会发生什么。

新标题的字符串包含在调用进程的地址空间里。所以这个在调用进程空间的字符串的地址将传递给l P a r a m参数。当C a l c u l a t o r的窗口的窗口过程收到这个消息时,它要查看l P a r a m参数,并要读取这个以零结尾的字符串,使其成为新的标题。

但l P a r a m中的地址指向调用进程的地址空间中的字符串,而不是C a l c u l a t o r的地址空间。这会发生内存存取违规这种严重问题。但当你执行上面的代码时,你会看到执行是成功的,为什么会是这样?

答案是系统特别要检查W M _ S E T T E X T消息,并用与处理其他消息不同的方法来处理这个消息。当调用S e n d M e s s a g e时,函数中的代码要检查是否要发送一个W M _ S E T T E X T消息。如果是,就将以零结尾的字符串从调用进程的地址空间放入到一个内存映像文件中,该内存映像文件可在进程间共享。然后再发送消息到其他进程的线程。当接收线程已准备好处理W M _ S E T T E X T消息时,它在自己的地址空间中确定包含新的窗口文本标题的共享内存映像文件的位置,再将W M _ S E T T E X T消息派送到相应的窗口过程。在处理完消息之后,内存映像文件被删除。这样做看起来是不是太麻烦了一些。

幸而大多数消息不要求这种类型的处理。仅当这种消息是程序在进程间发送的消息,特别是消息的w P a r a m或l P a r a m参数表示一个指向数据结构的指针时,要做这样的处理。

我们再来看另外一个要求系统特殊处理的例子—— W M _ G E T T E X T消息。假定一个程序包含下面的代码:

char szBuf[200];
SendMessage(FindWindow(NULL, "Calculator"), WM_GETTEXT,
sizeof(szBuf), (LPARAM) szBuf);
W M _ G E T T E X T消息请求C a l c u l a t o r的窗口过程用该窗口的标题填充s z B u f所指定的缓冲区。当一个进程向另一个进程的窗口发送这个消息时,系统实际上必须发送两个消息。首先,系统要向那个窗口发送一个W M _ G E T T E X T L E N G T H消息。窗口过程通过返回窗口标题的字符数来响应这个消息。系统利用这个数字来建立一个内存映像文件,用于在两个进程之间共享。

当内存映像文件被建立时,系统就发送消息来填充它。然后系统再转回到最初调用S e n d M e s s a g e的进程,从共享内存映像文件中将数据复制到s z B u f所指定的缓冲区中,然后从S e n d M e s s a g e调用返回。

对于系统已经知道的消息,发送消息时都可以按相应的方式来处理。如果你要建立自己的(W M _ U S E R+x)消息,并从一个进程向另一个进程的窗口发送,那又会怎么样?系统不知道你要用内存映像文件并在发送消息时改变指针。为此,微软建立了一个特殊的窗口消息,W M _ C O P Y D ATA以解决这个问题:

COPYDATASTRUCT cds;
SendMessage(hwndReceiver, WM_COPYDATA,
(WPARAM)hwndSender, (LPARAM) &cds);
C O P Y D ATA S T R U C T是一个结构,定义在Wi n U s e r. h文件中,形式如下面的样子:

typedef struct tagCOPYDATASTRUCT 
{
ULONG_PTR dwData;
DWORD cbData;
PVOID lpData;
} COPYDATASTRUCT;
当一个进程要向另一个进程的窗口发送一些数据时,必须先初始化C O P Y D ATA S T R U C T结构。数据成员d w D a t a是一个备用的数据项,可以存放任何值。例如,你有可能向另外的进程发送不同类型或不同类别的数据。可以用这个数据来指出要发送数据的内容。

c b D a t a数据成员规定了向另外的进程发送的字节数, l p D a t a数据成员指向要发送的第一个字节。l p D a t a所指向的地址,当然在发送进程的地址空间中。

当S e n d M e s s a g e看到要发送一个W M _ C O P Y D ATA消息时,它建立一个内存映像文件,大小是c b D a t a字节,并从发送进程的地址空间中向这个内存映像文件中复制数据。然后再向目的窗口发送消息。在接收消息的窗口过程处理这个消息时, l P a r a m参数指向已在接收进程地址空间的一个C O P Y D ATA S T R U C T结构。这个结构的l p D a t a成员指向接收进程地址空间中的共享内存映像文件的视图。

关于W M _ C O P Y D ATA消息,应该注意三个重要问题:

• 只能发送这个消息,不能登记这个消息。不能登记一个W M _ C O P Y D ATA消息,因为在接收消息的窗口过程处理完消息之后,系统必须释放内存映像文件。如果登记这个消息,系统不知道这个消息何时被处理,所以也不能释放复制的内存块。

• 系统从另外的进程的地址空间中复制数据要花费一些时间。所以不应该让发送程序中运行的其他线程修改这个内存块,直到S e n d M e s s a g e调用返回。

• 利用W M _ C O P Y D ATA消息,可以实现1 6位和3 2位之间的通信。它也能实现3 2位与6 4位之间的通信。这是使新程序同旧程序交流的便捷方法。注意在Windows 2000和Wi n d o w s9 8上完全支持W M _ C O P Y D ATA。但如果你依然在编写1 6位Wi n d o w s程序, M i c r o s o f eVisual C++ 1.52没有W M _ C O P Y D ATA消息的定义,也没有C O P Y D ATA S T R U C T结构的定义。需要手工添加这些代码:

// Manually include this in your 16-bit Windows source code.
#define WM_COPYDATA0x004A

typedef VOID FAR* PVOID;
typedef struct tagCOPYDATASTRUCT
{
DWORD dwData;
DWORD cbData;
PVOID lpData;
} COPYDATASTRUCT, FAR* PCOPYDATASTRUCT;


在解决进程间的通信问题方面, W M _ C O P Y D ATA消息是一个非常好的工具,可以节省程序员的许多时间。关于使用W M _ C O P Y D ATA消息的一个精采例子,见第2 2章的L a s t M s gB o x I n f o示例程序。

C o p y D a t a示例程序

清单2 6 - 1所列的C o p y D a t a程序(“26 CopyData.exe)说明了如何使用W M _ C O P Y D ATA消息从一个程序向另一个程序发送一个数据块。该程序的源代码和资源文件在本书所附光盘的2 6 - C o p y D a t a目录下。要看它如何工作,至少需要让两个C o p y D a t a的实例运行。每次启动一个C o p y D a t a时,它要显示如图2 6 - 3所示的对话框。


图26-3 CopyData Application 对话框

为了观看从一个程序到另一个程序的数据复制,首先改变D a t a 1和D a t a 2编辑控制框中的文本。然后点击某个Send Data* to Other Wi n d o w s按钮,程序向所有运行的C o p y D a t a的实例发数据。每个实例更新自己的编辑框中的内容来反应新数据。

下面描述C o p y D a t a如何工作。当一个用户点击图2 6 - 3中两个按钮中的某一个时,C o p y D a t a执行下面的动作。

  1. 如果用户点击Send Data1 To Other Wi n d o w s按钮,用0来初始化C O P Y D ATA S T R U C T的d w D a t a成员,如果用户点击Send Data2 To Other Wi n d o w s按钮,则用1来初始化d w D a t a成员。

  2. 从相应的文本框中求取文本串的长度(按字符数计),并加1(对应一个零结束符)。这个值乘以S i z e o f ( T C H A R ),从字符数转换成字节数。结果存入C O P Y D ATA S T R U C T的c b D a t a成员中。

  3. 调用_ a l l o c a分配一个内存块,大小足以容纳编辑框中的字符串加上零结束符。这个块的地址存放在C O P Y D ATA S T R U C T结构的l p D a t a成员中。

  4. 从编辑框向分配的内存块复制字符串。

这个时候,一切就绪,准备向其他窗口发送数据。为了确定要向哪个窗口发送W M _ C O P Y D ATA消息,C o p y D a t a调用F i n d Wi n d o w E x函数传递它自己的对话框标题,以便只有其他的C o p y D a t a程序实例才会被枚举。当找到每个实例的窗口时,发送W M _ C O P Y D ATA消息,每个实例更新它的编辑控制框。

清单26-1 CopyData示例程序

/******************************************************************************
Module:  CopyData.cpp
Notices: Copyright (c) 2000 Jeffrey Richter
******************************************************************************/#include "..\CmnHdr.h"  /* See Appendix A. */
#include <windowsx.h>
#include <tchar.h>
#include <malloc.h>
#include "Resource.h"///////////////////////////////////////////////////////////////////////////////// WindowsX.h doesn't have a prototype for Cls_OnCopyData, so here it is
/* BOOL Cls_OnCopyData(HWND hwnd, HWND hwndFrom, PCOPYDATASTRUCT pcds) *////////////////////////////////////////////////////////////////////////////////BOOL Dlg_OnCopyData(HWND hwnd, HWND hwndFrom, PCOPYDATASTRUCT cds) {

Edit_SetText(GetDlgitem(hwnd, cds->dwData ? IDC_DATA2 : IDC_DATA1),
(PTSTR) cds->lpData);

return(TRUE);
}///////////////////////////////////////////////////////////////////////////////BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) {

chSETDLGICONS(hwnd, IDI_COPYDATA);

// Initialize the edit controls with some test data.
Edit_SetText(GetDlgitem(hwnd, IDC_DATA1), TEXT("Some test data"));
Edit_SetText(GetDlgItem(hwnd, IDC_DATA2), TEXT("Some more test data"));
return(TRUE);
}///////////////////////////////////////////////////////////////////////////////void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) {

switch (id) {
case IDCANCEL:
EndDialog(hwnd, id);
break;

case IDC_COPYDATA1:
case IDC_COPYDATA2:
if (codeNotify != BN_CLICKED)
break;

HWND hwndEdit = GetDlgItem(hwnd,
(id == IDC_COPYDATA1) ? IDC_DATA1 : IDC_DATA2);

// Prepare the COPYDATASTRUCT.
COPYDATASTRUCT cds;

// Indicate which data field we're sending (0=ID_DATA1, 1=ID_DATA2)
cds.dwData = (DWORD) ((id == IDC_COPYDATA1) ? 0 : 1);

// Get the length (in bytes) of the data block we're sending.
cds.cbData = (Edit_GetTextLength(hwndEdit) + 1) * sizeof(TCHAR);

// Allocate a block of memory to hold the string.
cds.lpData = _alloca(cds.cbData);

// Put the edit control's string in the data block.
Edit_GetText(hwndEdit, (PTSTR) cds.lpData, cds.cbData);

// Get the caption of our window.
TCHAR szCaption[100];
GetWindowText(hwnd, szCaption, chDIMOF(szCaption));

// Enumerate through all the top-level windows with the same caption
HWND hwndT = NULL;
do {
hwndT = FindWindowEx(NULL, hwndT, NULL, szCaption);
if (hwndT != NULL) {
FORWARD_WM_COPYDATA(hwndT, hwnd, &cds, SendMessage);
}
} while (hwndT != NULL);
break;
}
}///////////////////////////////////////////////////////////////////////////////INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {

switch (uMsg) {
chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog);
chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand);
chHANDLE_DLGMSG(hwnd, WM_COPYDATA,Dlg_OnCopyData);
}
return(FALSE);
}///////////////////////////////////////////////////////////////////////////////int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) {

DialogBox(hinstExe, MAKEINTRESOURCE(IDD_COPYDATA), NULL, Dlg_Proc);
return(0);
}//////////////////////////////// End of File //////////////////////////////////

//Microsoft Developer Studio generated resource script.
//
#include "Resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
// English (U.S.) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _win32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_win32

/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//

IDD_COPYDATA DIALOG DISCARDABLE 38, 36, 220, 42
STYLE WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "CopyData Application"
FONT 8, "MS Sans Serif"
BEGIN
LTEXT "Data&1:",IDC_STATIC,4,4,24,12
EDITTEXT IDC_DATA1,28,4,76,12
PUSHBUTTON"&Send Data1 to other windows",IDC_COPYDATA1,112,4,104,14,WS_GROUP
LTEXT "Data&2:",IDC_STATIC,4,24,24,12
EDITTEXT IDC_DATA2,28,24,76,12
PUSHBUTTON"Send &Data2 to other windows",IDC_COPYDATA2,112,24,104,14,WS_GROUP
END/////////////////////////////////////////////////////////////////////////////
//
// Icon
//

// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_COPYDATAICON DISCARDABLE "CopyData.Ico"

#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE
BEGIN
"Resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE
BEGIN
"#include ""afxres.h""\r\n"
"\0"
END

3 TEXTINCLUDE DISCARDABLE
BEGIN
"\r\n"
"\0"
END

#endif // APSTUDIO_INVOKED

#endif // English (U.S.) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
///////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED


26.6 Windows如何处理ANSI/Unicode字符和字符串

Windows 98只支持A N S I窗口类和A N S I窗口过程。

当你注册一个新的窗口类时,必须将负责为这个类处理消息的窗口过程的地址告诉系统。对某些消息(如W M _ S E T T E X T),消息的l P a r a m参数指向一个字符串。在此之前,为了派送消息,使它被正确地处理,系统需要知道窗口过程要求该字符串是A N S I字符串还是U n i c o d e字符串。

告诉系统一个窗口过程是要求A N S I字符串还是U n i c o d e字符串,实际上取决于注册窗口类时所使用的函数。如果构造W N D C L A S S结构,并调用R e g i s t e r C l a s s A,系统就认为窗口过程要求所有的字符串和字符都属于A N S I。而用R e g i s t e r C l a s s W注册窗口类,则系统就向窗口过程派送U n i c o d e字符串和字符。宏R e g i s t e r C l a s s对R e g i s t e r C l a s s A和R e g i s t e r C l a s s W都做了扩展,究竟代表哪一个要看在编译源模块时是否定义了U N I C O D E。

如果有了一个窗口句柄,就可以确定窗口过程所要求的字符和字符串类型。这可以通过调用下面的函数实现:

BOOL IsWindowUnicode(HWND hwnd);
如果这个窗口的窗口过程要求U n i c o d e,这个函数返回T R U E,否则返回FA L S E。

如果你建立一个A N S I串,并向一个窗口过程要求U n i c o d e串的窗口发送W M _ S E T T E X T消息,则系统在发送消息之前,为你自动地转换字符串。很少需要调用I s Window Unicode函数。

如果你对窗口过程派生子类,系统也会为你执行自动的转换。假定一个编辑控制框的窗口过程要求字符和字符串是U n i c o d e。在你的程序的某处建立了一个编辑控制框,并建立窗口过程的子类,这可以调用

LONG_PTR SetWindowLongPtrA(
HWND hwnd,
int nIndex,
LONG_PTR dwNewLong);

LONG_PTR SetWindowLongPtrW(
HWND hwnd,
int nIndex,
LONG_PTR dwNewLong);

并将n I n d e x参数设置成G C L P _ W N D P R O C,d w N e w L o n g参数设置成子类过程的地址。如果这个子类过程要求A N S I字符和字符串会出现什么情况?这可能引起严重的问题。系统决定怎样转换字符串和字符,要取决于究竟是用上面两个函数中的哪一个来建立子类。如果是调用S e t Wi n d o w L o n g P t r A,就是告诉系统新的窗口过程(即子类过程)要接收A N S I字符和字符串。实际上,如果在调用S e t Wi n d o w L o n g P t r A之后调用I s Wi n d o w U n i c o d e函数,将返回FA L S E,表示这个子类的编辑窗口过程不再要求U n i c o d e字符和字符串。

但现在又有一个新的问题:如何能够保证原来的窗口过程得到正确的字符和字符串类型?系统需要有两条信息,才能正确地转换字符和字符串。第一条信息就是字符和字符串当前所具有的形式。这可以通过调用C a l l Wi n d o w P r o c A或C a l l Wi n d o w P r o c W来告诉系统:

LRESULT CallWindowProcA(
WNDPROC wndprcPrev,
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);

LRESULT CallWindowProcW(
WNDPROC wndprcPrev,
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);


如果子类过程要把A N S I字符串传递给原来的窗口过程,子类过程必须调用C a l l Wi n d o wP r o c A。如果子类过程要把U n i c o d e字符串传递给原来的窗口过程,则子类过程必须调用C a l l Wi n d o w P r o c W。

系统需要的第二条信息是原来的窗口过程所要求的字符和字符串类型。系统从原来窗口过程的地址获取这个信息。当调用S e t Wi n d o w L o n g P t r A或S e t Wi n d o w L o n g P t r W函数时,系统要查看是否使用了一个A N S I子类过程派生了一个U n i c o d e窗口过程,或用一个U n i c o d e子类过程派生了一个A N S I窗口过程。如果没有改变所要求的字符串类型,则S e t Wi n d o w L o n g P t r只返回原先窗口过程的地址。如果改变了窗口过程要求的字符和字符串类型, S e t Wi n d o w L o n g P t r不是返回原先窗口过程的实际地址,而是返回一个内部子系统数据结构的句柄。

这个数据结构包含原先窗口过程的地址及一个数值,用来指示窗口过程是要求A N S I还是要求U n i c o d e字符串。当调用C a l l Wi n d o w P r o c时,系统要查看是传递了某个内部数据结构的地址,还是传递了一个窗口过程的地址。如果传递了一个窗口过程的地址,则调用原先的窗口过程,不需要执行字符和字符串转换。

如果传递了一个内部数据结构的句柄,则系统要将字符和字符串转换成适当的类型(A N S I或U n i c o d e),然后调用原先的窗口过程。


出品,一剑[  

第24章 异常处理程序和软件异常


第24章 异常处理程序和软件异常

异常是我们不希望有的事件。在编写程序的时候,程序员不会想去存取一个无效的内存地址或用0来除一个数值。不过,这样的错误还是常常会发生的。C P U负责捕捉无效内存访问和用0除一个数值这种错误,并相应引发一个异常作为对这些错误的反应。C P U引发的异常,就是所谓的硬件异常( hardware exception)。在本章的后面,我们还会看到操作系统和应用程序也可以引发相应的异常,称为软件异常( software exception)

当出现一个硬件或软件异常时,操作系统向应用程序提供机会来考察是什么类型的异常被引发,并能够让应用程序自己来处理异常。下面就是异常处理程序的文法:

__try 
{
//Guarded body

...

}
__except(exception filter)
{
// Exception handler

...

}


注意__e x c e p t关键字。每当你建立一个t r y块,它必须跟随一个f i n a l l y块或一个e x c e p t块。一个try 块之后不能既有f i n a l l y块又有e x c e p t块。但可以在t r y - e x c e p t块中嵌套t r y - f i n a l l y块,反过来也可以。



24.1 通过例子理解异常过滤器和异常处理程序


与结束处理程序(前一章讨论过)不同,异常过滤器( exception filter)和异常处理程序是通过操作系统直接执行的,编译程序在计算异常过滤器表达式和执行异常处理程序方面不做什么事。下面几节的内容举例说明t r y - e x c e p t块的正常执行,解释操作系统如何以及为什么计算异常过滤器,并给出操作系统执行异常处理程序中代码的环境。


24.1.1 Funcmeister1


这里是一个t r y - e x c e p t i o n块的更具体的例子。

DWORD Funcmeister1() 
{
DWORD dwTemp;

//1. Do any processing here.

...

__try
{
//2. Perform some operation.
dwTemp = 0;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
// Handle an exception; this never executes.

...

}

//3. Continue processing.
return(dwTemp);
}


在F u n c m e i s t e r 1的t r y块中,只是把一个0赋给d w Te m p变量。这个操作决不会造成异常的引发,所以e x c e p t块中的代码永远不会执行。注意这与t r y - f i n a l l y行为的不同。在d w Te m p被设置成0之后,下一个要执行的指令是r e t u r n语句。

尽管在结束处理程序的t r y块中使用r e t u r n、g o t o、c o n t i n u e和b r e a k语句遭到强烈地反对,但在异常处理程序的t r y块中使用这些语句不会产生速度和代码规模方面的不良影响。这样的语句出现在与e x c e p t块相结合的t r y块中不会引起局部展开的系统开销。


24.1.2 Funcmeister2


让我们修改这个函数,看会发生什么事情:

DWORD Funcmeister2() 
{
DWORD dwTemp = 0;

//1. Do any processing here.

...

__try
{
//2. Perform some operation(s).
dwTemp = 5 / dwTemp; // Generates an exception
dwTemp += 10; // Never executes
}
__except( /* 3. Evaluate filter. */ EXCEPTION_EXECUTE_HANDLER)
{
//4. Handle an exception.

MessageBeep(0);

... }//5. Continue processing.
return(dwTemp);
}


F u n c m e i s t e r 2中,t r y块中有一个指令试图以0来除5。C P U将捕捉这个事件,并引发一个硬件异常。当引发了这个异常时,系统将定位到e x c e p t块的开头,并计算异常过滤器表达式的值,过滤器表达式的结果值只能是下面三个标识符之一,这些标识符定义在Wi n d o w s的E x c p t . h文件中(见表2 4 - 1)。

表24-1 标识符及其定义

标识符定义为
E X C E P T I O N _ E X E C U T E _ H A N D L E R1
E X C E P T I O N _ C O N T I N U E _ S E A R C H0
E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N-1

下面几节将讨论这些标识符如何改变线程的执行。在阅读这些内容时可参阅图2 4 - 1,该图概括了系统如何处理一个异常的情况。


图24-1 系统如何处理一个异常


24.2 EXCEPTION_EXECUTE_HANDLER

在F u n c m e i s t e r 2中,异常过滤器表达式的值是E X C E P T I O N _ E X E C U T E _ H A N D L E R。这个值的意思是要告诉系统:“我认出了这个异常。即,我感觉这个异常可能在某个时候发生,我已编写了代码来处理这个问题,现在我想执行这个代码。”在这个时候,系统执行一个全局展开(本章后面将讨论),然后执行向e x c e p t块中代码(异常处理程序代码)的跳转。在e x c e p t块中代码执行完之后,系统考虑这个要被处理的异常并允许应用程序继续执行。这种机制使Wi n d o w s应用程序可以抓住错误并处理错误,再使程序继续运行,不需要用户知道错误的发生。

但是,当e x c e p t块执行后,代码将从何处恢复执行?稍加思索,我们就可以想到几种可能性。

第一种可能性是从产生异常的C P U指令之后恢复执行。在F u n c m e i s t e r 2中执行将从对d w Te m p加1 0的指令开始恢复。这看起来像是合理的做法,但实际上,很多程序的编写方式使得当前面的指令出错时,后续的指令不能够继续成功地执行。

在F u n c m e i s t e r 2中,代码可以继续正常执行,但是, F u n c m e i s t e r 2已不是正常的情况。代码应该尽可能地结构化,这样,在产生异常的指令之后的C P U指令有望获得有效的返回值。例如,可能有一个指令分配内存,后面一系列指令要执行对该内存的操作。如果内存不能够被分配,则所有后续的指令都将失败,上面这个程序重复地产生异常。

这里是另外一个例子,说明为什么在一个失败的C P U指令之后,执行不能够继续。我们用下面的程序行来替代F u n c m e i s t e r 2中产生异常的C语句:

malloc(5 / dwTemp);
对上面的程序行,编译程序产生C P U指令来执行除法,将结果压入栈中,并调用m a l l o c函数。如果除法失败,代码就不能继续执行。系统必须向栈中压东西,否则,栈就被破坏了。

所幸的是,微软没有让系统从产生异常的指令之后恢复指令的执行。这种决策使我们免于面对上面的问题。

第二种可能性是从产生异常的指令恢复执行。这是很有意思的可能性。如果在e x c e p t块中有这样的语句会怎么样呢:

dwTemp = 2;

在e x c e p t块中有了这个赋值语句,可以从产生异常的指令恢复执行。这一次,将用2来除5,执行将继续,不会产生其他的异常。可以做些修改,让系统重新执行产生异常的指令。你会发现这种方法将导致某些微妙的行为。我们将在“ EXCEPTION_ CONTINUE_EXECUTION”一节中讨论这种技术。


第三种可能性是从e x c e p t块之后的第一条指令开始恢复执行。这实际是当异常过滤器表达式的值为E X C E P T I O N _ E X E C U T E _ H A N D L E R时所发生的事。在e x c e p t块中的代码结束执行后,控制从e x c e p t块之后的第一条指令恢复。

24.2.1 一些有用的例子

假如要实现一个完全强壮的应用程序,该程序需要每周7天,每天2 4小时运行。在今天的世界里,软件变得这么复杂,有那么多的变量和因子来影响程序的性能,笔者认为如果不用S E H,要实现完全强壮的应用程序简直是不可能的。我们先来看一个样板程序,即C的运行时函数s t r c p y:

char* strcpy(
char* strDestination,
const char* strSource);

这是一个相当简单的函数,它怎么会引起一个进程结束呢?如果调用者对这些参数中的某一个传递N U L L(或任何无效的地址),s t r c p y就引起一个存取异常,并且导致整个进程结束。


使用S E H,就可以建立一个完全强壮的s t r c p y函数:

char* RobustStrCpy(char* strDestination, const char* strSource)
{
__try
{
strcpy(strDestination, strSource);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
// Nothing to do here
}

return(strDestination);
}


这个函数所做的一切就是将对s t r c p y的调用置于一个结构化的异常处理框架中。如果s t r c p y执行成功,函数就返回。如果s t r c p y引起一个存取异常,异常过滤器返回E X C E P T I O N _E X E C U T E _ H A N D L E R,导致该线程执行异常处理程序代码。在这个函数中,处理程序代码什么也不做,R o b u s t S t r C p y只是返回到它的调用者,根本不会造成进程结束。


我们再看另外一个例子。这个函数返回一个字符串里的以空格分界的符号个数:

int RobustHowManyToken(const char* str) 
{
int nHowManyTokens = -1;  // -1 indicates failure
char* strTemp = NULL;  // Assume failure

__try
{
// Allocate a temporary buffer
strTemp = (char*) malloc(strlen(str) + 1);

// Copy the original string to the temporary buffer
strcpy(strTemp, str);

// Get the first token
char* pszToken = strtok(strTemp, " ");

// Iterate through all the tokens
for(; pszToken != NULL; pszToken = strtok(NULL, " "))
nHowManyTokens++;

nHowManyTokens++;// Add 1 since we started at -1
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
// Nothing to do here
}

//Free the temporary buffer (guaranteed)
free(strTemp);

return(nHowManyTokens);
}


这个函数分配一个临时缓冲区并将一个字符串复制到里面。然后该函数用C运行时函数strtok来获取字符串的符号。临时缓冲区是必要的,因strtok要修改它所操作的串。感谢有了SEH,这个非常简单的函数就处理了所有的可能性。我们来看在几个不同的情况下函数是如何执行的。

首先,如果调用者向函数传递了N U L L(或任何无效的内存地址),n H o w M a n y To k e n s被初始化成-1。在t r y块中对s t r l e n的调用会引起存取异常。异常过滤器获得控制并将控制转移给e x c e p t块,e x c e p t块什么也不做。在e x c e p t块之后,调用f r e e来释放临时内存块。但是,这个内存从未分配,所以结束调用f r e e,向它传递N U L L作为参数。ANSI C明确说明用N U L L作为参数调用f r e e是合法的。这时f r e e什么也不做,这并不是错误。最后,函数返回-1,指出失败。注意进程并没有结束。


其次,调用者可能向函数传递了一个有效的地址,但对m a l l o c的调用(在t r y块中)可能失败并返回N U L L。这将导致对s t r c p y的调用引起一个存取异常。同样,异常过滤器被调用,e x c e p t块执行(什么也不做),f r e e被调用,传递给它N U L L(什么也不做),返回-1,告诉调用程序该函数失败。注意进程也没有结束。


最后,假定调用者向函数传递了一个有效的地址,并且对m a l l o c的调用也成功了。这种情况下,其余的代码也会成功地在n H o w M a n y To k e n s变量中计算符号的数量。在t r y块的结尾,异常过滤器不会被求值, e x c e p t块中代码不会被执行,临时内存缓冲区将被释放,并向调用者返回n H o w M a n y To k e n s。


使用S E H会感觉很好。R o b u s t H o w M a n y To k e n函数说明了如何在不使用t r y - f i n a l l y的情况下保证释放资源。在异常处理程序之后的代码也都能保证被执行(假定函数没有从t r y块中返回—应避免的事情)。


我们再看一个特别有用的S E H例子。这里的函数重复一个内存块:

PBYTE RobustMemDup(PBYTE pbSrc, size_t cb)
{
PBYTE pbDup = NULL; // Assume failure

__try
{
// Allocate a buffer for the duplicate memory block
pbDup = (PBYTE) malloc(cb);

memcpy(pbDup, pbSrc, cb);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
free(pbDup);
pbDup = NULL;
}

return(pbDup);
}


这个函数分配一个内存缓冲区,并从源块向目的块复制字节。然后函数将复制的内存缓冲区的地址返回给调用程序(如果函数失败则返回N U L L)。希望调用程序在不需要缓冲区时释放它。这是在e x c e p t块中实际有代码的第一个例子。我们看一看这个函数在不同条件下是如何执行的。


• 如果调用程序对p b S r c参数传递了一个无效地址,或者如果对m a l l o c的调用失败(返回N U L L),m e m c p y将引起一个存取异常。该存取异常执行过滤器,将控制转移到e x c e p t块。在e x c e p t块内,内存缓冲区被释放, p b D u p被设置成N U L L以便调用程序能够知道函数失败。这里,注意ANSI C允许对f r e e传递N U L L。


• 如果调用程序给函数传递一个有效地址,并且如果对m a l l o c的调用成功,则新分配内存块的地址返回给调用程序。

24.2.2 全局展开

当一个异常过滤器的值为E X C E P T I O N _ E X E C U T E _ H A N D L E R时,系统必须执行一个全局展开(global unwind)。这个全局展开使所有那些在处理异常的t r y _ e x c e p t块之后开始执行但未完成的t r y - f i n a l l y块恢复执行。图2 4 - 2是描述系统如何执行全局展开的流程图,在解释后面的例子时,请参阅这个图。

void FuncOStimpy1()
{
//1. Do any processing here.

...

__try
{
//2. Call another function.
FuncORen1();

// Code here never executes.
}

__except( /* 6. Evaluate filter. */ EXCEPTION_EXECUTE_HANDLER)
{
//8. After the unwind, the exception handler executes.
MessageBox(…);
}

//9. Exception handled--continue execution.
}
void FuncORen1()
{
DWORD dwTemp = 0;

//3. Do any processing here.
__try
{
//4. Request permission to access protected data.
WaitForSingleObject(g_hSem, INFINITE);

//5. Modify the data.
// An exception is generated here.
g_dwProtectedData = 5 / dwTemp;
}
__finally
{
//7. Global unwind occurs because filter evaluated
// to EXCEPTION_EXECUTE_HANDLER.

// Allow others to use protected data.
ReleaseSemaphore(g_hSem, 1, NULL);
}

// Continue processing--never executes....
}


函数F u n c O S t i m p y 1和F u n c O r e n 1结合起来可以解释S E H最令人疑惑的方面。程序中注释的标号给出了执行的次序,我们现在开始做一些分析。


F u n c OSt i m p y 1开始执行,进入它的t r y块并调用F u n c O R e n 1。F u n c O R e n 1开始执行,进入它的t r y块并等待获得信标。当它得到信标,F u n c O R e n 1试图改变全局数据变量g _ d w P r o t e c t e d D a t a。但由于除以0而产生一个异常。系统因此取得控制,开始搜索一个与e x c e p t块相配的t r y块。因为F u n c O R e n 1中的t r y与同一个f i n a l l y块相配,所以系统再上溯寻找另外的t r y块。这里,系统在F u n c O S t i m p y 1中找到一个t r y块,并且发现这个t r y块与一个e x c e p t块相配。


系统现在计算与F u n c O S t i m p y 1中e x c e p t块相联的异常过滤器的值,并等待返回值。当系统看到返回值是E X C E P T I O N _ E X E C U T E _ H A N D L E R的,系统就在F u n c O R e n 1的f i n a l l y块中开始一个全局展开。注意这个展开是在系统执行F u n c O S t i m p y 1的e x c e p t块中的代码之前发生的。对于一个全局展开,系统回到所有未完成的t r y块的结尾,查找与f i n a l l y块相配的t r y块。在这里,系统发现的f i n a l l y块是F u n c O R e n 1中所包含的f i n a l l y块。


当系统执行F u n c O R e n 1 的f i n a l l y块中的代码时,就可以清楚地看到S E H的作用了。F u n c O R e n 1释放信标,使另一个线程恢复执行。如果这个f i n a l l y块中不包含R e l e a s e S e m a p h o r e的调用,则信标不会被释放。


在f i n a l l y块中包含的代码执行完之后,系统继续上溯,查找需要执行的未完成f i n a l l y块。在这个例子中已经没有这样的f i n a l l y块了。系统到达要处理异常的t r y - e x c e p t块就停止上溯。这时,全局展开结束,系统可以执行e x c e p t块中所包含的代码。


结构化异常处理就是这样工作的。S E H比较难于理解,是因为在代码的执行当中与系统牵扯太多。程序代码不再是从头到尾执行,系统使代码段按照它的规定次序执行。这种执行次序虽然复杂,但可以预料。按图2 4 - 1和图2 4 - 2的流程图去做,就可以有把握地使用S E H。



图24-2 系统如何执行一个全局展开

为了更好地理解这个执行次序,我们再从不同的角度来看发生的事情。当一个过滤器返回E X C E P T I O N _ E X E C U T E _ H A N D L E R时,过滤器是在告诉系统,线程的指令指针应该指向e x c e p t块中的代码。但这个指令指针在F u n c O R e n 1的t r y块里。回忆一下第2 3章,每当一个线程要从一个t r y - f i n a l l y块离开时,必须保证执行f i n a l l y块中的代码。在发生异常时,全局展开就是保证这条规则的机制。


24.2.3 暂停全局展开


通过在f i n a l l y块里放入一个r e t u r n语句,可以阻止系统去完成一个全局展开。请看下面的代码:

void FuncMonkey() 
{
__try 
{
FuncFish();
}
__except(EXCEPTION_EXECUTE_HANDLER) 
{
MessageBeep(0);
}
MessageBox(…);
}

void FuncFish()
{
FuncPheasant();
MessageBox(…);
}

void FuncPheasant()
{
__try
{
strcpy(NULL, NULL);
}

__finally
{
return;
}
}


在F u n c P h e a s a n t的t r y块中,当调用s t r c p y函数时,会引发一个内存存取异常。当异常发生时,系统开始查看是否有一个过滤器可以处理这个异常。系统会发现在F u n c M o n k e y中的异常过滤器是处理这个异常的,并且系统开始一个全局展开。

全局展开启动,先执行F u n c P h e a s a n t的f i n a l l y块中的代码。这个代码块包含一个r e t u r n语句。这个r e t u r n语句使系统停止做展开, F u n c P h e a s a n t将实际返回到F u n c F i s h。然后F u n c F i s h又返回到函数F u n c M o n k e y。F u n c M o n k e y中的代码继续执行,调用M e s s a g e B o x。

注意F u n c M o n k e y的异常块中的代码从不会执行对M e s s a g e B e e p的调用。F u n c P h e a s a n t的f i n a l l y块中的r e t u r n语句使系统完全停止了展开,继续执行,就像什么也没有发生。

微软专门设计S E H按这种方式工作。程序员有可能希望使展开停止,让代码继续执行下去。这种方法为程序员提供了一种手段。原则上,应该小心避免在f i n a l l y块中安排r e t u r n语句。



24.3 EXCEPTION_CONTINUE_EXECUTION


我们再仔细考察一下异常过滤器,看它是如何计算出定义在E x c p t . h中的三个异常标识符之一的。在“F u n c m e i s t e r 2”一节中,为简单起见,在过滤器里直接硬编码了标识符E X C E P T I O N _E X E C U T E _ H A N D L E R,但实际上可以让过滤器调用一个函数确定应该返回哪一个标识符。这里是另外一个例子。

char g_szBuffer[100];

void FunclinRoosevelt1()
{
int x = 0;
char *pchBuffer = NULL;

__try
{
*pchBuffer = 'J';
x = 5 / x;
}
__except(OilFilter1(&pchBuffer))
{
MessageBox(NULL, "An exception occurred", NULL, MB_OK);
}
MessageBox(NULL, "Function completed", NULL, MB_OK);
}

LONG OilFilter1(char **ppchBuffer)
{
if(ppchBuffer == NULL)
{
ppchBuffer = g_szBuffer;
return(EXCEPTION_CONTINUE_EXECUTION);
}
return(EXCEPTION_EXECUTE_HANDLER);
}


这里,首先遇到的问题是在我们试图向p c h B u ff e r所指向的缓冲区中放入一个字母‘ J’时发生的。因为这里没有初始化p c h B u ff e r,使它指向全局缓冲区g _ s z B u ff u r。p c h B u ff e r实际指向N U L L。C P U将产生一个异常,并计算与异常发生的t r y块相关联的e x c e p t块的异常过滤器。在e x c e p t块中,对O i l F i l t e r函数传递了p c h B u ff e r变量的地址。

当O i l F i l t e r获得控制时,它要查看 p p c h B u ff e r是不是N U L L,如果是,把它设置成指向全局缓冲区g _ s z B u ff e r。然后这个过滤器返回E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N。当系统看到过滤器的值是E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N时,系统跳回到产生异常的指令,试图再执行一次。这一次,指令将执行成功,字母‘ J’将放在g _ s z B u ff e r的第一个字节。

随着代码继续执行,我们又在t r y块中碰到除以0的问题。系统又要计算过滤器的值。这一次,O i l F i l t e r看到 p p c h B u ff e r不是N U L L,就返回E X C E P T I O N _ E X E C U T E _ H A N D L E R,这是告诉系统去执行e x c e p t块中的代码。这会显示一个消息框,用文本串报告发生了异常。

如你所见,在异常过滤器中可以做很多的事情。当然过滤器必须返回三个异常标识符之一,但可以执行任何其他你想执行的任务。

使用带警告的E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N

我们已经知道,修改F u n c l i n R o o s e v e l t 1函数中的问题可使系统继续执行,也可能不执行,这要取决于程序的目标C P U,取决于编译程序为C/C + +语句生成的指令,取决于编译程序的选项设置。

一个编译程序对下面的C / C + +语句可能生成两条机器指令:

*pchBuffer = 'J';
机器指令可能是这个样子:

MOV EAX, [pchBuffer]  // Move the address into a register
MOV [EAX], 'J'  // Move 'J' into the address
第二条指令产生异常。异常过滤器可以捕获这个异常,修改p c h B u ff e r的值,并告诉系统重新执行第二条C P U指令。但问题是,寄存器的值可能不改变,不能反映装入到p c h B u ff e r的新值,这样重新执行C P U指令又产生另一个异常。这就发生了死循环。

如果编译程序优化了代码,继续执行可能顺利;如果编译程序没有优化代码,继续执行就可能失败。这可能是个非常难修复的b u g,需要检查源代码生成的汇编语言程序,确定程序出了什么错。这个例子的寓意就是在异常过滤器返回E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N时,要特别地小心。

有一种情况可保证E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N每次总能成功:当离散地向一个保留区域提交存储区时。在第1 5章讨论过如何保存一个大的地址空间,并向这个地址空间离散地提交存储区。V M A l l o c示例程序说明了这个例子。编写V M A l l o c程序的一种更好的办法是必要时使用S E H提交存储区,而不是每次调用Virtual Alloc函数。

在第1 6章,我们讨论了线程栈。特别是,我们讲解了系统如何为线程的栈保留一个1 M B的地址空间范围,以及在线程需要内存区时,系统如何自动向栈提交新的内存区。为此,系统在内部建立了一个S E H框架。当一个线程试图去存取并不存在的栈存储区时,就产生一个异常。系统的异常过滤器可以确定这个异常是源于试图存取栈的保留地址空间。异常过滤器调用Vi r t u a l A l l o c向线程的栈提交更多的存储区,然后过滤器返回E X C E P T I O N _ C O N T I N U E _E X E C U T I O N。这时,试图存取栈存储区的C P U指令可以成功执行,线程可以继续运行。

将虚拟内存技术同结构化异常处理结合起来,可以编写一些执行非常快,非常高效的程序。下一章的S p r e a d S h e e t示例程序将说明如何使用S E H有效地实现内存管理。这个代码执行得非常快。


24.4 EXCEPTION_CONTINUE_SEARCH

迄今为止我们看到的例子都很平常。通过增加一个函数调用,让我们来看看其他方面的问题:

char g_szBuffer[100];

void FunclinRoosevelt2()
{
char *pchBuffer = NULL;

__try
{
FuncAtude2(pchBuffer);
}
__except(OilFilter2(&pchBuffer))
{
MessageBox(…);
}
}

void FuncAtude2(char *sz)
{
*sz = 0;
}

LONG OilFilter2(char **ppchBuffer)
{
if(*ppchBuffer == NULL)
{
*ppchBuffer = g_szBuffer;
return(EXCEPTION_CONTINUE_EXECUTION);
}
return(EXCEPTION_EXECUTE_HANDLER);
}


当F u n c l i n R o o s e v e l t 2执行时,它调用F u n c A t u d e 2并传递参数N U L L。当F u n c A t u d e 2执行时,引发了一个异常。同前面的例子一样,系统计算与最近执行的t r y块相关联的异常过滤器的值。在这个例子中,F u n c l i n R o o s e v e l t 2中的t r y块是最近执行的t r y块,所以系统调用O i l F i l t e r 2函数来求异常过滤器的值——尽管这个异常是在F u n c l i n A t u d e 2函数中产生的。

现在我们让问题变得更复杂一点,在程序中再增加一个t r y _ e x c e p t块。

char g_szBuffer[100];

void FunclinRoosevelt3()
{
char *pchBuffer = NULL;

__try
{
FuncAtude3(pchBuffer);
}
__except(OilFilter3(&pchBuffer))
{
MessageBox(…);
}
}

void FuncAtude3(char *sz)
{
__try
{
*sz = 0;
}
__except(EXCEPTION_CONTINUE_SEARCH)
{
// This never executes.

...

}
}

LONG OilFilter3(char **ppchBuffer)
{
if(*ppchBuffer == NULL)
{
*ppchBuffer = g_szBuffer;
return(EXCEPTION_CONTINUE_EXECUTION);
}
return(EXCEPTION_EXECUTE_HANDLER);
}


现在,当FuncAtude 3试图向地址N U L L里存放0时,会引发一个异常。但这时将执行F u n c A t u d e 3 的异常过滤器。F u n c A t u d e 3的异常过滤器很简单,只是取值E X C E P T I O N CONTINUE SEARCH。这个标识符是告诉系统去查找前面与一个e x c e p t块相匹配的t r y块,并调用这个t r y块的异常处理器。

因为F u n c A t u d e 3的过滤器的值为E X C E P T I O N _ C O N T I N U E _ S E A R C H,系统将查找前面的t r y块(在F u n c l i n R o o s e v e l t 3里),并计算其异常过滤器的值,这里异常过滤器是O i l F i l t e r 3。O i l F i l t e r 3看到p c h B u ff e r是N U L L,将p c h B u ff e r设定为指向全局缓冲区,然后告诉系统恢复执行产生异常的指令。这将使F u n c A t u d e 3的t r y块中的代码执行,但不幸的是, F u n c A t u d e 3的局部变量s z没有变化,恢复执行失败的指令只是产生另一个异常。这样,又造成死循环。

前面说过,系统要查找最近执行的与e x c e p t块相匹配的t r y块,并计算它的过滤器值。这就是说,系统在查找过程当中,将略过那些与f i n a l l y块相匹配而不是与e x c e p t块相匹配的t r y块。这样做的理由很明显: f i n a l l y块没有异常过滤器,系统没有什么要计算的。如果前面例子中F u n c A t u d e 3包含一个f i n a l l y块而不是e x c e p t块,系统将在一开始就通过F u n c l i n R o o s e v e l t 3的O i l F i l t e r 3计算异常过滤器的值。

第2 5章提供有关E X C E P T I O N _ C O N T I N U E _ S E A R C H的更多信息。



24.5 GetExceptionCode


一个异常过滤器在确定要返回什么值之前,必须分析具体情况。例如,异常处理程序可能知道发生了除以0引起的异常时该怎么做,但是不知道该如何处理一个内存存取异常。异常过滤器负责检查实际情况并返回适当的值。

下面的代码举例说明了一种方法,指出所发生异常的类别:

__try 
{
x = 0;
y = 4 / x;
}

__except((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
// Handle divide by zero exception.
}


内部函数G e t E x c e p t i o n C o d e返回一个值,该值指出所发生异常的种类:

DWORD GetExceptionCode();
下面列出所有预定义的异常和相应的含意,这些内容取自Platform SDK文档。这些异常标识符可以在Wi n B a s e . h文件中找到。我们对这些异常做了分类。

1. 与内存有关的异常

• E X C E P T I O N _ A C C E S S _ V I O L AT I O N。线程试图对一个虚地址进行读或写,但没有做适当的存取。这是最常见的异常。

• E X C E P T I O N _ D ATAT Y P E _ M I S A L I G N M E N T。线程试图读或写不支持对齐( a l i g n m e n t)的硬件上的未对齐的数据。例如, 1 6位数值必须对齐在2字节边界上,3 2位数值要对齐在4字节边界上。

• E X C E P T I O N _ A R R AY _ B O U N D S _ E X C E E D E D。线程试图存取一个越界的数组元素,相应的硬件支持边界检查。

• E X C E P T I O N _ I N _ PA G E _ E R R O R。由于文件系统或一个设备启动程序返回一个读错误,造成不能满足要求的页故障。

• E X C E P T I O N _ G U A R D _ PA G E。一个线程试图存取一个带有PA G E _ G U A R D保护属性的内存页。该页是可存取的,并引起一个E X C E P T I O N _ G U A R D _ PA G E异常。

• EXCEPTION_STA C K _ O V E R F L O W。线程用完了分配给它的所有栈空间。

• E X C E P T I O N _ I L L E G A L _ I N S T R U C T I O N。线程执行了一个无效的指令。这个异常由特定的C P U结构来定义;在不同的C P U上,执行一个无效指令可引起一个陷井错误。

• E X C E P T I O N _ P R I V _ I N S T R U C T I O N。线程执行一个指令,其操作在当前机器模式中不允许。

2. 与异常相关的异常

• E X C E P T I O N _ I N VA L I D _ D I S P O S I T I O N。一个异常过滤器返回一值,这个值不是E X C E P T I O N _ E X E C U T E _ H A N D L E R 、E X C E P T I O N _ C O N T I N U E _ S E A R C H、E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N三者之一。

• E X C E P T I O N _ N O N C O N T I N U A B L E _ E X C E P T I O N。一个异常过滤器对一个不能继续的异常返回E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N。

  1. 与调试有关的异常

• EXCEPTION_BREAKPOINT。遇到一个断点。

• E X C E P T I O N _ S I N G L E _ S T E P。一个跟踪陷井或其他单步指令机制告知一个指令已执行完毕。

• E X C E P T I O N _ I N VA L I D _ H A N D L E。向一个函数传递了一个无效句柄。

  1. 与整数有关的异常

• EXCEPTION_INT_DIVIDE_BY_ZERO。线程试图用整数0来除一个整数

• EXCEPTION_INT_OVERFLOW。一个整数操作的结果超过了整数值规定的范围。

  1. 与浮点数有关的异常

• E X C E P T I O N _ F LT _ D E N O R M A L _ O P E R A N D。浮点操作中的一个操作数不正常。不正常的值是一个太小的值,不能表示标准的浮点值。

• EXCEPTION_FLT _ D I V I D E _ B Y _ Z E R O。线程试图用浮点数0来除一个浮点。

• EXCEPTION_FLT _ I N E X A C T _ R E S U LT。浮点操作的结果不能精确表示成十进制小数。

• EXCEPTION_FLT _ I N VA L I D _ O P E R AT I O N。表示任何没有在此列出的其他浮点数异常。

• EXCEPTION_FLT _ O V E R F L O W。浮点操作的结果超过了允许的值。

• EXCEPTION_FLT _ S TA C K _ C H E C K。由于浮点操作造成栈溢出或下溢。

• EXCEPTION_FLT _ U N D E R F L O W。浮点操作的结果小于允许的值。

内部函数G e t E x c e p t i o n C o d e只能在一个过滤器中调用( - - e x c e p t之后的括号里),或在一个异常处理程序中被调用。下面的代码是合法的:

__try 
{
y = 0;
x = 4 / y;
}

__except(
((GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION) ||
(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO)) ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
switch(GetExceptionCode())
{
case EXCEPTION_ACCESS_VIOLATION:
//Handle the access violation.

...

break;

case EXCEPTION_INT_DIVIDE_BY_ZERO:
//Handle the integer divide by?.

...

break;
}
}

但是,不能在一个异常过滤器函数里面调用G e t E x c e p t i o n C o d e。编译程序会捕捉这样的错误。当编译下面的代码时,将产生编译错。

__try 
{
y = 0;
x = 4 / y;
}

__except(CoffeeFilter())
{
// Handle the exception.

...

}

LONG CoffeeFilter(void)
{
//Compilation error: illegal call to GetExceptionCode.
return((GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION) ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);
}


可以按下面的形式改写代码:

__try 
{
y = 0;
x = 4 / y;
}

__except(CoffeeFilter(GetExceptionCode()))
{
//Handle the exception.

...

}

LONG CoffeeFilter(DWORD dwExceptionCode)
{
return((dwExceptionCode == EXCEPTION_ACCESS_VIOLATION) ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);
}


异常代码遵循在Wi n E r r o r. h文件中定义的有关错误代码的规则。每个D W O R D被划分如表2 4 - 2所示。

表24-2 一个错误代码的构成

3 1 - 3 0 2 9 2 8 2 7 - 1 6 1 5 - 0
内容严重性系数微软/客户 保留 设备代码 异常代码
意义0=成功 1=信息 2=警告 3=错误0=微软定义 的代码 1=客户定义 的代码 必须为0 微软定义 (见表2 4 - 3 ) 微软/客户定义

目前,微软定义了下面一些设备代码(见表2 4 - 3 )。

表24-3 设备代码及其值

设备代码 设备代码
FA C I L I T Y _ N U L L 0 FA C I L I T Y _ C O N T R O L10
FA C I L I T Y _ R P C1 FA C I L I T Y _ C E RT11
FA C I L I T Y _ D I S PAT C H2 FA C I L I T Y _ I N T E R N E T12
FA C I L I T Y _ S TO R A G E3 FA C I L I T Y _ M E D I A S E RV E R13
FA C I L I T Y _ I T F4 FA C I L I T Y _ M S M Q14
FA C I L I T Y _ W I N 3 27 FA C I L I T Y _ S E T U PA P I15
FA C I L I T Y _ W I N D O W S8 FA C I L I T Y _ S C A R D16
FA C I L I T Y _ S E C U R I T Y9 FA C I L I T Y _ C O M P L U S17

我们将E X C E P T I O N _ A C C E S S _ V I O L AT I O N异常代码拆开来,看各位( b i t )都是什么。在Wi n B a s e . h中找到E X C E P T I O N _ A C C E S S _ V I O L AT I O N,它的值为0 x C 0 0 0 0 0 0 5:

  C  0 0  0  0  0  0  5(16进制)
1100  0000  0000  0000  0000  0000  0000  0101  (2进制)
第3 0位和第3 1位都是1,表示存取异常是一个错误(线程不能继续运行)。第2 9位是0,表示M i c r o s o f t已经定义了这个代码。第2 8位是0,留待后用。第1 6 位至2 7位是0,代表FA C I L I T Y _ N U L L(存取异常可发生在系统中任何地方,不是使用特定设备才发生的异常)。第0位到第1 5位包含一个数5,表示微软将存取异常这种异常的代码定义成5。


24.6 GetExceptionInformation

当一个异常发生时,操作系统要向引起异常的线程的栈里压入三个结构,这三个结构是:E X C E P T I O N _ R E C O R D结构、C O N T E X T结构和E X C E P T I O N _ P O I N T E R S结构。

E X C E P T I O N _ R E C O R D结构包含有关已发生异常的独立于C P U的信息,C O N T E X T结构包含已发生异常的依赖于C P U的信息。E X C E P T I O N _ P O I N T E R S结构只有两个数据成员,二者都是指针,分别指向被压入栈的E X C E P T I O N _ R E C O R D和C O N T E X T结构:

typedef struct _EXCEPTION_POINTERS
{
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

为了取得这些信息并在你自己的程序中使用这些信息,需要调用G e t E x c e p t i o n I n f o r m a t i o n函数:

PEXCEPTION_POINTERS GetExceptionInformation();

这个内部函数返回一个指向E X C E P T I O N _ P O I N T E R S结构的指针。

关于G e t E x c e p t i o n I n f o r m a t i o n函数,要记住的最重要事情是它只能在异常过滤器中调用,因为仅仅在处理异常过滤器时, C O N T E X T、E X C E P T I O N _ R E C O R D和E X C E P T I O N _P O I N T E R S才是有效的。一旦控制被转移到异常处理程序,栈中的数据就被删除。

如果需要在你的异常处理程序块里面存取这些异常信息(虽然很少有必要这样做),必须将E X C E P T I O N _ P O I N T E R S结构所指向的C O N T E X T数据结构和/或E X C E P T I O N _ R E C O R D数据结构保存在你所建立的一个或多个变量里。下面的代码说明了如何保存E X C E P T I O N _R E C O R D和C O N T E X T数据结构:

void FuncSkunk() 
{
//Declare variables that we can use to save the exception
//record and the context if an exception should occur.
EXCEPTION_RECORD SavedExceptRec;
CONTEXT SavedContext;

...

__try
{

...

}

__except(
SavedExceptRec =
*(GetExceptionInformation())->ExceptionRecord,
SavedContext =
*(GetExceptionInformation())->ContextRecord,
EXCEPTION_EXECUTE_HANDLER)
{
//We can use the SavedExceptRec and SavedContext
//variables inside the handler code block.
switch(SavedExceptRec.ExceptionCode)
{

...

}
}

...

}

注意在异常过滤器中c语言逗号(,)操作符的使用。许多程序员不习惯使用这个操作符。它告诉编译程序从左到右执行以逗号分隔的各表达式。当所有的表达式被求值之后,返回最后的(或最右的)表达式的结果。

在F u n c S k u n k中,左边的表达式将执行,将栈中的E X C E P T I O N _ R E C O R D结构保存在Saved ExceptRec局部变量里。这个表达式的结果是S a v e d E x c e p t R e c的值。这个结果被丢弃,再计算右边下一个表达式。第二个表达式将栈中的C O N T E X T结构保存在S a v e d C o n t e x t局部变量里。第二个表达式的结果是S a v e d C o n t e x t,同样当计算第三个表达式时丢弃第二个表达式的结果。第三个表达式很简单,只是一个数值E X C E P T I O N _ E X E C U T E _ H A N D L E R。这个最右边的表达式的结果就是整个由逗号分隔的表达式组的结果。

由于异常过滤器的值是E X C E P T I O N _ E X E C U T E _ H A N D L E R,e x c e p t块中的代码要执行。这时,已被初始过的S a v e d E x c e p t R e c和S a v e d C o n t e x t变量可以在e x c e p t块中使用。要记住,S a v e d E x c e p t R e c和S a v e d C o n t e x t变量要在t r y块之外说明,这一点很重要。

我们都可以猜到, E X C E P T I O N _ P O I N T E R S 结构的E x c e p t i o n R e c o r d 成员指向E X C E P T I O N _ R E C O R D结构:

typedef struct _EXCEPTION_RECORD 
{
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

E X C E P T I O N _ R E C O R D结构包含有关最近发生的异常的详细信息,这些信息独立于C P U:

• ExceptionCode包含异常的代码。这同内部函数G e t E x c e p t i o n C o d e返回的信息是一样的。

• E x c e p t i o n F l a g s包含有关异常的标志。当前只有两个值,分别是0(指出一个可以继续的异常)和E X C E P T I O N _ N O N C O N T I N U A B L E(指出一个不可继续的异常)。在一个不可继续的异常之后,若要继续执行,会引发一个E X C E P T I O N _ N O N C O N T I N U A B L E _E X C E P T I O N异常。

• E x c e p t i o n R e c o r d指向另一个未处理异常的E X C E P T I O N _ R E C O R D结构。在处理一个异常的时候,有可能引发另外一个异常。例如,异常过滤器中的代码就可能用零来除一个数。当嵌套异常发生时,可将异常记录链接起来,以提供另外的信息。如果在处理一个异常过滤器的过程当中又产生一个异常,就发生了嵌套异常。如果没有未处理异常,这个成员就包含一个N U L L。

• ExceptionAddress指出产生异常的C P U指令的地址。

• N u m b e r P a r a m e t e r s 规定了与异常相联系的参数数量( 0 到1 5 )。这是在E x c e p t i o n I n f o r m a t i o n数组中定义的元素数量。对几乎所有的异常来说,这个值都是零。

• E x c e p t i o n I n f o r m a t i o n规定一个附加参数的数组,用来描述异常。对大多数异常来说,数组元素是未定义的。

E X C E P T I O N _ R E C O R D结构的最后两个成员,N u m b e r P a r a m e t e r s和E x c e p t i o n I n f o r m a t i o n向异常过滤器提供一些有关异常的附加信息。目前只有一种类型的异常提供附加信息,就是E X C E P T I O N _ A C C E S S _ V I O L AT I O N。所有其他可能的异常都将N u m b e r P a r a m e t e r s设置成零。我们可以检验E x c e p t i o n I n f o r m a t i o n的数组成员来查看关于所产生异常的附加信息。

对于一个E X C E P T I O N _ A C C E S S _ V I O L AT I O N异常来说,E x c e p t i o n I n f o r m a t i o n [ 0 ]包含一个标志,指出引发这个存取异常的操作的类型。如果这个值是0,表示线程试图要读不可访问的数据。如果这个值是1,表示线程要写不可访问的数据。ExceptionInformation[1] 指出不可访问数据的地址。

通过使用这些成员,我们可以构造异常过滤器,提供大量有关程序的信息。例如,可以这样编写异常过滤器:

__try
{
...
}
__except(ExpFltr(GetExceptionInformation()->ExceptionRecord))
{
...
}

LONG ExpFltr(PEXCEPTION_RECORD pER)
{
char szBuf[300], *p;
DWORD dwExceptionCode = pER->ExceptionCode;

sprintf(szBuf, "Code = %x, Address = %p",
dwExceptionCode, pER->ExceptionAddress);

//Find the end of the string.
p = strchr(szBuf, 0);

// I used a switch statement in case Microsoft adds
// information for other exception codes in the future.
switch(dwExceptionCode)
{
case EXCEPTION_ACCESS_VIOLATION:
sprintf(p, "Attempt to %s data at address %p",
pER->ExceptionInformation[

default:
break;
}

MessageBox(NULL, szBuf, "Exception", MB_OK | MB_ICONEXCLAMATION);

return(EXCEPTION_CONTINUE_SEARCH);
}

E X C E P T I O N _ P O I N T E R S结构的C o n t e x t R e c o r d成员指向一个C O N T E X T结构(第7章讨论过)。这个结构是依赖于平台的,也就是说,对于不同的C P U平台,这个结构的内容也不一样。

本质上,对C P U上每一个可用的寄存器,这个结构相应地包含一个成员。当一个异常被引发时,可以通过检验这个结构的成员找到更多的信息。遗憾的是,为了得到这种可能的好处,要求程序员编写依赖于平台的代码,以确认程序所运行的机器,使用适当的C O N T E X T结构。最好的办法是在代码中安排一个# i f d e f s指令。Wi n d o w s支持的不同C P U的C O N T E X T结构定义在Wi n N T. h文件中。



24.7 软件异常


迄今为止,我们一直在讨论硬件异常,也就是C P U捕获一个事件并引发一个异常。在代码中也可以强制引发一个异常。这也是一个函数向它的调用者报告失败的一种方法。传统上,失败的函数要返回一些特殊的值来指出失败。函数的调用者应该检查这些特殊值并采取一种替代的动作。通常,这个调用者要清除所做的事情并将它自己的失败代码返回给它的调用者。这种错误代码的逐层传递会使源程序的代码变得非常难于编写和维护。

另外一种方法是让函数在失败时引发异常。用这种方法,代码更容易编写和维护,而且也执行得更好,因为通常不需要执行那些错误测试代码。实际上,仅当发生失败时也就是发生异常时才执行错误测试代码。

但令人遗憾的是,许多开发人员不习惯于在错误处理中使用异常。这有两方面的原因。第一个原因是多数开发人员不熟悉S E H。即使有一个程序员熟悉它,但其他程序员可能不熟悉它。如果一个程序员编写了一个引发异常的函数,但其他程序员并不编写S E H框架来捕获这个异常,那么进程就会被操作系统结束。

开发人员不使用S E H的第二个原因是它不能移植到其他操作系统。许多公司的产品要面向多种操作系统,因此希望有单一的源代码作为产品的基础,这是可以理解的。S E H是专门针对Wi n d o w s的技术。

本段讨论通过异常返回错误有关的内容。首先,让我们看一看Windows Heap函数,例如H e a p C r e a t e、h e a p A l l o c等。回顾第1 8章的内容,我们知道这些函数向开发人员提供一种选择。通常当某个堆( h e a p)函数失败,它会返回N U L L来指出失败。然而可以对这些堆函数传递H E A P _ G E N E R AT E _ E X C E P T I O N S标志。如果使用这个标志并且函数失败,函数不会返回N U L L,而是由函数引发一个S TAT U S _ N O _ M E M O RY软件异常,程序代码的其他部分可以用S E H框架来捕获这个异常。

如果想利用这个异常,可以编写你的t r y块,好像内存分配总能成功。如果内存分配失败,可以利用e x c e p t块来处理这个异常,或通过匹配t r y块与f i n a l l y块,清除函数所做的事。这非常方便。

程序捕获软件异常采取的方法与捕获硬件异常完全相同。也就是说,前一章介绍的内容可以同样适用于软件异常。

本节重讨论如何让你自己的函数引发软件异常,作为指出失败的方法。实际上,可以用类似于微软实现堆函数的方法来实现你的函数:让函数的调用者传递一个标志,告诉函数如何指出失败。

引发一个软件异常很容易,只需要调用R a i s e E x c e p t i o n函数:

VOID RaiseException(
DWORD dwExceptionCode,
DWORD dwExceptionFlags,
DWORD nNumberOfArguments,
CONST ULONG_PTR *pArguments);
第一个参数d w E x c e p t i o n C o d e是标识所引发异常的值。H e a p A l l o c函数对这个参数设定S TAT U S _ N O _ M E M O RY。如果程序员要定义自己的异常标识符,应该遵循标准Wi n d o w s错误代码的格式,像Wi n E r r o r. h文件中定义的那样。参阅表2 4 - 1。

如果要建立你自己的异常代码,要填充D W O R D的4个部分:

• 第3 1位和第3 0位包含严重性系数( s e v e r i t y )。

• 第2 9位是1(0表示微软建立的异常,如H e a p A l l o c的S TAT U S _ N O _ M E M O RY)。

• 第2 8位是0。

• 第2 7位到1 6位是某个微软定义的设备代码。

• 第1 5到0位是一个任意值,用来标识引起异常的程序段。

R a i s e E x c e p t i o n 的第二个参数d w E x c e p t i o n F l a g s ,必须是0 或E X C E P T I O N _N O N C O N T I N U A B L E。本质上,这个标志是用来规定异常过滤器返回E X C E P T I O N _CONTINUE EXECUTION来响应所引发的异常是否合法。如果没有向R a i s e E x c e p t i o n传递EXCEPTION NONCONTINUABLE参数值,则过滤器可以返回E X C E P T I O N _ C O N T I N U E E X E C U T I O N。正常情况下,这将导致线程重新执行引发软件异常的同一C P U指令。但微软已做了一些动作,所以在调用R a i s e E x c e p t i o n函数之后,执行会继续进行。

如果你向R a i s e E x c e p t i o n传递了E X C E P T I O N _ N O N C O N T I N U A B L E标志,你就是在告诉系统,你引发异常的类型是不能被继续执行的。这个标志在操作系统内部被用来传达致命(不可恢复)的错误信息。另外,当H e a p A l l o c引发S TAT U S _ N O _ M E M O RY软件异常时,它使用E X C E P T I O N _ N O N C O N T I N U A B L E标志来告诉系统,这个异常不能被继续。意思就是没有办法强制分配内存并继续运行。

如果一个过滤器忽略E X C E P T I O N _ N O N C O N T I N U A B L E并返回E X C E P T I O N _ C O N T I N U E E X E C U T I O N,系统会引发新的异常:E X C E P T I O N _ N O N C O N T I N U A B L E _ E X C E P T I O N。

当程序在处理一个异常的时候,有可能又引发另一个异常。比如说,一个无效的内存存取有可能发生在一个f i n a l l y块、一个异常过滤器、或一个异常处理程序里。当发生这种情况时,系统压栈异常。回忆一下G e t E x c e p t i o n I n f o r m a t i o n函数。这个函数返回EXCEPTION POINTERS结构的地址。E X C E P T I O N _ P O I N T E R S的E x c e p t i o n R e c o r d成员指向一个EXCEPTION R E C O R D结构,这个结构包含另一个E x c e p t i o n R e c o r d成员。这个成员是一个指向另外的E X C E P T I O N _R E C O R D的指针,而这个结构包含有关以前引发异常的信息。

通常系统一次只处理一个异常,并且E x c e p t i o n R e c o r d成员为N U L L。然而如果处理一个异常的过程中又引发另一个异常,第一个E X C E P T I O N _ R E C O R D结构包含有关最近引发异常的信息,并且这个E X C E P T I O N _ R E C O R D结构的E x c e p t i o n R e c o r d成员指向以前发生的异常的E X C E P T I O N R E C O R D结构。如果增加的异常没有完全处理,可以继续搜索这个E X C E P T I O N _ R E C O R D结构的链表,来确定如何处理异常。

R a i s e E x c e p t i o n的第三个参数n N u m b e r O f A rg u m e n t s和第四个参数p A rg u m e n t s,用来传递有关所引发异常的附加信息。通常,不需要附加的参数,只需对p A rg u m e n t s参数传递N U L L,这种情况下, R a i s e E x c e p t i o n函数忽略n N u m b e r O f A rg u m e n t s参数。如果需要传递附加参数,n N u m b e r O f A rg u m e n t s参数必须规定由p A rg u m e n t s参数所指向的U L O N G _ P T R数组中的元素数目。这个数目不能超过E X C E P T I O N _ M A X I M U M _ PA R A M E T E R S,EXCEPTION MAXIMUM_PARAMETERS 在Wi n N T. h中定义成1 5。

在处理这个异常期间,可使异常过滤器参照E X C E P T I O N _ R E C O R D结构中的N u m b e rP a r a m e t e r s和E x c e p t i o n I n f o r m a t i o n成员来检查n N u m b e r O f A rg u m e n t s和p A rg u m e n t s参数中的信息。

你可能由于某种原因想在自己的程序中产生自己的软件异常。例如,你可能想向系统的事件日志发送通知消息。每当程序中的一个函数发现某种问题,你可以调用R a i s e E x c e p t i o n并让某些异常处理程序上溯调用树查看特定的异常,或者将异常写到日志里或弹出一个消息框。你还可能想建立软件异常来传达程序内部致使错误的信息。


出品,一剑[