Skip to content

Commit 5b3e076

Browse files
committed
Improve documentation and simplify API
1 parent 75d7990 commit 5b3e076

File tree

8 files changed

+121
-111
lines changed

8 files changed

+121
-111
lines changed

README.md

+14-8
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,33 @@ Per-thread async rust executor for windows.
44
Each task is backed by a [message-only window][1].
55
The executor thread runs the native [windows message loop][2] which dispatches wake messages to the tasks window procedure which polls the task future.
66

7-
As a thin wrapper for WinAPI calls the whole executor is implemented in around 300 lines of code.
8-
97
## Features
108

11-
- Easy data sharing within a thread because `Send` or `Sync` is not required for the task future
12-
- A task can spawn new tasks on the same thread
9+
- Easy data sharing within a thread because `Send` or `Sync` is not required for the task future.
10+
- Runs multiply tasks on the same thread. Tasks can spawn new tasks and await the result.
1311
- Modal windows like menus do not block other tasks running on the same thread.
1412
- Helper code to implement window procedures with closures that can have state.
1513

14+
## Alternative Backend: `async-task`
15+
16+
Selected by the `backend-async-task` cargo feature.
17+
Uses `async-task`s task abstraction instead of a window per task to store the future.
18+
Scheduling a tasks means posting its runnable to the threads message queue (similar to `windows-executor` see below).
19+
1620
## Comparison with similar crates
1721

22+
Both of those listed crates run one taks/future per thread in their and expose
23+
only `block_on()`.
24+
[Is block_on an executor?](https://github.com/rust-lang/async-book/issues/219)
25+
1826
### [`windows-exeuctor`](https://github.com/haileys/windows-executor/)
1927

20-
- Supports only a single task.
21-
- Polls directly from the message loop.
28+
- Polls its future directly from the message loop.
2229
- Does not create a windows at all: Waker stores the message loops thread id and notifies it with `PostThreadMessage()`.
23-
- Does not quit the message loop correctly when the task futures returns.
30+
- Does not close the threads message loop (no `PostQuitMessage()` call) when the task futures returns.
2431

2532
### [`windows-async-rs`](https://github.com/saelay/windows-async-rs/)
2633

27-
- Supports only a single task.
2834
- Polls directly from the message loop even when receiving broadcast messages unrelated to the task.
2935
- Questionable use of unsafe code
3036

examples/basic.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{future::poll_fn, task::Poll};
22

3-
use winmsg_executor::{spawn, MessageLoop};
3+
use winmsg_executor::{block_on, spawn};
44

55
async fn poll_n_times(mut n_poll: usize) {
66
poll_fn(|cx| {
@@ -18,8 +18,7 @@ async fn poll_n_times(mut n_poll: usize) {
1818

1919
fn main() {
2020
println!("hello");
21-
let msg_loop = MessageLoop::new();
22-
msg_loop.block_on(async {
21+
block_on(async {
2322
let task = spawn(async {
2423
println!("async hello 1");
2524
poll_n_times(3).await;

examples/threads.rs

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{future::poll_fn, task::Poll, thread};
22

3-
use winmsg_executor::MessageLoop;
3+
use winmsg_executor::block_on;
44

55
async fn poll_n_times(mut n_poll: usize) {
66
poll_fn(|cx| {
@@ -19,8 +19,7 @@ async fn poll_n_times(mut n_poll: usize) {
1919
fn main() {
2020
thread::spawn(|| {
2121
println!("thread hello");
22-
let msg_loop = MessageLoop::new();
23-
msg_loop.block_on(async {
22+
block_on(async {
2423
println!("thread async hello");
2524
poll_n_times(3).await;
2625
println!("thread async bye");
@@ -29,8 +28,7 @@ fn main() {
2928
});
3029

3130
println!("main hello");
32-
let msg_loop = MessageLoop::new();
33-
msg_loop.block_on(async {
31+
block_on(async {
3432
println!("main async hello");
3533
poll_n_times(3).await;
3634
println!("main async bye");

src/backend/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
#[cfg(all(feature = "backend-windows", feature = "backend-async-task"))]
2+
compile_error!("only one `backend-*` feature can be selected at a time");
3+
14
#[cfg(feature = "backend-windows")]
25
#[path = "window.rs"]
36
mod _backend;

src/backend/window.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub const fn dispatch(_msg: &MSG) -> bool {
1919

2020
const MSG_ID_WAKE: u32 = WM_USER;
2121

22-
// Use same terminology as `async-task` crate.
22+
// Same terminology as `async-task` crate.
2323
enum TaskState<F: Future> {
2424
Running(F, Option<Waker>),
2525
Completed(F::Output),
@@ -40,7 +40,7 @@ unsafe impl<F: Future> Sync for Task<F> {}
4040
impl<F: Future> Wake for Task<F> {
4141
fn wake(self: Arc<Self>) {
4242
// Ideally the waker would know if the task has completed to decide if
43-
// its necessary to send a wake message. But that also means access to
43+
// its necessary to send a wake message. But that also means access that
4444
// task state must be made thread safe. Instead, always post the wake
4545
// message and let the receiver side (which runs on the same thread the
4646
// task was created on) decide if a task needs to be polled.

src/lib.rs

+61-90
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ pub mod util;
66
use std::{
77
cell::Cell,
88
future::Future,
9-
marker::PhantomData,
109
mem::MaybeUninit,
1110
pin::pin,
1211
ptr,
@@ -17,107 +16,77 @@ use windows_sys::Win32::{
1716
Foundation::*, System::Threading::GetCurrentThreadId, UI::WindowsAndMessaging::*,
1817
};
1918

20-
thread_local! {
21-
static MESSAGE_LOOP_RUNNING: Cell<bool> = const { Cell::new(false) };
22-
}
23-
24-
/// Represents this threads [windows message loop][1].
25-
/// To execute async code within the message loop use the
26-
/// [`block_on()`](MessageLoop::block_on) method.
19+
/// Runs a future to completion on the calling threads message loop.
20+
///
21+
/// This runs the provided future on the current thread, blocking until it
22+
/// is complete. Any tasks spawned which the future spawns internally will
23+
/// be executed no the same thread.
2724
///
28-
/// [1]: https://learn.microsoft.com/en-us/windows/win32/winmsg/messages-and-message-queues
29-
pub struct MessageLoop {
30-
_not_send: PhantomData<*const ()>,
25+
/// Any spawned tasks will be suspended after `block_on` returns. Calling
26+
/// `block_on` again will resume previously spawned tasks.
27+
///
28+
/// # Panics
29+
///
30+
/// Panics when the message loops is running already. This happens when
31+
/// `block_on` or `run` is called from async tasks running on this executor.
32+
pub fn block_on<T: 'static>(future: impl Future<Output = T> + 'static) -> T {
33+
// Wrap the future so it quits the message loop when finished.
34+
let task = backend::spawn(async move {
35+
let result = future.await;
36+
unsafe { PostQuitMessage(0) };
37+
result
38+
});
39+
run_message_loop();
40+
poll_assume_ready(task)
3141
}
3242

33-
impl Default for MessageLoop {
34-
fn default() -> Self {
35-
Self::new()
36-
}
37-
}
43+
/// Runs the message loop.
44+
///
45+
/// Executes previously [`spawn`]ed tasks.
46+
///
47+
/// # Panics
48+
///
49+
/// Panics when the message loops is running already. This happens when
50+
/// `block_on` or `run` is called from async tasks running on this executor.
51+
pub fn run_message_loop() {
52+
thread_local!(static MESSAGE_LOOP_RUNNING: Cell<bool> = const { Cell::new(false) });
53+
assert!(
54+
!MESSAGE_LOOP_RUNNING.replace(true),
55+
"a message loop is running already"
56+
);
3857

39-
impl MessageLoop {
40-
/// Creates a message queue for the current thread but does not run the
41-
/// dispatch loop for it yet.
42-
#[must_use]
43-
pub const fn new() -> Self {
44-
Self {
45-
_not_send: PhantomData,
58+
// Any modal window (i.e. a right-click menu) blocks the main message loop
59+
// and dispatches messages internally. To keep the executor running use a
60+
// hook to get access to modal windows internal message loop.
61+
unsafe extern "system" fn hook_proc(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
62+
if code >= 0 && backend::dispatch(&*(lparam as *const MSG)) {
63+
1
64+
} else {
65+
CallNextHookEx(0, code, wparam, lparam)
4666
}
4767
}
68+
let hook = unsafe { SetWindowsHookExA(WH_MSGFILTER, Some(hook_proc), 0, GetCurrentThreadId()) };
4869

49-
/// Runs a future to completion on the message loop.
50-
///
51-
/// This runs the provided future on the current thread, blocking until it
52-
/// is complete. Any tasks spawned which the future spawns internally will
53-
/// be executed no the same thread.
54-
///
55-
/// Any spawned tasks will be suspended after `block_on` returns. Calling
56-
/// `block_on` again will resume previously spawned tasks.
57-
///
58-
/// # Panics
59-
///
60-
/// Panics when the message loops is running already. This happens when
61-
/// `block_on` or `run` is called from async tasks running on this executor.
62-
pub fn block_on<T: 'static>(&self, future: impl Future<Output = T> + 'static) -> T {
63-
// Wrap the future so it quits the message loop when finished.
64-
let task = backend::spawn(async move {
65-
let result = future.await;
66-
unsafe { PostQuitMessage(0) };
67-
result
68-
});
69-
self.run();
70-
poll_assume_ready(task)
71-
}
72-
73-
/// Runs the message loop.
74-
///
75-
/// Executes previously [`spawn`]ed tasks.
76-
///
77-
/// # Panics
78-
///
79-
/// Panics when the message loops is running already. This happens when
80-
/// `block_on` or `run` is called from async tasks running on this executor.
81-
pub fn run(&self) {
82-
assert!(
83-
!MESSAGE_LOOP_RUNNING.replace(true),
84-
"a message loop is running already"
85-
);
86-
87-
// Any modal window (i.e. a right-click menu) blocks the main message loop
88-
// and dispatches messages internally. To keep the executor running use a
89-
// hook to get access to modal windows internal message loop.
90-
unsafe extern "system" fn hook_proc(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
91-
if code >= 0 && backend::dispatch(&*(lparam as *const MSG)) {
92-
1
93-
} else {
94-
CallNextHookEx(0, code, wparam, lparam)
95-
}
96-
}
97-
let hook =
98-
unsafe { SetWindowsHookExA(WH_MSGFILTER, Some(hook_proc), 0, GetCurrentThreadId()) };
99-
100-
loop {
101-
let mut msg = MaybeUninit::uninit();
102-
unsafe {
103-
let ret = GetMessageA(msg.as_mut_ptr(), 0, 0, 0);
104-
let msg = msg.assume_init();
105-
match ret {
106-
1 => {
107-
if !backend::dispatch(&msg) {
108-
TranslateMessage(&msg);
109-
DispatchMessageA(&msg);
110-
}
70+
loop {
71+
let mut msg = MaybeUninit::uninit();
72+
unsafe {
73+
let ret = GetMessageA(msg.as_mut_ptr(), 0, 0, 0);
74+
let msg = msg.assume_init();
75+
match ret {
76+
1 => {
77+
if !backend::dispatch(&msg) {
78+
TranslateMessage(&msg);
79+
DispatchMessageA(&msg);
11180
}
112-
0 => break,
113-
_ => unreachable!(),
11481
}
82+
0 => break,
83+
_ => unreachable!(),
11584
}
11685
}
117-
118-
unsafe { UnhookWindowsHookEx(hook) };
119-
MESSAGE_LOOP_RUNNING.set(false);
12086
}
87+
88+
unsafe { UnhookWindowsHookEx(hook) };
89+
MESSAGE_LOOP_RUNNING.set(false);
12190
}
12291

12392
fn poll_assume_ready<T>(future: impl Future<Output = T>) -> T {
@@ -136,6 +105,8 @@ fn poll_assume_ready<T>(future: impl Future<Output = T>) -> T {
136105
}
137106
}
138107

108+
/// An owned permission to join on a task (await its termination).
109+
///
139110
/// If a `JoinHandle` is dropped, then its task continues running in the background
140111
/// and its return value is lost.
141112
pub type JoinHandle<F> = backend::JoinHandle<F>;

src/util/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
//! Helper code to work with windows.
2+
13
mod window;
24
pub use window::*;

0 commit comments

Comments
 (0)