[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)