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.lpfnWndProc = Some(wndproc_setup);
138 wnd_class.hInstance = get_instance_handle();
139 wnd_class.lpszClassName = class_name;
140 unsafe { RegisterClassW(&wnd_class) };
141 });
142
143 // Pass the closure and state as user data to our typed window process.
144 let subclassinfo = SubClassInformation {
145 wndproc: wndproc_typed::<S, F>,
146 user_data: Box::into_raw(Box::new(UserData { state, wndproc })).cast(),
147 };
148
149 let hwnd = unsafe {
150 CreateWindowExW(
151 ex_style,
152 class_name,
153 None,
154 WINDOW_STYLE(0),
155 CW_USEDEFAULT,
156 CW_USEDEFAULT,
157 CW_USEDEFAULT,
158 CW_USEDEFAULT,
159 match window_type {
160 WindowType::TopLevel => None,
161 WindowType::MessageOnly => Some(HWND_MESSAGE),
162 },
163 None,
164 Some(get_instance_handle()),
165 // The subclass info can be passed as a pointer to the
166 // stack-allocated variable because it will only be accessed
167 // during the `CreateWindowExW()` call and not afterwards.
168 Some(ptr::from_ref(&subclassinfo).cast()),
169 )
170 }
171 .map_err(|_| WindowCreationError)?;
172
173 Ok(Self {
174 hwnd,
175 _state: PhantomData,
176 })
177 }
178
179 /// Same as [`Window::new()`] but allows the closure to be `FnMut`.
180 ///
181 /// Internally uses a `RefCell` for the closure to prevent it from being
182 /// re-entered by nested message loops (e.g., from modal dialogs). Forwards
183 /// nested messages to the default wndproc procedure.
184 pub fn new_checked<F>(
185 window_type: WindowType,
186 state: S,
187 wndproc: F,
188 ) -> Result<Self, WindowCreationError>
189 where
190 F: FnMut(Pin<&S>, WindowMessage) -> Option<LRESULT> + 'static,
191 {
192 Self::new_checked_ex(window_type, WINDOW_EX_STYLE(0), state, wndproc)
193 }
194
195 /// Same as [`Window::new_checked()`] but allows specifying extended window
196 /// styles (the `dwExStyle` parameter), such as `WS_EX_NOREDIRECTIONBITMAP`.
197 /// See [`Window::new_ex()`].
198 pub fn new_checked_ex<F>(
199 window_type: WindowType,
200 ex_style: WINDOW_EX_STYLE,
201 state: S,
202 wndproc: F,
203 ) -> Result<Self, WindowCreationError>
204 where
205 F: FnMut(Pin<&S>, WindowMessage) -> Option<LRESULT> + 'static,
206 {
207 let wndproc = RefCell::new(wndproc);
208 Self::new_ex(window_type, ex_style, state, move |state, msg| {
209 // Detect when `wndproc` is re-entered, which can happen when the user
210 // provided handler creates a modal dialog (e.g., a popup-menu). Rust rules
211 // do not allow us to create a second mutable reference to the user-provided
212 // handler. Run the default window procedure instead.
213 let mut wndproc = wndproc.try_borrow_mut().ok()?;
214 wndproc(state, msg)
215 })
216 }
217
218 fn user_data(&self) -> &UserData<S, ()> {
219 unsafe { &*(GetWindowLongPtrW(self.hwnd, GWLP_USERDATA) as *const _) }
220 }
221
222 /// Returns this window's raw window handle.
223 pub fn hwnd(&self) -> HWND {
224 self.hwnd
225 }
226
227 /// Returns a reference to the state shared with the `wndproc` closure.
228 pub fn state(&self) -> Pin<&S> {
229 unsafe { Pin::new_unchecked(&self.user_data().state) }
230 }
231}
232
233unsafe extern "system" fn wndproc_setup(
234 hwnd: HWND,
235 msg: u32,
236 wparam: WPARAM,
237 lparam: LPARAM,
238) -> LRESULT {
239 if msg == WM_NCCREATE {
240 let create_params = lparam.0 as *const CREATESTRUCTW;
241 let subclassinfo = &*((*create_params).lpCreateParams as *const SubClassInformation);
242
243 // Replace our `wndproc` with the one using the correct type.
244 SetWindowLongPtrW(hwnd, GWLP_WNDPROC, subclassinfo.wndproc as usize as _);
245
246 // Attach user data to the window so it can be accessed from the
247 // `wndproc` callback function when receiving other messages.
248 // https://devblogs.microsoft.com/oldnewthing/20191014-00/?p=102992
249 SetWindowLongPtrW(hwnd, GWLP_USERDATA, subclassinfo.user_data as _);
250
251 // Forward this message to the freshly registered subclass wndproc.
252 SendMessageW(hwnd, msg, Some(wparam), Some(lparam))
253 } else {
254 // This code path is only reached for messages before `WM_NCCREATE`.
255 // On Windows 10/11 `WM_GETMINMAXINFO` is the first and only message
256 // before `WM_NCCREATE`.
257 DefWindowProcW(hwnd, msg, wparam, lparam)
258 }
259}
260
261unsafe extern "system" fn wndproc_typed<S, F>(
262 hwnd: HWND,
263 msg: u32,
264 wparam: WPARAM,
265 lparam: LPARAM,
266) -> LRESULT
267where
268 F: Fn(Pin<&S>, WindowMessage) -> Option<LRESULT> + 'static,
269{
270 let user_data_ptr: NonNull<UserData<S, F>> = if mem::size_of::<UserData<S, F>>() == 0 {
271 NonNull::dangling()
272 } else {
273 NonNull::new_unchecked(GetWindowLongPtrW(hwnd, GWLP_USERDATA) as _)
274 };
275 let user_data = user_data_ptr.as_ref();
276
277 let ret = (user_data.wndproc)(
278 Pin::new_unchecked(&user_data.state),
279 WindowMessage {
280 hwnd,
281 msg,
282 wparam,
283 lparam,
284 },
285 );
286
287 if msg == WM_CLOSE {
288 // We manage the window lifetime ourselves. Prevent the default
289 // handler from calling `DestroyWindow()` to keep the state
290 // allocated until the window wrapper struct is dropped.
291 return LRESULT(0);
292 }
293
294 if msg == WM_NCDESTROY {
295 // This is the very last message received by this function before
296 // the window is destroyed. Deallocate the window user data.
297 drop(Box::from_raw(user_data_ptr.as_ptr()));
298 return LRESULT(0);
299 }
300
301 ret.unwrap_or_else(|| DefWindowProcW(hwnd, msg, wparam, lparam))
302}
303
304#[cfg(test)]
305mod test {
306 use crate::{FilterResult, MessageLoop};
307
308 use super::*;
309 use std::{
310 cell::Cell,
311 rc::{Rc, Weak},
312 };
313
314 #[test]
315 fn create_destroy_messages() {
316 let mut expected_messages = [WM_NCCREATE, WM_CREATE, WM_DESTROY, WM_NCDESTROY].into_iter();
317 let mut expected_message = expected_messages.next();
318
319 // Cannot be passed as state because we need to investigate it after drop.
320 let match_cnt = Rc::new(Cell::new(0));
321
322 let w = Window::new_checked(WindowType::TopLevel, (), {
323 let match_cnt = match_cnt.clone();
324 move |_, msg| {
325 dbg!(msg.msg);
326 if msg.msg == expected_message.unwrap() {
327 expected_message = expected_messages.next();
328 match_cnt.set(match_cnt.get() + 1);
329 }
330 None
331 }
332 })
333 .unwrap();
334
335 assert_eq!(match_cnt.get(), 2); // received WM_NCCREATE, WM_CREATE in order
336 drop(w);
337 assert_eq!(match_cnt.get(), 4); // received WM_DESTROY, WM_NCDESTROY in order
338 }
339
340 // Reminder for myself for why `state` cannot be mutable.
341 #[test]
342 fn reenter_state() {
343 let state = RefCell::new(false);
344
345 let w = Rc::new_cyclic(move |this: &Weak<Window<RefCell<bool>>>| {
346 let this = this.clone();
347 Window::new(WindowType::MessageOnly, state, move |state, _msg| {
348 let mut state = state.borrow_mut();
349 if let Some(w) = this.upgrade() {
350 // here we would get the second mutable alias
351 if w.state().try_borrow_mut().is_err() {
352 *state = true;
353 let _ = unsafe {
354 PostMessageW(Some(w.hwnd()), WM_USER, WPARAM(0), LPARAM(0))
355 };
356 }
357 }
358 None
359 })
360 .unwrap()
361 });
362
363 // Emulate a message from a user clicking on the window somewhere.
364 let _ = unsafe { PostMessageW(Some(w.hwnd()), WM_USER, WPARAM(0), LPARAM(0)) };
365 MessageLoop::run(|msg_loop, _| {
366 if *w.state().borrow() {
367 msg_loop.quit();
368 }
369 FilterResult::Forward
370 });
371 }
372}