Skip to main content

servo_base/generic_channel/
callback.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! # Generic Callbacks
6//!
7//! When sending cross-process messages, we sometimes want to run custom callbacks when the
8//! recipient has finished processing. The callback should run in the sender's address space, and
9//! could be something like enqueuing a task.
10//! In Multi-process mode we can implement this by providing an `IpcSender` to the recipient,
11//! which the recipient can use to send some data back to the senders process.
12//! To avoid blocking the sender, we can pass the callback to the ROUTER, which runs the callback
13//! when receiving the Ipc message.
14//! The callback will be run on every reply message from the recipient. `IpcSender`s are also
15//! `Clone`able, so the Router will sequentialise callbacks.
16//!
17//! ## Callback scenario visualization
18//!
19//! The following visualization showcases how Ipc and the router thread are currently used
20//! to run callbacks asynchronously on the sender process. The recipient may keep the
21//! ReplySender alive and send an arbitrary amount of messages / replies.
22//!
23//! ```none
24//!               Process A                      |              Process B
25//!                                              |
26//! +---------+   IPC: SendMessage(ReplySender)  |          +-------------+  clone  +-------------+
27//! | Sender  |-------------------------------------------> |  Recipient  | ------> | ReplySender |
28//! +---------+                                  |          +-------------+         +-------------+
29//!   |                                          |                 |                       |
30//!   | RegisterCallback A  +---------+          |  Send Reply 1   |        Send Reply 2   |
31//!   + ------------------> | Router  | <--------------------------+-----------------------+
32//!                         +---------+          |
33//!                             | A(reply1)      |
34//!                             | A(reply2)      |
35//!                             |     ...        |
36//!                             v                |
37//!                                              |
38//! ```
39//!
40//!
41//! ## Optimizing single-process mode.
42//!
43//! In Single-process mode, there is no need for the Recipient to send an IpcReply,
44//! since they are in the same address space and could just execute the callback directly.
45//! Since we want to create an abstraction over such callbacks, we need to consider constraints
46//! that the existing multiprocess Ipc solution imposes on us:
47//!
48//! - Support for `FnMut` callbacks (internal mutable state + multiple calls)
49//! - The abstraction should be `Clone`able
50//!
51//! These constraints motivate the [GenericCallback] type, which supports `FnMut` callbacks
52//! and is clonable. This requires wrapping the callback with `Arc<Mutex<>>`, which also adds
53//! synchronization, which could be something that existing callbacks rely on.
54//!
55//! ### Future work
56//!
57//! - Further abstractions for callbacks with fewer constraints, e.g. callbacks
58//!   which don't need to be cloned by the recipient, or non-mutable callbacks.
59//! - A tracing option to measure callback runtime and identify callbacks which misbehave (block)
60//!   for a long time.
61
62use std::fmt;
63use std::marker::PhantomData;
64use std::sync::{Arc, Mutex};
65
66use ipc_channel::ipc::IpcSender;
67use ipc_channel::router::ROUTER;
68use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
69use serde::de::VariantAccess;
70use serde::{Deserialize, Deserializer, Serialize, Serializer};
71use servo_config::opts;
72
73use crate::generic_channel::{
74    GenericReceiver, GenericReceiverVariants, SendError, SendResult, use_ipc,
75};
76
77/// The callback type of our messages.
78///
79/// This is equivalent to [TypedRouterHandler][ipc_channel::router::TypedRouterHandler],
80/// except that this type is not wrapped in a Box.
81/// The callback will be wrapped in either a Box or an Arc, depending on if it is run on
82/// the router, or passed to the recipient.
83pub type MsgCallback<T> = dyn FnMut(Result<T, SendError>) + Send;
84
85/// A mechanism to run a callback in the process this callback was constructed in.
86///
87/// The GenericCallback can be sent cross-process (in multi-process mode). In this case
88/// the callback will be executed on the [ROUTER] thread.
89/// In single-process mode the callback will be executed directly.
90pub struct GenericCallback<T>(GenericCallbackVariants<T>)
91where
92    T: Serialize + Send + 'static;
93
94enum GenericCallbackVariants<T>
95where
96    T: Serialize + Send + 'static,
97{
98    CrossProcess(IpcSender<T>),
99    InProcess(Arc<Mutex<MsgCallback<T>>>),
100}
101
102impl<T> Clone for GenericCallback<T>
103where
104    T: Serialize + Send + 'static,
105{
106    fn clone(&self) -> Self {
107        let variant = match &self.0 {
108            GenericCallbackVariants::CrossProcess(sender) => {
109                GenericCallbackVariants::CrossProcess((*sender).clone())
110            },
111            GenericCallbackVariants::InProcess(callback) => {
112                GenericCallbackVariants::InProcess(callback.clone())
113            },
114        };
115        GenericCallback(variant)
116    }
117}
118
119impl<T> MallocSizeOf for GenericCallback<T>
120where
121    T: Serialize + Send + 'static,
122{
123    fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize {
124        0
125    }
126}
127
128impl<T> GenericCallback<T>
129where
130    T: for<'de> Deserialize<'de> + Serialize + Send + 'static,
131{
132    /// Creates a new GenericCallback.
133    ///
134    /// The callback should not do any heavy work and not block.
135    pub fn new<F: FnMut(Result<T, SendError>) + Send + 'static>(
136        mut callback: F,
137    ) -> Result<Self, SendError> {
138        let generic_callback = if use_ipc() {
139            let (ipc_sender, ipc_receiver) =
140                ipc_channel::ipc::channel().map_err(SendError::from)?;
141            let new_callback = move |msg: Result<T, ipc_channel::SerDeError>| {
142                callback(msg.map_err(|error| error.into()))
143            };
144            ROUTER.add_typed_route(ipc_receiver, Box::new(new_callback));
145            GenericCallback(GenericCallbackVariants::CrossProcess(ipc_sender))
146        } else {
147            let callback = Arc::new(Mutex::new(callback));
148            GenericCallback(GenericCallbackVariants::InProcess(callback))
149        };
150        Ok(generic_callback)
151    }
152
153    /// Produces a GenericCallback and a channel. You can block on this channel for the result.
154    pub fn new_blocking() -> Result<(Self, GenericReceiver<T>), SendError> {
155        if use_ipc() {
156            let (sender, receiver) = ipc_channel::ipc::channel().map_err(SendError::from)?;
157            let generic_callback = GenericCallback(GenericCallbackVariants::CrossProcess(sender));
158            let receiver = GenericReceiver(GenericReceiverVariants::Ipc(receiver));
159            Ok((generic_callback, receiver))
160        } else {
161            let (sender, receiver) = crossbeam_channel::bounded(1);
162            let callback = Arc::new(Mutex::new(move |msg| {
163                if sender.send(msg).is_err() {
164                    log::error!("Error in callback");
165                }
166            }));
167            let generic_callback = GenericCallback(GenericCallbackVariants::InProcess(callback));
168            let receiver = GenericReceiver(GenericReceiverVariants::Crossbeam(receiver));
169            Ok((generic_callback, receiver))
170        }
171    }
172
173    /// Send `value` to the callback.
174    ///
175    /// Note that a return value of `Ok()` simply means that value was sent successfully
176    /// to the callback. The callback itself does not return any value.
177    /// The caller may not assume that the callback is executed synchronously.
178    pub fn send(&self, value: T) -> SendResult {
179        match &self.0 {
180            GenericCallbackVariants::CrossProcess(sender) => {
181                sender.send(value).map_err(|error| match error {
182                    ipc_channel::IpcError::SerializationError(ser_de_error) => {
183                        SendError::SerializationError(ser_de_error.to_string())
184                    },
185                    ipc_channel::IpcError::Io(_) | ipc_channel::IpcError::Disconnected => {
186                        SendError::Disconnected
187                    },
188                })
189            },
190            GenericCallbackVariants::InProcess(callback) => {
191                let mut cb = callback.lock().expect("poisoned");
192                (*cb)(Ok(value));
193                Ok(())
194            },
195        }
196    }
197}
198
199impl<T> Serialize for GenericCallback<T>
200where
201    T: Serialize + Send + 'static,
202{
203    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
204        match &self.0 {
205            GenericCallbackVariants::CrossProcess(sender) => {
206                s.serialize_newtype_variant("GenericCallback", 0, "CrossProcess", sender)
207            },
208            // The only reason we need / want serialization in single-process mode is to support
209            // sending GenericCallbacks over existing IPC channels. This allows us to
210            // incrementally port IPC channels to the GenericChannel, without needing to follow a
211            // top-to-bottom approach.
212            // Long-term we can remove this branch in the code again and replace it with
213            // unreachable, since likely all IPC channels would be GenericChannels.
214            GenericCallbackVariants::InProcess(wrapped_callback) => {
215                if opts::get().multiprocess {
216                    return Err(serde::ser::Error::custom(
217                        "InProcess callback can't be serialized in multiprocess mode",
218                    ));
219                }
220                // Due to the signature of `serialize` we need to clone the Arc to get an owned
221                // pointer we can leak.
222                // We additionally need to Box to get a thin pointer.
223                let cloned_callback = Box::new(wrapped_callback.clone());
224                let sender_clone_addr = Box::leak(cloned_callback) as *mut Arc<_> as usize;
225                s.serialize_newtype_variant("GenericCallback", 1, "InProcess", &sender_clone_addr)
226            },
227        }
228    }
229}
230
231struct GenericCallbackVisitor<T> {
232    marker: PhantomData<T>,
233}
234
235impl<'de, T> serde::de::Visitor<'de> for GenericCallbackVisitor<T>
236where
237    T: Serialize + Deserialize<'de> + Send + 'static,
238{
239    type Value = GenericCallback<T>;
240
241    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
242        formatter.write_str("a GenericCallback variant")
243    }
244
245    fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
246    where
247        A: serde::de::EnumAccess<'de>,
248    {
249        #[derive(Deserialize)]
250        enum GenericCallbackVariantNames {
251            CrossProcess,
252            InProcess,
253        }
254
255        let (variant_name, variant_data): (GenericCallbackVariantNames, _) = data.variant()?;
256
257        match variant_name {
258            GenericCallbackVariantNames::CrossProcess => variant_data
259                .newtype_variant::<IpcSender<T>>()
260                .map(|sender| GenericCallback(GenericCallbackVariants::CrossProcess(sender))),
261            GenericCallbackVariantNames::InProcess => {
262                if use_ipc() {
263                    return Err(serde::de::Error::custom(
264                        "InProcess callback found in multiprocess mode",
265                    ));
266                }
267                let addr = variant_data.newtype_variant::<usize>()?;
268                let ptr = addr as *mut Arc<Mutex<_>>;
269                // SAFETY: We know we are in the same address space as the sender, so we can safely
270                // reconstruct the Arc, that we previously leaked with `into_raw` during
271                // serialization.
272                // Attention: Code reviewers should carefully compare the deserialization here
273                // with the serialization above.
274                #[expect(unsafe_code)]
275                let callback = unsafe { Box::from_raw(ptr) };
276                Ok(GenericCallback(GenericCallbackVariants::InProcess(
277                    *callback,
278                )))
279            },
280        }
281    }
282}
283
284impl<'a, T> Deserialize<'a> for GenericCallback<T>
285where
286    T: Serialize + Deserialize<'a> + Send + 'static,
287{
288    fn deserialize<D>(d: D) -> Result<GenericCallback<T>, D::Error>
289    where
290        D: Deserializer<'a>,
291    {
292        d.deserialize_enum(
293            "GenericCallback",
294            &["CrossProcess", "InProcess"],
295            GenericCallbackVisitor {
296                marker: PhantomData,
297            },
298        )
299    }
300}
301
302impl<T> fmt::Debug for GenericCallback<T>
303where
304    T: Serialize + Send + 'static,
305{
306    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
307        write!(f, "GenericCallback(..)")
308    }
309}
310
311#[cfg(test)]
312mod single_process_callback_test {
313    use std::sync::Arc;
314    use std::sync::atomic::{AtomicUsize, Ordering};
315
316    use crate::generic_channel::{GenericCallback, SendError};
317
318    #[test]
319    fn generic_callback() {
320        let number = Arc::new(AtomicUsize::new(0));
321        let number_clone = number.clone();
322        let callback =
323            move |msg: Result<usize, SendError>| number_clone.store(msg.unwrap(), Ordering::SeqCst);
324        let generic_callback = GenericCallback::new(callback).unwrap();
325        std::thread::scope(|s| {
326            s.spawn(move || generic_callback.send(42));
327        });
328        assert_eq!(number.load(Ordering::SeqCst), 42);
329    }
330
331    #[test]
332    fn generic_callback_via_generic_sender() {
333        let number = Arc::new(AtomicUsize::new(0));
334        let number_clone = number.clone();
335        let callback =
336            move |msg: Result<usize, SendError>| number_clone.store(msg.unwrap(), Ordering::SeqCst);
337        let generic_callback = GenericCallback::new(callback).unwrap();
338        let (tx, rx) = crate::generic_channel::channel().unwrap();
339
340        tx.send(generic_callback).unwrap();
341        std::thread::scope(|s| {
342            s.spawn(move || {
343                let callback = rx.recv().unwrap();
344                callback.send(42).unwrap();
345            });
346        });
347        assert_eq!(number.load(Ordering::SeqCst), 42);
348    }
349
350    #[test]
351    fn generic_callback_via_ipc_sender() {
352        let number = Arc::new(AtomicUsize::new(0));
353        let number_clone = number.clone();
354        let callback =
355            move |msg: Result<usize, SendError>| number_clone.store(msg.unwrap(), Ordering::SeqCst);
356        let generic_callback = GenericCallback::new(callback).unwrap();
357        let (tx, rx) = ipc_channel::ipc::channel().unwrap();
358
359        tx.send(generic_callback).unwrap();
360        std::thread::scope(|s| {
361            s.spawn(move || {
362                let callback = rx.recv().unwrap();
363                callback.send(42).unwrap();
364            });
365        });
366        assert_eq!(number.load(Ordering::SeqCst), 42);
367    }
368
369    #[test]
370    fn generic_callback_blocking() {
371        let (callback, receiver) = GenericCallback::new_blocking().unwrap();
372        std::thread::spawn(move || {
373            std::thread::sleep(std::time::Duration::from_secs(1));
374            assert!(callback.send(42).is_ok());
375        });
376        assert_eq!(receiver.recv().unwrap(), 42);
377    }
378}