Skip to main content

ferroui_browser/interop/
thread_proxy.rs

1//! The threads of the module: the id of the calling thread, and a call made
2//! on another one.
3//!
4//! Not from upstream, where the runtime carries a call from one thread to
5//! another. A module built with threads runs each thread in a web worker,
6//! and a worker only does what arrives at its event loop: a call for another
7//! thread is put into a queue of the module and the thread is notified. The
8//! two uses are the wake-up of the dispatcher from the render thread and the
9//! frame out of turn the thread of the user interface asks the render thread
10//! for (`docs/porting/browser-render-worker.md`, section 3 and "B2.3").
11//!
12//! The work only carries what is `Send`: no object of the page crosses.
13//!
14//! The runtime itself carries calls too, from any thread to the main thread:
15//! the functions of the C library that only the main thread can serve. Those
16//! make the calling thread wait; [`proxied_calls`] is their count.
17//!
18//! In a build without threads there is one thread, its id is 0 and no call
19//! is ever queued.
20
21use wasm_bindgen::prelude::*;
22
23#[wasm_bindgen(raw_module = "./ferroui.js")]
24extern "C" {
25    #[wasm_bindgen(js_namespace = FerroExports, js_name = proxiedCalls)]
26    fn js_proxied_calls() -> f64;
27
28    #[wasm_bindgen(js_namespace = FerroExports, js_name = lastProxiedFunction)]
29    fn js_last_proxied_function() -> f64;
30
31    #[wasm_bindgen(js_namespace = FerroExports, js_name = reportRenderThreadPanic)]
32    fn js_report_render_thread_panic(message: &str);
33}
34
35/// Tells the page that the render thread panicked: the script of the calling
36/// thread, which has to be the thread of the page, logs `message` as an error
37/// on the console of the page and keeps it
38/// (`FerroExports.renderThreadPanics`).
39///
40/// Not from upstream, whose render loop logs the exception of a frame through
41/// the logger of the process. A thread of this port has no log sink, and
42/// what a worker writes to its console is not on the console of the page.
43pub(crate) fn report_render_thread_panic(message: &str) {
44    js_report_render_thread_panic(message);
45}
46
47/// How many calls the runtime has carried from the calling thread to the
48/// main thread of the page since the thread started: the calls of the C
49/// library that only the main thread can serve (a line on the console, a
50/// file, the environment), each of which makes the calling thread wait for
51/// the main thread. `None` when the script of the thread does not count them:
52/// on the main thread itself, and in a build without threads.
53///
54/// The calls of [`run_on_thread`] are not among them: they are queued and
55/// nobody waits for them.
56///
57/// Not from upstream. The count is kept by the script that is linked into a
58/// module built with threads (`scripts/browser/threads/ferroui-worker-attach.js`);
59/// it is how a render thread shows that a frame does not depend on the
60/// thread of the page (`docs/porting/browser-render-worker.md`, "B2.6").
61pub(crate) fn proxied_calls() -> Option<u64> {
62    let count = js_proxied_calls();
63    (count >= 0.0).then_some(count as u64)
64}
65
66/// The index of the function of the last call [`proxied_calls`] counted, in
67/// the table of the script of the module; `None` when there was none.
68pub(crate) fn last_proxied_function() -> Option<i64> {
69    let index = js_last_proxied_function();
70    (index >= 0.0).then_some(index as i64)
71}
72
73/// What is run on another thread.
74pub(crate) type ThreadWork = Box<dyn FnOnce() + Send>;
75
76/// The signature of [`run_on_thread`]; a test puts a recorder in its place.
77pub(crate) type RunOnThread = fn(usize, ThreadWork) -> bool;
78
79/// The id of the calling thread: the key of its worker in the table of
80/// threads of the module. 0 in a build without threads.
81pub fn current_thread() -> usize {
82    native::current_thread()
83}
84
85/// Queues `work` for `thread` (what [`current_thread`] returned there) and
86/// returns at once. The work runs when the thread is at its event loop:
87/// never inside a call the thread is making, and not while the thread waits
88/// for a lock or a condition.
89///
90/// The thread has to be one that does not end: the callers of the crate pass
91/// the thread of the dispatcher, which is the thread of the page, and a
92/// render thread, which is kept alive for as long as the page. The id of a
93/// thread that has ended names memory the runtime has released.
94///
95/// Returns whether the work was queued; `false` in a build without threads,
96/// and the work is dropped then.
97pub(crate) fn run_on_thread(thread: usize, work: ThreadWork) -> bool {
98    native::run_on_thread(thread, work)
99}
100
101#[cfg(all(target_os = "emscripten", target_feature = "atomics"))]
102mod native {
103    use super::ThreadWork;
104    use std::ffi::c_void;
105    use std::sync::OnceLock;
106
107    /// A queue of calls between threads (`em_proxying_queue`).
108    #[repr(C)]
109    struct ProxyingQueue {
110        _opaque: [u8; 0],
111    }
112
113    extern "C" {
114        fn pthread_self() -> usize;
115        fn em_proxying_queue_create() -> *mut ProxyingQueue;
116        fn emscripten_proxy_async(
117            queue: *mut ProxyingQueue,
118            target_thread: usize,
119            func: extern "C" fn(*mut c_void),
120            arg: *mut c_void,
121        ) -> bool;
122    }
123
124    /// The queue of the platform, as an address; 0 when it could not be
125    /// created. It is never destroyed.
126    ///
127    /// A queue of its own, not the system queue of the runtime: the main
128    /// thread runs the work of the system queue wherever it waits for a lock,
129    /// in the middle of whatever it was doing, and runs the work of any other
130    /// queue only from its event loop.
131    static QUEUE: OnceLock<usize> = OnceLock::new();
132
133    pub(super) fn current_thread() -> usize {
134        // SAFETY: the function takes no argument and returns the address of
135        // the descriptor of the calling thread.
136        unsafe { pthread_self() }
137    }
138
139    /// Runs on the target thread.
140    extern "C" fn run(arg: *mut c_void) {
141        // SAFETY: `arg` is the pointer `run_on_thread` made with
142        // `Box::into_raw` for this call, and the queue calls the function at
143        // most once for it.
144        let work = unsafe { Box::from_raw(arg.cast::<ThreadWork>()) };
145        work();
146    }
147
148    pub(super) fn run_on_thread(thread: usize, work: ThreadWork) -> bool {
149        // SAFETY: the function takes no argument and returns a new queue or
150        // null.
151        let queue = *QUEUE.get_or_init(|| unsafe { em_proxying_queue_create() } as usize) as *mut ProxyingQueue;
152        if queue.is_null() || thread == 0 {
153            return false;
154        }
155        // A box of the box: the closure is a wide pointer, the queue carries
156        // a thin one.
157        let arg = Box::into_raw(Box::new(work));
158        // SAFETY: `queue` is a live queue, `run` has the signature the queue
159        // calls and `arg` stays valid until `run` takes it back. `thread`
160        // is the descriptor of a thread that has not ended, as the callers
161        // of the crate guarantee (see `super::run_on_thread`).
162        let queued = unsafe { emscripten_proxy_async(queue, thread, run, arg.cast()) };
163        if !queued {
164            // SAFETY: the queue refused the work, so `run` will not be called
165            // and the pointer is still the only one to the box.
166            drop(unsafe { Box::from_raw(arg) });
167        }
168        queued
169    }
170}
171
172#[cfg(not(all(target_os = "emscripten", target_feature = "atomics")))]
173mod native {
174    use super::ThreadWork;
175
176    pub(super) fn current_thread() -> usize {
177        0
178    }
179
180    pub(super) fn run_on_thread(_thread: usize, _work: ThreadWork) -> bool {
181        false
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn without_threads_there_is_one_thread_and_nothing_is_queued() {
191        assert_eq!(0, current_thread());
192        assert!(!run_on_thread(1, Box::new(|| panic!("the work must not run"))));
193    }
194}