Cały temat jest bardzo ciekawy, można go rozwiązać na wiele różnych sposobów. Każdy z nich ma swoje plusy i minusy.
Metoda, którą przedstawił Azarien (statyczny WndProc, SetWindowLongPtr
i GWLP_USERDATA
) jest prosta, ale ma kilka minusów. Po pierwsze używa GWLP_USERDATA
, co może powodować problemy. Istnieją programy, które są na tyle zdolne, że potrafią ustawić GWLP_USERDATA
dla nieswoich okien i wtedy zepsują nam program. Po drugie nie pozwala nam obsłużyć wszystkich komunikatów. System wysyła już komunikaty w momencie wywołania CreateWindow
i w tym wariancie nie ma możliwości podpiąć się pod pierwszy z nich. Po trzecie, co jest chyba najważniejsze, nie jest tak wydajna jak można to zrobić - z każdym komunikatem musimy wywołać GetWindowLongPtr
i wywołać procedurę okna w naszej klasie.
MFC oraz Qt, upraszczając, mają mapę HWND -> obiekt, do której jest robiony lookup przy każdej wiadomości. Rozwiązuje to pierwszy z problemów, które opisałem na początku.
Sam Microsoft obecnie używa innej metody, z tzw. thunkiem. Jest ona zastosowana w ATL/WTL oraz pośrednio, w .NET jako element ogólnego marshallingu delegat do pointerów, co jest również użyte w obsłużeniu procedury okna. Thunk to najogólniej mówiąc kawałek kodu, który pomaga nam w wywołaniu innej funkcji. Natomiast jest to o tyle ciekawy kawałek kodu, że stworzymy go sami podczas działania programu. Z tego też względu jest to sposób nieprzenośny, tj. trzeba zapewnić implementację dla każdej architektury. W przypadku Windows nie jest to wielki problem, bo wystarczy zrobić to dla x86, x64 i ARM. Adres do naszego thunka podamy jako procedurę okna. W przypadku x86 thunk zamieni nam pierwszy parametr na stosie, czyli esp+4
(hWnd), na pointer do obiektu okna. Potem skoczymy do funkcji, która wywoła nam już metodę w klasie okna (zakładając, że jest ona wirtualna - ale tutaj jest kolejny element, w którym możemy pobawić się w optymalizacje, np. ze statycznym dziedziczeniem jak to w ATL/WTL zorganizowano - wtedy kompilator wyoptymalizuje nam to wywołanie w całości).
Cała procedura wygląda tak:
Przygotowujemy mapę "thread id" -> "pointer na obiekt okna". Mapa ta jest używana tylko i wyłącznie przy pierwszym otrzymanym komunikacie, który dostaniemy już przy wywołaniu CreateWindow
. Do mapy dodajemy nasz pointer tuż przed wywołaniem CreateWindow
i zaraz po tym możemy z niej nasze mapowanie usunąć. Dzięki temu jesteśmy pewni, że w tym momencie w naszym wątku nie będzie tworzone inne okno. Do tego dodajemy mutex. Przy rejestracji klasy okna początkowo ustawiamy procedurę okna na funkcję, która przy pierwszym komunikacie pobierze wskaźnik do obiektu okna z mapy oraz zmieni GWLP_WNDPROC na nasz thunk.
Tadam. Mamy procedurę okna jako metodę klasy przy jedynym overheadzie w postaci jednego mov i jednego jmp.
Tak wygląda przygotowanie thunku dla x86:
unsigned char payload[] = "\xC7\x44\x24\x04\xDE\xAD\xBA\xBE\xE9\xDE\xAD\xBA\xBE";
memcpy(thunk, payload, _countof(thunk));
int32_t wndProcAddress = (int32_t)&RedirectWndProc;
*(int32_t*)(&thunk[4]) = (int32_t)this;
*(int32_t*)(&thunk[9]) = wndProcAddress - ((int32_t)&thunk + _countof(thunk));
DWORD dummy;
VirtualProtect(thunk, _countof(thunk), PAGE_EXECUTE_READWRITE, &dummy);
FlushInstructionCache(GetCurrentProcess(), thunk, _countof(thunk));
Który ustawiamy potem zwykłym:
SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)&window->thunk);