Skip to main content

wintf_winmsg_executor/util/
window.rs

1use std::{
2    cell::RefCell,
3    marker::PhantomData,
4    mem,
5    pin::Pin,
6    ptr::{self, NonNull},
7    sync::Once,
8};
9
10use windows::core::w;
11use windows::Win32::{Foundation::*, UI::WindowsAndMessaging::*};
12
13/// Returns the [`HINSTANCE`] of the module that contains the calling code.
14///
15/// Unlike `GetModuleHandleW(None)`, this also works correctly inside DLLs.
16// Taken from:
17// https://github.com/rust-windowing/winit/blob/v0.30.0/src/platform_impl/windows/util.rs#L140
18pub fn get_instance_handle() -> HINSTANCE {
19    // Gets the instance handle by taking the address of the
20    // pseudo-variable created by the Microsoft linker:
21    // https://devblogs.microsoft.com/oldnewthing/20041025-00/?p=37483
22
23    // This is preferred over GetModuleHandle(NULL) because it also works in DLLs:
24    // https://stackoverflow.com/questions/21718027/getmodulehandlenull-vs-hinstance
25
26    extern "C" {
27        static __ImageBase: u8;
28    }
29    HINSTANCE(ptr::from_ref(unsafe { &__ImageBase }) as _)
30}
31
32struct SubClassInformation {
33    wndproc: unsafe extern "system" fn(HWND, u32, WPARAM, LPARAM) -> LRESULT,
34    // Erased pointer type allows `wndproc_setup` to be free of generics.
35    // It simply forwards the pointer and does not need to know type details.
36    user_data: *const (),
37}
38
39/// Wrapper for the arguments to the [`WNDPROC callback function`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-wndproc).
40///
41/// The `hwnd`/`wparam`/`lparam` fields are now `windows` crate newtypes
42/// (`HWND`/`WPARAM`/`LPARAM`), a SemVer-breaking change from the previous
43/// `windows-sys` raw integer/pointer types.
44#[derive(Debug, Clone)]
45pub struct WindowMessage {
46    pub hwnd: HWND,
47    pub msg: u32,
48    pub wparam: WPARAM,
49    pub lparam: LPARAM,
50}
51
52#[repr(C)]
53struct UserData<S, F> {
54    state: S,
55    wndproc: F,
56}
57
58/// Owned window handle.
59///
60/// Dropping the handle destroys the window.
61#[derive(Debug)]
62pub struct Window<S> {
63    hwnd: HWND,
64    _state: PhantomData<S>,
65}
66
67impl<S> Drop for Window<S> {
68    fn drop(&mut self) {
69        let _ = unsafe { DestroyWindow(self.hwnd) };
70    }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum WindowType {
75    /// Visible window which receives broadcast messages from the desktop.
76    TopLevel,
77
78    /// [Message-Only Windows] are useful for windows that do not need to be
79    /// visible and do not need access to broadcast messages from the desktop.
80    ///
81    /// [Message-Only Windows]:
82    /// https://learn.microsoft.com/en-us/windows/win32/winmsg/window-features#message-only-windows
83    MessageOnly,
84}
85
86/// Window could not be created.
87///
88/// Possible failure reasons:
89/// * `WM_NCCREATE` message was handled but did not return 0
90/// * `WM_CREATE` message was handled but returned -1
91/// * Reached the maximum number of 10000 window handles per process:
92///   <https://devblogs.microsoft.com/oldnewthing/20070718-00/?p=25963>
93#[derive(Debug)]
94pub struct WindowCreationError;
95
96impl<S> Window<S> {
97    /// Creates a new window with a `wndproc` closure.
98    ///
99    /// The `state` parameter will be allocated alongside the closure. It is
100    /// meant as a convenient alternative to `Rc<State>` to access variables
101    /// from both inside and outside the closure. A pinned reference to the
102    /// state is passed as the first parameter to the closure.
103    /// Use [`Window::state()`] to access the state from the outside.
104    pub fn new<F>(
105        window_type: WindowType,
106        state: S,
107        wndproc: F,
108    ) -> Result<Self, WindowCreationError>
109    where
110        F: Fn(Pin<&S>, WindowMessage) -> Option<LRESULT> + 'static,
111    {
112        Self::new_ex(window_type, WINDOW_EX_STYLE(0), state, wndproc)
113    }
114
115    /// Same as [`Window::new()`] but allows specifying extended window styles
116    /// (the `dwExStyle` parameter of `CreateWindowExA`), such as
117    /// [`WS_EX_NOREDIRECTIONBITMAP`].
118    ///
119    /// [`WS_EX_NOREDIRECTIONBITMAP`]:
120    /// https://learn.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles
121    pub fn new_ex<F>(
122        window_type: WindowType,
123        ex_style: WINDOW_EX_STYLE,
124        state: S,
125        wndproc: F,
126    ) -> Result<Self, WindowCreationError>
127    where
128        F: Fn(Pin<&S>, WindowMessage) -> Option<LRESULT> + 'static,
129    {
130        let class_name = w!("wintf-winmsg-executor");
131
132        // A class must only be unregistered when it was registered from a DLL which
133        // is unloaded during program execution: For now, an unsupported use case.
134        static CLASS_REGISTRATION: Once = Once::new();
135        CLASS_REGISTRATION.call_once(|| {
136            let mut wnd_class: WNDCLASSW = unsafe { std::mem::zeroed() };
137            wnd_class.style = CS_DBLCLKS;
138            wnd_class.lpfnWndProc = Some(wndproc_setup);
139            wnd_class.hInstance = get_instance_handle();
140            wnd_class.hCursor = unsafe { LoadCursorW(None, IDC_ARROW) }.unwrap_or_default();
141            wnd_class.lpszClassName = class_name;
142            unsafe { RegisterClassW(&wnd_class) };
143        });
144
145        // Pass the closure and state as user data to our typed window process.
146        let subclassinfo = SubClassInformation {
147            wndproc: wndproc_typed::<S, F>,
148            user_data: Box::into_raw(Box::new(UserData { state, wndproc })).cast(),
149        };
150
151        let hwnd = unsafe {
152            CreateWindowExW(
153                ex_style,
154                class_name,
155                None,
156                WINDOW_STYLE(0),
157                CW_USEDEFAULT,
158                CW_USEDEFAULT,
159                CW_USEDEFAULT,
160                CW_USEDEFAULT,
161                match window_type {
162                    WindowType::TopLevel => None,
163                    WindowType::MessageOnly => Some(HWND_MESSAGE),
164                },
165                None,
166                Some(get_instance_handle()),
167                // The subclass info can be passed as a pointer to the
168                // stack-allocated variable because it will only be accessed
169                // during the `CreateWindowExW()` call and not afterwards.
170                Some(ptr::from_ref(&subclassinfo).cast()),
171            )
172        }
173        .map_err(|_| WindowCreationError)?;
174
175        Ok(Self {
176            hwnd,
177            _state: PhantomData,
178        })
179    }
180
181    /// Same as [`Window::new()`] but allows the closure to be `FnMut`.
182    ///
183    /// Internally uses a `RefCell` for the closure to prevent it from being
184    /// re-entered by nested message loops (e.g., from modal dialogs). Forwards
185    /// nested messages to the default wndproc procedure.
186    pub fn new_checked<F>(
187        window_type: WindowType,
188        state: S,
189        wndproc: F,
190    ) -> Result<Self, WindowCreationError>
191    where
192        F: FnMut(Pin<&S>, WindowMessage) -> Option<LRESULT> + 'static,
193    {
194        Self::new_checked_ex(window_type, WINDOW_EX_STYLE(0), state, wndproc)
195    }
196
197    /// Same as [`Window::new_checked()`] but allows specifying extended window
198    /// styles (the `dwExStyle` parameter), such as `WS_EX_NOREDIRECTIONBITMAP`.
199    /// See [`Window::new_ex()`].
200    pub fn new_checked_ex<F>(
201        window_type: WindowType,
202        ex_style: WINDOW_EX_STYLE,
203        state: S,
204        wndproc: F,
205    ) -> Result<Self, WindowCreationError>
206    where
207        F: FnMut(Pin<&S>, WindowMessage) -> Option<LRESULT> + 'static,
208    {
209        let wndproc = RefCell::new(wndproc);
210        Self::new_ex(window_type, ex_style, state, move |state, msg| {
211            // Detect when `wndproc` is re-entered, which can happen when the user
212            // provided handler creates a modal dialog (e.g., a popup-menu). Rust rules
213            // do not allow us to create a second mutable reference to the user-provided
214            // handler. Run the default window procedure instead.
215            let mut wndproc = wndproc.try_borrow_mut().ok()?;
216            wndproc(state, msg)
217        })
218    }
219
220    fn user_data(&self) -> &UserData<S, ()> {
221        unsafe { &*(GetWindowLongPtrW(self.hwnd, GWLP_USERDATA) as *const _) }
222    }
223
224    /// Returns this window's raw window handle.
225    pub fn hwnd(&self) -> HWND {
226        self.hwnd
227    }
228
229    /// Returns a reference to the state shared with the `wndproc` closure.
230    pub fn state(&self) -> Pin<&S> {
231        unsafe { Pin::new_unchecked(&self.user_data().state) }
232    }
233}
234
235unsafe extern "system" fn wndproc_setup(
236    hwnd: HWND,
237    msg: u32,
238    wparam: WPARAM,
239    lparam: LPARAM,
240) -> LRESULT {
241    if msg == WM_NCCREATE {
242        let create_params = lparam.0 as *const CREATESTRUCTW;
243        let subclassinfo = &*((*create_params).lpCreateParams as *const SubClassInformation);
244
245        // Replace our `wndproc` with the one using the correct type.
246        SetWindowLongPtrW(hwnd, GWLP_WNDPROC, subclassinfo.wndproc as usize as _);
247
248        // Attach user data to the window so it can be accessed from the
249        // `wndproc` callback function when receiving other messages.
250        // https://devblogs.microsoft.com/oldnewthing/20191014-00/?p=102992
251        SetWindowLongPtrW(hwnd, GWLP_USERDATA, subclassinfo.user_data as _);
252
253        // Forward this message to the freshly registered subclass wndproc.
254        SendMessageW(hwnd, msg, Some(wparam), Some(lparam))
255    } else {
256        // This code path is only reached for messages before `WM_NCCREATE`.
257        // On Windows 10/11 `WM_GETMINMAXINFO` is the first and only message
258        // before `WM_NCCREATE`.
259        DefWindowProcW(hwnd, msg, wparam, lparam)
260    }
261}
262
263unsafe extern "system" fn wndproc_typed<S, F>(
264    hwnd: HWND,
265    msg: u32,
266    wparam: WPARAM,
267    lparam: LPARAM,
268) -> LRESULT
269where
270    F: Fn(Pin<&S>, WindowMessage) -> Option<LRESULT> + 'static,
271{
272    let user_data_ptr: NonNull<UserData<S, F>> = if mem::size_of::<UserData<S, F>>() == 0 {
273        NonNull::dangling()
274    } else {
275        NonNull::new_unchecked(GetWindowLongPtrW(hwnd, GWLP_USERDATA) as _)
276    };
277    let user_data = user_data_ptr.as_ref();
278
279    let ret = (user_data.wndproc)(
280        Pin::new_unchecked(&user_data.state),
281        WindowMessage {
282            hwnd,
283            msg,
284            wparam,
285            lparam,
286        },
287    );
288
289    if msg == WM_CLOSE {
290        // We manage the window lifetime ourselves. Prevent the default
291        // handler from calling `DestroyWindow()` to keep the state
292        // allocated until the window wrapper struct is dropped.
293        return LRESULT(0);
294    }
295
296    if msg == WM_NCDESTROY {
297        // This is the very last message received by this function before
298        // the window is destroyed. Deallocate the window user data.
299        drop(Box::from_raw(user_data_ptr.as_ptr()));
300        return LRESULT(0);
301    }
302
303    ret.unwrap_or_else(|| DefWindowProcW(hwnd, msg, wparam, lparam))
304}
305
306#[cfg(test)]
307mod test {
308    use crate::{FilterResult, MessageLoop};
309
310    use super::*;
311    use std::{
312        cell::Cell,
313        rc::{Rc, Weak},
314    };
315
316    #[test]
317    fn create_destroy_messages() {
318        let mut expected_messages = [WM_NCCREATE, WM_CREATE, WM_DESTROY, WM_NCDESTROY].into_iter();
319        let mut expected_message = expected_messages.next();
320
321        // Cannot be passed as state because we need to investigate it after drop.
322        let match_cnt = Rc::new(Cell::new(0));
323
324        let w = Window::new_checked(WindowType::TopLevel, (), {
325            let match_cnt = match_cnt.clone();
326            move |_, msg| {
327                dbg!(msg.msg);
328                if msg.msg == expected_message.unwrap() {
329                    expected_message = expected_messages.next();
330                    match_cnt.set(match_cnt.get() + 1);
331                }
332                None
333            }
334        })
335        .unwrap();
336
337        assert_eq!(match_cnt.get(), 2); // received WM_NCCREATE, WM_CREATE in order
338        drop(w);
339        assert_eq!(match_cnt.get(), 4); // received WM_DESTROY, WM_NCDESTROY in order
340    }
341
342    // Reminder for myself for why `state` cannot be mutable.
343    #[test]
344    fn reenter_state() {
345        let state = RefCell::new(false);
346
347        let w = Rc::new_cyclic(move |this: &Weak<Window<RefCell<bool>>>| {
348            let this = this.clone();
349            Window::new(WindowType::MessageOnly, state, move |state, _msg| {
350                let mut state = state.borrow_mut();
351                if let Some(w) = this.upgrade() {
352                    // here we would get the second mutable alias
353                    if w.state().try_borrow_mut().is_err() {
354                        *state = true;
355                        let _ = unsafe {
356                            PostMessageW(Some(w.hwnd()), WM_USER, WPARAM(0), LPARAM(0))
357                        };
358                    }
359                }
360                None
361            })
362            .unwrap()
363        });
364
365        // Emulate a message from a user clicking on the window somewhere.
366        let _ = unsafe { PostMessageW(Some(w.hwnd()), WM_USER, WPARAM(0), LPARAM(0)) };
367        MessageLoop::run(|msg_loop, _| {
368            if *w.state().borrow() {
369                msg_loop.quit();
370            }
371            FilterResult::Forward
372        });
373    }
374}