WinApi:消息循环可以被异步过程调用中断吗?

WinApi: Can the message loop be interrupted by an Async Procedure Call?

以下代码注册一个低级鼠标钩子来全局监控鼠标事件。
这是我能得到的最简单的工作示例。
使用 VC++ 2010 编译:cl test.cpp /link /entry:mainCRTStartup /subsystem:windows

#include <windows.h>

HWND label1 ;

//THE HOOK PROCEDURE
LRESULT CALLBACK mouseHookProc(int aCode, WPARAM wParam, LPARAM lParam){
    static int msgCount = 0 ;
    static char str[20] ;
    SetWindowText( label1, itoa(++msgCount, str, 10) ) ;
    return CallNextHookEx(NULL, aCode, wParam, lParam) ;
}

int main(){
    /**///  STANDARD WINDOW CREATION PART //////////////////////////////////////////////////////
    /**/        
    /**/    WNDCLASSEX classStruct = { sizeof(WNDCLASSEX), 0, DefWindowProc, 0, 0, GetModuleHandle(NULL), NULL,
    /**/                            LoadCursor(NULL, IDC_ARROW), HBRUSH(COLOR_BTNFACE+1), NULL, "winClass", NULL } ;
    /**/    RegisterClassEx(&classStruct) ;
    /**/
    /**/    HWND mainWin = CreateWindow("winClass", "", 0, 200,200, 100,100, NULL, NULL, NULL, NULL) ;
    /**/    ShowWindow(mainWin, SW_SHOWDEFAULT) ;
    /**/
    /**/    label1 = CreateWindow("static", "0", WS_CHILD, 5,5, 80,20, mainWin, NULL, NULL, NULL) ;
    /**/    ShowWindow(label1, SW_SHOWNOACTIVATE) ;
    /**/
    /**///  END OF WINDOW CREATION PART ////////////////////////////////////////////////////////

    //HOOK INSTALATION  
    HHOOK hookProc = SetWindowsHookEx(WH_MOUSE_LL, mouseHookProc, GetModuleHandle(NULL), 0) ;

    //MESSAGE LOOP
    MSG msg ;
    while( GetMessage(&msg, NULL, 0, 0) ){
        TranslateMessage(&msg) ;
        DispatchMessage(&msg) ;
    }

    UnhookWindowsHookEx(hookProc) ;
}

这是基本的一个线程,一个 window,一个消息泵示例。除了鼠标挂钩。

我怀疑这段代码所做的事情与我在 SO、MSDN、论坛、博客等中反复阅读的两件事相矛盾。

  1. 全局挂钩程序必须驻留在 DLL 中
    MSDN documentation for SetWindowsHookEx 证实了这一点:

    If the dwThreadId parameter is zero, the lpfn parameter MUST point to a hook procedure in a DLL

  2. 无法中断 GUI 线程(带有消息泵的线程),因为 GetMessase 的等待状态不可警报。这意味着当 GetMessage 阻塞等待更多消息时,它无法接收到中断其等待状态的信号。

然而,这里没有任何DLL可以看到,并且钩子程序必须中断线程,否则程序将无法运行,但确实如此(我假设该程序中只有一个线程).

所以要么我完全误解了这两点,要么这段代码的工作方式与我期望的异步过程调用方法不匹配。

不管怎样,我对这里发生的事情一无所知。

能否解释一下这段代码的工作原理。
是单线程程序吗?
钩子程序是否中断了线程?
以上两点是真的吗?

仅当需要将挂钩注入另一个进程时,挂钩过程才必须在 DLL 中。 WH_MOUSE_LL

However, the WH_MOUSE_LL hook is not injected into another process. Instead, the context switches back to the process that installed the hook and it is called in its original context. Then the context switches back to the application that generated the event.

所以这里不需要DLL(干嘛??),钩子程序也可以放在EXE里。

Is it a single-thread program?

总的来说是的。如果不考虑可能的系统工作线程,但是在第一个线程的上下文中调用的所有消息和挂钩

Is the hook procedure interrupting the thread?

来自 MSDN

This hook is called in the context of the thread that installed it. The call is made by sending a message to the thread that installed the hook. Therefore, the thread that installed the hook must have a message loop.

所以可以说mouseHookProc里面叫了GetMessage叫了。此函数在内核中等待消息。当系统需要调用挂钩程序时,它通过 KiUserCallbackDispatcher 调用来实现。 "interrupting the thread" - 打断是什么意思?

1.) 如本例所示,这是不正确的。钩子过程必须在 DLL 中,只有当钩子必须在接收消息的线程的上下文中调用时,所以在任意进程上下文中 - 在这种情况下我们需要将代码注入另一个进程 - 因为这个和 DLL 需要。如果我们总是在自进程上下文中调用 - 没有注入,不需要 DLL

2.) GetMessage确实等待不处于可报警状态,所以在GetMessage中代码等待时任何APC都无法传递,但这里的APC绝对不相关。 APC 和 windows 消息传递不同的功能。当然存在和相似点。对于 APC 和一些 windows 消息传递,线程必须在内核中等待。对于处于可警报状态的 APC,对于 GetMessagePeekMessage 中的 windows 条消息。对于 APC 传递系统调用 KiUserApcDispatcher,对于 windows 条消息 KiUserCallbackDispatcher。两者都是来自内核模式的回调,但它是在不同的条件下调用的


当代码执行可以在任意位置中断并且中断例程开始执行时,中断的确切含义是。 从这个意义上说,中断在 windows 用户模式下根本不存在。 APC 或 windows 消息(挂钩消息是 windows 消息的特例)永远不会在任意位置中断执行,而是使用回调机制。例外是特例。线程必须首先调用一些 api 以进入内核 space(对于 windows 消息,这是 GetMessagePeekMessage,对于 APC - 在可警报状态函数中等待或ZwTestAlert)。然后内核可以使用回调给用户 space 来传递 APC 或 windows 消息。目前从内核到用户只有3个回调点space(这个从win2000到win10没有变化) KiUserApcDispatcher - 用于 APC 交付, KiUserCallbackDispatcher - 用于调用 window 过程或挂钩过程 KiUserExceptionDispatcher - 用于异常基础设施 - 这最接近于中断,

此行为明确记录在 LowLevelMouseProc:

This hook is called in the context of the thread that installed it. The call is made by sending a message to the thread that installed the hook.

当鼠标输入事件即将被放入线程的消息队列时,系统会向安装钩子的线程发送消息,等待它return,然后继续处理输入事件。

钩子不需要在 DLL 中就可以工作。所有这些都是在鼠标输入事件被放入目标线程的输入队列之前完成的,因此不需要中断消息检索功能。一切按顺序执行:

  • 从硬件输入队列中拾取输入事件。
  • 向所有低级挂钩发送消息。
  • 如果任何挂钩return输入了非零值,则删除输入事件。
  • 否则,将其放入目标线程的消息队列。
  • 目标线程可以使用任何消息检索函数(GetMessagePeekMessage 等)获取输入事件。


关于如何在挂钩应用程序中实现的一些注意事项:

挂钩应用程序需要 运行 消息循环,因为系统会向它发送消息,以通知它有关原始输入线程即将放入目标线程的输入队列的输入事件. GetMessage 调用(在挂钩应用程序中)在挂钩消息的情况下充当调度程序,并在 returning 之前调用挂钩过程。即使 GetMessage 是一个阻塞调用,它也可以被唤醒(不会被警告1))。每当消息可用于检索时,它可能正在等待收到信号的 Event object。顺便说一句,在您的挂钩应用程序中不需要对 TranslateMessageDispatchMessage 的调用。


1) 参考:The alertable wait is the non-GUI analog to pumping messages.