Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Hier können Artikel, Tutorials, Bücherrezensionen, Dokumente aller Art, Texturen, Sprites, Sounds, Musik und Modelle zur Verfügung gestellt bzw. verlinkt werden.
Forumsregeln
Möglichst sinnvolle Präfixe oder die Themensymbole nutzen.
Antworten
Benutzeravatar
Krishty
Establishment
Beiträge: 8238
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von Krishty »

Dieses Tutorial ist veraltet: http://zfx.info/viewtopic.php?f=11&t=12

Zuerst einmal existiert der Diskussions-Thread nicht.

Zum zweiten ist das Vorgehen maximal ineffizient, und – vor allem – viel komplexer als es nötig wäre. (Das soll jetzt kein Meckern gegen Aramis werden; ich habe ja selber Jahre gebraucht, um zu kapieren, wie es richtig geht™ und weiß einfach nicht, wo ich es sonst posten soll …)
Speichern des Pointers auf das Fensterobjekt im HWND selber mittels SetWindowLongPtr() und GetWindowLongPtr(). Nachteil: Eventuell Überschneidungen. Möglichkeit a): Übermittlung des Pointers auf das Fensterobjekt in der WM_NCCREATE-Nachricht Möglichkeit b): Ablegen des Fensterobjektes in einem TLS-Index (Thread Local Storage). Bei der ersten Nachricht für die GetWindowLongPtr() 0 zurückgibt wird der gespeicherte Pointer aus dem TLS geholt und im Fensterhandle abgelegt.
Das ist ein absolut überflüssiger Umweg, denn ganz exakt dafür ist der LPVOID lpParam-Parameter von CreateWindowEx() vorgesehen. Der ist in WM_NCCREATE durch das CREATESTRUCT abrufbar und kann zu diesem Zeitpunkt via SetWindowLongPtr() als zusätzlicher Fensterkontext zugewiesen werden, so dass ab WM_NCCREATE alle Nachrichten auch diesen Kontext erreichen.

WM_NCCREATE ist zwar nicht die allererste Nachricht, die ein Fenster erreicht, aber bei der Entwicklung der WinAPI wurde durchaus darauf geachtet, dass das für Objektorientierung kein Problem darstellt: Vorher kommt nur WM_GETMINMAXINFO, das in diesem Fall keinen Kontext beansprucht, weil wenn ich ein Fenster mit Kontext erzeuge, dann weiß ich ja, welche Größe ich voraussetze.

Ein großer Vorteil der Methode ist, dass nun angenommen werden kann, dass der Kontext-Zeiger nach WM_NCCREATE immer existiert (denn der Besitzer von dem Kram in GWL_USERDATA ist immer die Fensterklasse). Wir haben die Fensterklasse also um einen riesigen Batzen Zustand bereinigt und brauchen nicht mehr ständig testen, ob der Zeiger gesetzt ist; außer in WM_GETMINMAXINFO.

Das Ganze wäre etwa konform zu diesem Artikel hier.

Worauf man noch zusätzlich achten müsste: Viele Leute erben von der Fensterklasse und überschreiben dann was. In diesem Fall sollte sichergestellt werden, dass die Basisklasse vollständig initialisiert ist, bevor sie die Nachrichten verarbeitet – dafür habe auch ich dann ein zusätzliches Flag gebraucht.

Wenn man das tut, sollte man übrigens das Curiously recurring template pattern nutzen, um direkt einen Zeiger auf die abgeleitete Klasse als Kontext zu speichern, und nicht auf die Basisklasse. Das spart pro Abfrage eine Adressberechnung (fuck yea!). Idealerweise sollte man sowie keine dynamische Vererbung nutzen, sondern das statisch lösen, so wie in dots Beispiel. Und ich persönlich versuche mich gerade daran, die Flexibilität mit Policy-based Design zusammenzupflücken. Das sollte aber wohl alles nicht mehr Thema des Basistutorials sein.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Biolunar
Establishment
Beiträge: 154
Registriert: 27.06.2005, 17:42
Alter Benutzername: dLoB

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von Biolunar »

Ich habe mal den alten Thread ausgegraben: http://old.zfx.info/DisplayThread.php?TID=19856&Page=1

Sagen wir es mal so: Damals war ich jung und dumm, jetzt bin ich immerhin älter aber genauso dumm ;) Ein Problem war glaube ich, dass MDI Anwendungen nicht möglich sind, wenn der void* Parameter von CreateWindowEx für die Klasse verwendet wird. Wie so oft kommt es halt immer auf die jeweilige Anwendung an. Und nicht zu vergessen die Exceptionsicherheit; Wenn ich wieder daran denken muss, wird mir wieder ganz schlecht. WinAPI passt einfach nicht zu C++.

Inzwischen folge ich den Regeln:
1.) Programmiere nicht für Windows.
2.) Wenn eine der vorherigen Regeln nicht zutrifft, programmiere gar nicht.

Daher kann ich inzwischen leider nicht mehr viel Informatives in die Diskussion mit beisteuern.
odenter
Establishment
Beiträge: 207
Registriert: 26.02.2009, 11:58

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von odenter »

Bei MDI Anwendungen muss (ok in der Hilfe steht "sollte") der Parameter ein Zeiger auf die Struktur "CLIENTCREATESTRUCT" bzw. "MDICREATESTRUCT" sein.
Was auch immer gemacht wird, das Tutorial sollte klar und einfach zu verstehen sein.

Bin gerade bei der Arbeit und habe keinen Zugriff auf meine Sourcen zuhause, ich hab sowas nur einmal gemacht um es gemacht zu haben. Und ich bin mir ziehmlich sicher das ich den Zeiger nicht im TLS abgelegt habe und auch nicht als Parameter an CreateWindowEx mit übergeben habe.
Ich bin mir fast sicher SetWindowLongPtr und GWLP_USERDATA verwendet zu haben, schaue heute Abend mal nach.

Habe gerade mal fix überflogen wie die das in wxWidgets machen, hab das auf die schnelle aber nicht verstanden. Sieht noch komplizierter aus. ^^
Benutzeravatar
BeRsErKeR
Establishment
Beiträge: 689
Registriert: 27.04.2002, 22:01

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von BeRsErKeR »

Krishty hat geschrieben:
Speichern des Pointers auf das Fensterobjekt im HWND selber mittels SetWindowLongPtr() und GetWindowLongPtr(). Nachteil: Eventuell Überschneidungen. Möglichkeit a): Übermittlung des Pointers auf das Fensterobjekt in der WM_NCCREATE-Nachricht Möglichkeit b): Ablegen des Fensterobjektes in einem TLS-Index (Thread Local Storage). Bei der ersten Nachricht für die GetWindowLongPtr() 0 zurückgibt wird der gespeicherte Pointer aus dem TLS geholt und im Fensterhandle abgelegt.
Das ist ein absolut überflüssiger Umweg, denn ganz exakt dafür ist der LPVOID lpParam-Parameter von CreateWindowEx() vorgesehen. Der ist in WM_NCCREATE durch das CREATESTRUCT abrufbar und kann zu diesem Zeitpunkt via SetWindowLongPtr() als zusätzlicher Fensterkontext zugewiesen werden, so dass ab WM_NCCREATE alle Nachrichten auch diesen Kontext erreichen.
Versteh dich nicht ganz. Dein Vorschlag ist doch unter Möglichkeit a) genauso aufgeführt. Es ist vielleicht etwas undeutlich ausgedrückt, aber ich verstehe das genau wie deine Idee, nämlich dass man innerhalb von WM_NCCREATE dann mittels SetWindowLongPtr() den Kontext setzt. Hier fehlt maximal noch der Hinweis, wie man den Kontext an die WM_NCCREATE übergibt. Das hast du ja bereits gesagt.

Oder habe ich dich jetzt falsch verstanden?
Ohne Input kein Output.
CrystalCoder
Beiträge: 54
Registriert: 03.03.2002, 17:51
Kontaktdaten:

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von CrystalCoder »

odenter hat geschrieben:Bei MDI Anwendungen muss (ok in der Hilfe steht "sollte") der Parameter ein Zeiger auf die Struktur "CLIENTCREATESTRUCT" bzw. "MDICREATESTRUCT" sein.
Das ist korrekt, allerdings kann man das ganze etwas umgehen, indem man sich eine Struktur baut, die einem dabei hilft festzustellen, was übergeben wurde, in der Router Methode kann man dann anhand dieser Struktur herausfinden, ob es nun ein MDI- oder ein Dialogfenster ist.

Grob erklärt kann man es so anstellen:

Man baue eine Struktur die so aussieht:

Code: Alles auswählen

struct SWindowCreationParams
{
    size_t      m_cbSize;
    CWindow*    m_pReceiverWindow;
}; // struct SWindowCreationParams;
CWindow ist die Fensterklasse, die alles Wichtige enthält (Fensterhandle, Programminstanz, usw.). Es kann als MDI Parent, sowie als MDI Child dienen (oder als normales Dialogfenster)
Der Größenparameter ist hierbei das Schlüsselelement(und sollte auch als erstes deklariert sein in der Struktur).

Die Struktur "MDICREATESTRUCT" hat einen Parameter lParam, den man frei nutzen kann.
Ruft man nun CreateWindow auf und erstellt ein Dialogfeld, dann übergibt man einfach einen Pointer auf "SWindowCreationParams" als letzten Parameter;
Erstellt man aber ein MDI Fenster, dann übergibt man einen Pointer auf "MDICREATESTRUCT" als letzten Parameter, wobei der lParam von der MDICREATESTRUCT auf einen Pointer von SWindowCreationParams zeigt.

In dem MessageRouter kann man nun leicht herausfinden, was hier übergeben wurde, indem man zunächst auf SWindowCreationParams castet und prüft, ob die Größe stimmt und falls nicht castet man auf LPMDICREATESTRUCT und bedient sich dem Member "lParam":

Code: Alles auswählen

LRESULT CALLBACK CWindow::MessageRouter(HWND hWnd, UINT unMessage, WPARAM wParam, LPARAM lParam)
{
    // Needed to identify if the actual window is an MDI-child window
    SWindowCreationParams*	pWndCreationParams	= NULL;

    // Save pointer on WM_NCCREATE message. This is the first message that is sent to the window and contains the pointer to the
    // window object while it was given to the CreateWindowEx WINAPI function
    if(unMessage == WM_NCCREATE)
    {
        // Retrieve window instance from window creation data
        pWndCreationParams = reinterpret_cast<SWindowCreationParams *>(((LPCREATESTRUCT) lParam)->lpCreateParams);
        // This could be an MDI-child-window if the parent is set
        if(pWndCreationParams->m_cbSize != sizeof(SWindowCreationParams))
        {
            // Wrong size, so the data cannot be right and it must be the creation of an MDI-child-window
            pWndCreationParams
                = reinterpret_cast<SWindowCreationParams *>(((LPMDICREATESTRUCT) ((LPCREATESTRUCT) lParam)->lpCreateParams)->lParam);

            // Check size again
            if(pWndCreationParams->m_cbSize != sizeof(SWindowCreationParams))
            {
                MessageBoxW(hWnd, _TEXT("Error on identifying the window creation type (not default and not an MDI-child as well)"),
                    _TEXT("ERROR in CWindow::MessageRouter()"), MB_ICONERROR | MB_OK);
                return 0;
            }
        }

        // Bind the CWindow-object pointer to the window-handle
        ::SetWindowLongW(hWnd, GWL_USERDATA, reinterpret_cast<int> (pWndCreationParams));

        // Save window handle
        pWndCreationParams->m_pReceiverWindow->setWindowHandle(hWnd);
    }

    // [ ... weiterer Code ... ]

    // [ hier kann man nun den Fehnsterhandler der Klasse aufrufen, wo alle weiteren Nachrichten verarbeitet werden können ]

    // DefWindowProg aufrufen, wenn nötig
}
Das ist ein Ausschnitt aus meinem aktuellen WinApi-Wrapper Projekt, und die Vorgehensweise funktioniert sehr gut, solange man genau weiß, dass nur zwei verschiedene Parameter möglich sind (was in den meisten Projekten wohl auch der Fall ist).

Übrigens noch ein Hinweis:
Nach dem CreateWindow Aufruf ist es bereits zu spät, um SetWindowLong noch aufzurufen, weil schon während des Aufrufs von CreateWindow bereits die WM_NCCREATE Nachricht verschickt wird. Man kommt also nicht drum herum es über den letzten Parameter von CreateWindow zu lösen (ausser es macht einem nichts aus, eine spätere Nachricht für die Fensterdaten zu benutzen - aber der ganze Post geht ja nur darum eben das zu vermeiden).

EDIT: Wegen "WM_GETMINMAXINFO" als erste Nachricht: Ich hab das schon öfter gelesen dass dies so sein soll, allerdings hab ich bei Tests immer die WM_NCCREATE Nachricht als erstes gehabt. Könnte evtl. an der Windows Version liegen. Das ist auch der Grund warum ich immer die WM_NCCREATE Nachricht als erste verarbeite.
Niki
Establishment
Beiträge: 309
Registriert: 01.01.2013, 21:52

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von Niki »

Ich bin sowohl von SetWindowLong als auch von lpParam weggegangen. Mein Kopf raucht schon, aber ich kann mich beim besten Willen nicht erinnern warum ich den lpParam auch verworfen habe. Alles was ich noch weiß ist, dass ich irgendein vertracktes Problem beim Umstieg auf Windows 7 hatte. Was auch immer genau los war, es hat mich dazu getrieben den MFC Source Code auseinander zu pflücken.
Das Ergebnis: ich benutze mittlerweile einen Computer-Based-Training Hook mit HBCT_CREATEWND Filter um ein echtes Binding zwischen HWND und C++ Fensterklasse zu erzeugen. In WM_NCDESTROY hebe ich das Binding wieder auf. Für mich hat das alle Probleme gelöst, auch wenn die Methode ganz klar aufwendiger ist.
Zuletzt geändert von Niki am 26.03.2013, 19:54, insgesamt 1-mal geändert.
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von dot »

Und wie genau funktioniert das? Und was genau verstehst du unter einem "echten Binding" im Kontrast zu einem "unechten Binding"?
Niki
Establishment
Beiträge: 309
Registriert: 01.01.2013, 21:52

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von Niki »

Autsch, dot, das wird eine längere Post. Aber hey, Du hast mir ja auch viel im Grafikforum geholfen :) Gib mir 1-2 Stunden für die Antwort.
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von dot »

Muss keine lange Antwort sein, mich würd einfach nur prinzipiell interessieren, wie genau ein CBT Hook eine Verbindung zwischen einem HWND und einem Objekt herstellen kann...
Niki
Establishment
Beiträge: 309
Registriert: 01.01.2013, 21:52

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von Niki »

dot hat geschrieben:Muss keine lange Antwort sein, mich würd einfach nur prinzipiell interessieren, wie genau ein CBT Hook eine Verbindung zwischen einem HWND und einem Objekt herstellen kann...
Ok, ich will's versuchen. Erst mal Deine Fragen zum "echten" Binding. Mein Binding ist natürlich nicht wirklich echter als jedes andere Binding auch. Alles was ich damit meine ist, dass ich eine Datenstruktur habe, die HWND's auf Fenterobjekte mapped (meint Instanzen der C++ Fensterklasse).

In meiner Fensterklasse rufe ich CreateWindowEx() mit lpParam = this auf. Das macht Ihr ja hier genau so. Bevor ich CreateWindowEx() aufrufe installiere ich den CBT-Hook:

Code: Alles auswählen

bool QxNativeWindowBinding::BeginWindowCreation(QxNativeWindow * pWindow)
{
    qxAssert(ms_cbtHookHandle == qxNull && ms_pWindowBeingCreated == qxNull);

    ms_cbtHookHandle = ::SetWindowsHookEx(WH_CBT, QxNativeWindowBinding::CbtHookProcedure, qxNull, ::GetCurrentThreadId());
    if (ms_cbtHookHandle != qxNull)
    {
        ms_pWindowBeingCreated = pWindow;
    }
    return ms_cbtHookHandle != qxNull;
}
Das Präfix ms_ steht bei mir für static member, also statische Membervariablen die zu einer Klasse gehören. In diesem Falle zu QxNativeWindowBinding.

Nach dem Aufruf von CreateWindowEx() wird der CBT-Hook wieder entfernt:

Code: Alles auswählen

void QxNativeWindowBinding::EndWindowCreation(QxNativeWindow * pWindow)
{
    if (ms_cbtHookHandle != qxNull)
    {
        ::UnhookWindowsHookEx(ms_cbtHookHandle);
        ms_cbtHookHandle = qxNull;
        ms_pWindowBeingCreated = qxNull;
    }
}
Windows geht nun daher und ruft die CBT-Hook Prozedur auf bevor noch die erste Message an das Fenster gesendet wird. Diese Gelegenheit nutze ich um das Binding herzustellen:

Code: Alles auswählen

LRESULT CALLBACK QxNativeWindowBinding::CbtHookProcedure(int code, WPARAM wParam, LPARAM lParam)
{
    if (code < 0 || code != HCBT_CREATEWND)
    {
        return ::CallNextHookEx(ms_cbtHookHandle, code, wParam, lParam);
    }
    else
    {
        CBT_CREATEWND * pCreateWnd = reinterpret_cast<CBT_CREATEWND *>(lParam);
        if (pCreateWnd != qxNull)
        {
            CREATESTRUCT * pCreateStruct = pCreateWnd->lpcs;
            if (pCreateStruct != qxNull)
            {
                void * pCreateParams = pCreateStruct->lpCreateParams;

                if (pCreateParams != qxNull && pCreateParams == ms_pWindowBeingCreated)
                {
                    CreateWindowBinding(reinterpret_cast<HWND>(wParam), ms_pWindowBeingCreated);
                    ms_pWindowBeingCreated = qxNull;
                }
            }
        }
    }

    return ::CallNextHookEx(ms_cbtHookHandle, code, wParam, lParam);
}
Meine Fensterprozedur kann nun das Fensterobjekt per HWND ermitteln, und die Messages entsprechend weiterleiten. In meinem Fall leite ich nur bestimmte Messages weiter, weshalb ich einen längeren Switch/Case Block habe. Es geht aber natürlich auch ohne.

Code: Alles auswählen

LRESULT CALLBACK QxNativeWindowBinding::WindowProcedure(HWND windowHandle, UINT message, WPARAM wParam, LPARAM lParam)
{
    QxNativeWindow * pWindow = ObjectFromHandle(windowHandle);

    if (pWindow != qxNull)
    {
        switch (message)
        {
            case WM_DESTROY:
                ::PostQuitMessage(pWindow->OnDestroy());
                return 0;

            case WM_PAINT:
                pWindow->OnPaint(wParam, lParam);
                return 0;

            case WM_SIZE:
                pWindow->OnSize(wParam, lParam);
                return 0;

            case WM_KEYDOWN:
                pWindow->OnKeyDown(wParam, lParam);
                return 0;

            case WM_KEYUP:
                pWindow->OnKeyUp(wParam, lParam);
                return 0;

            case WM_CHAR:
                pWindow->OnChar(wParam, lParam);
                return 0;

            case WM_LBUTTONDOWN:
                pWindow->OnMouseLButtonDown(wParam, lParam);
                break;

            case WM_LBUTTONUP:
                pWindow->OnMouseLButtonUp(wParam, lParam);
                break;

            case WM_MBUTTONDOWN:
                pWindow->OnMouseMButtonDown(wParam, lParam);
                break;

            case WM_MBUTTONUP:
                pWindow->OnMouseMButtonUp(wParam, lParam);
                break;

            case WM_RBUTTONDOWN:
                pWindow->OnMouseRButtonDown(wParam, lParam);
                break;

            case WM_RBUTTONUP:
                pWindow->OnMouseRButtonUp(wParam, lParam);
                break;

            case WM_MOUSEMOVE:
                pWindow->OnMouseMove(wParam, lParam);
                break;

            case WM_MOUSEWHEEL:
                pWindow->OnMouseWheel(wParam, lParam);
                break;

            case WM_CAPTURECHANGED:
                pWindow->OnCaptureChanged(wParam, lParam);
                break;

            case WM_NCDESTROY:
                RemoveWindowBinding(windowHandle);
                return 0;
        }
    }

    return ::DefWindowProc(windowHandle, message, wParam, lParam);
}
Das einzige was mir an der Method nicht schmeckt sind die static members, welches die Datenstruktur für's Binding einschliesst. Das kann man sicher schöner machen, ist aber für meine Zwecke ausreichened.

Wenn irgendwas nicht klar sein sollte dann frag bitte :)

Hmmm... funktionieren Spaces am Anfang von Forum Code Blöcken nicht? Sieht etwas unformatiert aus.
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von dot »

Ok, wenn ich das also richtig verstehe, hast du einfach irgendwo eine globale Hashmap, die Window Handles auf Objekte mapped, der CBT Hook dient nur dazu, den Eintrag in dieser Map zu erstellen, bevor die WndProc noch das erste Mal aufgerufen wird? Ist natürlich ein Weg, ich find eine globale Map allerdings relativ unschön, allein schon weil die Behandlung jeder Nachricht dann einen Lookup in einer Map mit sich bringt. Auf jeden Fall danke für die ausführliche Erklärung... ;)
Niki
Establishment
Beiträge: 309
Registriert: 01.01.2013, 21:52

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von Niki »

Du hast schon recht mit der globalen Map. Das ist hässlich was ich ja auch selber zugebe. Aber das war mir hier wirklich egal weil ich nur wenige Fenster habe. Meist nur 1, manchmal 2-3 wenn ich Multimonitor haben will. Deshalb habe ich auch nur ein simples Array für's Binding, und keine Map. Ich habe das ganze aber auch schon mal für komplexere Windows GUI's gemacht und das Prinzip ist dasselbe, auch wenn Du mehrere Fenster Prozeduren hast (für Dialoge und MDI Kinder). Na, ja, muss ja auch tun, sonst hätte die MFC ein Problem :D

EDIT: Der Lookup in der Fensterprozedur ist übrigens ziemlich Wurscht. Bei einer Engine hast du nur wenige Fenster. Und bei einer richtigen GUI Anwendung macht das auch nichts, denn die Anzahl der Messages ist im Vergleich zur ungenutzten CPU-Zeit praktisch 0.
Zuletzt geändert von Niki am 26.03.2013, 20:50, insgesamt 1-mal geändert.
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von dot »

Schon klar, genau das Herstellen des Mapping von HWND auf Objekt ist imo aber das eigentliche Problem und genau das lässt sich durch SetWindowLongPtr() z.B. relativ elegant lösen, da man damit einfach seinen Pointer direkt im Window Objekt des Betriebssystems ablegen kann. Krishtys Idee, das C++ Objekt direkt im Window Extra Memory zu konstruieren, find ich auch sehr interessant, hat imo aber den Nachteil, dass man sich um die Lebensdauer des Objekts dann selbst kümmern muss und am Ende effektiv wieder nix gewonnen hat...

Edit: Ja, für die Performance isses effektiv ziemlich sicher völlig egal, ich denk es geht hier wohl hauptsächlich darum, was denn nun die eleganteste Lösung für das Problem ist... ;)
Benutzeravatar
Krishty
Establishment
Beiträge: 8238
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von Krishty »

dot hat geschrieben:Krishtys Idee, das C++ Objekt direkt im Window Extra Memory zu konstruieren, find ich auch sehr interessant, hat imo aber den Nachteil, dass man sich um die Lebensdauer des Objekts dann selbst kümmern muss
IIRC habe ich hinterher festgestellt, dass das Schwachsinn ist. Ich weiß nicht mehr genau, warum (vielleicht konnte man nicht beliebig Speicher anfordern; vielleicht hat man das Problem auch nur verschoben weil man nun einen Zeiger auf eine Struktur variabler Lebensdauer halten muss statt einer kompletten Klasse); aber es stellte sich als ziemlicher Brainfart heraus. Achja genau: Man fordert damit keinen zusammenhängenden Speicherbereich an, sondern nur eine zusätzliche Anzahl von Slots für GetWindowLong(). Also eher unbrauchbar für diesen Zweck.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von dot »

Also ich hab damals auch etwas drüber nachgedacht und ich würde es nicht unbedingt als Brainfart bezeichnen. Es ist imo einfach eine andere Konfiguration die effektiv mehr oder weniger zum selben Ergebnis führt. Bei der SetWindowLongPtr() Variante ist dein Objekt gleichzeitig der Smartpointer, bei der Variate übers Window Extra ist dein Objekt im Fenster und du brauchst noch einen Smartpointer. Es hätte einen potentiellen Vorteil in Fällen, wo man das C++ Objekt sowieso auch dynamisch anlegen möchte, da man sich damit evtl. eine extra Allokation spart, weil man das Objekt gleich in einen Block packt, der so oder so allokiert würde. Ein leicht anderer Tradeoff, im Groben und Ganzen aber imo relativ gleichwertige Lösungen...

Edit: Tatsächlich? Ich dachte das Window Extra wäre ein zusammenhängender Block!? D.h. ich ging davon aus, dass der Code in Windows in irgendwie so aussieht:

Code: Alles auswählen

struct Window
{
  // ... lots of stuff
  char extra[];
};
Edit 2: Ah, stimmt, Get/SetWindowLongPtr() liefern keine Pointer auf das Extra Memory, sondern holen/setzen lediglich LONGs darin. Auch logisch, wär ansonsten wohl eine relativ offensichtliche Sicherheitslücke bzw. auch gar nicht anders möglich, falls die Window Objekte im Kernel Space liegen...

Edit 3: Ok, jetzt würd mich allerdings sehr interessieren, ob GetWindowLongPtr() einen Kernel Mode Switch verursacht. Da Fenster afaik Kernel Objekte sind, ist das wohl nicht nur gut möglich, sondern sogar sehr wahrscheinlich unvermeidbar, was ja furchtbar wäre...
Zuletzt geändert von dot am 26.03.2013, 21:31, insgesamt 13-mal geändert.
Niki
Establishment
Beiträge: 309
Registriert: 01.01.2013, 21:52

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von Niki »

dot hat geschrieben:Schon klar, genau das Herstellen des Mapping von HWND auf Objekt ist imo aber das eigentliche Problem und genau das lässt sich durch SetWindowLongPtr() z.B. relativ elegant lösen, da man damit einfach seinen Pointer direkt im Window Objekt des Betriebssystems ablegen kann... ;)
Ja, mit ein wenig Vorsicht haut das bei einfachen Fenstern wohl hin, da Du mit Sicherheit weißt das GWL_USERDATA unbenutzt ist. Und für eine Engine braucht man ja meist nicht mehr :) Ich benutze meinen Hook Code, weil er mich noch nie im Stich gelassen hat.
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von dot »

The Horror...
the_horror.png
:shock:

Macht natürlich absolut Sinn, auch wieso man keinen direkten Zugriff auf das Window Extra Memory bekommt, hab nur nie wirklich drüber nachgedacht. Also doch Thunks...
Niki
Establishment
Beiträge: 309
Registriert: 01.01.2013, 21:52

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von Niki »

Sieht so aus als gäb's wieder was neues für mich zu lernen :D Was ist ein Thunk und warum ist der so schlecht?
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von dot »

Nein, Thunks sind damit seit heute 21:40 herum die effizienteste noch irgendwie praktikable Lösung für das Problem, die ich kenne. Die Idee ist ganz einfach: Man generiert dynamisch beim Erzeugen seines Objektes ein Stück Maschinencode, das den Aufruf des WndProc Callback direkt vor Ort in einen Aufruf der Methode des jeweiligen Objektes umwandelt und verwendet als WndProc einen Pointer auf selbiges. Wie du dir denken kannst, ist die Umsetzung davon aber nicht ganz unproblematisch...
Niki
Establishment
Beiträge: 309
Registriert: 01.01.2013, 21:52

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von Niki »

Dynamischer Maschinencode? Du meinst so mit VirtualAlloc und Zeugs für executable pages? Das hört sich schon echt heftig an.

EDIT: Das erinnert mich an selbstmodifizierenden Code zu Amiga und DOS Zeiten :D
Benutzeravatar
eXile
Establishment
Beiträge: 1136
Registriert: 28.02.2009, 13:27

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von eXile »

Eine hervorragende Zusammenfassung findet sich hier.

Die dort gezeigte Implementierung entspricht auch so ziemlich der Referenzimplementierung in der VC/atlmfc/include/atlstdthunk.h (mit Ausnahme des grandiosen union_casts); kein Wunder, in den Referenzen taucht die ATL auf, d.h. der gezeigte Code ist von dort geklaut inspiriert.

Aber wichtig: In der atlstdthunk.h gibt es auch eine x64-Variante. Und: Mit diesem Patch hat man auch den Code für operator new und delete des Thunk-Objekts (welches ja mit auf einer Page mit PAGE_EXECUTE_READWRITE liegen muss). Damit hat man alles zusammen was man für die Thunks braucht. Außer die Zeit, um das jetzt auch noch zusammenzukopieren schreiben.
Benutzeravatar
eXile
Establishment
Beiträge: 1136
Registriert: 28.02.2009, 13:27

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von eXile »

Ich habe dann mal Thunks implementiert.

Erst einmal den vollständigen, kompilierbaren Code, welcher bei der Fenstererstellung noch das Hello-World-Beispiel aus der MSDN benutzt, um das es hier aber ja schließlich nicht geht:

Code: Alles auswählen

// Suppress warnings from Windows and STL headers
#pragma warning(disable : 4668 4710 4711 4820 4986)

#include <Windows.h>
#include <cstdint>
#include <string>
#include <iostream>
#include <type_traits>

// Helpers
namespace memory
{
template <typename Type>
void * AllocateReadWriteVirtualMemory()
{
	return ::VirtualAlloc(0, sizeof(Type), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
}

template <typename Type>
Type * MakeMemoryExecutable(
	Type * thePointer
) {
	unsigned long oldProtection;

	VirtualProtect(thePointer, sizeof(Type), PAGE_EXECUTE, &oldProtection);

	return thePointer;
}

void DeallocateVirtualMemory(
	void * thePointer
) {
	::VirtualFree(thePointer, 0, MEM_RELEASE);
}
}

namespace meta
{
namespace addressOf
{
template<class Type>
struct DisableConversionOperators
{
	Type & myValue;
 
	inline DisableConversionOperators(
		Type & theValue
	) : myValue(theValue)
	{
		return;
	}

	inline operator Type&() const
	{
		return myValue;
	}
 
private:
	DisableConversionOperators & operator=(
		DisableConversionOperators const &
	);
};
 
template<class Type>
struct ReturnFunctionAddress
{
	static inline Type * AddressOf(
		Type & theValue, 
		long
	) {
		return reinterpret_cast<Type*>(&const_cast<char &>(reinterpret_cast<const volatile char &>(theValue)));
	}
 
	static inline Type * AddressOf(
		Type * theValue, 
		int
	) {
		return theValue;
	}
};
 
template<class Type>
Type * AddressOf(
	Type & theValue
) {
	return ReturnFunctionAddress<Type>::AddressOf(DisableConversionOperators<Type>(theValue), 0);
}
}

using namespace addressOf;

namespace unionCast
{
template<typename DestinationType, typename SourceType>
inline DestinationType UnionCast(SourceType theSource)
{
	static_assert(sizeof(DestinationType) == sizeof(SourceType), "UnionCast size mismatch.");

	union
	{
		SourceType mySource;
		DestinationType myDestination;
	} unionForCast;

	unionForCast.mySource = theSource;

	return unionForCast.myDestination;
}
}

using namespace unionCast;
}

// Windows
namespace windows
{
typedef ::LRESULT (__stdcall * WindowProcedureType)(::HWND, unsigned int, ::WPARAM, ::LPARAM);

// Thunks
namespace thunks
{
namespace
{
template <typename SourceType>
void FillArray(
	char * theArray,
	size_t & theOffset, 
	SourceType theSource
) {
	SourceType * destination = reinterpret_cast<SourceType *>(theArray + theOffset);
	*destination = theSource;
	theOffset += sizeof(SourceType);
}
}

#ifdef _M_IX86

template <typename WindowClassType>
class ThunkImplementation
{
public:
	explicit ThunkImplementation(
		WindowClassType & theWindowClass, 
		WindowProcedureType theWindowProcedure
	) {
		size_t offset = 0u;

		// mov dword ptr [esp+4], <pointer to theWindowClass>
		FillArray<uint32_t>(myCode, offset, 0x042444c7);
		FillArray(myCode, offset, meta::AddressOf(theWindowClass));

		// jmp <relative address to theWindowProcedure>
		FillArray<uint8_t>(myCode, offset, 0xe9);
		FillArray(myCode, offset, meta::UnionCast<char *>(theWindowProcedure) - reinterpret_cast<char *>(this) - sizeof(*this));

		::FlushInstructionCache(GetCurrentProcess(), this, sizeof(*this));
	}

	operator WindowProcedureType() const
	{
		return meta::UnionCast<WindowProcedureType>(myCode); 
	}

	void * operator new(
		size_t
	) {
		return memory::AllocateReadWriteVirtualMemory<ThunkImplementation>();
	}

	void operator delete(
		void * thePointer
	) {
		memory::DeallocateVirtualMemory(thePointer);
	}

private:
	typedef char ThunkCode[sizeof(uint32_t) + sizeof(WindowClassType *) + sizeof(uint8_t) + sizeof(ptrdiff_t)];
	ThunkCode myCode;
};

#elif defined _M_AMD64

template <typename WindowClassType>
class ThunkImplementation
{
public:
	explicit ThunkImplementation(
		WindowClassType & theWindowClass, 
		WindowProcedureType theWindowProcedure
	) {
		size_t offset = 0u;

		// mov rcx, <pointer to theWindowClass>
		FillArray<uint16_t>(myCode, offset, 0xb948);
		FillArray(myCode, offset, meta::AddressOf(theWindowClass));

		// mov rax, <pointer to theWindowProcedure>
		FillArray<uint16_t>(myCode, offset, 0xb848);
		FillArray(myCode, offset, meta::UnionCast<char *>(theWindowProcedure));

		// jmp rax
		FillArray<uint16_t>(myCode, offset, 0xe0ff); 

		::FlushInstructionCache(GetCurrentProcess(), this, sizeof(*this));
	}

	operator WindowProcedureType() const
	{
		return meta::UnionCast<WindowProcedureType>(myCode); 
	}

	void * operator new(
		size_t
	) {
		return memory::AllocateReadWriteVirtualMemory<ThunkImplementation>();
	}

	void operator delete(
		void * thePointer
	) {
		memory::DeallocateVirtualMemory(thePointer);
	}

private:
	typedef char ThunkCode[sizeof(uint16_t) + sizeof(WindowClassType *) + sizeof(uint16_t) + sizeof(ptrdiff_t) + sizeof(uint16_t)];
	ThunkCode myCode;
};

#else
#error Unsupported architecture.
#endif

template <typename WindowClassType>
class Thunk
{
public:
	Thunk(
		WindowClassType & theWindowClass, 
		WindowProcedureType theWindowProcedure
	) : myThunk(memory::MakeMemoryExecutable(new ThunkImplementation<WindowClassType>(theWindowClass, theWindowProcedure)))
	{
		return;
	}

	~Thunk()
	{
		delete myThunk;
	}

	operator WindowProcedureType() const
	{
		return *myThunk;
	}

private:
	thunks::ThunkImplementation<WindowClassType> * myThunk;
};
}

namespace
{
void SetWindowProcedure(
	::HWND theWindowhandle, 
	WindowProcedureType theWindowProcedure
) {
	::SetWindowLongPtrW(theWindowhandle, GWLP_WNDPROC, reinterpret_cast<std::make_signed<size_t>::type>(theWindowProcedure));
}
}

template <typename DerivedWindowProcedure>
class ThunkedWindowProcedure
{
public:
	explicit ThunkedWindowProcedure(
		::HWND theWindowHandle, 
		DerivedWindowProcedure * theThisPointer
	) : myThunk(*theThisPointer, DefaultWindowProcedure), 
		myWindowHandle(theWindowHandle)
	{
		SetWindowProcedure(theWindowHandle, static_cast<WindowProcedureType>(myThunk));
	}

	void SetWindowHandle(
		::HWND theWindowHandle
	) {
		myWindowHandle = theWindowHandle;
	}

	void GetWindowHandle()
	{
		return myWindowHandle;
	}

protected:
	::HWND myWindowHandle;
	thunks::Thunk<DerivedWindowProcedure> myThunk;

private:
	static ::LRESULT __stdcall DefaultWindowProcedure(
		::HWND theWindowHandle, 
		unsigned int theMessage, 
		::WPARAM theAdditionalMessageInformation1, 
		::LPARAM theAdditionalMessageInformation2
	) {
		DerivedWindowProcedure * thisPointer = reinterpret_cast<DerivedWindowProcedure * >(theWindowHandle);

		return thisPointer->WindowProcedure
		(
			theMessage, 
			theAdditionalMessageInformation1, 
			theAdditionalMessageInformation2
		);
	}
};

class ExampleWindowProcedure : public ThunkedWindowProcedure<ExampleWindowProcedure>
{
public:
	typedef std::basic_string<wchar_t> WideString;

	// This is legal code, see N3337 12.6.2 Clause 12
#pragma warning(push)
#pragma warning(disable : 4355)
	explicit ExampleWindowProcedure(
		::HWND theWindowHandle, 
		WideString theString
	) : ThunkedWindowProcedure(theWindowHandle, this), 
		myString(theString)
	{
		return;
	}
#pragma warning(pop)

public:
	::LRESULT WindowProcedure(
		unsigned int theMessage, 
		::WPARAM theAdditionalMessageInformation1, 
		::LPARAM theAdditionalMessageInformation2
	) {
		if(theMessage == WM_DESTROY)
		{
			::PostQuitMessage(0);
		}
		else if(theMessage == WM_PAINT)
		{
			PAINTSTRUCT paintInformation;
			auto displayDeviceContext = ::BeginPaint(myWindowHandle, &paintInformation);

			TextOut(displayDeviceContext, 5, 5, myString.c_str(), static_cast<int>(myString.length()));

			::EndPaint(myWindowHandle, &paintInformation);
		}
		else
		{
			return DefWindowProc
			(
				myWindowHandle, 
				theMessage, 
				theAdditionalMessageInformation1, 
				theAdditionalMessageInformation2
			);
		}

		return 0;
	}

private: 
	WideString myString;
};
}

// The following code is taken from the "Hello, World!" sample
// http://msdn.microsoft.com/en-us/library/vstudio/bb384843

#include <windows.h>
#include <stdlib.h>
#include <string.h>
#include <tchar.h>

// Global variables

// The main window class name.
static TCHAR szWindowClass[] = _T("win32app");

// The string that appears in the application's title bar.
static TCHAR szTitle[] = _T("Win32 Guided Tour Application");

int WINAPI WinMain(HINSTANCE hInstance,
				   HINSTANCE hPrevInstance,
				   LPSTR lpCmdLine,
				   int nCmdShow)
{
	WNDCLASSEX wcex;

	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.style          = CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc    = ::DefWindowProc;
	wcex.cbClsExtra     = 0;
	wcex.cbWndExtra     = 0;
	wcex.hInstance      = hInstance;
	wcex.hIcon          = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
	wcex.hCursor        = LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
	wcex.lpszMenuName   = NULL;
	wcex.lpszClassName  = szWindowClass;
	wcex.hIconSm        = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_APPLICATION));

	if (!RegisterClassEx(&wcex))
	{
		MessageBox(NULL,
			_T("Call to RegisterClassEx failed!"),
			_T("Win32 Guided Tour"),
			NULL);

		return 1;
	}

	// The parameters to CreateWindow explained:
	// szWindowClass: the name of the application
	// szTitle: the text that appears in the title bar
	// WS_OVERLAPPEDWINDOW: the type of window to create
	// CW_USEDEFAULT, CW_USEDEFAULT: initial position (x, y)
	// 500, 100: initial size (width, length)
	// NULL: the parent of this window
	// NULL: this application does not have a menu bar
	// hInstance: the first parameter from WinMain
	// NULL: not used in this application
	HWND hWnd = CreateWindow(
		szWindowClass,
		szTitle,
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT,
		500, 100,
		NULL,
		NULL,
		hInstance,
		NULL
	);

	if (!hWnd)
	{
		MessageBox(NULL,
			_T("Call to CreateWindow failed!"),
			_T("Win32 Guided Tour"),
			NULL);

		return 1;
	}

	windows::ExampleWindowProcedure window(hWnd, L"Hello, World!");

	// The parameters to ShowWindow explained:
	// hWnd: the value returned from CreateWindow
	// nCmdShow: the fourth parameter from WinMain
	ShowWindow(hWnd,
		nCmdShow);
	UpdateWindow(hWnd);

	// Main message loop:
	MSG msg;
	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int) msg.wParam;
}
Funktionsweise:
  1. Die Fensterklasse wcex kriegt zunächst einmal die ::DefWindowProc als Fensterprozedur:
    • Wir werden die später umsetzen
    • Das bedeutet aber auch, dass wir hier ein paar Nachrichten, wie etwa WM_NCCREATE, gar nicht mitkriegen, weil die an ::DefWindowProc gehen
    • In der jetztigen Implementierung gibt es nichts, was diese Nachrichten bräuchte; wenn ihr die jedoch unbedingt selbst behandeln müsst, gibt es keinen Grund, bereits bei Erstellung von wcex schon eine eigene Fensterprozedur anzugeben
  2. Nach einem gewöhnlichen Aufruf an ::CreateWindow erstellen wir eine Instanz von Typ ExampleWindowProcedure, welche unsere Beispiels-Fensterprozedur halten wird:
    • Die Klasse kriegt im Konstruktor das Fenster-Handle vom zuvor erstellten Fenster (HWND) und einen String, hier „Hello, World!“, übergeben
    • Diese Fensterprozedur macht jetzt erstmal nichts anderes, als diesen Member-String ins Fenster zu malen – zumindest ein kleines Beispiel für die Verwendung den this-Zeigers
  3. Unsere ExampleWindowProcedure ist von ThunkedWindowProcedure<ExampleWindowProcedure> abgeleitet, einer generischen Basisklasse für gethunkte Fensterprozeduren (hier wird ein CRTP benutzt)
    • ThunkedWindowProcedure<DerivedWindowProcedure> enthält ein Thunk-Objekt vom Typ thunks::Thunk<DerivedWindowProcedure> (Template-Parameter hat den Typ der abgeleiteten Klasse) namens myThunk
    • Dieses Thunk-Objekt wird eine Thunk-Implementierung im Speicher anlegen
  4. Dieses Thunk-Objekt myThunk wird erstellt und nimmt im Konstruktor zwei Argumente entgegen:
    • Einen this-Pointer auf die abgeleitete Klasse
    • Einen Pointer auf die Fensterprozedur als Member-Funktion der abgeleiteten Klasse
  5. Der Thunk-Konstruktor für den Typ Thunk erstellt mit diesen zwei Parametern eine Thunk-Implementierung, welche abhängig von der Architektur ist und die ganze Low-Level-Arbeit macht:
    • Die Thunk-Implementierung myThunk (ja, ich weiß, gleicher Name wie oben, das ist jetzt doof) ist ein Pointer vom Typ ThunkImplementation<WindowClassType>, welcher dynamisch allokiert und freigegeben wird
    • Bei dieser Allokation und Freigabe werden operator new/operator delete der ThunkImplementation aufgerufen
    • Diese setzen mittels AllocateReadWriteVirtualMemory die Thunk-Implementierung an den Anfang einer Page, welche lesbar und beschreibbar, aber nicht ausführbar ist.
  6. Die Thunk-Implementierung hat ein char-Array, und dieses wird mit ein wenig Daten als Code gefüllt:
    • Für x86: sizeof(uint32_t) + sizeof(WindowClassType *) + sizeof(uint8_t) + sizeof(ptrdiff_t)
      mov dword ptr [esp+4], <pointer to theWindowClass>
      jmp <relative address to theWindowProcedure>
    • Für x64: sizeof(uint16_t) + sizeof(WindowClassType *) + sizeof(uint16_t) + sizeof(ptrdiff_t) + sizeof(uint16_t)
      mov rcx, <pointer to theWindowClass>
      mov rax, <pointer to theWindowProcedure>
      jmp rax
    • Was der Code genau macht, dazu komme ich später
  7. Anschließend wird die Page auf nicht mehr lesbar oder schreibbar gesetzt, sondern nur noch auf ausführbar (MakeMemoryExecutable)
  8. Nun setzen wir endlich die Fensterprozedur unseres Fensters mittels ::SetWindowLongPtrW mit Parameter GWLP_WNDPROC um, und zwar auf die Speicheradresse, an der unser char-Array der ThunkImplementation-Instanz liegt
    • Damit wird der obige Code, den wir als Daten in unser char-Array gesteckt haben, ausgeführt
  9. Das einzige, was noch fehlt, ist zu erklären, was der Code nun macht:
    • Für x86:
      • Metavariablen:
        • <pointer to theWindowClass> ist ein this-Pointer auf unsere ExampleWindowProcedure-Instanz
        • <relative address to theWindowProcedure> ist eine relative Adresse auf eine statische Methode mit __stdcall-Konvention namens DefaultWindowProcedure in der Elternklasse von ExampleWindowProcedure
      • Da wir __stdcall-Konvention haben, und da alles über den Stack läuft, überschreiben wir den ersten Parameter an DefaultWindowProcedure (das ist ::HWND theWindowHandle) mit dem this-Pointer
      • Dann machen wir einen jmp auf DefaultWindowProcedure
      • Dort frickeln wir uns mittels DerivedWindowProcedure * thisPointer = reinterpret_cast<DerivedWindowProcedure * >(theWindowHandle); wieder unseren this-Pointer zurück
      • Und rufen dann mittels thisPointer->WindowProcedure(…) die Fensterprozedur endlich auf
    • Für x64:
      • Eigentlich so wie x86, nur dass __stdcall vom Kompiler ignoriert wird, und x64-Konvention benutzt wird.
      • Dadurch geht erstmal nichts über den Stack, sondern über die Register; hierbei steckt in rcx der erste Parameter an DefaultWindowProcedure
      • Wir überschreiben dieses Register mit dem this-Pointer, und der Rest funktioniert wie unter x86
Das ganze ist unter x86 und x64 unter Windows 8 getestet und funktioniert.

Abschließend noch zur Performance unter x64 im Release-Modus:
  • Windows ruft unsere benutzerdefiniert gesetzte Fensterprozedur in user32.dll!UserCallWinProcCheckWow auf:
    • In unserem Beispiel hat die CPU 00000000024A0000 in r14 geladen
    • user32.dll!UserCallWinProcCheckWow() + 0x13b bytes:
      000007FEC980171B 41 FF D6 call r14
  • Dies ruft den Code in unserer Thunk-Implementierung auf:
    00000000024A0000 48 B9 68 F9 A7 00 00 00 00 00 mov rcx,0A7F968h
    00000000024A000A 48 B8 20 15 DC 13 F6 07 00 00 mov rax,7F613DC1520h
    00000000024A0014 FF E0 jmp rax
  • An der in rax geladenen Speicheradresse 000007F613DC1520 steht unsere DefaultWindowProcedure-Funktion, welche der Optimizer auf einen einzigen unkonditionalen jmp optimiert hat:
    static ::LRESULT __stdcall DefaultWindowProcedure(
        ::HWND theWindowHandle,
        unsigned int theMessage,
        ::WPARAM theAdditionalMessageInformation1,
        ::LPARAM theAdditionalMessageInformation2
    ) {
        DerivedWindowProcedure * thisPointer = reinterpret_cast<DerivedWindowProcedure * >(theWindowHandle);

        return thisPointer->WindowProcedure
        (
            theMessage,
            theAdditionalMessageInformation1,
            theAdditionalMessageInformation2
        );
    }

    000007F613DC1520 E9 FB FB FF FF jmp ExampleWindowProcedure::WindowProcedure (7F613DC1120h)
  • Und der jmp geht dann an unsere Fensterprozedur in unserer Instanz:
    ::LRESULT WindowProcedure(
        unsigned int theMessage,
        ::WPARAM theAdditionalMessageInformation1,
        ::LPARAM theAdditionalMessageInformation2
    ) {
    000007F613DC1120 40 53 push rbx
    000007F613DC1122 48 81 EC 90 00 00 00 sub rsp,90h
    000007F613DC1129 48 8B 05 F0 3E 00 00 mov rax,qword ptr [__security_cookie (7F613DC5020h)]
    000007F613DC1130 48 33 C4 xor rax,rsp
    000007F613DC1133 48 89 84 24 80 00 00 00 mov qword ptr [rsp+80h],rax
    000007F613DC113B 48 8B D9 mov rbx,rcx
        if(theMessage == WM_DESTROY)
    000007F613DC113E 83 FA 02 cmp edx,2
    000007F613DC1141 75 0C jne ExampleWindowProcedure::WindowProcedure+2Fh (7F613DC114Fh)
        {
Mein Code ist stark durch die Links aus meinem vorherigen Beitrag inspiriert, insbesondere dem hier; jedoch kann man die dortigen Methoden nicht unter x64 direkt so anwenden. Die Microsoft ATL implementiert es genau so wie ich es nun getan habe. Damit dürfte man diese Methode als mit Microsofts Segen behaftet ansehen.

Im Übrigen funktioniert sämtlicher sonstiger Thunking-Code aus dem Internet wegen DEP nicht mehr. Dieser Post ist das einzige unter x64 funktionierende Thunking-Beispiel, welches mir bekannt ist.

Ich habe mal getestet, was passiert, wenn man direkt die Instanzmethode anspringen würde (was nur unter x64 aufgrund der Aufrufkonvention geht): Der Compiler erstellt korrekterweise einen Eintrag in die Relocation-Table, und ist damit genauso effizient wie unsere obige Implementierung; nur dass die obige auch unter x86 funktioniert, da der Compiler dort mehr Zwischencode als Kleber zwischen den Aufrufkonventionen generiert.

Bild

Der einzige Overhead in obiger Fassung ist ein einziger jmp, wie bei einer Relokationstabelle. Womit der Code wohl ziemlich optimal ist.
Benutzeravatar
Aramis
Moderator
Beiträge: 1458
Registriert: 25.02.2009, 19:50
Echter Name: Alexander Gessler
Wohnort: 2016
Kontaktdaten:

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von Aramis »

Der Thread war mir voellig entgangen - wir sollten das Original-Tutorial entsprechend editieren oder alternativ ganz verschwinden lassen. Wenn ich mir das so durchlese, sehe ich hauptsaechlich mein juengeres selbst vor mir :-)
Benutzeravatar
Krishty
Establishment
Beiträge: 8238
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von Krishty »

Microsoft hat ein eigenes deutschsprachiges Tutorial geschrieben, das die WinAPI erklärt. Dabei ist das WndProc()-in-Klasse-Problem als Verwalten des Anwendungsservers eingetragen. Ich würde sagen, dass es unseren Thread um Längen schlägt:

http://msdn.microsoft.com/de-de/library ... p/ff381400
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
eXile
Establishment
Beiträge: 1136
Registriert: 28.02.2009, 13:27

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von eXile »

Krishty hat geschrieben:Ich würde sagen, dass es unseren Thread um Längen schlägt
Naja, die dort haben aber auch bei jeder Nachricht das Problem, dass GetWindowLongPtr laut dot ja einmal in den Kernel abtaucht. (Übrigens ist der englische Titel „Managing Application State“; nur, falls der deutsche Titel irgendeinen hier verwirrt haben sollte. ;))
Benutzeravatar
Krishty
Establishment
Beiträge: 8238
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von Krishty »

Ich bin kein Fan von Thunks. Sie haben schon einmal Kompatibilitätsprobleme bei einer ganzen Generation von Programmen verursacht, und werden es bestimmt wieder tun. Ich persönlich bleibe deshalb beim „offiziell empfohlenen“ Weg.

Und ja; der deutsche Titel ist … exotisch.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8238
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Was ist mit unserem WNDPROC-in-Klasse-Tutorial?

Beitrag von Krishty »

GWL_USERDATA ist bei Dialogen unbrauchbar, weil wir nicht Besitzer der Fensterklasse sind. Für diesen Fall scheint es aber DWL_USER zu geben. Falls also jemand aus Fenster in Klasse wrappen ein Dialog in Klasse wrappen machen will, ist das die Lösung (aber in WM_INITDIALOG statt WM_NCCREATE).

Raymond Chen schreibt zwar im StackOverflow-Thread Win32 API check if current window is dialog or normal window, dass DWL_USER jemand anders gehöre; aber ich glaube, dass sich das nur auf das Subclassing bezieht (wo man stattdessen GetProp() / SetProp() verwenden sollte), zumal er DWL_USER in einigen seiner Artikel selber benutzt (z.B. in A different type of dialog procedure oder The dialog manager, part 5: Converting a non-modal dialog box to modal).
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Antworten