windows - 两个应用程序之间相互的SendMessage-ing如何工作?

标签 windows multithreading winapi sendmessage

假设我有2个应用程序A和B。每个应用程序在主线程中创建一个窗口,而没有其他线程。

当按下应用程序A窗口的“关闭”按钮时,将发生以下情况:

  • 应用程序A收到WM_CLOSE消息并按以下方式进行处理:
    DestroyWindow(hWnd_A);
    return 0;
    
  • WM_DESTROY应用程序A上的行为类似于:
    SendMessage(hWnd_B, WM_REGISTERED_MSG, 0, 0); //key line!!
    PostQuitMessage(0);
    return 0;
    
  • WM_REGISTERED_MSG应用程序B上运行:
    SendMessage(hWnd_A, WM_ANOTHER_REGISTERED_MSG, 0, 0);
    return 0;
    
  • WM_ANOTHER_REGISTERED_MSG应用程序A上运行:
    OutputDebugString("Cannot print this");
    return 0;
    

  • 就是这样。

    MSDN中,我读到,当一条消息发送到另一个线程创建的窗口时,调用线程被阻塞,并且它只能处理非排队消息。

    现在,由于上面的代码可以正常工作并且不会挂起,我猜想从应用程序B(点3)对SendMessage的调用向应用程序A的窗口过程发送了一个非排队消息,该消息在应用程序B的主线程上下文中对其进行处理 。实际上,在第4点中没有显示带有OutputDebugString的调试输出。

    这也可以通过以下事实证明:用点2的SendMessage中的SendMessageTimeout标志替换为SMTO_BLOCKkey line会使整个事情实际上被阻塞。 (请参阅SendMessagedocumentation)

    然后,我的问题是:
  • 实际上,非排队消息仅仅是对进程B中SendMessage进行的窗口过程的简单直接调用吗?
  • SendMessage如何知道何时发送已排队或未排队的消息?


  • 更新

    仍然,我不明白A如何处理WM_ANOTHER_REGISTERED_MSG。我期望的是,当发送该消息时,A的线程应该正在等待其对SendMessage的调用返回。

    有见识吗?

    给读者的建议

    我建议阅读Adrian的答案,作为对RbMm的答案的介绍,该答案遵循相同的思路,但更为详尽。

    最佳答案

    所描述的行为确实很好。

    How does SendMessage know when to send queued or non-queued messages?



    来自Nonqueued Messages

    Some functions that send nonqueued messages are ... SendMessage ...



    因此 SendMessage 总是总是发送非排队消息。

    并从 SendMessage 文档中:

    However, the sending thread will process incoming nonqueued messages while waiting for its message to be processed.



    这意味着可以在SendMessage调用中调用窗口过程。并处理从另一个线程通过SendMessage发送的传入消息。这是如何实现的?

    当我们将SendMessage消息调用到另一个线程窗口时,它将进入内核模式。内核模式始终记住用户模式堆栈指针。然后我们切换到内核堆栈。当我们从内核返回到用户模式时-内核通常返回到用户模式从那里调用它并保存到堆栈的位置。但是存在和异常(exception)。其中之一:
    NTSYSCALLAPI
    NTSTATUS
    NTAPI
    KeUserModeCallback
    (
        IN ULONG RoutineIndex,
        IN PVOID Argument,
        IN ULONG ArgumentLength,
        OUT PVOID* Result,
        OUT PULONG ResultLenght
    );
    

    这是已导出但未记录的api。但是,win32k.sys始终将其用于调用窗口过程。这个api是如何工作的?

    首先,它在当前以下分配额外的内核堆栈框架。而不是保存用户模式堆栈指针并在其下方复制一些数据(参数)。最后,我们从内核退出到用户模式,但没有指出调用内核的位置,而是针对特殊功能(从ntdll.dll导出)-
    void
    KiUserCallbackDispatcher
    (
        IN ULONG RoutineIndex,
        IN PVOID Argument,
        IN ULONG ArgumentLength
    );
    

    并且堆栈位于堆栈指针下方的处,我们从那里早进入内核。KiUserCallbackDispatcher调用RtlGetCurrentPeb()->KernelCallbackTable[RoutineIndex](Argument, ArgumentLength)-通常这是user32.dll中的某些功能。该函数已经调用了相应的窗口程序。从窗口过程中,我们可以回叫内核-因为KeUserModeCallback分配了其他内核框架-我们将在此框架内进入内核,而不会损坏先前的内核。当窗口过程返回时-再次调用特殊的api
    __declspec(noreturn)
    NTSTATUS
    NTAPI
    ZwCallbackReturn
    (
        IN PVOID Result OPTIONAL,
        IN ULONG ResultLength,
        IN NTSTATUS Status
    );
    

    此api(如果没有错误)一定不要返回内核侧-已分配的内核帧将被取消分配,并且我们返回到KeUserModeCallback内的先前内核堆栈。所以我们终于从调用KeUserModeCallback的地方返回。然后我们回到用户模式,恰好从我们调用内核的那一点开始,就在同一堆栈上。

    GetMessage
    调用中,窗口过程究竟如何称为?正是通过这个。调用流为:
    GetMessage...
    --- kernel mode ---
    KeUserModeCallback...
    push additional kernel stack frame
    --- user mode --- (stack below point from where GetMessage enter kernel)
    KiUserCallbackDispatcher
    WindowProc
    ZwCallbackReturn
    -- kernel mode --
    pop kernel stack frame
    ...KeUserModeCallback
    --- user mode ---
    ...GetMessage
    

    与阻塞SendMessage完全相同。

    因此,当thread_A通过SendMessage将message_1发送到thread_B时,我们进入内核,信号为gui event_B,thread_B等待了它。并开始在gui event_A上等待当前线程。如果thread_B执行在thread_B中调用的消息检索代码(调用GetMessagePeekMessage)KeUserModeCallback。结果执行了它的窗口过程。在这里,它调用SendMessage将一些message_2发送回thread_A。结果,我们将event_A设置为在其中thread_A等待并在event_B上开始等待。 thread_A将被唤醒并调用KeUserModeCallback。它将用此消息调用Window过程。当它返回时(假设这次我们不再调用SendMessage),我们再次发信号给event_B并开始等待event_A。
    现在thread_B从SendMessage返回,然后从窗口过程返回-完成句柄原始message_1。将设置为event_A。 thread_A唤醒并从SendMessage返回。调用流程将是下一个:
    thread_A                        thread_B
    ----------------------------------------------------
                                    GetMessage...
                                    wait(event_B)
    SendMessage(WM_B)...
    set(event_B)
    wait(event_A)
                                    begin process WM_B...
                                    KeUserModeCallback...
                                        KiUserCallbackDispatcher
                                        WindowProc(WM_B)...
                                        SendMessage(WM_A)...
                                        set(event_A)
                                        wait(event_B)
    begin process WM_A...
    KeUserModeCallback...
        KiUserCallbackDispatcher
        WindowProc(WM_A)...
        ...WindowProc(WM_A)
        ZwCallbackReturn
    ...KeUserModeCallback
    set(event_B)
    ...end process WM_A
    wait(event_A)
                                        ...SendMessage(WM_A)
                                        ...WindowProc(WM_B)
                                        ZwCallbackReturn
                                    ...KeUserModeCallback
                                    set(event_A)
                                    ...end process WM_B
                                    wait(event_B)
    ...SendMessage(WM_B)
                                    ...GetMessage
    

    还要注意,当我们处理WM_DESTROY消息时-窗口仍然有效,并调用处理传入的消息。我们可以实现下一个演示:首先,我们不需要两个过程。具有2个线程的绝对足够的单个进程。不需要特殊的注册消息。为什么不使用say WM_APP作为测试消息?

    自我WM_CREATE中的
  • thread_A创建thread_B并将其自己的窗口句柄传递给它。
  • thread_B创建自己的窗口,但是在WM_CREATE上,仅返回-1(用于失败创建窗口)
  • WM_DESTROY中的
  • thread_B调用SendMessage(hwnd_A, WM_APP, 0, hwnd_B)(以lParam身份传递自己的所有权)
  • thread_A得到了WM_APP并调用SendMessage(hwnd_B, WM_APP, 0, 0)
  • thread_B获得了WM_APP(因此递归调用了WindowProc,在堆栈下面是WM_DESTROY
  • thread_B打印“无法打印”,并将自己的ID返回给thread_A
  • 从调用SendMessage返回的
  • thread_A并将自身ID返回给thread_B
  • SendMessage内的调用WM_DESTROY返回的
  • thread_B

  • ULONG WINAPI ThreadProc(PVOID hWnd);
    
    struct WNDCTX 
    {
        HANDLE hThread;
        HWND hWndSendTo;
    };
    
    LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
        WNDCTX* ctx = reinterpret_cast<WNDCTX*>(GetWindowLongPtrW(hWnd, GWLP_USERDATA));
    
        switch (uMsg)
        {
        case WM_NULL:
            DestroyWindow(hWnd);
            break;
        case WM_APP:
            DbgPrint("%x:%p>WM_APP:(%p, %p)\n", GetCurrentThreadId(), _AddressOfReturnAddress(), wParam, lParam);
    
            if (lParam)
            {
                DbgPrint("%x:%p>Send WM_APP(0)\n", GetCurrentThreadId(), _AddressOfReturnAddress());
                LRESULT r = SendMessage((HWND)lParam, WM_APP, 0, 0);
                DbgPrint("%x:%p>SendMessage=%p\n", GetCurrentThreadId(), _AddressOfReturnAddress(), r);
                PostMessage(hWnd, WM_NULL, 0, 0);
            }
            else
            {
                DbgPrint("%x:%p>Cannot print this\n", GetCurrentThreadId(), _AddressOfReturnAddress());
            }
    
            return GetCurrentThreadId();
    
        case WM_DESTROY:
    
            if (HANDLE hThread = ctx->hThread)
            {
                WaitForSingleObject(hThread, INFINITE);
                CloseHandle(hThread);
            }
    
            if (HWND hWndSendTo = ctx->hWndSendTo)
            {
                DbgPrint("%x:%p>Send WM_APP(%p)\n", GetCurrentThreadId(), _AddressOfReturnAddress(), hWnd);
                LRESULT r = SendMessage(hWndSendTo, WM_APP, 0, (LPARAM)hWnd);
                DbgPrint("%x:%p>SendMessage=%p\n", GetCurrentThreadId(), _AddressOfReturnAddress(), r);
            }
            break;
    
        case WM_NCCREATE:
            SetLastError(0);
    
            SetWindowLongPtr(hWnd, GWLP_USERDATA, 
                reinterpret_cast<LONG_PTR>(reinterpret_cast<CREATESTRUCT*>(lParam)->lpCreateParams));
    
            if (GetLastError())
            {
                return 0;
            }
            break;
    
        case WM_CREATE:
    
            if (ctx->hWndSendTo)
            {
                return -1;
            }
            if (ctx->hThread = CreateThread(0, 0, ThreadProc, hWnd, 0, 0))
            {
                break;
            }
            return -1;
    
        case WM_NCDESTROY:
            PostQuitMessage(0);
            break;
        }
    
        return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
    
    static const WNDCLASS wndcls = { 
        0, WindowProc, 0, 0, (HINSTANCE)&__ImageBase, 0, 0, 0, 0, L"lpszClassName" 
    };
    
    ULONG WINAPI ThreadProc(PVOID hWndSendTo)
    {
        WNDCTX ctx = { 0, (HWND)hWndSendTo };
    
        CreateWindowExW(0, wndcls.lpszClassName, 0, 0, 0, 0, 0, 0, HWND_MESSAGE, 0, 0, &ctx);
    
        return 0;
    }
    
    void DoDemo()
    {
        DbgPrint("%x>test begin\n", GetCurrentThreadId());
    
        if (RegisterClassW(&wndcls))
        {
            WNDCTX ctx = { };
    
            if (CreateWindowExW(0, wndcls.lpszClassName, 0, 0, 0, 0, 0, 0, HWND_MESSAGE, 0, 0, &ctx))
            {
                MSG msg;
    
                while (0 < GetMessage(&msg, 0, 0, 0))
                {
                    DispatchMessage(&msg);
                }
            }
    
            UnregisterClassW(wndcls.lpszClassName, (HINSTANCE)&__ImageBase);
        }
    
        DbgPrint("%x>test end\n", GetCurrentThreadId());
    }
    

    我得到下一个输出:
    d94>test begin
    6d8:00000008884FEFD8>Send WM_APP(0000000000191BF0)
    d94:00000008880FF4F8>WM_APP:(0000000000000000, 0000000000191BF0)
    d94:00000008880FF4F8>Send WM_APP(0)
    6d8:00000008884FEB88>WM_APP:(0000000000000000, 0000000000000000)
    6d8:00000008884FEB88>Cannot print this
    d94:00000008880FF4F8>SendMessage=00000000000006D8
    6d8:00000008884FEFD8>SendMessage=0000000000000D94
    d94>test end
    

    递归调用WM_APP时,thread_B的最有趣的外观堆栈跟踪

    enter image description here

    关于windows - 两个应用程序之间相互的SendMessage-ing如何工作?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48528666/

    相关文章:

    python - 如何在 Python 中处理 PyCapsule 类型

    c++ - 从 char* 到 wchar_t 的转换

    windows - 无法在 Windows Server 2008 R2 标准上运行 windbg

    android - 我需要一种从线程获取数据到另一个 Activity 的好方法

    windows - 有什么理由不在 COM+ 应用程序中承载 COM 服务器?

    multithreading - c++11线程的优势

    c# - 激活线程 C#

    c++ - 在 WIN32 下无窗口显示 HTML 内容

    使用 phpseclib 通过 SSH 连接到 MySQL 的 PHP 代码

    .net - 在未安装 Excel 的情况下以编程方式填充 Excel 模板