[HN Gopher] Closures as Win32 Window Procedures
___________________________________________________________________
Closures as Win32 Window Procedures
Author : ibobev
Score : 92 points
Date : 2025-12-13 23:39 UTC (20 hours ago)
(HTM) web link (nullprogram.com)
(TXT) w3m dump (nullprogram.com)
| cyberax wrote:
| This approach was used in the ATL/WTL (Active Template Library,
| Windows Template Library) in the early 2000-s. It was a bad idea,
| because you need to generate executable code, interfering with
| NX-bit memory protection.
|
| Windows actually had a workaround in its NX-bit implementation
| that recognized the byte patterns of these trampolines from the
| fault handler:
| https://web.archive.org/web/20090123222148/http://support.mi...
| kmeisthax wrote:
| I'm genuinely surprised Microsoft's attitude towards "wndprocs
| don't have a context pointer" was "let's JIT compile a
| trampoline to hold the context pointer" and not to add support
| for a five-parameter wndproc into USER.dll, or have a wrapper
| that grabs GWLP_USERDATA and copies it to the register this
| lives in.
| pjc50 wrote:
| Doesn't x32 only have four registers available in the calling
| convention, AX-DX?
| ack_complete wrote:
| The stdcall calling convention used APIs and API callbacks
| on Windows x86 doesn't use registers at all, all parameters
| are passed on the stack. MSVC does support
| thiscall/fastcall/vectorcall conventions that pass some
| values in registers, but the system APIs and COM interfaces
| all use stdcall.
|
| Windows x64 and ARM64 do use register passing, with 4
| registers for x64 (rcx/rdx/r8/r9) and 8 registers for ARM64
| (x0-x7). Passing an additional parameter on the stack would
| be cheap compared to the workarounds that everyone has to
| do now.
| pwdisswordfishy wrote:
| The x32 ABI uses the same fastcall convention as the
| regular x86-64 ABI. It's mostly syscall numbers that are
| affected by the shrunk pointers.
| Const-me wrote:
| > I'm genuinely surprised Microsoft's attitude towards
| "wndprocs don't have a context pointer"
|
| They designed windows classes to be reusable, and assumed
| many developers going to reuse windows classes across
| windows.
|
| Consider the following use case. Programmer creates a window
| class for a custom control, registers the class. Designs a
| dialog template with multiple of these custom controls in a
| single dialog. Then creates the dialog by calling DialogBoxW
| or similar.
|
| These custom controls are created automatically multiple at
| once, hard to provide context pointers for each control.
| barrkel wrote:
| It was also used by Delphi in 90s.
| Philpax wrote:
| Hah! I usually allocate trampolines at runtime, as the article
| suggests, but reserving R/W space for them within the
| application's memory space is a cute trick.
|
| Probably not useful for most of my use cases (I'm usually
| injecting a payload, so I'd still have the pointer-distance issue
| between the executable and my payload), but it's still
| potentially handy. Will have to keep that around!
| RossBencina wrote:
| > This is more work than going through GWLP_USERDATA
|
| Indeed, aside from a party trick, why build an executable
| trampoline at runtime when you can store and retrieve the
| context, or a pointer to the context, with SetWindowLong() /
| GetWindowLong() [1]?
|
| Slightly related: in my view Win32 windows are a faithful
| implementation of the Actor Model. The window proc of a window is
| mutable, it represents the _current_ behavior, and can be changed
| in response to any received message. While I haven 't personally
| seen this used in Win32 programs it is a powerful feature as it
| allows for implementing interaction state machines in a very
| natural way (the same way that Miro Samek promotes in his book.)
|
| [1] https://learn.microsoft.com/en-
| us/windows/win32/api/winuser/...
| ack_complete wrote:
| There's an annoying corner case when using
| SetWindowLongPtr/GetWindowLongPtr() -- Windows sends
| WM_GETMINMAXINFO before WM_NCCREATE. This can be worked around
| with a thread local, but a trampoline inherently handles it.
| Trampolines are also useful for other Win32 user functions that
| don't have an easy way to store context data, such as
| SetWindowsHookEx(). They're also slightly faster, though
| GetWindowLongPtr() at least seems able to avoid a syscall.
|
| The code as written, though, is missing a call to
| FlushInstructionCache() and might not work in processes that
| prohibit dynamic code generation. An alternative is to just
| pregenerate an array of trampolines in a code segment, each
| referencing a mutable pointer in a parallel array in the data
| segment. These can be generated straightforwardly with a little
| template magic. This adds size to the executable unlike an
| empty RWX segment, but doesn't run afoul of any dynamic codegen
| restrictions or require I-cache flushing. The number of
| trampolines must be predetermined, but the RWX segment has the
| same limitation.
| rovingeye wrote:
| I wasn't aware of the thread local trick, I solve this
| problem by not setting WS_VISIBLE and calling SetWindowPos &
| ShowWindow after CreateWindow returns (this solves some other
| problems as well..)
| 201984 wrote:
| FlushInstructionCache isn't needed on x86_64. I-cache and
| D-cache are coherent.
| timokr wrote:
| The combination `GWLP_USERDATA` to pass state and
| `GWLP_WNDPROC` to update the actual wnd procedure is what I
| used in my [rust
| wrapper](https://github.com/timokroeger/winmsg-
| executor/blob/main/src...).
|
| This two step approach is the only way I found to use rust
| closures for wndproc without double allocation and additional
| indirection.
| userbinator wrote:
| This somewhat reminds me of the old MakeProcInstance mechanism in
| Win16, which was quickly rendered obsolete by someone who made an
| important realisation: https://www.geary.com/fixds.html
|
| Another seemingly underutilised feature closely related to
| {Get,Set}WindowLong is cbClsExtra/cbWndExtra which lets you
| allocate additional data associated with a window, and store
| whatever you want there. The indices to the GWL/SWL function are
| quite revealing of how this mechanism works:
|
| https://learn.microsoft.com/en-us/windows/win32/api/winuser/...
| rovingeye wrote:
| All my window classes uses cbWndExtra, and I leave
| GWLP_USERDATA for the user who is creating windows.
| pjmlp wrote:
| Or I don't know, just use C++ lambdas instead?
| LegionMammal978 wrote:
| You can't turn a capturing C++ lambda into a WNDPROC, which is
| an ordinary function pointer. You'd still have to ferry the
| lambda via a context pointer, which is what this blog post and
| the other solutions in the comments are all about.
| pjmlp wrote:
| You kind of can, that is one of their design points,
| naturally you need to move the context into the body and know
| what to cast back from.
|
| I guess I need to prove a point on my Github during next
| week.
| rovingeye wrote:
| I assume by "move the context into the body" you mean using
| GetWindowLongPtr? Why not just use a static wndproc at that
| point?
| pjmlp wrote:
| I mean using a static C++ lambda that moves the context
| into the lambda body via capture specifier.
|
| C++ lambdas are basically old style C++ functors that are
| compiled generated, with the calling address being the
| operator().
| rovingeye wrote:
| That doesn't sound like a valid wndproc
| LegionMammal978 wrote:
| If you mean that you can call a C++ lambda from a static C
| callback via a context pointer, of course you can do that,
| it's not very mind-boggling. Rust FFI libraries similarly
| have to do that trick all the time to turn a closure into a
| C callback. The primary problem with WNDPROC is _how to get
| that context pointer_ in the first place, which is the part
| that OP and everyone in the comments are talking about.
___________________________________________________________________
(page generated 2025-12-14 20:01 UTC)