windows_helpers/win32_app/msg_loop.rs
1//! Functions to run a blocking Win32 message loop with [`GetMessageW()`][1] etc. Necessary for window procedures, hook callbacks, timer callbacks and more.
2//!
3//! [1]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessagew
4
5use crate::{core::ResultExt, windows, Null};
6use std::cell::Cell;
7use windows::Win32::{
8 Foundation::{HWND, LPARAM, WPARAM},
9 UI::WindowsAndMessaging::{
10 DispatchMessageW, GetMessageW, PostQuitMessage, TranslateMessage, MSG, WM_QUIT,
11 },
12};
13
14thread_local! {
15 /// The exit code set with [`quit_now()`]. Same data type as with [`PostQuitMessage()`][1].
16 ///
17 /// [1]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postquitmessage
18 static QUIT_NOW_EXIT_CODE: Cell<Option<i32>> = const { Cell::new(None) };
19}
20
21pub fn run() -> windows::core::Result<usize> {
22 //! Runs a message loop, ignoring custom thread messages.
23 //!
24 //! If successful, returns the exit code received via [`WM_QUIT`][1] from [`PostQuitMessage()`][2] that the process should return. If unsuccessful and you can handle the error, the function can be rerun in a loop.
25 //!
26 //! [1]: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-quit
27 //! [2]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postquitmessage
28
29 loop {
30 let msg = run_till_thread_msg()?;
31 if msg.message == WM_QUIT {
32 break Ok(msg.wParam.0);
33 }
34 }
35}
36
37pub fn run_till_thread_msg() -> windows::core::Result<MSG> {
38 //! Runs a message loop until a thread message is received.
39 //!
40 //! In most programs, the only thread message will be [`WM_QUIT`][1] (sent via [`PostQuitMessage()`][1]). But others are possible via [`PostThreadMessageW()`][3] and [`PostMessageW()`][4].
41 //!
42 //! [1]: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-quit
43 //! [2]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postquitmessage
44 //! [3]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postthreadmessagew
45 //! [4]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postmessagew
46
47 let mut msg = MSG::default();
48
49 loop {
50 // (`GetMessageW()` calls hook callbacks without returning.)
51 let mut get_msg_retval = unsafe { GetMessageW(&mut msg, HWND::NULL, 0, 0).0 };
52
53 if get_msg_retval == -1 {
54 break Result::err_from_win32();
55 } else {
56 if let Some(exit_code) = QUIT_NOW_EXIT_CODE.get() {
57 get_msg_retval = 0;
58 msg.hwnd = HWND::NULL;
59 msg.message = WM_QUIT;
60 msg.wParam = WPARAM(exit_code as _);
61 msg.lParam = LPARAM(0);
62 }
63
64 if get_msg_retval == 0 {
65 // Received `WM_QUIT` thread message. Caller must check `msg.message` against `WM_QUIT`.
66 // (`GetMessageW()` return value is checked instead of treating `WM_QUIT` like all thread messages, in case abusive behavior caused `msg.hwnd` to be non-zero, which is possible via `PostMessageW()`.)
67 break Ok(msg);
68 } else {
69 // Propagate window message to window procedure.
70 // As confirmed by a test, `DispatchMessageW()` also calls the timer callback on `WM_TIMER` when `msg.hwnd` is 0. Official example code also does it this way. (https://learn.microsoft.com/en-us/windows/win32/winmsg/using-messages-and-message-queues) So, the calls are just made for all thread messages. Custom thread messages are ignored by them. (Docs: "DispatchMessage will call the TimerProc callback function specified in the call to the SetTimer function used to install the timer." [https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-timer])
71 unsafe {
72 TranslateMessage(&msg);
73 DispatchMessageW(&msg);
74 }
75
76 // Return thread message.
77 if msg.hwnd.is_null() {
78 break Ok(msg);
79 }
80 }
81 }
82 }
83}
84
85pub fn quit_now(exit_code: i32) {
86 //! Causes the message loop to quit as soon as possible.
87 //!
88 //! Can be used in case of exceptional errors. Note that this function doesn't have the never return type (`!`).
89 //!
90 //! The function saves the exit code in thread-local storage and posts a message. The very next message that the message loop retrieves will then be changed to a `WM_QUIT` message with that exit code, which causes the loop to return.
91
92 QUIT_NOW_EXIT_CODE.set(Some(exit_code));
93 unsafe { PostQuitMessage(exit_code) };
94}
95
96#[cfg(all(test, feature = "windows_latest_compatible_all"))]
97mod tests {
98 use crate::{windows, Null};
99 use windows::Win32::{
100 Foundation::HWND,
101 UI::WindowsAndMessaging::{PostQuitMessage, SetTimer},
102 };
103
104 #[ignore]
105 #[test]
106 fn set_timer() -> windows::core::Result<()> {
107 extern "system" fn on_timer(hwnd: HWND, msg_id: u32, event_id: usize, time: u32) {
108 println!("timer event: {hwnd:?}, 0x{msg_id:x?}, {event_id:?}, {time:?}");
109 unsafe { PostQuitMessage(0) };
110 }
111
112 unsafe {
113 SetTimer(HWND::NULL, 0, 500 /*ms*/, Some(on_timer))
114 };
115 super::run()?;
116 println!("after msg loop");
117
118 Ok(())
119 }
120}