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