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}