1use std::collections::{HashMap, VecDeque};
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::sync::mpsc::{Receiver, Sender};
24use std::sync::{Arc, Mutex};
25use std::time::Duration;
26
27use anyhow::Result;
28use wasmtime::*;
29
30use crate::capabilities::{
31 console_log, read_guest_bytes, read_guest_string, register_host_functions, write_guest_bytes,
32 ConsoleEntry, ConsoleLevel, HostState,
33};
34use crate::engine::ModuleLoader;
35use crate::url::OxideUrl;
36
37enum Inbox {
39 Message(Vec<u8>),
41 Terminate,
43}
44
45struct Worker {
47 inbox_tx: Sender<Inbox>,
49 outbox: Arc<Mutex<VecDeque<Vec<u8>>>>,
51 alive: Arc<AtomicBool>,
53}
54
55pub struct WorkerState {
57 workers: HashMap<u32, Worker>,
58 next_id: u32,
59}
60
61impl WorkerState {
62 fn new() -> Self {
63 Self {
64 workers: HashMap::new(),
65 next_id: 1,
66 }
67 }
68
69 fn alloc_id(&mut self) -> u32 {
70 let id = self.next_id;
71 self.next_id = self.next_id.wrapping_add(1).max(1);
72 id
73 }
74
75 fn spawn(&mut self, url: String, parent: &HostState) -> u32 {
81 let loader = match parent.module_loader.clone() {
82 Some(l) => l,
83 None => return 0,
84 };
85
86 let id = self.alloc_id();
87 let (inbox_tx, inbox_rx) = std::sync::mpsc::channel::<Inbox>();
88 let outbox: Arc<Mutex<VecDeque<Vec<u8>>>> = Arc::new(Mutex::new(VecDeque::new()));
89 let alive = Arc::new(AtomicBool::new(true));
90
91 let child = HostState {
92 module_loader: Some(loader.clone()),
93 kv_db: parent.kv_db.clone(),
94 console: parent.console.clone(),
95 current_url: Arc::new(Mutex::new(url.clone())),
96 worker_outbox: Some(outbox.clone()),
97 worker_current_msg: Arc::new(Mutex::new(None)),
98 ..Default::default()
99 };
100
101 let console = parent.console.clone();
102 let alive_thread = alive.clone();
103 std::thread::spawn(move || {
104 worker_main(url, loader, child, inbox_rx, alive_thread, console);
105 });
106
107 self.workers.insert(
108 id,
109 Worker {
110 inbox_tx,
111 outbox,
112 alive,
113 },
114 );
115 id
116 }
117
118 fn post(&self, id: u32, data: Vec<u8>) -> bool {
119 match self.workers.get(&id) {
120 Some(w) => w.inbox_tx.send(Inbox::Message(data)).is_ok(),
121 None => false,
122 }
123 }
124
125 fn recv(&self, id: u32) -> Option<Vec<u8>> {
126 self.workers.get(&id)?.outbox.lock().unwrap().pop_front()
127 }
128
129 fn terminate(&mut self, id: u32) -> bool {
130 match self.workers.remove(&id) {
131 Some(w) => {
132 w.alive.store(false, Ordering::Relaxed);
133 let _ = w.inbox_tx.send(Inbox::Terminate);
134 true
135 }
136 None => false,
137 }
138 }
139}
140
141fn ensure_workers(state: &Arc<Mutex<Option<WorkerState>>>) {
142 let mut g = state.lock().unwrap();
143 if g.is_none() {
144 *g = Some(WorkerState::new());
145 }
146}
147
148fn fetch_worker_bytes(url: &str) -> Result<Vec<u8>> {
150 let parsed = OxideUrl::parse(url).map_err(|e| anyhow::anyhow!("{e}"))?;
151 if parsed.is_fetchable() {
152 let client = reqwest::blocking::Client::builder()
153 .timeout(Duration::from_secs(30))
154 .build()?;
155 let resp = client
156 .get(parsed.as_str())
157 .header("Accept", "application/wasm")
158 .send()?;
159 if !resp.status().is_success() {
160 anyhow::bail!("HTTP {}", resp.status());
161 }
162 Ok(resp.bytes()?.to_vec())
163 } else if parsed.is_local_file() {
164 let path = parsed
165 .to_file_path()
166 .ok_or_else(|| anyhow::anyhow!("cannot convert file URL to path: {url}"))?;
167 Ok(std::fs::read(&path)?)
168 } else {
169 anyhow::bail!("unsupported worker URL scheme: {}", parsed.scheme())
170 }
171}
172
173fn worker_main(
176 url: String,
177 loader: Arc<ModuleLoader>,
178 host_state: HostState,
179 inbox_rx: Receiver<Inbox>,
180 alive: Arc<AtomicBool>,
181 console: Arc<Mutex<Vec<ConsoleEntry>>>,
182) {
183 let wasm_bytes = match fetch_worker_bytes(&url) {
184 Ok(b) => b,
185 Err(e) => {
186 console_log(
187 &console,
188 ConsoleLevel::Error,
189 format!("[WORKER] fetch failed for {url}: {e}"),
190 );
191 return;
192 }
193 };
194
195 let module = match Module::new(&loader.engine, &wasm_bytes) {
196 Ok(m) => m,
197 Err(e) => {
198 console_log(
199 &console,
200 ConsoleLevel::Error,
201 format!("[WORKER] compile failed: {e}"),
202 );
203 return;
204 }
205 };
206
207 let mut store = Store::new(&loader.engine, host_state);
208 if store.set_fuel(loader.fuel_limit).is_err() {
209 return;
210 }
211
212 let mut linker = Linker::new(&loader.engine);
213 if register_host_functions(&mut linker).is_err() {
214 return;
215 }
216
217 let mem_type = MemoryType::new(1, Some(loader.max_memory_pages));
218 let memory = match Memory::new(&mut store, mem_type) {
219 Ok(m) => m,
220 Err(_) => return,
221 };
222 if linker.define(&store, "oxide", "memory", memory).is_err() {
223 return;
224 }
225 store.data_mut().memory = Some(memory);
226
227 let instance = match linker.instantiate(&mut store, &module) {
228 Ok(i) => i,
229 Err(e) => {
230 console_log(
231 &console,
232 ConsoleLevel::Error,
233 format!("[WORKER] instantiate failed: {e}"),
234 );
235 return;
236 }
237 };
238
239 if let Some(guest_mem) = instance.get_memory(&mut store, "memory") {
240 store.data_mut().memory = Some(guest_mem);
241 }
242
243 if let Ok(start_app) = instance.get_typed_func::<(), ()>(&mut store, "start_app") {
244 let _ = store.set_fuel(loader.fuel_limit);
245 if let Err(e) = start_app.call(&mut store, ()) {
246 console_log(
247 &console,
248 ConsoleLevel::Error,
249 format!("[WORKER] start_app trapped: {e}"),
250 );
251 return;
252 }
253 }
254
255 let on_message = instance
256 .get_typed_func::<u32, ()>(&mut store, "on_message")
257 .ok();
258
259 while alive.load(Ordering::Relaxed) {
260 let bytes = match inbox_rx.recv() {
261 Ok(Inbox::Message(b)) => b,
262 Ok(Inbox::Terminate) | Err(_) => break,
263 };
264 let Some(ref on_message) = on_message else {
265 continue;
266 };
267 let len = bytes.len() as u32;
268 *store.data().worker_current_msg.lock().unwrap() = Some(bytes);
269 let _ = store.set_fuel(loader.fuel_limit);
270 if let Err(e) = on_message.call(&mut store, len) {
271 let msg = if e.to_string().contains("fuel") {
272 "[WORKER] on_message fuel limit exceeded".to_string()
273 } else {
274 format!("[WORKER] on_message trapped: {e}")
275 };
276 console_log(&console, ConsoleLevel::Error, msg);
277 break;
278 }
279 *store.data().worker_current_msg.lock().unwrap() = None;
280 }
281}
282
283pub fn register_worker_functions(linker: &mut Linker<HostState>) -> Result<()> {
285 linker.func_wrap(
289 "oxide",
290 "api_spawn_worker",
291 |caller: Caller<'_, HostState>, url_ptr: u32, url_len: u32| -> i32 {
292 let console = caller.data().console.clone();
293 let mem = match caller.data().memory {
294 Some(m) => m,
295 None => return -1,
296 };
297 let url = match read_guest_string(&mem, &caller, url_ptr, url_len) {
298 Ok(s) => s,
299 Err(_) => return -1,
300 };
301 let workers = caller.data().workers.clone();
302 ensure_workers(&workers);
303 let id = workers
304 .lock()
305 .unwrap()
306 .as_mut()
307 .unwrap()
308 .spawn(url.clone(), caller.data());
309 if id == 0 {
310 console_log(
311 &console,
312 ConsoleLevel::Error,
313 "[WORKER] spawn failed (no module loader)".into(),
314 );
315 return -1;
316 }
317 console_log(
318 &console,
319 ConsoleLevel::Log,
320 format!("[WORKER] spawned {url} (handle={id})"),
321 );
322 id as i32
323 },
324 )?;
325
326 linker.func_wrap(
330 "oxide",
331 "api_worker_post_message",
332 |caller: Caller<'_, HostState>, handle: u32, ptr: u32, len: u32| -> i32 {
333 let mem = match caller.data().memory {
334 Some(m) => m,
335 None => return -1,
336 };
337 let data = match read_guest_bytes(&mem, &caller, ptr, len) {
338 Ok(b) => b,
339 Err(_) => return -1,
340 };
341 let workers = caller.data().workers.clone();
342 let g = workers.lock().unwrap();
343 match g.as_ref() {
344 Some(s) if s.post(handle, data) => 0,
345 _ => -1,
346 }
347 },
348 )?;
349
350 linker.func_wrap(
354 "oxide",
355 "api_worker_recv",
356 |mut caller: Caller<'_, HostState>, handle: u32, out_ptr: u32, out_cap: u32| -> i64 {
357 let workers = caller.data().workers.clone();
358 let msg = {
359 let g = workers.lock().unwrap();
360 g.as_ref().and_then(|s| s.recv(handle))
361 };
362 let msg = match msg {
363 Some(m) => m,
364 None => return -1,
365 };
366 let mem = match caller.data().memory {
367 Some(m) => m,
368 None => return -1,
369 };
370 let to_write = if msg.len() > out_cap as usize {
371 &msg[..out_cap as usize]
372 } else {
373 &msg[..]
374 };
375 if write_guest_bytes(&mem, &mut caller, out_ptr, to_write).is_err() {
376 return -1;
377 }
378 to_write.len() as i64
379 },
380 )?;
381
382 linker.func_wrap(
386 "oxide",
387 "api_worker_terminate",
388 |caller: Caller<'_, HostState>, handle: u32| -> i32 {
389 let workers = caller.data().workers.clone();
390 let mut g = workers.lock().unwrap();
391 let terminated = g.as_mut().map(|s| s.terminate(handle)).unwrap_or(false);
392 i32::from(terminated)
393 },
394 )?;
395
396 linker.func_wrap(
400 "oxide",
401 "api_worker_post",
402 |caller: Caller<'_, HostState>, ptr: u32, len: u32| -> i32 {
403 let mem = match caller.data().memory {
404 Some(m) => m,
405 None => return -1,
406 };
407 let data = match read_guest_bytes(&mem, &caller, ptr, len) {
408 Ok(b) => b,
409 Err(_) => return -1,
410 };
411 match caller.data().worker_outbox.clone() {
412 Some(outbox) => {
413 outbox.lock().unwrap().push_back(data);
414 0
415 }
416 None => -1,
417 }
418 },
419 )?;
420
421 linker.func_wrap(
426 "oxide",
427 "api_worker_message_read",
428 |mut caller: Caller<'_, HostState>, out_ptr: u32, out_cap: u32| -> u32 {
429 let msg_arc = caller.data().worker_current_msg.clone();
430 let mem = match caller.data().memory {
431 Some(m) => m,
432 None => return 0,
433 };
434 let guard = msg_arc.lock().unwrap();
435 let bytes = match guard.as_ref() {
436 Some(b) => b,
437 None => return 0,
438 };
439 let to_write = if bytes.len() > out_cap as usize {
440 &bytes[..out_cap as usize]
441 } else {
442 &bytes[..]
443 };
444 if write_guest_bytes(&mem, &mut caller, out_ptr, to_write).is_err() {
445 return 0;
446 }
447 to_write.len() as u32
448 },
449 )?;
450
451 Ok(())
452}