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}