Skip to content

Commit 9feb63d

Browse files
committed
Add global ShowWindow hooks for pre-show/hide events
Introduces IAT patching to hook ShowWindow and ShowWindowAsync globally, enabling pre-show/hide hooks to be invoked before window visibility changes. Replaces WM_SHOWWINDOW handling with WM_WINDOWPOSCHANGING for more accurate pre-event interception. Hooks are installed or removed dynamically when will_show or will_hide hooks are set or cleared.
1 parent b24a33c commit 9feb63d

File tree

1 file changed

+271
-9
lines changed

1 file changed

+271
-9
lines changed

src/platform/windows/window_manager_windows.cpp

Lines changed: 271 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,275 @@
22
#include <iostream>
33
#include <string>
44

5+
#include <psapi.h>
56
#include "../../window.h"
67
#include "../../window_event.h"
78
#include "../../window_manager.h"
89
#include "string_utils_windows.h"
910

11+
#pragma comment(lib, "psapi.lib")
12+
1013
namespace nativeapi {
1114

15+
namespace {
16+
17+
using PFN_ShowWindow = BOOL(WINAPI*)(HWND, int);
18+
using PFN_ShowWindowAsync = BOOL(WINAPI*)(HWND, int);
19+
20+
static PFN_ShowWindow g_original_show_window = nullptr;
21+
static PFN_ShowWindowAsync g_original_show_window_async = nullptr;
22+
static bool g_hooks_installed = false;
23+
24+
static bool IsShowCommandVisible(int cmd) {
25+
switch (cmd) {
26+
case SW_SHOW:
27+
case SW_SHOWNORMAL:
28+
case SW_SHOWDEFAULT:
29+
case SW_SHOWMAXIMIZED:
30+
case SW_SHOWNOACTIVATE:
31+
case SW_RESTORE:
32+
return true;
33+
default:
34+
return false;
35+
}
36+
}
37+
38+
static void InvokePreShowHideHooks(HWND hwnd, int cmd) {
39+
auto& manager = WindowManager::GetInstance();
40+
Window temp_window(hwnd);
41+
WindowId window_id = temp_window.GetId();
42+
if (cmd == SW_HIDE) {
43+
manager.InvokeWillHideHook(window_id);
44+
} else if (IsShowCommandVisible(cmd)) {
45+
manager.InvokeWillShowHook(window_id);
46+
}
47+
}
48+
49+
static BOOL WINAPI HookedShowWindow(HWND hwnd, int nCmdShow) {
50+
InvokePreShowHideHooks(hwnd, nCmdShow);
51+
if (g_original_show_window) {
52+
return g_original_show_window(hwnd, nCmdShow);
53+
}
54+
// Fallback to direct call if original not captured
55+
auto p = reinterpret_cast<PFN_ShowWindow>(
56+
GetProcAddress(GetModuleHandleW(L"user32.dll"), "ShowWindow"));
57+
return p ? p(hwnd, nCmdShow) : FALSE;
58+
}
59+
60+
static BOOL WINAPI HookedShowWindowAsync(HWND hwnd, int nCmdShow) {
61+
InvokePreShowHideHooks(hwnd, nCmdShow);
62+
if (g_original_show_window_async) {
63+
return g_original_show_window_async(hwnd, nCmdShow);
64+
}
65+
auto p = reinterpret_cast<PFN_ShowWindowAsync>(
66+
GetProcAddress(GetModuleHandleW(L"user32.dll"), "ShowWindowAsync"));
67+
return p ? p(hwnd, nCmdShow) : FALSE;
68+
}
69+
70+
static bool CaseInsensitiveEquals(const char* a, const char* b) {
71+
if (!a || !b)
72+
return false;
73+
while (*a && *b) {
74+
char ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a;
75+
char cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b;
76+
if (ca != cb)
77+
return false;
78+
++a;
79+
++b;
80+
}
81+
return *a == *b;
82+
}
83+
84+
static void PatchIATInModule(HMODULE module,
85+
FARPROC target,
86+
FARPROC replacement,
87+
const char* func_name) {
88+
if (!module)
89+
return;
90+
91+
auto base = reinterpret_cast<BYTE*>(module);
92+
auto dos = reinterpret_cast<PIMAGE_DOS_HEADER>(base);
93+
if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE)
94+
return;
95+
96+
auto nt = reinterpret_cast<PIMAGE_NT_HEADERS>(base + dos->e_lfanew);
97+
if (!nt || nt->Signature != IMAGE_NT_SIGNATURE)
98+
return;
99+
100+
auto& import_dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
101+
if (import_dir.VirtualAddress == 0)
102+
return;
103+
104+
auto import_desc = reinterpret_cast<PIMAGE_IMPORT_DESCRIPTOR>(base + import_dir.VirtualAddress);
105+
for (; import_desc->Name != 0; ++import_desc) {
106+
auto dll_name = reinterpret_cast<const char*>(base + import_desc->Name);
107+
// Only hook USER32.dll to reduce risk
108+
if (!dll_name)
109+
continue;
110+
if (!(CaseInsensitiveEquals(dll_name, "user32.dll")))
111+
continue;
112+
113+
auto orig_thunk = reinterpret_cast<PIMAGE_THUNK_DATA>(base + import_desc->OriginalFirstThunk);
114+
auto thunk = reinterpret_cast<PIMAGE_THUNK_DATA>(base + import_desc->FirstThunk);
115+
if (!orig_thunk || !thunk)
116+
continue;
117+
118+
for (; orig_thunk->u1.AddressOfData != 0; ++orig_thunk, ++thunk) {
119+
if (IMAGE_SNAP_BY_ORDINAL(orig_thunk->u1.Ordinal)) {
120+
continue; // Skip ordinals
121+
}
122+
auto import = reinterpret_cast<PIMAGE_IMPORT_BY_NAME>(base + orig_thunk->u1.AddressOfData);
123+
if (!import || !import->Name)
124+
continue;
125+
const char* name = reinterpret_cast<const char*>(import->Name);
126+
if (!CaseInsensitiveEquals(name, func_name))
127+
continue;
128+
129+
// Change protection and write new function pointer
130+
DWORD old_protect;
131+
if (VirtualProtect(&thunk->u1.Function, sizeof(void*), PAGE_READWRITE, &old_protect)) {
132+
// Store original (first time only)
133+
(void)target; // target kept for symmetry; not used here
134+
thunk->u1.Function = reinterpret_cast<ULONG_PTR>(replacement);
135+
VirtualProtect(&thunk->u1.Function, sizeof(void*), old_protect, &old_protect);
136+
FlushInstructionCache(GetCurrentProcess(), &thunk->u1.Function, sizeof(void*));
137+
}
138+
}
139+
}
140+
}
141+
142+
static void RestoreIATInModule(HMODULE module, FARPROC original, const char* func_name) {
143+
if (!module || !original)
144+
return;
145+
146+
auto base = reinterpret_cast<BYTE*>(module);
147+
auto dos = reinterpret_cast<PIMAGE_DOS_HEADER>(base);
148+
if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE)
149+
return;
150+
151+
auto nt = reinterpret_cast<PIMAGE_NT_HEADERS>(base + dos->e_lfanew);
152+
if (!nt || nt->Signature != IMAGE_NT_SIGNATURE)
153+
return;
154+
155+
auto& import_dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
156+
if (import_dir.VirtualAddress == 0)
157+
return;
158+
159+
auto import_desc = reinterpret_cast<PIMAGE_IMPORT_DESCRIPTOR>(base + import_dir.VirtualAddress);
160+
for (; import_desc->Name != 0; ++import_desc) {
161+
auto dll_name = reinterpret_cast<const char*>(base + import_desc->Name);
162+
if (!dll_name)
163+
continue;
164+
if (!(CaseInsensitiveEquals(dll_name, "user32.dll")))
165+
continue;
166+
167+
auto orig_thunk = reinterpret_cast<PIMAGE_THUNK_DATA>(base + import_desc->OriginalFirstThunk);
168+
auto thunk = reinterpret_cast<PIMAGE_THUNK_DATA>(base + import_desc->FirstThunk);
169+
if (!orig_thunk || !thunk)
170+
continue;
171+
172+
for (; orig_thunk->u1.AddressOfData != 0; ++orig_thunk, ++thunk) {
173+
if (IMAGE_SNAP_BY_ORDINAL(orig_thunk->u1.Ordinal)) {
174+
continue;
175+
}
176+
auto import = reinterpret_cast<PIMAGE_IMPORT_BY_NAME>(base + orig_thunk->u1.AddressOfData);
177+
if (!import || !import->Name)
178+
continue;
179+
const char* name = reinterpret_cast<const char*>(import->Name);
180+
if (!CaseInsensitiveEquals(name, func_name))
181+
continue;
182+
183+
DWORD old_protect;
184+
if (VirtualProtect(&thunk->u1.Function, sizeof(void*), PAGE_READWRITE, &old_protect)) {
185+
thunk->u1.Function = reinterpret_cast<ULONG_PTR>(original);
186+
VirtualProtect(&thunk->u1.Function, sizeof(void*), old_protect, &old_protect);
187+
FlushInstructionCache(GetCurrentProcess(), &thunk->u1.Function, sizeof(void*));
188+
}
189+
}
190+
}
191+
}
192+
193+
static void ForEachProcessModule(std::function<void(HMODULE)> fn) {
194+
HMODULE modules[1024];
195+
DWORD bytes_needed = 0;
196+
if (!EnumProcessModules(GetCurrentProcess(), modules, sizeof(modules), &bytes_needed)) {
197+
// Fallback: at least patch main module
198+
fn(GetModuleHandle(nullptr));
199+
return;
200+
}
201+
size_t count = bytes_needed / sizeof(HMODULE);
202+
for (size_t i = 0; i < count; ++i) {
203+
fn(modules[i]);
204+
}
205+
}
206+
207+
static void InstallGlobalShowHooks() {
208+
if (g_hooks_installed)
209+
return;
210+
HMODULE user32 = GetModuleHandleW(L"user32.dll");
211+
if (!user32)
212+
user32 = LoadLibraryW(L"user32.dll");
213+
if (!user32)
214+
return;
215+
216+
g_original_show_window = reinterpret_cast<PFN_ShowWindow>(GetProcAddress(user32, "ShowWindow"));
217+
g_original_show_window_async =
218+
reinterpret_cast<PFN_ShowWindowAsync>(GetProcAddress(user32, "ShowWindowAsync"));
219+
if (!g_original_show_window)
220+
return;
221+
222+
ForEachProcessModule([](HMODULE m) {
223+
PatchIATInModule(m, reinterpret_cast<FARPROC>(g_original_show_window),
224+
reinterpret_cast<FARPROC>(HookedShowWindow), "ShowWindow");
225+
if (g_original_show_window_async) {
226+
PatchIATInModule(m, reinterpret_cast<FARPROC>(g_original_show_window_async),
227+
reinterpret_cast<FARPROC>(HookedShowWindowAsync), "ShowWindowAsync");
228+
}
229+
});
230+
231+
g_hooks_installed = true;
232+
}
233+
234+
static void UninstallGlobalShowHooks() {
235+
if (!g_hooks_installed)
236+
return;
237+
ForEachProcessModule([](HMODULE m) {
238+
if (g_original_show_window) {
239+
RestoreIATInModule(m, reinterpret_cast<FARPROC>(g_original_show_window), "ShowWindow");
240+
}
241+
if (g_original_show_window_async) {
242+
RestoreIATInModule(m, reinterpret_cast<FARPROC>(g_original_show_window_async),
243+
"ShowWindowAsync");
244+
}
245+
});
246+
g_hooks_installed = false;
247+
}
248+
249+
} // namespace
250+
12251
// Custom window procedure to handle window messages
13252
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
14253
switch (uMsg) {
15-
case WM_SHOWWINDOW: {
16-
// wParam: TRUE if the window is being shown, FALSE if hidden
17-
auto& manager = WindowManager::GetInstance();
18-
Window temp_window(hwnd);
19-
WindowId window_id = temp_window.GetId();
20-
if (wParam) {
21-
manager.InvokeWillShowHook(window_id);
22-
} else {
23-
manager.InvokeWillHideHook(window_id);
254+
case WM_WINDOWPOSCHANGING: {
255+
// Intercept visibility changes BEFORE they happen (pre-show/hide "swizzle")
256+
// This is the closest Windows analogue to method swizzling NSWindow show/hide
257+
WINDOWPOS* pos = reinterpret_cast<WINDOWPOS*>(lParam);
258+
if (pos) {
259+
auto& manager = WindowManager::GetInstance();
260+
Window temp_window(hwnd);
261+
WindowId window_id = temp_window.GetId();
262+
if (pos->flags & SWP_SHOWWINDOW) {
263+
manager.InvokeWillShowHook(window_id);
264+
}
265+
if (pos->flags & SWP_HIDEWINDOW) {
266+
manager.InvokeWillHideHook(window_id);
267+
}
24268
}
25269
return DefWindowProc(hwnd, uMsg, wParam, lParam);
26270
}
271+
case WM_SHOWWINDOW:
272+
// Keep default processing; pre-hooks are handled in WM_WINDOWPOSCHANGING
273+
return DefWindowProc(hwnd, uMsg, wParam, lParam);
27274
case WM_CLOSE:
28275
// User clicked the close button
29276
DestroyWindow(hwnd);
@@ -124,6 +371,8 @@ void WindowManager::DispatchWindowEvent(const WindowEvent& event) {
124371
Emit(event);
125372
}
126373

374+
// Note: Global ShowWindow hooks are installed/uninstalled when hooks are set/cleared
375+
127376
// Create a new window with the given options
128377
std::shared_ptr<Window> WindowManager::Create(const WindowOptions& options) {
129378
HINSTANCE hInstance = GetModuleHandle(nullptr);
@@ -236,10 +485,23 @@ std::shared_ptr<Window> WindowManager::GetCurrent() {
236485

237486
void WindowManager::SetWillShowHook(std::optional<WindowWillShowHook> hook) {
238487
pimpl_->will_show_hook_ = std::move(hook);
488+
// Install or uninstall global hooks based on whether any hook is present
489+
bool has_any = pimpl_->will_show_hook_.has_value() || pimpl_->will_hide_hook_.has_value();
490+
if (has_any) {
491+
InstallGlobalShowHooks();
492+
} else {
493+
UninstallGlobalShowHooks();
494+
}
239495
}
240496

241497
void WindowManager::SetWillHideHook(std::optional<WindowWillHideHook> hook) {
242498
pimpl_->will_hide_hook_ = std::move(hook);
499+
bool has_any = pimpl_->will_show_hook_.has_value() || pimpl_->will_hide_hook_.has_value();
500+
if (has_any) {
501+
InstallGlobalShowHooks();
502+
} else {
503+
UninstallGlobalShowHooks();
504+
}
243505
}
244506

245507
void WindowManager::InvokeWillShowHook(WindowId id) {

0 commit comments

Comments
 (0)