两个应用程序之间的互惠SendMessage如何工作?

时间:2018-01-30 18:59:09

标签: windows multithreading winapi sendmessage

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

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

  1. 应用程序A收到WM_CLOSE消息并将其处理为:

    DestroyWindow(hWnd_A);
    return 0;
    
  2. WM_DESTROY应用程序A上的行为如下:

    SendMessage(hWnd_B, WM_REGISTERED_MSG, 0, 0); //key line!!
    PostQuitMessage(0);
    return 0;
    
  3. WM_REGISTERED_MSG应用程序B上运行:

    SendMessage(hWnd_A, WM_ANOTHER_REGISTERED_MSG, 0, 0);
    return 0;
    
  4. WM_ANOTHER_REGISTERED_MSG应用程序A上运行:

    OutputDebugString("Cannot print this");
    return 0;
    
  5. 就是这样。

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

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

    事实证明,将SendMessage替换为SendMessageTimeout并使用{2} SMTO_BLOCK中的key line标记,会使整个事实实际阻塞。 (参见SendMessage的{​​{3}})

    然后,我的问题是:

    • 实际上,非排队的消息只是简单地直接调用过程B中SendMessage所做的窗口过程吗?

    • SendMessage如何知道何时发送排队或非排队的邮件?

    更新

    但是,我不明白A进程WM_ANOTHER_REGISTERED_MSG是怎样的。我期望的是,当发送该消息时,A的线程应该等待其SendMessage的调用返回。

    有什么见解?

    对读者的建议

    我建议将Adrian的答案作为RbMm的一个介绍,它遵循同样的思路,但更详细。

2 个答案:

答案 0 :(得分:2)

所描述的行为确实很有效。

  

SendMessage如何知道何时发送排队或非排队   消息?

来自Nonqueued Messages

  

发送非排队邮件的一些函数是...... SendMessage   ...

所以SendMessage总是发送非排队的消息。

SendMessage文档:

  

但是,发送线程将处理传入的非排队消息   在等待处理它的消息时。

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

当我们将SendMessage消息调用到另一个线程窗口时,它进入内核模式。内核模式始终记住usermode堆栈指针。然后我们切换到内核堆栈。当我们从内核返回到用户模式时 - 内核通常返回点,从用户模式调用它到保存堆栈。但存在和例外。其中之一:

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 < / em>, thread_B 等待。并开始等待当前线程的gui event_A 。如果 thread_B 执行 thread_B 中调用的消息检索代码(call GetMessagePeekMessageKeUserModeCallback。结果执行了窗口程序。在这里,它会调用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个进程。绝对足够的单个进程有2个线程。这里不需要特别注册的消息。为什么不使用说WM_APP作为测试信息?

    来自自我WM_CREATE
  1. thread_A 创建 thread_B 并将自己的窗口句柄传递给它。
  2. thread_B 创建自我窗口,但在WM_CREATE上只返回-1(对于失败创建窗口)
  3. 来自WM_DESTROY来电SendMessage(hwnd_A, WM_APP, 0, hwnd_B)
  4. thread_B (将自己 hwnd 传递给 lParam
  5. thread_A 获得WM_APP并致电SendMessage(hwnd_B, WM_APP, 0, 0)
  6. thread_B 获得WM_APP(因此WindowProc被递归调用,在下面的堆栈上WM_DESTROY
  7. thread_B 打印&#34;无法打印此&#​​34;并将自身ID返回到 thread_A
  8. thread_A 从调用SendMessage返回并将自身ID返回到 thread_B
  9. {li> thread_B SendMessage内的电话WM_DESTROY返回
    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

答案 1 :(得分:1)

  

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

A 中的SendMessage 等待它发送的消息(从A到B)完成,但是,等待它的时候,它能够将从其他线程发送的消息分派给该线程。

当在同一个线程上为一个窗口调用SendMessage时,我们认为它就像一串函数调用,最终导致目标windowproc并最终返回给调用者。

但是当消息跨越线程边界时,它并不那么简单。它就像一个客户端 - 服务器应用程序。 SendMessage将消息打包并向目标线程发出信号,表明它有消息要处理。那时候,它等了。

目标线程最终(我们希望)达到屈服点,它会检​​查该信号,获取消息并处理它。然后,目标线程发出信号表明它已完成工作。

原始帖子看到&#34;我完成了!&#34; signal并返回结果值。对于SendMessage的调用者来说,它看起来只是一个函数调用,但它实际上已经编排好了将消息编组到另一个线程并将结果编组回来。

几个Windows API调用是&#34;屈服点,&#34;检查是否有消息从另一个线程发送到当前线程的地方。最知名的是GetMessagePeekMessage,但某些类型的等待 - 包括SendMessage内的等待 - 也是屈服点。它的收益点使得A可以在等待B完成处理第一条消息的同时响应从B发回的消息。

这是A从B收到WM_ANOTHER_REGISTERED_MSG时的调用堆栈的一部分(步骤4):

A.exe!MyWnd::OnFromB(unsigned int __formal, unsigned int __formal, long __formal, int & __formal)
A.exe!MyWnd::ProcessWindowMessage(HWND__ * hWnd, unsigned int uMsg, unsigned int wParam, long lParam, long & lResult, unsigned long dwMsgMapID)
A.exe!ATL::CWindowImplBaseT<ATL::CWindow,ATL::CWinTraits<114229248,262400> >::WindowProc(HWND__ * hWnd, unsigned int uMsg, unsigned int wParam, long lParam)
atlthunk.dll!AtlThunk_Call(unsigned int,unsigned int,unsigned int,long)
atlthunk.dll!AtlThunk_0x00(struct HWND__ *,unsigned int,unsigned int,long)
user32.dll!__InternalCallWinProc@20()
user32.dll!UserCallWinProcCheckWow()
user32.dll!DispatchClientMessage()
user32.dll!___fnDWORD@4()
ntdll.dll!_KiUserCallbackDispatcher@12()
user32.dll!SendMessageW()
A.exe!MyWnd::OnClose(unsigned int __formal, unsigned int __formal, long __formal, int & __formal)

您可以在OnClose内看到SendMessageW ,但是,嵌套在其中,它从B获取回调消息并将其路由到A& #39;窗口程序。