windows - Hvordan fungerer gensidig SendMessage-ing mellem to applikationer?

Indlæg af Hanne Mølgaard Plasc

Problem



Antag at jeg har 2 ansøgninger, A og B. Hver af dem skaber et vindue i hovedtråden og har ingen andre tråde.


Når knappen 'Luk' i vinduet med applikation A trykkes, sker der følgende:



  1. Applikation A modtager en WM\_CLOSE besked og håndterer den som:


    DestroyWindow(hWnd\_A);
    return 0;
    

  2. WM\_DESTROY applikation A opfører sig som:


    SendMessage(hWnd\_B, WM\_REGISTERED\_MSG, 0, 0); //key line!!
    PostQuitMessage(0);
    return 0;
    

  3. WM\_REGISTERED\_MSG ansøgning B kører:


    SendMessage(hWnd\_A, WM\_ANOTHER\_REGISTERED\_MSG, 0, 0);
    return 0;
    

  4. WM\_ANOTHER\_REGISTERED\_MSG ansøgning A kører:


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



Og det er det.


Fra MSDN læser jeg, at når en besked sendes til et vindue, der er oprettet af en anden tråd, er opkaldstråden blokeret, og den kan kun behandle meddelelser uden for køen. [77]


Nu, da ovenstående kode virker og ikke hænger, antager jeg, at opkaldet til SendMessage fra ansøgning B (punkt 3) sender en besked uden for køen til applikationens vinduesprocedure, som behandler det i forbindelse med ansøgning B 's hovedtråd . Faktisk vises ingen fejlfejl output med OutputDebugString i punkt 4.


Dette fremgår også af, at erstatning af SendMessage med SendMessageTimeout med SMTO\_BLOCK -flagget i
SendMessage(hWnd\_B, WM\_REGISTERED\_MSG, 0, 0); //key line!!
PostQuitMessage(0);
return 0;
af punkt 2 gør hele ting faktisk blokeret. (Se dokumentation for SendMessage) [78]


Derefter er mine spørgsmål:



  • Faktisk er meddelelser uden for køen bare enkle direkte opkald til vindueproceduren lavet af SendMessage i proces B?

  • Hvordan kan SendMessage hvornår man sender meddelelser i kø eller i kø?






UPDATE


Stadig forstår jeg ikke, hvordan en proces WM\_ANOTHER\_REGISTERED\_MSG. Hvad jeg ville forvente er, at når denne besked er sendt, skal A 's tråd vente på sit opkald til SendMessage for at vende tilbage.


Enhver indsigt?





FORSLAG TIL LÆSERNE


Jeg vil foreslå at læse Adrian svar som en introduktion til RbMm s, som følger den samme tankegang, men går mere detaljeret.

Bedste reference


den beskrevne adfærd fungerer virkelig godt.



  Hvordan kan SendMessage, hvornår de skal sendes i kø eller i kø
  Beskeder?



fra Nonqueued Messages [79]



  Nogle funktioner, der sender nonqueued meddelelser, er ... SendMessage
  ... [80]



SendMessage sender simpelthen altid nonqueued meddelelser. [81]


og fra SendMessage dokumentation: [82]



  Den sendte tråd vil dog behandle indkommende ikke-beregnede meddelelser
  mens han venter på, at meddelelsen bliver behandlet.



dette betyder at vinduet procedure kan kaldes indenfor SendMessage call. og behandle indgående meddelelser sendt via SendMessage fra en anden tråd. hvordan dette gennemføres?


Når vi kalder SendMessage besked til et andet trådvindue, går det ind i kernel-tilstand. kernel mode husker altid usermode stack pointer. og vi skiftede til kernel stack. Når vi vender tilbage fra kerne til brugertilstand, returnerer kerne normalt tilbage til punkt, hvor brugermodus kaldte det og til gemt stak. men eksisterer og undtagelser. en af ​​følgende:


NTSYSCALLAPI
NTSTATUS
NTAPI
KeUserModeCallback
(
    IN ULONG RoutineIndex,
    IN PVOID Argument,
    IN ULONG ArgumentLength,
    OUT PVOID* Result,
    OUT PULONG ResultLenght
);


Dette eksporteres, men undokumenteres api. men det hele tiden bruges af win32k.sys til call window procedure. hvordan denne api fungerede?


Først og fremmest tildeler du yderligere kernelastramme under nuværende. end det tager den gemte brugertilstands stackpeger og kopier nogle data (argumenter) under den. Endelig går vi fra kernen til brugerens tilstand, men ikke til at pege, hvorfra kernen blev kaldt, men til special (eksporteret fra ntdll.dll) funktionen -


void
KiUserCallbackDispatcher
(
    IN ULONG RoutineIndex,
    IN PVOID Argument,
    IN ULONG ArgumentLength
);


og stakken var under stakpeger, hvorfra vi indtaster kerne tidligt.
KiUserCallbackDispatcher call RtlGetCurrentPeb()->KernelCallbackTable[RoutineIndex](Argument, ArgumentLength) - normalt er dette en funktion i user32.dll. Denne funktion kalder allerede korresponderende vinduet procedure. fra vindueproceduren kan vi kalde kerne tilbage - fordi KeUserModeCallback tildeler yderligere kernerammer - vi kommer til at indtaste i kernen inde i denne ramme og ikke beskadige tidligere. når vinduet procedure vender tilbage - igen specielt api kaldes


\_\_declspec(noreturn)
NTSTATUS
NTAPI
ZwCallbackReturn
(
    IN PVOID Result OPTIONAL,
    IN ULONG ResultLength,
    IN NTSTATUS Status
);


Denne api (hvis ingen fejl) skal aldrig returnere - i kernel side - er den tildelte kernelramme allokeret, og vi vender tilbage til tidligere kernelack indenfor KeUserModeCallback. så vi vender endelig tilbage fra punkt, hvorfra KeUserModeCallback blev kaldt. så går vi tilbage til brugerens tilstand, lige fra det punkt, hvor vi kalder kerne, på samme stak.


virkelig hvordan hedder vinduet procedure indeni ring til GetMessage? præcis ved dette. call flow var:


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


præcis det samme var med blokering SendMessage.


så når thread\_A send message\_1 til thread\_B via SendMessage - vi indtaster til kernel, signalgui event\_B , hvor thread\_B ventede. og begynd at vente på gui event\_A for den aktuelle tråd. Hvis thread\_B udfører besked hentningskode (kald GetMessage eller PeekMessage) KeUserModeCallback kaldes i thread\_B . som resultat udført det vindue procedure. her ringer det SendMessage for at sende nogle message\_2 til thread\_A tilbage. Som resultat indstiller vi event\_A , hvor thread\_A venter og begynder at vente på event\_B . thread\_A vil vække og kalde KeUserModeCallback. Det vinduesprocedure vil blive kaldt med denne besked. når den vender tilbage (antag denne gang, ringe vi ikke mere SendMessage) vi igen signalere event\_B og begynde at vente på event\_A .
Nu thread\_B vende tilbage fra SendMessage og vende tilbage fra vinduet procedure - færdiggør håndtere original message\_1 . vil være event\_A sæt. thread\_A vækker og vender tilbage fra SendMessage. call flow vil være næste:


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


Bemærk også, at når vi håndterer WM\_DESTROY besked - vinduet stadig er gyldigt og opkaldsprocessen indgående meddelelser. vi kan implementere næste demo: i starten behøver vi ikke 2 processer. absolut nok enkelt proces med 2 tråde. og særskilt registreret besked her behøver ikke. hvorfor ikke bruge sige WM\_APP som testmeddelelse?



  1. thread\_A fra selv WM\_CREATE opret thread\_B og send eget vindueshåndtag til det.

  2. thread\_B opret selvvindue, men på WM\_CREATE returnerer du simpelthen -1 (for fejlfremstillingsvindue)

  3. thread\_B fra WM\_DESTROY ring SendMessage(hwnd\_A, WM\_APP, 0, hwnd\_B) (send selv hwnd som lParam )

  4. thread\_A fik WM\_APP og kalder SendMessage(hwnd\_B, WM\_APP, 0, 0)

  5. thread\_B fik WM\_APP (så WindowProc blev rekursivt kaldt på stack bellow WM\_DESTROY

  6. thread\_B print 'Kan ikke udskrive dette' og returnere selv-id til thread\_A

  7. thread\_A returneres fra opkald SendMessage og returnere selv-id til thread\_B

  8. thread\_B returneres fra opkald SendMessage indenfor 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)
", 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());
}





jeg har næste output:


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


mest interessante look stack spor af thread\_B , når det rekursivt opfordres til WM\_APP


Indtast billedbeskrivelse her [83]

Andre referencer 1



  Stadig forstår jeg ikke, hvordan en proces WM\_ANOTHER\_REGISTERED\_MSG. Det, jeg ville forvente, er, at når denne besked er sendt, skal A 's tråd vente på sit opkald til SendMessage for at vende tilbage.



SendMessage i A er venter på den besked, den har sendt (fra A til B) for at fuldføre, men mens den venter, kan den sende meddelelser sendt fra andre tråde til denne tråd.


Når SendMessage bliver kaldt til et vindue på samme tråd, tænker vi på det som en kæde af funktionskald, der til sidst fører til målvinduet og til sidst vender tilbage til opkalderen.


Men når meddelelsen krydser trådgrænser, er det ikke så enkelt. Det bliver som en klient-server applikation. SendMessage pakker op meddelelsen og signalerer måltråden, som den har en besked til at behandle. På det tidspunkt, det venter.


Måltråden til sidst (vi håber) når et udbyttepunkt, hvor det kontrollerer signalet, får beskeden og behandler det. Måltråden signalerer derefter, at det er gjort arbejdet.


Den oprindelige tråd ser signalet 'Jeg' m gjort! 'Og returnerer resultatværdien. Til opkalderen på SendMessage ser det ud til, at det kun var et funktionsopkald, men det blev faktisk koreograferet for at marskale meddelelsen til den anden tråd og marskalderen resultatet tilbage.


Flere Windows API-opkald er 'udbyttepunkter', steder, der kontrollerer for at se, om der sendes en besked til den aktuelle tråd fra en anden tråd. De mest kendte er GetMessage og PeekMessage, men Visse typer venter - inklusive ventetiden inden for en SendMessage - er også udbyttepunkter. Det er dette udbyttepunkt, der gør det muligt for A at svare på meddelelsen, der sendes tilbage fra B, mens man venter på B til Afslut behandling af den første meddelelse.


Her er en del af opkaldsstakken til A, når den modtager WM\_ANOTHER\_REGISTERED\_MSG tilbage fra B (trin 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)


Du kan se, at OnClose er stadig inde i SendMessageW, men indlejret i det, får den tilbagekaldelsesmeddelelsen fra B og dirigerer den til A 's vindueprocedure.