native_executor/mailbox.rs
1//! Mailbox-based message passing for safe cross-thread communication.
2//!
3//! This module provides a [`Mailbox`] type that enables asynchronous message passing
4//! to a value owned by a background task. The mailbox ensures thread-safe access
5//! to the contained value by serializing all operations through a message queue.
6//!
7//! # Overview
8//!
9//! The mailbox pattern is useful when you need to:
10//! - Share mutable state across threads safely
11//! - Process operations on a value sequentially
12//! - Avoid blocking when sending updates
13//! - Make async calls that return values
14//!
15//! # Examples
16//!
17//! ```rust
18//! use native_executor::Mailbox;
19//! use std::collections::HashMap;
20//!
21//! // Create a mailbox containing a HashMap on the main executor
22//! let mailbox = Mailbox::main(HashMap::<String, i32>::new());
23//!
24//! // Send updates to the value (non-blocking)
25//! mailbox.handle(|map| {
26//! map.insert("key".to_string(), 42);
27//! });
28//!
29//! // Make async calls that return values
30//! let value = mailbox.call(|map| {
31//! map.get("key").copied().unwrap_or(0)
32//! }).await;
33//! ```
34
35use async_channel::{Sender, unbounded};
36use executor_core::LocalExecutor;
37
38use crate::NativeExecutor;
39
40type Job<T> = Box<dyn Send + FnOnce(&T)>;
41
42/// A mailbox for sending messages to a value owned by a background task.
43///
44/// `Mailbox<T>` provides thread-safe access to a value of type `T` by serializing
45/// all operations through an async message queue. The value is owned by a background
46/// task that processes incoming messages sequentially.
47///
48/// # Type Parameters
49///
50/// * `T` - The type of value contained in the mailbox. Must be `'static` to ensure
51/// the background task can own it safely.
52///
53/// # Thread Safety
54///
55/// The mailbox enables other threads to safely access a value living on another thread
56/// without explicit locks. The mailbox handle itself is always `Send` and `Sync`,
57/// allowing it to be shared across threads. All operations on the value are serialized
58/// through an async message queue, providing lock-free concurrent access. When `T` is
59/// not `Send`, the value remains pinned to its original thread but can still be safely
60/// accessed from other threads through the mailbox.
61#[derive(Debug)]
62pub struct Mailbox<T: 'static> {
63 sender: Sender<Job<T>>,
64}
65
66impl<T: 'static> Mailbox<T> {
67 /// Creates a new mailbox with the given value on the specified executor.
68 ///
69 /// The value will be moved to a background task that processes incoming
70 /// messages. The executor is consumed to spawn the background task.
71 ///
72 /// # Parameters
73 ///
74 /// * `executor` - The executor to spawn the background task on
75 /// * `value` - The value to be owned by the background task
76 ///
77 /// # Examples
78 ///
79 /// ```rust
80 /// use native_executor::{Mailbox, MainExecutor};
81 /// use std::collections::HashMap;
82 ///
83 /// let mailbox = Mailbox::new(MainExecutor, HashMap::<String, i32>::new());
84 /// ```
85 #[allow(clippy::needless_pass_by_value)]
86 pub fn new<E: LocalExecutor>(executor: E, value: T) -> Self {
87 let (sender, receiver) = unbounded::<Box<dyn Send + FnOnce(&T)>>();
88
89 let _fut = executor.spawn_local(async move {
90 while let Ok(update) = receiver.recv().await {
91 update(&value);
92 }
93 });
94 Self { sender }
95 }
96
97 /// Creates a new mailbox with the given value on the main executor.
98 ///
99 /// This is a convenience method equivalent to `Mailbox::new(MainExecutor, value)`.
100 /// The background task will be spawned on the main executor.
101 ///
102 /// # Parameters
103 ///
104 /// * `value` - The value to be owned by the background task
105 ///
106 /// # Examples
107 ///
108 /// ```rust
109 /// use native_executor::Mailbox;
110 /// use std::collections::HashMap;
111 ///
112 /// let mailbox = Mailbox::main(HashMap::<String, i32>::new());
113 /// ```
114 pub fn main(value: T) -> Self {
115 Self::new(NativeExecutor, value)
116 }
117
118 /// Sends a non-blocking update to the mailbox value.
119 ///
120 /// The provided closure will be called with a reference to the value
121 /// in the background task. This operation is non-blocking and will
122 /// not wait for the update to be processed.
123 ///
124 /// If the background task has been dropped or the channel is full,
125 /// the update may be silently discarded.
126 ///
127 /// # Parameters
128 ///
129 /// * `update` - A closure that will be called with a reference to the value
130 ///
131 /// # Examples
132 ///
133 /// ```rust
134 /// use native_executor::Mailbox;
135 /// use std::collections::HashMap;
136 ///
137 /// let mailbox = Mailbox::main(HashMap::<String, i32>::new());
138 ///
139 /// // Send a non-blocking update
140 /// mailbox.handle(|map| {
141 /// map.insert("key".to_string(), 42);
142 /// });
143 /// ```
144 pub fn handle(&self, update: impl FnOnce(&T) + Send + 'static) {
145 let _ = self.sender.try_send(Box::new(update));
146 }
147
148 /// Makes an asynchronous call to the mailbox value and returns the result.
149 ///
150 /// The provided closure will be called with a reference to the value
151 /// in the background task, and the result will be returned to the caller.
152 /// This operation blocks until the call is processed and the result is available.
153 ///
154 /// # Parameters
155 ///
156 /// * `f` - A closure that will be called with a reference to the value and returns a result
157 ///
158 /// # Returns
159 ///
160 /// The result returned by the closure after it has been executed on the value.
161 ///
162 /// # Panics
163 ///
164 /// Panics if the background task has been dropped or the channel is closed,
165 /// making it impossible to receive the result.
166 ///
167 /// # Examples
168 ///
169 /// ```rust
170 /// use native_executor::Mailbox;
171 /// use std::collections::HashMap;
172 ///
173 /// # async fn example() {
174 /// let mailbox = Mailbox::main(HashMap::<String, i32>::new());
175 ///
176 /// // Make an async call that returns a value
177 /// let value = mailbox.call(|map| {
178 /// map.get("key").copied().unwrap_or(0)
179 /// }).await;
180 /// # }
181 /// ```
182 pub async fn call<R>(&self, f: impl FnOnce(&T) -> R + Send + 'static) -> R
183 where
184 R: Send + 'static,
185 {
186 let (s, r) = async_channel::bounded(1);
187 self.handle(move |v| {
188 let res = f(v);
189 let _ = s.try_send(res);
190 });
191 r.recv().await.expect("Mailbox call failed")
192 }
193}