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}