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