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 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}