关于windows:两个应用程序之间的互惠SendMessage-ing是如何工作的?

How does reciprocal SendMessage-ing between two applications work?

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

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

  • 应用程序 A 接收到 WM_CLOSE 消息并按如下方式处理它:

    1
    2
    DestroyWindow(hWnd_A);
    return 0;
  • WM_DESTROY 应用程序 A 的行为类似于:

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

    1
    2
    SendMessage(hWnd_A, WM_ANOTHER_REGISTERED_MSG, 0, 0);
    return 0;
  • WM_ANOTHER_REGISTERED_MSG 应用程序 A 运行:

    1
    2
    OutputDebugString("Cannot print this");
    return 0;

  • 就是这样。

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

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

    这也被以下事实证明:将 SendMessage 替换为 SendMessageTimeout 并在第 2 点的 key line 中使用 SMTO_BLOCK 标志,使整个事情实际上阻塞。 (参见 SendMessage 的文档)

    那么,我的问题是:

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

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

    更新

    不过,我还是不明白 A 是如何处理 WM_ANOTHER_REGISTERED_MSG 的。我期望的是,当发送该消息时,A\\ 的线程应该等待它对 SendMessage 的调用返回。

    有什么见解吗?

    给读者的建议

    我建议阅读 Adrian 的回答作为 RbMm 的介绍,它遵循相同的思路,但更详细。


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

    How does SendMessage know when to send queued or non-queued
    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消息到另一个线程窗口时,它进入内核模式。内核模式总是记住用户模式堆栈指针。我们切换到内核堆栈。当我们从内核返回到用户模式时 - 内核通常会返回到用户模式调用它的位置并保存到堆栈。但存在和例外。其中之一:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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 导出)函数 -

    1
    2
    3
    4
    5
    6
    7
    void
    KiUserCallbackDispatcher
    (
        IN ULONG RoutineIndex,
        IN PVOID Argument,
        IN ULONG ArgumentLength
    );

    并且堆栈位于堆栈指针下方,我们从这里提前进入内核。
    KiUserCallbackDispatcher 调用 RtlGetCurrentPeb()->KernelCallbackTable[RoutineIndex](Argument, ArgumentLength) - 通常这是 user32.dll 中的一些函数。这个函数已经调用了对应的窗口过程。从窗口过程中我们可以回调内核——因为 KeUserModeCallback 分配了额外的内核框架——我们将在这个框架内进入内核并且不会损坏之前的内核。当窗口过程返回时 - 再次称为

    的特殊 api

    1
    2
    3
    4
    5
    6
    7
    8
    9
    __declspec(noreturn)
    NTSTATUS
    NTAPI
    ZwCallbackReturn
    (
        IN PVOID Result OPTIONAL,
        IN ULONG ResultLength,
        IN NTSTATUS Status
    );

    这个 api(如果没有错误)永远不能返回 - 在内核端 - 分配的内核帧被释放,我们返回到 KeUserModeCallback 内的先前内核堆栈。所以我们最终从调用 KeUserModeCallback 的地方返回。然后我们回到用户模式,正是从我们调用内核的点开始,在同一个堆栈上。

    真的是如何在调用 GetMessage 中调用窗口过程?正是通过这个。呼叫流程是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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 向 thread_B 发送 message_1 时 - 我们进入内核,发送信号 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。将使用此消息调用它的窗口过程。当它返回时(假设这次我们不再调用 SendMessage)我们再次返回 event_B 并开始等待 event_A。
    现在 thread_B 从 SendMessage 返回,然后从窗口过程返回 - 完成处理原始 message_1。将是 event_A 集。 thread_ASendMessage 唤醒并返回。接下来是呼叫流程:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    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 个线程的单个进程。并且这里不需要特别注册的消息。为什么不使用say WM_APP 作为测试消息?

  • 来自 self WM_CREATE 的 thread_A 创建 thread_B 并将自己的窗口句柄传递给它。
  • thread_B 创建自己的窗口,但在 WM_CREATE 上简单地返回 -1(失败创建窗口)
  • WM_DESTROY 中的 thread_B 调用 SendMessage(hwnd_A, WM_APP, 0, hwnd_B)(将 self hwnd 作为 lParam 传递)
  • thread_A 得到 WM_APP 并调用 SendMessage(hwnd_B, WM_APP, 0, 0)
  • thread_B 得到 WM_APP (所以 WindowProc 被递归调用,在堆栈 WM_DESTROY
  • thread_B print "Cannot print this" 并将自身 ID 返回给 thread_A
  • 从调用 SendMessage 返回的 thread_A 并将自身 ID 返回给 thread_B
  • WM_DESTROY 内的调用 SendMessage 返回的 thread_B
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    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)\
    ", GetCurrentThreadId(), _AddressOfReturnAddress(), wParam, lParam);

            if (lParam)
            {
                DbgPrint("%x:%p>Send WM_APP(0)\
    ", GetCurrentThreadId(), _AddressOfReturnAddress());
                LRESULT r = SendMessage((HWND)lParam, WM_APP, 0, 0);
                DbgPrint("%x:%p>SendMessage=%p\
    ", GetCurrentThreadId(), _AddressOfReturnAddress(), r);
                PostMessage(hWnd, WM_NULL, 0, 0);
            }
            else
            {
                DbgPrint("%x:%p>Cannot print this\
    ", 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)\
    ", GetCurrentThreadId(), _AddressOfReturnAddress(), hWnd);
                LRESULT r = SendMessage(hWndSendTo, WM_APP, 0, (LPARAM)hWnd);
                DbgPrint("%x:%p>SendMessage=%p\
    ", 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\
    ", 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\
    ", GetCurrentThreadId());
    }

    我得到了下一个输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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

    thread_B 在 WM_APP

    上递归调用时最有趣的堆栈跟踪

    enter


    Still, I do not understand how does A process WM_ANOTHER_REGISTERED_MSG. What I would expect is that when that message is sent, A's thread should be waiting for its call to SendMessage to return.

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

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

    但是当消息跨越线程边界时,就没有那么简单了。它变得像一个客户端-服务器应用程序。 SendMessage 将消息打包并通知目标线程它有消息要处理。到那时,它会等待。

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

    原始线程看到"I\\'m done!"信号并返回结果值。对于 SendMessage 的调用者来说,它看起来只是一个函数调用,但实际上它被编排为将消息编组到另一个线程并将结果编组返回。

    几个 Windows API 调用是"让步点",用于检查是否有消息从另一个线程发送到当前线程。最著名的是 GetMessagePeekMessage,但某些类型的等待——包括 SendMessage 内的等待——也是让步点。正是这个让步点使 A 可以在等待 B 完成处理第一条消息的同时响应从 B 发回的消息。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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 的窗口过程。