When you start dragging a window’s title bar to move it or adjust its size by dragging its border, your program first receives a WM_ENTERSIZEMOVE
message. This message simply notifies you that dragging is about to begin. Subsequently, a WM_SYSCOMMAND
message arrives, where the wParam
value determines the action type. You’ll use switch (GET_SC_WPARAM(wParam))
to handle it. When the switch hits case SC_MOVE:
or case SC_SIZE:
, it indicates that the system is entering drag mode. If you directly forward this message to DefWindowProc()
, it blocks by starting its own internal message loop, handling mouse movements, window resizing, and other complex operations internally. When you release the mouse, DefWindowProc()
returns 0
, ending the blocking. Finally, it sends a WM_EXITSIZEMOVE
message to notify you that drag mode has ended.
For scenarios like game development, video player implementations, etc.—where you cannot rely on blocking-style GetMessage()
loops with timers (Timer
callbacks) and must avoid blocking during message processing (e.g., DefWindowProc()
blocking)—yet still need to support window dragging and resizing, here’s a workaround technique to interrupt and recover from blocking.
First, my window message handler is non-blocking. It’s named PollWindowEvents()
and is called repeatedly by the user’s main loop. The caller handles its own tasks (e.g., rendering dynamic content) and then invokes PollWindowEvents()
to process window messages, check keyboard inputs, etc. Similar to GLFW’s design, as long as PollWindowEvents()
doesn’t block, the caller’s loop keeps running.
The logic of PollWindowEvents()
roughly looks like this:
while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
if (msg.message == WM_QUIT)
{
w->should_quit = 1;
w->exit_code = (int)msg.wParam;
}
else
{
DispatchMessageW(&msg);
}
}
During window dragging, although DefWindowProc()
blocks, timer events (WM_TIMER
) still reach your window procedure. Additionally, WM_MOVE
, WM_SIZE
, and WM_SIZING
messages are delivered frequently during drag/resize operations.
If within these messages, I can break out of DefWindowProc()
’s context and make PollWindowEvents()
return, blocking is resolved. On the next call to PollWindowEvents()
, I jump back into the message handler as if nothing happened, resuming DefWindowProc()
’s drag logic to complete the cycle.
Achieving this jump requires setjmp()
and longjmp()
from the C standard library. Unfortunately, they’re problematic here. MSVC’s longjmp()
enforces RAII resource cleanup rules in C++ and handles uncaught exceptions. It assumes longjmp()
is a non-returning operation, requiring all stack resources and exceptions to be resolved before jumping. This "safety" conflicts with our need, where jumping back into the context later shouldn’t disrupt normal RAII or exception handling.
Jumping is only part of the problem. The blocking DefWindowProc()
uses its stack frame, while returning from PollWindowEvents()
lets the caller use its stack frame. If we longjmp()
out (which restores the stack pointer to the setjmp()
state), two stack frames are used simultaneously—leading to data corruption and stack overwrites. Thus, we need two separate stacks.
longjmp()
: Bypass MSVC’s extra safety checks using custom shellcode._aligned_malloc()
to allocate a 16-byte aligned memory block as the new stack.jmp_to_new_stack()
, which does three things:
my_longjmp()
to return from the new stack to the old stack’s setjmp()
position.Why my_longjmp()
instead of restoring the old stack pointer directly? Because both stacks are active; the original stack pointer becomes invalid after leaving its context.
Setting jump targets:
PollWindowEvents()
(before PeekMessageW
), use setjmp()
to set a "re-entry point" for resuming blocked operations.PollWindowEvents()
(after the PeekMessageW
loop), use setjmp()
to set an "escape point" for breaking out of blocking.WM_TIMER
/WM_MOVE
/WM_SIZE
handlers, use setjmp()
to set "re-entry points" for jumping back into the blocking function.Entering blocking mode:
WM_SYSCOMMAND
handler (for SC_MOVE
/SC_SIZE
), create a timer, allocate the new stack, then call jmp_to_new_stack()
. The provided callback invokes:DefWindowProcW(w->Window, WM_SYSCOMMAND, wParam, 0); // Enters blocking
Escaping blocking mode:
WM_TIMER
/WM_MOVE
/WM_SIZE
handlers (executing on the new stack), use my_longjmp()
to jump to the "escape point" at PollWindowEvents()
’s end—returning control to the caller.Re-entry logic:
PollWindowEvents()
call detects the interrupted state and uses my_longjmp()
to jump back to the handler’s "re-entry point."Exiting blocking mode:
DefWindowProcW()
returns (after drag ends), use my_longjmp()
to jump to PollWindowEvents()
’s start, letting normal message processing resume._my_longjmp()
__declspec(noreturn)
void _my_longjmp(jmp_buf jb, int value)
{
#ifdef _M_IX86
static const uint32_t shellcode_my_longjmp[] =
{
0x0424548B,
0x0824448B,
0x8301F883,
0x2A8B00D0,
0x8B045A8B,
0x728B087A,
0x10628B0C,
0xFF04C483,
0x90901462,
};
#elif defined(_M_X64)
static const uint64_t shellcode_my_longjmp[] =
{
0x4808598B48D08948,
0x4818698B4810618B,
0x4C28798B4820718B,
0x4C38698B4C30618B,
0x0F48798B4C40718B,
0x5C69D9E2DB5851AE,
0x6F0F6660716F0F66,
0x80816F0F44667079,
0x896F0F4466000000,
0x6F0F446600000090,
0x0F4466000000A091,
0x4466000000B0996F,
0x66000000C0A16F0F,
0x000000D0A96F0F44,
0x0000E0B16F0F4466,
0x00F0B96F0F446600,
0x9090905061FF0000,
};
#else
// If your computer is not x86 nor x64, implement your own `longjmp()` shellcode for your CPU.
longjmp(jb, value);
#endif
static DWORD dwOldProtect = 0;
if (!dwOldProtect) VirtualProtect((void *)shellcode_my_longjmp, sizeof shellcode_my_longjmp, PAGE_EXECUTE_READ, &dwOldProtect);
void(*my_longjmp)(jmp_buf jb, int value) = (void *)shellcode_my_longjmp;
my_longjmp(jb, value);
}
_jmp_to_new_stack()
__declspec(noreturn)
void _jmp_to_new_stack(void *stack_buffer, size_t stack_size, void(*function_to_run)(void *userdata), void *userdata, jmp_buf returning, int longjmp_val)
{
#ifdef _M_IX86
static const uint32_t shellcode_jmp_to_new_stack[] =
{
0x608be089,
0x08600304,
0xff1870ff,
0x70ff1470,
0x0c50ff10,
0x6804c483,
0xdeadbeef,
0x909002eb,
0x0424548b,
0x0824448b,
0x8301f883,
0x2a8b00d0,
0x8b045a8b,
0x728b087a,
0x10628b0c,
0xff04c483,
0x90901462,
};
#elif defined(_M_X64)
static const uint64_t shellcode_jmp_to_new_stack[] =
{
0x0148cc8948e08948,
0x4c2870ff3070ffd4,
0xff4120ec8348c989,
0xeb5a5920c48348d0,
0x9090909090909007,
0x4808598b48d08948,
0x4818698b4810618b,
0x4c28798b4820718b,
0x4c38698b4c30618b,
0x0f48798b4c40718b,
0x5c69d9e2db5851ae,
0x6f0f6660716f0f66,
0x80816f0f44667079,
0x896f0f4466000000,
0x6f0f446600000090,
0x0f4466000000a091,
0x4466000000b0996f,
0x66000000c0a16f0f,
0x000000d0a96f0f44,
0x0000e0b16f0f4466,
0x00f0b96f0f446600,
0x9090905061ff0000,
};
#else
fprintf(stderr, "[UNIMPLEMENTED] Please provide your shellcode for your CPU to implement `jmp_to_new_stack()` by doing these steps:\n");
fprintf(stderr, "[UNIMPLEMENTED] Save your current stack pointer register to a volatile register whichever you'd like to use, the volatile register stores your original stack pointer and could help you to retrieve your parameters;\n");
fprintf(stderr, "[UNIMPLEMENTED] Set your stack pointer register to the end of my stack buffer: `(size_t)stack_buffer + stack_size`;\n");
fprintf(stderr, "[UNIMPLEMENTED] After moving to the new stack, save your 5th and 6th paramters to the new stack;\n");
fprintf(stderr, "[UNIMPLEMENTED] Retrieve your 4th parameter `userdata` from the original stack (using the saved stack pointer in the volatile register);\n");
fprintf(stderr, "[UNIMPLEMENTED] Your 3rd parameter is a pointer to a callback function (the function to run on the new stack). Call it and pass `userdata`. NOTE: This function will destroy your volatile register as usual occasion;\n");
fprintf(stderr, "[UNIMPLEMENTED] After calling the function, balance your stack;\n");
fprintf(stderr, "[UNIMPLEMENTED] Retrieve your 5th (`jmp_buf`) and 6th (`longjmp_value`) parameters from where you saved them on the new stack, these parameters are for returning to your previous stack via a `longjmp()`;\n");
fprintf(stderr, "[UNIMPLEMENTED] IMPORTANT: Both stacks are actively changing, do not attempt to restore the stack pointer directly;\n");
fprintf(stderr, "[UNIMPLEMENTED] Implement and execute a `longjmp(jmp_buf, longjmp_value)` to return to the original stack\n");
assert(0);
#endif
static DWORD dwOldProtect = 0;
if (!dwOldProtect) VirtualProtect((void *)shellcode_jmp_to_new_stack, sizeof shellcode_jmp_to_new_stack, PAGE_EXECUTE_READ, &dwOldProtect);
void(*jmp_to_new_stack)(void *, size_t, void(*)(void *), void *, jmp_buf, int) = (void *)shellcode_jmp_to_new_stack;
jmp_to_new_stack(stack_buffer, stack_size, function_to_run, userdata, returning, longjmp_val);
}
The arrays contain shellcode (machine code) for context switching. Implementations:
x86 my_longjmp
:
bits 32
;void my_longjmp(jmp_buf jb, int value);
my_longjmp:
mov edx, [esp + 4]
mov eax, [esp + 8]
cmp eax, 1
adc eax, 0
mov ebp, [edx + 0]
mov ebx, [edx + 4]
mov edi, [edx + 8]
mov esi, [edx + 12]
mov esp, [edx + 16]
add esp, 4
jmp [edx + 20]
times 4 - ($ - $$ & 3) nop ; Alignment
x64 my_longjmp
:
bits 64
;void my_longjmp(jmp_buf jb, int value);
my_longjmp:
mov rax, rdx
mov rbx, [rcx + 0x08]
mov rsp, [rcx + 0x10]
mov rbp, [rcx + 0x18]
mov rsi, [rcx + 0x20]
mov rdi, [rcx + 0x28]
mov r12, [rcx + 0x30]
mov r13, [rcx + 0x38]
mov r14, [rcx + 0x40]
mov r15, [rcx + 0x48]
;rip = [rcx + 0x50]
ldmxcsr [rcx + 0x58]
fnclex
fldcw [rcx + 0x5C]
movdqa xmm6, [rcx + 0x60]
movdqa xmm7, [rcx + 0x70]
movdqa xmm8, [rcx + 0x80]
movdqa xmm9, [rcx + 0x90]
movdqa xmm10, [rcx + 0xA0]
movdqa xmm11, [rcx + 0xB0]
movdqa xmm12, [rcx + 0xC0]
movdqa xmm13, [rcx + 0xD0]
movdqa xmm14, [rcx + 0xE0]
movdqa xmm15, [rcx + 0xF0]
jmp [rcx + 0x50]
times 8 - ($ - $$ & 7) nop ; Alignment
x86 jmp_to_new_stack
:
bits 32
;void jmp_to_new_stack(void *stack_buffer, size_t stack_size, void(*function_to_run)(void *userdata), void *userdata, jmp_buf returning, int longjmp_value);
jmp_to_new_stack:
mov eax, esp ; Save old ESP
mov esp, [eax + 4] ; Set ESP = stack_buffer
add esp, [eax + 8] ; ESP = stack_buffer + stack_size
; Set up args for callback & my_longjmp
push dword [eax + 24] ; longjmp_val
push dword [eax + 20] ; returning jmp_buf
push dword [eax + 16] ; userdata
call dword [eax + 12] ; Call func(userdata) on new stack
add esp, 4 ; Cleanup
push dword 0xDEADBEEF ; There should be a returning address, but we don't return this way.
jmp my_longjmp
times 4 - ($ - $$ & 3) nop ; Alignment
%include "x86_my_longjmp.asm"
x64 jmp_to_new_stack
bits 64
;void jmp_to_new_stack(void *stack_buffer, size_t stack_size, void(*function_to_run)(void *userdata), void *userdata, jmp_buf returning, int longjmp_value);
jmp_to_new_stack:
mov rax, rsp ; Save old RSP
mov rsp, rcx ; Set RSP = stack_buffer
add rsp, rdx ; RSP = stack_buffer + stack_size
push qword [rax + 48] ; longjmp_val
push qword [rax + 40] ; returning jmp_buf
mov rcx, r9 ; userdata
sub rsp, 32 ; x64 call needs to pre-allocate stack memory for 4 parameters for the home of RCX, RDX, R8, R9
call r8 ; Call func(userdata) on new stack
add rsp, 32 ; Cleanup
pop rcx ; Retrieve longjmp_val
pop rdx ; Retrieve returning jmp_buf
jmp my_longjmp
times 8 - ($ - $$ & 7) nop ; Alignment
%include "x64_my_longjmp.asm"
State Tracking Structure:
typedef struct
{
void *new_stack; // Allocated stack memory
size_t new_stack_size; // Stack size
volatile int hack_is_on; // 1 if in blocking escape mode
volatile int is_returned_from_timer; // Flag for escaped state
WPARAM syscommand_wparam; // Saved WM_SYSCOMMAND param
jmp_buf jb_returning; // Escape point (Poll end)
jmp_buf jb_reentering; // Re-entry point (handlers)
jmp_buf jb_exit_hacking; // Exit point (blocking ends)
}HackWayAntiBlocking;
Code to be added into PollWindowEvents()
before PeekMessage()
:
if (setjmp(w->hack.jb_exit_hacking) == 1)
{ // After blocking ends (DefWindowProc returns)
KillTimer(w->Window, 1);
w->hack.hack_is_on = 0;
}
if (w->hack.hack_is_on)
{ // Re-enter blocked context if needed
if (w->hack.is_returned_from_timer)
_my_longjmp(w->hack.jb_reentering, 1);
}
Code to be added into PollWindowEvents()
before returning:
if (setjmp(w->hack.jb_returning) == 1)
{ // Escape from the blocked call
return;
}
WndProc
Handler Additions:
WM_SYSCOMMAND
:case WM_SYSCOMMAND:
switch (GET_SC_WPARAM(wp))
{
case SC_MOVE:
case SC_SIZE:
w = (void *)GetWindowLongPtrW(hWnd, 0);
assert(w->hack_is_on == 0);
w->hack.syscommand_wparam = wp;
if (!w->hack.new_stack)
{ // Allocate memory for the new stack, 64 KiB is not too much or too less
w->hack.new_stack_size = (size_t)1 << 16;
w->hack.new_stack = _aligned_malloc(w->hack.new_stack_size, 16);
}
if (w->hack.new_stack)
{ // From here, we are going to switch to the new stack and call the blocking function.
w->hack.hack_is_on = 1;
// Start timer. When blocking, our `WndProc()` still can receive `WM_TIMER` event.
SetTimer(w->Window, 1, 1, NULL);
_jmp_to_new_stack(w->hack.new_stack, w->hack.new_stack_size, _run_blocking_proc, w, w->hack.jb_exit_hacking, 1);
}
else
{ // Can't allocate memory for the new stack? Give up here.
w->hack.new_stack_size = 0;
}
break;
default:
return DefWindowProcW(hWnd, msg, wp, lp);
}
break;
WM_TIMER
/WM_MOVE
/WM_SIZE
:case WM_MOVE:
case WM_SIZING:
case WM_SIZE:
case WM_TIMER:
if (msg != WM_TIMER || wp == 1)
{
w = (void *)GetWindowLongPtrW(hWnd, 0);
if (w->hack.hack_is_on)
{
int j = setjmp(w->hack.jb_reentering); // Set re-entry point and escape blocking
switch (j)
{
case 0:
// Escape blocking
w->hack.is_returned_from_timer = 1;
_my_longjmp(w->hack.jb_returning, 1);
break;
case 1:
// Re-entry to here
break;
default:
assert(0);
}
w->hack.is_returned_from_timer = 0;
}
}
break;
_run_blocking_proc()
:// Callback running on the new stack
void _run_blocking_proc(WindowsDemoGuts *w)
{ // Blocks here
DefWindowProcW(w->Window, WM_SYSCOMMAND, w->hack.syscommand_wparam, 0);
}
Cleanup:
_aligned_free(w->hack.new_stack);
w->hack.new_stack = 0;
w->hack.new_stack_size = 0;
With these modifications, dragging/resizing a window won’t block PollWindowEvents()
—it returns immediately—allowing the caller’s loop to maintain smooth rendering and audio playback.
Complete source code: