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}