Skip to main content

fui/
worker_runtime.rs

1use std::cell::{Cell, RefCell};
2thread_local! {
3    static WORKER_TERMINAL_SENT: Cell<bool> = const { Cell::new(false) };
4    static WORKER_CALLBACK_BUFFER: RefCell<Box<[u8]>> =
5        RefCell::new(vec![0u8; 1024 * 1024].into_boxed_slice());
6}
7
8#[cfg(target_arch = "wasm32")]
9#[link(wasm_import_module = "fui_worker_host")]
10unsafe extern "C" {
11    #[link_name = "fui_worker_report_progress"]
12    fn host_worker_report_progress(ptr: usize, len: u32);
13    #[link_name = "fui_worker_complete_string"]
14    fn host_worker_complete_string(ptr: usize, len: u32);
15    #[link_name = "fui_worker_fail"]
16    fn host_worker_fail(ptr: usize, len: u32);
17    #[link_name = "fui_worker_is_cancelled"]
18    fn host_worker_is_cancelled() -> bool;
19    #[link_name = "fui_worker_request_yield"]
20    fn host_worker_request_yield();
21    #[link_name = "fui_worker_request_yield_delay"]
22    fn host_worker_request_yield_delay(delay_ms: i32);
23    #[link_name = "fui_file_read_chunk"]
24    fn host_file_read_chunk(offset_low: i32, offset_high: i32, max_bytes: i32) -> i32;
25    #[link_name = "fui_file_worker_write_chunk"]
26    fn host_file_worker_write_chunk(ptr: usize, len: i32);
27}
28
29#[cfg(not(target_arch = "wasm32"))]
30unsafe extern "C" {
31    #[link_name = "fui_native_worker_report_progress"]
32    fn host_native_worker_report_progress(ptr: *const u8, len: u32);
33    #[link_name = "fui_native_worker_complete_string"]
34    fn host_native_worker_complete_string(ptr: *const u8, len: u32);
35    #[link_name = "fui_native_worker_fail"]
36    fn host_native_worker_fail(ptr: *const u8, len: u32);
37    #[link_name = "fui_native_worker_is_cancelled"]
38    fn host_native_worker_is_cancelled() -> bool;
39    #[link_name = "fui_native_worker_request_yield"]
40    fn host_native_worker_request_yield(delay_ms: i32);
41}
42
43fn with_utf8(value: &str, callback: impl FnOnce(usize, u32)) {
44    let bytes = value.as_bytes();
45    callback(
46        if bytes.is_empty() {
47            0
48        } else {
49            bytes.as_ptr() as usize
50        },
51        bytes.len() as u32,
52    );
53}
54
55pub struct WorkerRuntime;
56
57impl WorkerRuntime {
58    /// # Safety
59    /// `input_ptr` must reference at least `input_len` readable bytes when `input_len` is non-zero.
60    pub unsafe fn entry_input(input_ptr: usize, input_len: u32) -> String {
61        if input_ptr == 0 || input_len == 0 {
62            return String::new();
63        }
64        let bytes =
65            unsafe { std::slice::from_raw_parts(input_ptr as *const u8, input_len as usize) };
66        String::from_utf8_lossy(bytes).into_owned()
67    }
68
69    pub fn report_progress(progress: impl AsRef<str>) {
70        if WORKER_TERMINAL_SENT.with(Cell::get) {
71            return;
72        }
73        #[cfg(target_arch = "wasm32")]
74        with_utf8(progress.as_ref(), |ptr, len| unsafe {
75            host_worker_report_progress(ptr, len);
76        });
77        #[cfg(not(target_arch = "wasm32"))]
78        with_utf8(progress.as_ref(), |ptr, len| unsafe {
79            host_native_worker_report_progress(ptr as *const u8, len);
80        });
81    }
82
83    pub fn complete(result: impl AsRef<str>) {
84        if WORKER_TERMINAL_SENT.with(Cell::get) {
85            return;
86        }
87        WORKER_TERMINAL_SENT.with(|sent| sent.set(true));
88        #[cfg(target_arch = "wasm32")]
89        with_utf8(result.as_ref(), |ptr, len| unsafe {
90            host_worker_complete_string(ptr, len);
91        });
92        #[cfg(not(target_arch = "wasm32"))]
93        with_utf8(result.as_ref(), |ptr, len| unsafe {
94            host_native_worker_complete_string(ptr as *const u8, len);
95        });
96    }
97
98    pub fn fail(message: impl AsRef<str>) {
99        if WORKER_TERMINAL_SENT.with(Cell::get) {
100            return;
101        }
102        WORKER_TERMINAL_SENT.with(|sent| sent.set(true));
103        #[cfg(target_arch = "wasm32")]
104        with_utf8(message.as_ref(), |ptr, len| unsafe {
105            host_worker_fail(ptr, len);
106        });
107        #[cfg(not(target_arch = "wasm32"))]
108        with_utf8(message.as_ref(), |ptr, len| unsafe {
109            host_native_worker_fail(ptr as *const u8, len);
110        });
111    }
112
113    pub fn is_cancelled() -> bool {
114        #[cfg(target_arch = "wasm32")]
115        {
116            unsafe { host_worker_is_cancelled() }
117        }
118        #[cfg(not(target_arch = "wasm32"))]
119        {
120            unsafe { host_native_worker_is_cancelled() }
121        }
122    }
123
124    pub fn r#yield(delay_ms: i32) -> bool {
125        if WORKER_TERMINAL_SENT.with(Cell::get) {
126            return false;
127        }
128        #[cfg(target_arch = "wasm32")]
129        unsafe {
130            if delay_ms > 0 {
131                host_worker_request_yield_delay(delay_ms);
132            } else {
133                host_worker_request_yield();
134            }
135        }
136        #[cfg(not(target_arch = "wasm32"))]
137        unsafe {
138            host_native_worker_request_yield(delay_ms.max(0));
139        }
140        true
141    }
142
143    pub fn yield_now(delay_ms: i32) -> bool {
144        Self::r#yield(delay_ms)
145    }
146}
147
148pub fn file_read_chunk(offset_low: i32, offset_high: i32, max_bytes: i32) -> i32 {
149    #[cfg(target_arch = "wasm32")]
150    {
151        unsafe { host_file_read_chunk(offset_low, offset_high, max_bytes) }
152    }
153    #[cfg(not(target_arch = "wasm32"))]
154    {
155        let _ = (offset_low, offset_high, max_bytes);
156        0
157    }
158}
159
160pub fn file_worker_write_chunk(ptr: usize, len: i32) {
161    #[cfg(target_arch = "wasm32")]
162    unsafe {
163        host_file_worker_write_chunk(ptr, len);
164    }
165    #[cfg(not(target_arch = "wasm32"))]
166    {
167        let _ = (ptr, len);
168    }
169}
170
171pub fn worker_text_buffer_ptr() -> usize {
172    WORKER_CALLBACK_BUFFER.with(|buffer| buffer.borrow().as_ptr() as usize)
173}
174
175pub fn worker_text_buffer_size() -> u32 {
176    WORKER_CALLBACK_BUFFER.with(|buffer| buffer.borrow().len() as u32)
177}
178
179pub fn reset_worker_runtime() {
180    WORKER_TERMINAL_SENT.with(|sent| sent.set(false));
181}
182
183#[cfg(test)]
184mod tests {
185    use super::{reset_worker_runtime, WorkerRuntime};
186    use crate::{fui_worker, WorkerJob, WorkerJobState};
187    use std::cell::{Cell, RefCell};
188
189    thread_local! {
190        static START_INPUT: RefCell<String> = const { RefCell::new(String::new()) };
191        static RUN_COUNT: Cell<u32> = const { Cell::new(0) };
192        static HOST_EVENTS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
193        static HOST_CANCELLED: Cell<bool> = const { Cell::new(false) };
194    }
195
196    unsafe fn host_text(ptr: *const u8, len: u32) -> String {
197        if ptr.is_null() || len == 0 {
198            return String::new();
199        }
200        String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(ptr, len as usize) })
201            .into_owned()
202    }
203
204    fn record(event: impl Into<String>) {
205        HOST_EVENTS.with(|events| events.borrow_mut().push(event.into()));
206    }
207
208    fn reset_host() {
209        START_INPUT.with(|value| value.borrow_mut().clear());
210        RUN_COUNT.with(|value| value.set(0));
211        HOST_EVENTS.with(|events| events.borrow_mut().clear());
212        HOST_CANCELLED.with(|cancelled| cancelled.set(false));
213        reset_worker_runtime();
214    }
215
216    #[no_mangle]
217    extern "C" fn fui_native_worker_report_progress(ptr: *const u8, len: u32) {
218        record(format!("progress:{}", unsafe { host_text(ptr, len) }));
219    }
220
221    #[no_mangle]
222    extern "C" fn fui_native_worker_complete_string(ptr: *const u8, len: u32) {
223        record(format!("complete:{}", unsafe { host_text(ptr, len) }));
224    }
225
226    #[no_mangle]
227    extern "C" fn fui_native_worker_fail(ptr: *const u8, len: u32) {
228        record(format!("error:{}", unsafe { host_text(ptr, len) }));
229    }
230
231    #[no_mangle]
232    extern "C" fn fui_native_worker_is_cancelled() -> bool {
233        HOST_CANCELLED.with(Cell::get)
234    }
235
236    #[no_mangle]
237    extern "C" fn fui_native_worker_request_yield(delay_ms: i32) {
238        record(format!("yield:{delay_ms}"));
239    }
240
241    #[derive(Default)]
242    struct TestJob {
243        state: WorkerJobState,
244    }
245
246    impl WorkerJob for TestJob {
247        fn state(&mut self) -> &mut WorkerJobState {
248            &mut self.state
249        }
250
251        fn on_start(&mut self, input: String) {
252            START_INPUT.with(|value| value.replace(input));
253        }
254
255        fn run(&mut self) {
256            let run_count = RUN_COUNT.with(|value| {
257                let next = value.get() + 1;
258                value.set(next);
259                next
260            });
261            if run_count == 1 {
262                self.report_progress("halfway ✅");
263                self.r#yield(-10);
264            } else {
265                self.complete("complete");
266            }
267        }
268    }
269
270    #[derive(Default)]
271    struct FailedJob {
272        state: WorkerJobState,
273    }
274
275    impl WorkerJob for FailedJob {
276        fn state(&mut self) -> &mut WorkerJobState {
277            &mut self.state
278        }
279
280        fn run(&mut self) {
281            self.fail("failed ❌");
282        }
283    }
284
285    #[derive(Default)]
286    struct PanickingJob {
287        state: WorkerJobState,
288    }
289
290    impl WorkerJob for PanickingJob {
291        fn state(&mut self) -> &mut WorkerJobState {
292            &mut self.state
293        }
294
295        fn run(&mut self) {
296            panic!("native worker panic");
297        }
298    }
299
300    fui_worker!(
301        test_worker_entry => TestJob,
302        failed_worker_entry => FailedJob,
303        panicking_worker_entry => PanickingJob,
304    );
305
306    #[test]
307    fn native_worker_entry_preserves_utf8_state_progress_yield_and_completion() {
308        reset_host();
309        let input = "start 🌍";
310
311        unsafe {
312            test_worker_entry(input.as_ptr() as usize, input.len() as u32);
313            test_worker_entry(0, 0);
314        }
315
316        assert_eq!(START_INPUT.with(|value| value.borrow().clone()), input);
317        assert_eq!(RUN_COUNT.with(Cell::get), 2);
318        assert_eq!(
319            HOST_EVENTS.with(|events| events.borrow().clone()),
320            ["progress:halfway ✅", "yield:0", "complete:complete"]
321        );
322    }
323
324    #[test]
325    fn native_worker_failure_and_terminal_calls_are_delivered_once() {
326        reset_host();
327        unsafe { failed_worker_entry(0, 0) };
328        WorkerRuntime::report_progress("late progress");
329        WorkerRuntime::complete("late complete");
330        WorkerRuntime::fail("late error");
331
332        assert_eq!(
333            HOST_EVENTS.with(|events| events.borrow().clone()),
334            ["error:failed ❌"]
335        );
336    }
337
338    #[test]
339    fn native_worker_observes_host_cancellation() {
340        reset_host();
341        assert!(!WorkerRuntime::is_cancelled());
342        HOST_CANCELLED.with(|cancelled| cancelled.set(true));
343        assert!(WorkerRuntime::is_cancelled());
344    }
345
346    #[test]
347    fn native_worker_panic_becomes_one_normalized_error() {
348        reset_host();
349        unsafe { panicking_worker_entry(0, 0) };
350
351        assert_eq!(
352            HOST_EVENTS.with(|events| events.borrow().clone()),
353            ["error:Worker panicked."]
354        );
355    }
356}