1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
//! Functions to run a blocking Win32 message loop with [`GetMessageW()`][1] etc. Necessary for window procedures, hook callbacks, timer callbacks and more.
//!
//! [1]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessagew

use crate::{core::ResultExt, windows, Null};
use std::cell::Cell;
use windows::Win32::{
    Foundation::{HWND, LPARAM, WPARAM},
    UI::WindowsAndMessaging::{
        DispatchMessageW, GetMessageW, PostQuitMessage, TranslateMessage, MSG, WM_QUIT,
    },
};

thread_local! {
    /// The exit code set with [`quit_now()`]. Same data type as with [`PostQuitMessage()`][1].
    ///
    /// [1]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postquitmessage
    static QUIT_NOW_EXIT_CODE: Cell<Option<i32>> = const { Cell::new(None) };
}

pub fn run() -> windows::core::Result<usize> {
    //! Runs a message loop, ignoring custom thread messages.
    //!
    //! 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.
    //!
    //! [1]: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-quit
    //! [2]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postquitmessage

    loop {
        let msg = run_till_thread_msg()?;
        if msg.message == WM_QUIT {
            break Ok(msg.wParam.0);
        }
    }
}

pub fn run_till_thread_msg() -> windows::core::Result<MSG> {
    //! Runs a message loop until a thread message is received.
    //!
    //! 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].
    //!
    //! [1]: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-quit
    //! [2]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postquitmessage
    //! [3]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postthreadmessagew
    //! [4]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postmessagew

    let mut msg = MSG::default();

    loop {
        // (`GetMessageW()` calls hook callbacks without returning.)
        let mut get_msg_retval = unsafe { GetMessageW(&mut msg, HWND::NULL, 0, 0).0 };

        if get_msg_retval == -1 {
            break Result::err_from_win32();
        } else {
            if let Some(exit_code) = QUIT_NOW_EXIT_CODE.get() {
                get_msg_retval = 0;
                msg.hwnd = HWND::NULL;
                msg.message = WM_QUIT;
                msg.wParam = WPARAM(exit_code as _);
                msg.lParam = LPARAM(0);
            }

            if get_msg_retval == 0 {
                // Received `WM_QUIT` thread message. Caller must check `msg.message` against `WM_QUIT`.
                // (`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()`.)
                break Ok(msg);
            } else {
                // Propagate window message to window procedure.
                // 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])
                unsafe {
                    TranslateMessage(&msg);
                    DispatchMessageW(&msg);
                }

                // Return thread message.
                if msg.hwnd.0 == 0 {
                    break Ok(msg);
                }
            }
        }
    }
}

pub fn quit_now(exit_code: i32) {
    //! Causes the message loop to quit as soon as possible.
    //!
    //! Can be used in case of exceptional errors. Note that this function doesn't have the never return type (`!`).
    //!
    //! 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.

    QUIT_NOW_EXIT_CODE.set(Some(exit_code));
    unsafe { PostQuitMessage(exit_code) };
}

#[cfg(all(test, feature = "windows_latest_compatible_all"))]
mod tests {
    use crate::{windows, Null};
    use windows::Win32::{
        Foundation::HWND,
        UI::WindowsAndMessaging::{PostQuitMessage, SetTimer},
    };

    #[ignore]
    #[test]
    fn set_timer() -> windows::core::Result<()> {
        extern "system" fn on_timer(hwnd: HWND, msg_id: u32, event_id: usize, time: u32) {
            println!("timer event: {hwnd:?}, 0x{msg_id:x?}, {event_id:?}, {time:?}");
            unsafe { PostQuitMessage(0) };
        }

        unsafe {
            SetTimer(HWND::NULL, 0, 500 /*ms*/, Some(on_timer))
        };
        super::run()?;
        println!("after msg loop");

        Ok(())
    }
}