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