1use std::collections::{HashMap, HashSet};
14use std::sync::{Arc, Mutex};
15use std::time::{Duration, Instant};
16
17use anyhow::{Context, Result};
18use image::GenericImageView;
19use wasmtime::*;
20
21use crate::bookmarks::SharedBookmarkStore;
22use crate::engine::ModuleLoader;
23use crate::navigation::NavigationStack;
24use crate::url as oxide_url;
25
26struct AudioChannel {
28 player: rodio::Player,
29 duration_ms: u64,
30 looping: bool,
31}
32
33pub struct AudioEngine {
39 _device_sink: rodio::stream::MixerDeviceSink,
40 channels: HashMap<u32, AudioChannel>,
41}
42
43impl AudioEngine {
44 fn try_new() -> Option<Self> {
45 let mut device_sink = rodio::DeviceSinkBuilder::open_default_sink().ok()?;
46 device_sink.log_on_drop(false);
47 Some(Self {
48 _device_sink: device_sink,
49 channels: HashMap::new(),
50 })
51 }
52
53 fn ensure_channel(&mut self, id: u32) -> &mut AudioChannel {
54 if !self.channels.contains_key(&id) {
55 let player = rodio::Player::connect_new(self._device_sink.mixer());
56 self.channels.insert(
57 id,
58 AudioChannel {
59 player,
60 duration_ms: 0,
61 looping: false,
62 },
63 );
64 }
65 self.channels.get_mut(&id).unwrap()
66 }
67
68 fn play_bytes_on(&mut self, channel_id: u32, data: Vec<u8>) -> bool {
69 use rodio::Source;
70
71 let cursor = std::io::Cursor::new(data);
72 let reader = std::io::BufReader::new(cursor);
73 let source = match rodio::Decoder::try_from(reader) {
74 Ok(s) => s,
75 Err(_) => return false,
76 };
77
78 let duration_ms = source
79 .total_duration()
80 .map(|d| d.as_millis() as u64)
81 .unwrap_or(0);
82
83 let ch = self.ensure_channel(channel_id);
84 ch.player.clear();
85 ch.duration_ms = duration_ms;
86
87 if ch.looping {
88 ch.player.append(source.repeat_infinite());
89 } else {
90 ch.player.append(source);
91 }
92 ch.player.play();
93 true
94 }
95}
96
97#[derive(Clone)]
104pub struct HostState {
105 pub console: Arc<Mutex<Vec<ConsoleEntry>>>,
107 pub canvas: Arc<Mutex<CanvasState>>,
109 pub storage: Arc<Mutex<HashMap<String, String>>>,
111 pub timers: Arc<Mutex<Vec<TimerEntry>>>,
113 pub timer_next_id: Arc<Mutex<u32>>,
115 pub clipboard: Arc<Mutex<String>>,
117 pub clipboard_allowed: Arc<Mutex<bool>>,
119 pub kv_db: Option<Arc<sled::Db>>,
121 pub memory: Option<Memory>,
123 pub module_loader: Option<Arc<ModuleLoader>>,
125 pub navigation: Arc<Mutex<NavigationStack>>,
127 pub hyperlinks: Arc<Mutex<Vec<Hyperlink>>>,
129 pub pending_navigation: Arc<Mutex<Option<String>>>,
131 pub current_url: Arc<Mutex<String>>,
133 pub input_state: Arc<Mutex<InputState>>,
135 pub widget_commands: Arc<Mutex<Vec<WidgetCommand>>>,
137 pub widget_states: Arc<Mutex<HashMap<u32, WidgetValue>>>,
139 pub widget_clicked: Arc<Mutex<HashSet<u32>>>,
141 pub canvas_offset: Arc<Mutex<(f32, f32)>>,
143 pub bookmark_store: SharedBookmarkStore,
145 pub audio: Arc<Mutex<Option<AudioEngine>>>,
147}
148
149#[derive(Clone, Debug)]
151pub struct ConsoleEntry {
152 pub timestamp: String,
154 pub level: ConsoleLevel,
156 pub message: String,
158}
159
160#[derive(Clone, Debug)]
162pub enum ConsoleLevel {
163 Log,
165 Warn,
167 Error,
169}
170
171#[derive(Clone, Debug)]
173pub struct CanvasState {
174 pub commands: Vec<DrawCommand>,
176 pub width: u32,
178 pub height: u32,
180 pub images: Vec<DecodedImage>,
182 pub generation: u64,
184}
185
186#[derive(Clone, Debug)]
188pub struct DecodedImage {
189 pub width: u32,
191 pub height: u32,
193 pub pixels: Vec<u8>,
195}
196
197#[derive(Clone, Debug)]
199pub enum DrawCommand {
200 Clear { r: u8, g: u8, b: u8, a: u8 },
202 Rect {
204 x: f32,
205 y: f32,
206 w: f32,
207 h: f32,
208 r: u8,
209 g: u8,
210 b: u8,
211 a: u8,
212 },
213 Circle {
215 cx: f32,
216 cy: f32,
217 radius: f32,
218 r: u8,
219 g: u8,
220 b: u8,
221 a: u8,
222 },
223 Text {
225 x: f32,
226 y: f32,
227 size: f32,
228 r: u8,
229 g: u8,
230 b: u8,
231 text: String,
232 },
233 Line {
235 x1: f32,
236 y1: f32,
237 x2: f32,
238 y2: f32,
239 r: u8,
240 g: u8,
241 b: u8,
242 thickness: f32,
243 },
244 Image {
246 x: f32,
247 y: f32,
248 w: f32,
249 h: f32,
250 image_id: usize,
251 },
252}
253
254#[derive(Clone, Debug)]
256pub struct TimerEntry {
257 pub id: u32,
259 pub fire_at: Instant,
261 pub interval: Option<Duration>,
263 pub callback_id: u32,
265}
266
267pub fn drain_expired_timers(timers: &Arc<Mutex<Vec<TimerEntry>>>) -> Vec<u32> {
274 let now = Instant::now();
275 let mut guard = timers.lock().unwrap();
276 let mut fired = Vec::new();
277 let mut i = 0;
278 while i < guard.len() {
279 if guard[i].fire_at <= now {
280 fired.push(guard[i].callback_id);
281 if let Some(interval) = guard[i].interval {
282 guard[i].fire_at = now + interval;
283 i += 1;
284 } else {
285 guard.swap_remove(i);
286 }
287 } else {
288 i += 1;
289 }
290 }
291 fired
292}
293
294#[derive(Clone, Debug)]
299pub struct Hyperlink {
300 pub x: f32,
302 pub y: f32,
304 pub w: f32,
306 pub h: f32,
308 pub url: String,
310}
311
312#[derive(Clone, Debug, Default)]
314pub struct InputState {
315 pub mouse_x: f32,
317 pub mouse_y: f32,
319 pub mouse_buttons_down: [bool; 3],
321 pub mouse_buttons_clicked: [bool; 3],
323 pub keys_down: Vec<u32>,
325 pub keys_pressed: Vec<u32>,
327 pub modifiers_shift: bool,
329 pub modifiers_ctrl: bool,
331 pub modifiers_alt: bool,
333 pub scroll_x: f32,
335 pub scroll_y: f32,
337}
338
339#[derive(Clone, Debug)]
343pub enum WidgetCommand {
344 Button {
346 id: u32,
347 x: f32,
348 y: f32,
349 w: f32,
350 h: f32,
351 label: String,
352 },
353 Checkbox {
355 id: u32,
356 x: f32,
357 y: f32,
358 label: String,
359 },
360 Slider {
362 id: u32,
363 x: f32,
364 y: f32,
365 w: f32,
366 min: f32,
367 max: f32,
368 },
369 TextInput { id: u32, x: f32, y: f32, w: f32 },
371}
372
373#[derive(Clone, Debug)]
375pub enum WidgetValue {
376 Bool(bool),
378 Float(f32),
380 Text(String),
382}
383
384impl Default for HostState {
385 fn default() -> Self {
386 Self {
387 console: Arc::new(Mutex::new(Vec::new())),
388 canvas: Arc::new(Mutex::new(CanvasState {
389 commands: Vec::new(),
390 width: 800,
391 height: 600,
392 images: Vec::new(),
393 generation: 0,
394 })),
395 storage: Arc::new(Mutex::new(HashMap::new())),
396 timers: Arc::new(Mutex::new(Vec::new())),
397 timer_next_id: Arc::new(Mutex::new(1)),
398 clipboard: Arc::new(Mutex::new(String::new())),
399 clipboard_allowed: Arc::new(Mutex::new(false)),
400 kv_db: None,
401 memory: None,
402 module_loader: None,
403 navigation: Arc::new(Mutex::new(NavigationStack::new())),
404 hyperlinks: Arc::new(Mutex::new(Vec::new())),
405 pending_navigation: Arc::new(Mutex::new(None)),
406 current_url: Arc::new(Mutex::new(String::new())),
407 input_state: Arc::new(Mutex::new(InputState::default())),
408 widget_commands: Arc::new(Mutex::new(Vec::new())),
409 widget_states: Arc::new(Mutex::new(HashMap::new())),
410 widget_clicked: Arc::new(Mutex::new(HashSet::new())),
411 canvas_offset: Arc::new(Mutex::new((0.0, 0.0))),
412 bookmark_store: crate::bookmarks::new_shared(),
413 audio: Arc::new(Mutex::new(None)),
414 }
415 }
416}
417
418fn read_guest_string(
419 memory: &Memory,
420 store: &impl AsContext,
421 ptr: u32,
422 len: u32,
423) -> Result<String> {
424 let start = ptr as usize;
425 let end = start
426 .checked_add(len as usize)
427 .context("guest string pointer arithmetic overflow")?;
428 let data = memory
429 .data(store)
430 .get(start..end)
431 .context("guest string out of bounds")?;
432 String::from_utf8(data.to_vec()).context("guest string is not valid utf-8")
433}
434
435fn read_guest_bytes(
436 memory: &Memory,
437 store: &impl AsContext,
438 ptr: u32,
439 len: u32,
440) -> Result<Vec<u8>> {
441 let start = ptr as usize;
442 let end = start
443 .checked_add(len as usize)
444 .context("guest buffer pointer arithmetic overflow")?;
445 let data = memory
446 .data(store)
447 .get(start..end)
448 .context("guest buffer out of bounds")?;
449 Ok(data.to_vec())
450}
451
452fn write_guest_bytes(
453 memory: &Memory,
454 store: &mut impl AsContextMut,
455 ptr: u32,
456 bytes: &[u8],
457) -> Result<()> {
458 let start = ptr as usize;
459 let end = start
460 .checked_add(bytes.len())
461 .context("guest write pointer arithmetic overflow")?;
462 memory
463 .data_mut(store)
464 .get_mut(start..end)
465 .context("guest buffer out of bounds")?
466 .copy_from_slice(bytes);
467 Ok(())
468}
469
470pub fn console_log(console: &Arc<Mutex<Vec<ConsoleEntry>>>, level: ConsoleLevel, message: String) {
474 console.lock().unwrap().push(ConsoleEntry {
475 timestamp: chrono::Local::now().format("%H:%M:%S%.3f").to_string(),
476 level,
477 message,
478 });
479}
480
481pub fn register_host_functions(linker: &mut Linker<HostState>) -> Result<()> {
492 linker.func_wrap(
495 "oxide",
496 "api_log",
497 |caller: Caller<'_, HostState>, ptr: u32, len: u32| {
498 let mem = caller.data().memory.expect("memory not set");
499 let msg = read_guest_string(&mem, &caller, ptr, len).unwrap_or_default();
500 console_log(&caller.data().console, ConsoleLevel::Log, msg);
501 },
502 )?;
503
504 linker.func_wrap(
505 "oxide",
506 "api_warn",
507 |caller: Caller<'_, HostState>, ptr: u32, len: u32| {
508 let mem = caller.data().memory.expect("memory not set");
509 let msg = read_guest_string(&mem, &caller, ptr, len).unwrap_or_default();
510 console_log(&caller.data().console, ConsoleLevel::Warn, msg);
511 },
512 )?;
513
514 linker.func_wrap(
515 "oxide",
516 "api_error",
517 |caller: Caller<'_, HostState>, ptr: u32, len: u32| {
518 let mem = caller.data().memory.expect("memory not set");
519 let msg = read_guest_string(&mem, &caller, ptr, len).unwrap_or_default();
520 console_log(&caller.data().console, ConsoleLevel::Error, msg);
521 },
522 )?;
523
524 linker.func_wrap(
527 "oxide",
528 "api_get_location",
529 |mut caller: Caller<'_, HostState>, out_ptr: u32, out_cap: u32| -> u32 {
530 let location = "37.7749,-122.4194"; let bytes = location.as_bytes();
532 let write_len = bytes.len().min(out_cap as usize);
533 let mem = caller.data().memory.expect("memory not set");
534 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
535 write_len as u32
536 },
537 )?;
538
539 linker.func_wrap(
542 "oxide",
543 "api_upload_file",
544 |mut caller: Caller<'_, HostState>,
545 name_ptr: u32,
546 name_cap: u32,
547 data_ptr: u32,
548 data_cap: u32|
549 -> u64 {
550 let dialog = rfd::FileDialog::new()
551 .set_title("Oxide: Select a file to upload")
552 .pick_file();
553
554 match dialog {
555 Some(path) => {
556 let file_name = path
557 .file_name()
558 .map(|n| n.to_string_lossy().to_string())
559 .unwrap_or_default();
560 let file_data = std::fs::read(&path).unwrap_or_default();
561
562 let mem = caller.data().memory.expect("memory not set");
563
564 let name_bytes = file_name.as_bytes();
565 let name_written = name_bytes.len().min(name_cap as usize);
566 write_guest_bytes(&mem, &mut caller, name_ptr, &name_bytes[..name_written])
567 .ok();
568
569 let data_written = file_data.len().min(data_cap as usize);
570 write_guest_bytes(&mem, &mut caller, data_ptr, &file_data[..data_written]).ok();
571
572 ((name_written as u64) << 32) | (data_written as u64)
573 }
574 None => 0,
575 }
576 },
577 )?;
578
579 linker.func_wrap(
582 "oxide",
583 "api_canvas_clear",
584 |caller: Caller<'_, HostState>, r: u32, g: u32, b: u32, a: u32| {
585 let mut canvas = caller.data().canvas.lock().unwrap();
586 canvas.commands.clear();
587 canvas.images.clear();
588 canvas.generation += 1;
589 canvas.commands.push(DrawCommand::Clear {
590 r: r as u8,
591 g: g as u8,
592 b: b as u8,
593 a: a as u8,
594 });
595 },
596 )?;
597
598 linker.func_wrap(
599 "oxide",
600 "api_canvas_rect",
601 |caller: Caller<'_, HostState>,
602 x: f32,
603 y: f32,
604 w: f32,
605 h: f32,
606 r: u32,
607 g: u32,
608 b: u32,
609 a: u32| {
610 caller
611 .data()
612 .canvas
613 .lock()
614 .unwrap()
615 .commands
616 .push(DrawCommand::Rect {
617 x,
618 y,
619 w,
620 h,
621 r: r as u8,
622 g: g as u8,
623 b: b as u8,
624 a: a as u8,
625 });
626 },
627 )?;
628
629 linker.func_wrap(
630 "oxide",
631 "api_canvas_circle",
632 |caller: Caller<'_, HostState>,
633 cx: f32,
634 cy: f32,
635 radius: f32,
636 r: u32,
637 g: u32,
638 b: u32,
639 a: u32| {
640 caller
641 .data()
642 .canvas
643 .lock()
644 .unwrap()
645 .commands
646 .push(DrawCommand::Circle {
647 cx,
648 cy,
649 radius,
650 r: r as u8,
651 g: g as u8,
652 b: b as u8,
653 a: a as u8,
654 });
655 },
656 )?;
657
658 linker.func_wrap(
659 "oxide",
660 "api_canvas_text",
661 |caller: Caller<'_, HostState>,
662 x: f32,
663 y: f32,
664 size: f32,
665 r: u32,
666 g: u32,
667 b: u32,
668 txt_ptr: u32,
669 txt_len: u32| {
670 let mem = caller.data().memory.expect("memory not set");
671 let text = read_guest_string(&mem, &caller, txt_ptr, txt_len).unwrap_or_default();
672 caller
673 .data()
674 .canvas
675 .lock()
676 .unwrap()
677 .commands
678 .push(DrawCommand::Text {
679 x,
680 y,
681 size,
682 r: r as u8,
683 g: g as u8,
684 b: b as u8,
685 text,
686 });
687 },
688 )?;
689
690 linker.func_wrap(
691 "oxide",
692 "api_canvas_line",
693 |caller: Caller<'_, HostState>,
694 x1: f32,
695 y1: f32,
696 x2: f32,
697 y2: f32,
698 r: u32,
699 g: u32,
700 b: u32,
701 thickness: f32| {
702 caller
703 .data()
704 .canvas
705 .lock()
706 .unwrap()
707 .commands
708 .push(DrawCommand::Line {
709 x1,
710 y1,
711 x2,
712 y2,
713 r: r as u8,
714 g: g as u8,
715 b: b as u8,
716 thickness,
717 });
718 },
719 )?;
720
721 linker.func_wrap(
722 "oxide",
723 "api_canvas_dimensions",
724 |caller: Caller<'_, HostState>| -> u64 {
725 let canvas = caller.data().canvas.lock().unwrap();
726 ((canvas.width as u64) << 32) | (canvas.height as u64)
727 },
728 )?;
729
730 linker.func_wrap(
733 "oxide",
734 "api_canvas_image",
735 |caller: Caller<'_, HostState>,
736 x: f32,
737 y: f32,
738 w: f32,
739 h: f32,
740 data_ptr: u32,
741 data_len: u32| {
742 let mem = caller.data().memory.expect("memory not set");
743 let raw = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
744 match image::load_from_memory(&raw) {
745 Ok(img) => {
746 let (iw, ih) = img.dimensions();
747 const MAX_IMAGE_PIXELS: u32 = 4096 * 4096; if iw.saturating_mul(ih) > MAX_IMAGE_PIXELS {
749 console_log(
750 &caller.data().console,
751 ConsoleLevel::Error,
752 format!(
753 "[IMAGE] Rejected: {iw}x{ih} exceeds maximum of {MAX_IMAGE_PIXELS} pixels"
754 ),
755 );
756 return;
757 }
758 let rgba = img.to_rgba8();
759 let (iw, ih) = (rgba.width(), rgba.height());
760 let decoded = DecodedImage {
761 width: iw,
762 height: ih,
763 pixels: rgba.into_raw(),
764 };
765 let mut canvas = caller.data().canvas.lock().unwrap();
766 let image_id = canvas.images.len();
767 canvas.images.push(decoded);
768 canvas.commands.push(DrawCommand::Image {
769 x,
770 y,
771 w,
772 h,
773 image_id,
774 });
775 }
776 Err(e) => {
777 console_log(
778 &caller.data().console,
779 ConsoleLevel::Error,
780 format!("[IMAGE] Failed to decode: {e}"),
781 );
782 }
783 }
784 },
785 )?;
786
787 linker.func_wrap(
790 "oxide",
791 "api_storage_set",
792 |caller: Caller<'_, HostState>, key_ptr: u32, key_len: u32, val_ptr: u32, val_len: u32| {
793 let mem = caller.data().memory.expect("memory not set");
794 let key = read_guest_string(&mem, &caller, key_ptr, key_len).unwrap_or_default();
795 let val = read_guest_string(&mem, &caller, val_ptr, val_len).unwrap_or_default();
796 caller.data().storage.lock().unwrap().insert(key, val);
797 },
798 )?;
799
800 linker.func_wrap(
801 "oxide",
802 "api_storage_get",
803 |mut caller: Caller<'_, HostState>,
804 key_ptr: u32,
805 key_len: u32,
806 out_ptr: u32,
807 out_cap: u32|
808 -> u32 {
809 let mem = caller.data().memory.expect("memory not set");
810 let key = read_guest_string(&mem, &caller, key_ptr, key_len).unwrap_or_default();
811 let val = caller
812 .data()
813 .storage
814 .lock()
815 .unwrap()
816 .get(&key)
817 .cloned()
818 .unwrap_or_default();
819 let bytes = val.as_bytes();
820 let write_len = bytes.len().min(out_cap as usize);
821 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
822 write_len as u32
823 },
824 )?;
825
826 linker.func_wrap(
827 "oxide",
828 "api_storage_remove",
829 |caller: Caller<'_, HostState>, key_ptr: u32, key_len: u32| {
830 let mem = caller.data().memory.expect("memory not set");
831 let key = read_guest_string(&mem, &caller, key_ptr, key_len).unwrap_or_default();
832 caller.data().storage.lock().unwrap().remove(&key);
833 },
834 )?;
835
836 linker.func_wrap(
839 "oxide",
840 "api_clipboard_write",
841 |caller: Caller<'_, HostState>, ptr: u32, len: u32| {
842 let allowed = *caller.data().clipboard_allowed.lock().unwrap();
843 if !allowed {
844 console_log(
845 &caller.data().console,
846 ConsoleLevel::Warn,
847 "[CLIPBOARD] Write blocked — clipboard access not permitted".into(),
848 );
849 return;
850 }
851 let mem = caller.data().memory.expect("memory not set");
852 let text = read_guest_string(&mem, &caller, ptr, len).unwrap_or_default();
853 *caller.data().clipboard.lock().unwrap() = text.clone();
854 if let Ok(mut ctx) = arboard::Clipboard::new() {
855 let _ = ctx.set_text(text);
856 }
857 },
858 )?;
859
860 linker.func_wrap(
861 "oxide",
862 "api_clipboard_read",
863 |mut caller: Caller<'_, HostState>, out_ptr: u32, out_cap: u32| -> u32 {
864 let allowed = *caller.data().clipboard_allowed.lock().unwrap();
865 if !allowed {
866 console_log(
867 &caller.data().console,
868 ConsoleLevel::Warn,
869 "[CLIPBOARD] Read blocked — clipboard access not permitted".into(),
870 );
871 return 0;
872 }
873 let text = arboard::Clipboard::new()
874 .and_then(|mut ctx| ctx.get_text())
875 .unwrap_or_default();
876 let bytes = text.as_bytes();
877 let write_len = bytes.len().min(out_cap as usize);
878 let mem = caller.data().memory.expect("memory not set");
879 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
880 write_len as u32
881 },
882 )?;
883
884 linker.func_wrap(
887 "oxide",
888 "api_time_now_ms",
889 |_caller: Caller<'_, HostState>| -> u64 {
890 std::time::SystemTime::now()
891 .duration_since(std::time::UNIX_EPOCH)
892 .unwrap_or_default()
893 .as_millis() as u64
894 },
895 )?;
896
897 linker.func_wrap(
902 "oxide",
903 "api_set_timeout",
904 |caller: Caller<'_, HostState>, callback_id: u32, delay_ms: u32| -> u32 {
905 let mut next = caller.data().timer_next_id.lock().unwrap();
906 let id = *next;
907 *next = next.wrapping_add(1).max(1);
908 drop(next);
909
910 let entry = TimerEntry {
911 id,
912 fire_at: Instant::now() + Duration::from_millis(delay_ms as u64),
913 interval: None,
914 callback_id,
915 };
916 caller.data().timers.lock().unwrap().push(entry);
917 id
918 },
919 )?;
920
921 linker.func_wrap(
922 "oxide",
923 "api_set_interval",
924 |caller: Caller<'_, HostState>, callback_id: u32, interval_ms: u32| -> u32 {
925 let mut next = caller.data().timer_next_id.lock().unwrap();
926 let id = *next;
927 *next = next.wrapping_add(1).max(1);
928 drop(next);
929
930 let interval = Duration::from_millis(interval_ms as u64);
931 let entry = TimerEntry {
932 id,
933 fire_at: Instant::now() + interval,
934 interval: Some(interval),
935 callback_id,
936 };
937 caller.data().timers.lock().unwrap().push(entry);
938 id
939 },
940 )?;
941
942 linker.func_wrap(
943 "oxide",
944 "api_clear_timer",
945 |caller: Caller<'_, HostState>, timer_id: u32| {
946 caller
947 .data()
948 .timers
949 .lock()
950 .unwrap()
951 .retain(|t| t.id != timer_id);
952 },
953 )?;
954
955 linker.func_wrap(
958 "oxide",
959 "api_random",
960 |_caller: Caller<'_, HostState>| -> u64 {
961 let mut buf = [0u8; 8];
962 getrandom(&mut buf);
963 u64::from_le_bytes(buf)
964 },
965 )?;
966
967 linker.func_wrap(
970 "oxide",
971 "api_notify",
972 |caller: Caller<'_, HostState>,
973 title_ptr: u32,
974 title_len: u32,
975 body_ptr: u32,
976 body_len: u32| {
977 let mem = caller.data().memory.expect("memory not set");
978 let title = read_guest_string(&mem, &caller, title_ptr, title_len).unwrap_or_default();
979 let body = read_guest_string(&mem, &caller, body_ptr, body_len).unwrap_or_default();
980 console_log(
981 &caller.data().console,
982 ConsoleLevel::Log,
983 format!("[NOTIFICATION] {title}: {body}"),
984 );
985 },
986 )?;
987
988 linker.func_wrap(
994 "oxide",
995 "api_fetch",
996 |mut caller: Caller<'_, HostState>,
997 method_ptr: u32,
998 method_len: u32,
999 url_ptr: u32,
1000 url_len: u32,
1001 ct_ptr: u32,
1002 ct_len: u32,
1003 body_ptr: u32,
1004 body_len: u32,
1005 out_ptr: u32,
1006 out_cap: u32|
1007 -> i64 {
1008 let mem = caller.data().memory.expect("memory not set");
1009 let method =
1010 read_guest_string(&mem, &caller, method_ptr, method_len).unwrap_or_default();
1011 let url = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
1012 let content_type = read_guest_string(&mem, &caller, ct_ptr, ct_len).unwrap_or_default();
1013 let body = if body_len > 0 {
1014 read_guest_bytes(&mem, &caller, body_ptr, body_len).unwrap_or_default()
1015 } else {
1016 Vec::new()
1017 };
1018
1019 console_log(
1020 &caller.data().console,
1021 ConsoleLevel::Log,
1022 format!("[FETCH] {method} {url}"),
1023 );
1024
1025 let (resp_tx, resp_rx) =
1026 std::sync::mpsc::sync_channel::<Result<(u16, Vec<u8>), String>>(1);
1027
1028 std::thread::spawn(move || {
1029 let result = (|| -> Result<(u16, Vec<u8>), String> {
1030 let client = reqwest::blocking::Client::builder()
1031 .timeout(Duration::from_secs(30))
1032 .build()
1033 .map_err(|e| e.to_string())?;
1034 let parsed: reqwest::Method = method.parse().unwrap_or(reqwest::Method::GET);
1035 let mut req = client.request(parsed, &url);
1036 if !content_type.is_empty() {
1037 req = req.header("Content-Type", &content_type);
1038 }
1039 if !body.is_empty() {
1040 req = req.body(body);
1041 }
1042 let resp = req.send().map_err(|e| e.to_string())?;
1043 let status = resp.status().as_u16();
1044 let bytes = resp.bytes().map_err(|e| e.to_string())?.to_vec();
1045 Ok((status, bytes))
1046 })();
1047 let _ = resp_tx.send(result);
1048 });
1049
1050 match resp_rx.recv() {
1051 Ok(Ok((status, response_body))) => {
1052 let write_len = response_body.len().min(out_cap as usize);
1053 write_guest_bytes(&mem, &mut caller, out_ptr, &response_body[..write_len]).ok();
1054 ((status as i64) << 32) | (write_len as i64)
1055 }
1056 Ok(Err(e)) => {
1057 console_log(
1058 &caller.data().console,
1059 ConsoleLevel::Error,
1060 format!("[FETCH ERROR] {e}"),
1061 );
1062 -1
1063 }
1064 Err(_) => -1,
1065 }
1066 },
1067 )?;
1068
1069 linker.func_wrap(
1076 "oxide",
1077 "api_load_module",
1078 |caller: Caller<'_, HostState>, url_ptr: u32, url_len: u32| -> i32 {
1079 let mem = caller.data().memory.expect("memory not set");
1080 let url = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
1081 let loader = match &caller.data().module_loader {
1082 Some(l) => l.clone(),
1083 None => return -1,
1084 };
1085 let mut child_state = caller.data().clone();
1086 child_state.memory = None;
1087 let console = caller.data().console.clone();
1088
1089 console_log(
1090 &console,
1091 ConsoleLevel::Log,
1092 format!("[LOAD] Fetching module: {url}"),
1093 );
1094
1095 let (tx, rx) = std::sync::mpsc::sync_channel::<Result<Vec<u8>, String>>(1);
1096 let fetch_url = url.clone();
1097 std::thread::spawn(move || {
1098 let result = (|| -> Result<Vec<u8>, String> {
1099 let client = reqwest::blocking::Client::builder()
1100 .timeout(Duration::from_secs(30))
1101 .build()
1102 .map_err(|e| e.to_string())?;
1103 let resp = client
1104 .get(&fetch_url)
1105 .header("Accept", "application/wasm")
1106 .send()
1107 .map_err(|e| e.to_string())?;
1108 if !resp.status().is_success() {
1109 return Err(format!("HTTP {}", resp.status()));
1110 }
1111 resp.bytes().map(|b| b.to_vec()).map_err(|e| e.to_string())
1112 })();
1113 let _ = tx.send(result);
1114 });
1115
1116 let wasm_bytes = match rx.recv() {
1117 Ok(Ok(bytes)) => bytes,
1118 Ok(Err(e)) => {
1119 console_log(&console, ConsoleLevel::Error, format!("[LOAD ERROR] {e}"));
1120 return -1;
1121 }
1122 Err(_) => return -1,
1123 };
1124
1125 let module = match Module::new(&loader.engine, &wasm_bytes) {
1126 Ok(m) => m,
1127 Err(e) => {
1128 console_log(
1129 &console,
1130 ConsoleLevel::Error,
1131 format!("[LOAD ERROR] Compile: {e}"),
1132 );
1133 return -2;
1134 }
1135 };
1136
1137 let mut store = Store::new(&loader.engine, child_state);
1138 if store.set_fuel(loader.fuel_limit).is_err() {
1139 return -3;
1140 }
1141
1142 let mut child_linker = Linker::new(&loader.engine);
1143 if register_host_functions(&mut child_linker).is_err() {
1144 return -3;
1145 }
1146
1147 let mem_type = MemoryType::new(1, Some(loader.max_memory_pages));
1148 let memory = match Memory::new(&mut store, mem_type) {
1149 Ok(m) => m,
1150 Err(_) => return -4,
1151 };
1152
1153 if child_linker
1154 .define(&store, "oxide", "memory", memory)
1155 .is_err()
1156 {
1157 return -5;
1158 }
1159 store.data_mut().memory = Some(memory);
1160
1161 let instance = match child_linker.instantiate(&mut store, &module) {
1162 Ok(i) => i,
1163 Err(e) => {
1164 console_log(
1165 &console,
1166 ConsoleLevel::Error,
1167 format!("[LOAD ERROR] Instantiate: {e}"),
1168 );
1169 return -6;
1170 }
1171 };
1172
1173 if let Some(guest_mem) = instance.get_memory(&mut store, "memory") {
1175 store.data_mut().memory = Some(guest_mem);
1176 }
1177
1178 let start_fn = match instance.get_typed_func::<(), ()>(&mut store, "start_app") {
1179 Ok(f) => f,
1180 Err(_) => {
1181 console_log(
1182 &console,
1183 ConsoleLevel::Error,
1184 "[LOAD ERROR] Module missing start_app".into(),
1185 );
1186 return -7;
1187 }
1188 };
1189
1190 match start_fn.call(&mut store, ()) {
1191 Ok(()) => {
1192 console_log(
1193 &console,
1194 ConsoleLevel::Log,
1195 format!("[LOAD] Module {url} executed successfully"),
1196 );
1197 0
1198 }
1199 Err(e) => {
1200 let msg = if e.to_string().contains("fuel") {
1201 "[LOAD ERROR] Child module fuel limit exceeded".to_string()
1202 } else {
1203 format!("[LOAD ERROR] Runtime: {e}")
1204 };
1205 console_log(&console, ConsoleLevel::Error, msg);
1206 -8
1207 }
1208 }
1209 },
1210 )?;
1211
1212 linker.func_wrap(
1215 "oxide",
1216 "api_hash_sha256",
1217 |mut caller: Caller<'_, HostState>, data_ptr: u32, data_len: u32, out_ptr: u32| -> u32 {
1218 use sha2::{Digest, Sha256};
1219 let mem = caller.data().memory.expect("memory not set");
1220 let data = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
1221 let hash = Sha256::digest(&data);
1222 write_guest_bytes(&mem, &mut caller, out_ptr, &hash).ok();
1223 hash.len() as u32
1224 },
1225 )?;
1226
1227 linker.func_wrap(
1230 "oxide",
1231 "api_base64_encode",
1232 |mut caller: Caller<'_, HostState>,
1233 data_ptr: u32,
1234 data_len: u32,
1235 out_ptr: u32,
1236 out_cap: u32|
1237 -> u32 {
1238 use base64::Engine;
1239 let mem = caller.data().memory.expect("memory not set");
1240 let data = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
1241 let encoded = base64::engine::general_purpose::STANDARD.encode(&data);
1242 let bytes = encoded.as_bytes();
1243 let write_len = bytes.len().min(out_cap as usize);
1244 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
1245 write_len as u32
1246 },
1247 )?;
1248
1249 linker.func_wrap(
1250 "oxide",
1251 "api_base64_decode",
1252 |mut caller: Caller<'_, HostState>,
1253 data_ptr: u32,
1254 data_len: u32,
1255 out_ptr: u32,
1256 out_cap: u32|
1257 -> u32 {
1258 use base64::Engine;
1259 let mem = caller.data().memory.expect("memory not set");
1260 let encoded = read_guest_string(&mem, &caller, data_ptr, data_len).unwrap_or_default();
1261 match base64::engine::general_purpose::STANDARD.decode(&encoded) {
1262 Ok(decoded) => {
1263 let write_len = decoded.len().min(out_cap as usize);
1264 write_guest_bytes(&mem, &mut caller, out_ptr, &decoded[..write_len]).ok();
1265 write_len as u32
1266 }
1267 Err(_) => 0,
1268 }
1269 },
1270 )?;
1271
1272 linker.func_wrap(
1277 "oxide",
1278 "api_kv_store_set",
1279 |caller: Caller<'_, HostState>,
1280 key_ptr: u32,
1281 key_len: u32,
1282 val_ptr: u32,
1283 val_len: u32|
1284 -> i32 {
1285 let mem = caller.data().memory.expect("memory not set");
1286 let key = read_guest_string(&mem, &caller, key_ptr, key_len).unwrap_or_default();
1287 let val = read_guest_bytes(&mem, &caller, val_ptr, val_len).unwrap_or_default();
1288 let origin = caller.data().current_url.lock().unwrap().clone();
1289 let prefixed_key = format!("{origin}::{key}");
1290 match &caller.data().kv_db {
1291 Some(db) => match db.insert(prefixed_key.as_bytes(), val) {
1292 Ok(_) => {
1293 let _ = db.flush();
1294 0
1295 }
1296 Err(e) => {
1297 console_log(
1298 &caller.data().console,
1299 ConsoleLevel::Error,
1300 format!("[KV] set failed: {e}"),
1301 );
1302 -1
1303 }
1304 },
1305 None => {
1306 console_log(
1307 &caller.data().console,
1308 ConsoleLevel::Error,
1309 "[KV] store not initialised".into(),
1310 );
1311 -1
1312 }
1313 }
1314 },
1315 )?;
1316
1317 linker.func_wrap(
1318 "oxide",
1319 "api_kv_store_get",
1320 |mut caller: Caller<'_, HostState>,
1321 key_ptr: u32,
1322 key_len: u32,
1323 out_ptr: u32,
1324 out_cap: u32|
1325 -> i32 {
1326 let mem = caller.data().memory.expect("memory not set");
1327 let key = read_guest_string(&mem, &caller, key_ptr, key_len).unwrap_or_default();
1328 let origin = caller.data().current_url.lock().unwrap().clone();
1329 let prefixed_key = format!("{origin}::{key}");
1330 match &caller.data().kv_db {
1331 Some(db) => match db.get(prefixed_key.as_bytes()) {
1332 Ok(Some(val)) => {
1333 let bytes = val.as_ref();
1334 let write_len = bytes.len().min(out_cap as usize);
1335 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
1336 write_len as i32
1337 }
1338 Ok(None) => -1,
1339 Err(e) => {
1340 console_log(
1341 &caller.data().console,
1342 ConsoleLevel::Error,
1343 format!("[KV] get failed: {e}"),
1344 );
1345 -2
1346 }
1347 },
1348 None => -2,
1349 }
1350 },
1351 )?;
1352
1353 linker.func_wrap(
1354 "oxide",
1355 "api_kv_store_delete",
1356 |caller: Caller<'_, HostState>, key_ptr: u32, key_len: u32| -> i32 {
1357 let mem = caller.data().memory.expect("memory not set");
1358 let key = read_guest_string(&mem, &caller, key_ptr, key_len).unwrap_or_default();
1359 let origin = caller.data().current_url.lock().unwrap().clone();
1360 let prefixed_key = format!("{origin}::{key}");
1361 match &caller.data().kv_db {
1362 Some(db) => match db.remove(prefixed_key.as_bytes()) {
1363 Ok(_) => {
1364 let _ = db.flush();
1365 0
1366 }
1367 Err(e) => {
1368 console_log(
1369 &caller.data().console,
1370 ConsoleLevel::Error,
1371 format!("[KV] delete failed: {e}"),
1372 );
1373 -1
1374 }
1375 },
1376 None => -1,
1377 }
1378 },
1379 )?;
1380
1381 linker.func_wrap(
1384 "oxide",
1385 "api_navigate",
1386 |caller: Caller<'_, HostState>, url_ptr: u32, url_len: u32| -> i32 {
1387 let mem = caller.data().memory.expect("memory not set");
1388 let raw_url = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
1389
1390 let resolved = {
1391 let cur = caller.data().current_url.lock().unwrap();
1392 if cur.is_empty() {
1393 raw_url.clone()
1394 } else if let Ok(base) = oxide_url::OxideUrl::parse(&cur) {
1395 base.join(&raw_url)
1396 .map(|u| u.as_str().to_string())
1397 .unwrap_or(raw_url.clone())
1398 } else {
1399 raw_url.clone()
1400 }
1401 };
1402
1403 if oxide_url::OxideUrl::parse(&resolved).is_err() {
1404 console_log(
1405 &caller.data().console,
1406 ConsoleLevel::Error,
1407 format!("[NAV] invalid URL: {resolved}"),
1408 );
1409 return -1;
1410 }
1411
1412 console_log(
1413 &caller.data().console,
1414 ConsoleLevel::Log,
1415 format!("[NAV] navigate → {resolved}"),
1416 );
1417 *caller.data().pending_navigation.lock().unwrap() = Some(resolved);
1418 0
1419 },
1420 )?;
1421
1422 linker.func_wrap(
1423 "oxide",
1424 "api_push_state",
1425 |caller: Caller<'_, HostState>,
1426 state_ptr: u32,
1427 state_len: u32,
1428 title_ptr: u32,
1429 title_len: u32,
1430 url_ptr: u32,
1431 url_len: u32| {
1432 let mem = caller.data().memory.expect("memory not set");
1433 let state = read_guest_bytes(&mem, &caller, state_ptr, state_len).unwrap_or_default();
1434 let title = read_guest_string(&mem, &caller, title_ptr, title_len).unwrap_or_default();
1435 let url_arg = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
1436
1437 let resolved_url = if url_arg.is_empty() {
1438 caller.data().current_url.lock().unwrap().clone()
1439 } else {
1440 let cur = caller.data().current_url.lock().unwrap();
1441 if cur.is_empty() {
1442 url_arg
1443 } else if let Ok(base) = oxide_url::OxideUrl::parse(&cur) {
1444 base.join(&url_arg)
1445 .map(|u| u.as_str().to_string())
1446 .unwrap_or(url_arg)
1447 } else {
1448 url_arg
1449 }
1450 };
1451
1452 let entry = crate::navigation::HistoryEntry::new(&resolved_url)
1453 .with_title(title)
1454 .with_state(state);
1455 caller.data().navigation.lock().unwrap().push(entry);
1456 *caller.data().current_url.lock().unwrap() = resolved_url;
1457 },
1458 )?;
1459
1460 linker.func_wrap(
1461 "oxide",
1462 "api_replace_state",
1463 |caller: Caller<'_, HostState>,
1464 state_ptr: u32,
1465 state_len: u32,
1466 title_ptr: u32,
1467 title_len: u32,
1468 url_ptr: u32,
1469 url_len: u32| {
1470 let mem = caller.data().memory.expect("memory not set");
1471 let state = read_guest_bytes(&mem, &caller, state_ptr, state_len).unwrap_or_default();
1472 let title = read_guest_string(&mem, &caller, title_ptr, title_len).unwrap_or_default();
1473 let url_arg = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
1474
1475 let resolved_url = if url_arg.is_empty() {
1476 caller.data().current_url.lock().unwrap().clone()
1477 } else {
1478 let cur = caller.data().current_url.lock().unwrap();
1479 if cur.is_empty() {
1480 url_arg
1481 } else if let Ok(base) = oxide_url::OxideUrl::parse(&cur) {
1482 base.join(&url_arg)
1483 .map(|u| u.as_str().to_string())
1484 .unwrap_or(url_arg)
1485 } else {
1486 url_arg
1487 }
1488 };
1489
1490 let entry = crate::navigation::HistoryEntry::new(&resolved_url)
1491 .with_title(title)
1492 .with_state(state);
1493 caller
1494 .data()
1495 .navigation
1496 .lock()
1497 .unwrap()
1498 .replace_current(entry);
1499 *caller.data().current_url.lock().unwrap() = resolved_url;
1500 },
1501 )?;
1502
1503 linker.func_wrap(
1504 "oxide",
1505 "api_get_url",
1506 |mut caller: Caller<'_, HostState>, out_ptr: u32, out_cap: u32| -> u32 {
1507 let url = caller.data().current_url.lock().unwrap().clone();
1508 let bytes = url.as_bytes();
1509 let write_len = bytes.len().min(out_cap as usize);
1510 let mem = caller.data().memory.expect("memory not set");
1511 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
1512 write_len as u32
1513 },
1514 )?;
1515
1516 linker.func_wrap(
1517 "oxide",
1518 "api_get_state",
1519 |mut caller: Caller<'_, HostState>, out_ptr: u32, out_cap: u32| -> i32 {
1520 let state_bytes = {
1521 let nav = caller.data().navigation.lock().unwrap();
1522 match nav.current() {
1523 Some(entry) if !entry.state.is_empty() => Some(entry.state.clone()),
1524 _ => None,
1525 }
1526 };
1527 match state_bytes {
1528 Some(bytes) => {
1529 let write_len = bytes.len().min(out_cap as usize);
1530 let mem = caller.data().memory.expect("memory not set");
1531 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
1532 write_len as i32
1533 }
1534 None => -1,
1535 }
1536 },
1537 )?;
1538
1539 linker.func_wrap(
1540 "oxide",
1541 "api_history_length",
1542 |caller: Caller<'_, HostState>| -> u32 {
1543 caller.data().navigation.lock().unwrap().len() as u32
1544 },
1545 )?;
1546
1547 linker.func_wrap(
1548 "oxide",
1549 "api_history_back",
1550 |caller: Caller<'_, HostState>| -> i32 {
1551 let mut nav = caller.data().navigation.lock().unwrap();
1552 match nav.go_back() {
1553 Some(entry) => {
1554 let url = entry.url.clone();
1555 *caller.data().current_url.lock().unwrap() = url.clone();
1556 *caller.data().pending_navigation.lock().unwrap() = Some(url);
1557 1
1558 }
1559 None => 0,
1560 }
1561 },
1562 )?;
1563
1564 linker.func_wrap(
1565 "oxide",
1566 "api_history_forward",
1567 |caller: Caller<'_, HostState>| -> i32 {
1568 let mut nav = caller.data().navigation.lock().unwrap();
1569 match nav.go_forward() {
1570 Some(entry) => {
1571 let url = entry.url.clone();
1572 *caller.data().current_url.lock().unwrap() = url.clone();
1573 *caller.data().pending_navigation.lock().unwrap() = Some(url);
1574 1
1575 }
1576 None => 0,
1577 }
1578 },
1579 )?;
1580
1581 linker.func_wrap(
1584 "oxide",
1585 "api_register_hyperlink",
1586 |caller: Caller<'_, HostState>,
1587 x: f32,
1588 y: f32,
1589 w: f32,
1590 h: f32,
1591 url_ptr: u32,
1592 url_len: u32|
1593 -> i32 {
1594 let mem = caller.data().memory.expect("memory not set");
1595 let raw_url = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
1596
1597 let resolved = {
1598 let cur = caller.data().current_url.lock().unwrap();
1599 if cur.is_empty() {
1600 raw_url.clone()
1601 } else if let Ok(base) = oxide_url::OxideUrl::parse(&cur) {
1602 base.join(&raw_url)
1603 .map(|u| u.as_str().to_string())
1604 .unwrap_or(raw_url.clone())
1605 } else {
1606 raw_url.clone()
1607 }
1608 };
1609
1610 caller.data().hyperlinks.lock().unwrap().push(Hyperlink {
1611 x,
1612 y,
1613 w,
1614 h,
1615 url: resolved,
1616 });
1617 0
1618 },
1619 )?;
1620
1621 linker.func_wrap(
1622 "oxide",
1623 "api_clear_hyperlinks",
1624 |caller: Caller<'_, HostState>| {
1625 caller.data().hyperlinks.lock().unwrap().clear();
1626 },
1627 )?;
1628
1629 linker.func_wrap(
1632 "oxide",
1633 "api_url_resolve",
1634 |mut caller: Caller<'_, HostState>,
1635 base_ptr: u32,
1636 base_len: u32,
1637 rel_ptr: u32,
1638 rel_len: u32,
1639 out_ptr: u32,
1640 out_cap: u32|
1641 -> i32 {
1642 let mem = caller.data().memory.expect("memory not set");
1643 let base_str = read_guest_string(&mem, &caller, base_ptr, base_len).unwrap_or_default();
1644 let rel_str = read_guest_string(&mem, &caller, rel_ptr, rel_len).unwrap_or_default();
1645
1646 let base = match oxide_url::OxideUrl::parse(&base_str) {
1647 Ok(u) => u,
1648 Err(_) => return -1,
1649 };
1650 let resolved = match base.join(&rel_str) {
1651 Ok(u) => u,
1652 Err(_) => return -2,
1653 };
1654
1655 let bytes = resolved.as_str().as_bytes();
1656 let write_len = bytes.len().min(out_cap as usize);
1657 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
1658 write_len as i32
1659 },
1660 )?;
1661
1662 linker.func_wrap(
1663 "oxide",
1664 "api_url_encode",
1665 |mut caller: Caller<'_, HostState>,
1666 input_ptr: u32,
1667 input_len: u32,
1668 out_ptr: u32,
1669 out_cap: u32|
1670 -> u32 {
1671 let mem = caller.data().memory.expect("memory not set");
1672 let input = read_guest_string(&mem, &caller, input_ptr, input_len).unwrap_or_default();
1673 let encoded = oxide_url::percent_encode(&input);
1674 let bytes = encoded.as_bytes();
1675 let write_len = bytes.len().min(out_cap as usize);
1676 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
1677 write_len as u32
1678 },
1679 )?;
1680
1681 linker.func_wrap(
1682 "oxide",
1683 "api_url_decode",
1684 |mut caller: Caller<'_, HostState>,
1685 input_ptr: u32,
1686 input_len: u32,
1687 out_ptr: u32,
1688 out_cap: u32|
1689 -> u32 {
1690 let mem = caller.data().memory.expect("memory not set");
1691 let input = read_guest_string(&mem, &caller, input_ptr, input_len).unwrap_or_default();
1692 let decoded = oxide_url::percent_decode(&input);
1693 let bytes = decoded.as_bytes();
1694 let write_len = bytes.len().min(out_cap as usize);
1695 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
1696 write_len as u32
1697 },
1698 )?;
1699
1700 linker.func_wrap(
1703 "oxide",
1704 "api_mouse_position",
1705 |caller: Caller<'_, HostState>| -> u64 {
1706 let input = caller.data().input_state.lock().unwrap();
1707 let offset = caller.data().canvas_offset.lock().unwrap();
1708 let x = input.mouse_x - offset.0;
1709 let y = input.mouse_y - offset.1;
1710 ((x.to_bits() as u64) << 32) | (y.to_bits() as u64)
1711 },
1712 )?;
1713
1714 linker.func_wrap(
1715 "oxide",
1716 "api_mouse_button_down",
1717 |caller: Caller<'_, HostState>, button: u32| -> u32 {
1718 let input = caller.data().input_state.lock().unwrap();
1719 if (button as usize) < 3 && input.mouse_buttons_down[button as usize] {
1720 1
1721 } else {
1722 0
1723 }
1724 },
1725 )?;
1726
1727 linker.func_wrap(
1728 "oxide",
1729 "api_mouse_button_clicked",
1730 |caller: Caller<'_, HostState>, button: u32| -> u32 {
1731 let input = caller.data().input_state.lock().unwrap();
1732 if (button as usize) < 3 && input.mouse_buttons_clicked[button as usize] {
1733 1
1734 } else {
1735 0
1736 }
1737 },
1738 )?;
1739
1740 linker.func_wrap(
1741 "oxide",
1742 "api_key_down",
1743 |caller: Caller<'_, HostState>, key: u32| -> u32 {
1744 let input = caller.data().input_state.lock().unwrap();
1745 if input.keys_down.contains(&key) {
1746 1
1747 } else {
1748 0
1749 }
1750 },
1751 )?;
1752
1753 linker.func_wrap(
1754 "oxide",
1755 "api_key_pressed",
1756 |caller: Caller<'_, HostState>, key: u32| -> u32 {
1757 let input = caller.data().input_state.lock().unwrap();
1758 if input.keys_pressed.contains(&key) {
1759 1
1760 } else {
1761 0
1762 }
1763 },
1764 )?;
1765
1766 linker.func_wrap(
1767 "oxide",
1768 "api_scroll_delta",
1769 |caller: Caller<'_, HostState>| -> u64 {
1770 let input = caller.data().input_state.lock().unwrap();
1771 ((input.scroll_x.to_bits() as u64) << 32) | (input.scroll_y.to_bits() as u64)
1772 },
1773 )?;
1774
1775 linker.func_wrap(
1776 "oxide",
1777 "api_modifiers",
1778 |caller: Caller<'_, HostState>| -> u32 {
1779 let input = caller.data().input_state.lock().unwrap();
1780 let mut flags = 0u32;
1781 if input.modifiers_shift {
1782 flags |= 1;
1783 }
1784 if input.modifiers_ctrl {
1785 flags |= 2;
1786 }
1787 if input.modifiers_alt {
1788 flags |= 4;
1789 }
1790 flags
1791 },
1792 )?;
1793
1794 linker.func_wrap(
1800 "oxide",
1801 "api_audio_play",
1802 |caller: Caller<'_, HostState>, data_ptr: u32, data_len: u32| -> i32 {
1803 let mem = caller.data().memory.expect("memory not set");
1804 let data = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
1805 if data.is_empty() {
1806 return -1;
1807 }
1808
1809 let audio = caller.data().audio.clone();
1810 let mut guard = audio.lock().unwrap();
1811 if guard.is_none() {
1812 *guard = AudioEngine::try_new();
1813 }
1814 match guard.as_mut() {
1815 Some(engine) => {
1816 if engine.play_bytes_on(0, data) {
1817 console_log(
1818 &caller.data().console,
1819 ConsoleLevel::Log,
1820 "[AUDIO] Playing from bytes".into(),
1821 );
1822 0
1823 } else {
1824 console_log(
1825 &caller.data().console,
1826 ConsoleLevel::Error,
1827 "[AUDIO] Failed to decode audio data".into(),
1828 );
1829 -2
1830 }
1831 }
1832 None => {
1833 console_log(
1834 &caller.data().console,
1835 ConsoleLevel::Error,
1836 "[AUDIO] No audio device available".into(),
1837 );
1838 -3
1839 }
1840 }
1841 },
1842 )?;
1843
1844 linker.func_wrap(
1845 "oxide",
1846 "api_audio_play_url",
1847 |caller: Caller<'_, HostState>, url_ptr: u32, url_len: u32| -> i32 {
1848 let mem = caller.data().memory.expect("memory not set");
1849 let url = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
1850
1851 console_log(
1852 &caller.data().console,
1853 ConsoleLevel::Log,
1854 format!("[AUDIO] Fetching {url}"),
1855 );
1856
1857 let (tx, rx) = std::sync::mpsc::sync_channel::<Result<Vec<u8>, String>>(1);
1858 let fetch_url = url.clone();
1859 std::thread::spawn(move || {
1860 let result = (|| -> Result<Vec<u8>, String> {
1861 let client = reqwest::blocking::Client::builder()
1862 .timeout(Duration::from_secs(30))
1863 .build()
1864 .map_err(|e| e.to_string())?;
1865 let resp = client.get(&fetch_url).send().map_err(|e| e.to_string())?;
1866 if !resp.status().is_success() {
1867 return Err(format!("HTTP {}", resp.status()));
1868 }
1869 resp.bytes().map(|b| b.to_vec()).map_err(|e| e.to_string())
1870 })();
1871 let _ = tx.send(result);
1872 });
1873
1874 let data = match rx.recv() {
1875 Ok(Ok(bytes)) => bytes,
1876 Ok(Err(e)) => {
1877 console_log(
1878 &caller.data().console,
1879 ConsoleLevel::Error,
1880 format!("[AUDIO] Fetch error: {e}"),
1881 );
1882 return -1;
1883 }
1884 Err(_) => return -1,
1885 };
1886
1887 let audio = caller.data().audio.clone();
1888 let mut guard = audio.lock().unwrap();
1889 if guard.is_none() {
1890 *guard = AudioEngine::try_new();
1891 }
1892 match guard.as_mut() {
1893 Some(engine) => {
1894 if engine.play_bytes_on(0, data) {
1895 console_log(
1896 &caller.data().console,
1897 ConsoleLevel::Log,
1898 format!("[AUDIO] Playing from URL: {url}"),
1899 );
1900 0
1901 } else {
1902 console_log(
1903 &caller.data().console,
1904 ConsoleLevel::Error,
1905 "[AUDIO] Failed to decode fetched audio".into(),
1906 );
1907 -2
1908 }
1909 }
1910 None => {
1911 console_log(
1912 &caller.data().console,
1913 ConsoleLevel::Error,
1914 "[AUDIO] No audio device available".into(),
1915 );
1916 -3
1917 }
1918 }
1919 },
1920 )?;
1921
1922 linker.func_wrap(
1923 "oxide",
1924 "api_audio_pause",
1925 |caller: Caller<'_, HostState>| {
1926 let audio = caller.data().audio.clone();
1927 let guard = audio.lock().unwrap();
1928 if let Some(engine) = guard.as_ref() {
1929 if let Some(ch) = engine.channels.get(&0) {
1930 ch.player.pause();
1931 }
1932 }
1933 },
1934 )?;
1935
1936 linker.func_wrap(
1937 "oxide",
1938 "api_audio_resume",
1939 |caller: Caller<'_, HostState>| {
1940 let audio = caller.data().audio.clone();
1941 let guard = audio.lock().unwrap();
1942 if let Some(engine) = guard.as_ref() {
1943 if let Some(ch) = engine.channels.get(&0) {
1944 ch.player.play();
1945 }
1946 }
1947 },
1948 )?;
1949
1950 linker.func_wrap(
1951 "oxide",
1952 "api_audio_stop",
1953 |caller: Caller<'_, HostState>| {
1954 let audio = caller.data().audio.clone();
1955 let guard = audio.lock().unwrap();
1956 if let Some(engine) = guard.as_ref() {
1957 if let Some(ch) = engine.channels.get(&0) {
1958 ch.player.stop();
1959 }
1960 }
1961 },
1962 )?;
1963
1964 linker.func_wrap(
1965 "oxide",
1966 "api_audio_set_volume",
1967 |caller: Caller<'_, HostState>, level: f32| {
1968 let audio = caller.data().audio.clone();
1969 let guard = audio.lock().unwrap();
1970 if let Some(engine) = guard.as_ref() {
1971 if let Some(ch) = engine.channels.get(&0) {
1972 ch.player.set_volume(level.clamp(0.0, 2.0));
1973 }
1974 }
1975 },
1976 )?;
1977
1978 linker.func_wrap(
1979 "oxide",
1980 "api_audio_get_volume",
1981 |caller: Caller<'_, HostState>| -> f32 {
1982 let audio = caller.data().audio.clone();
1983 let guard = audio.lock().unwrap();
1984 guard
1985 .as_ref()
1986 .and_then(|e| e.channels.get(&0))
1987 .map(|ch| ch.player.volume())
1988 .unwrap_or(1.0)
1989 },
1990 )?;
1991
1992 linker.func_wrap(
1993 "oxide",
1994 "api_audio_is_playing",
1995 |caller: Caller<'_, HostState>| -> u32 {
1996 let audio = caller.data().audio.clone();
1997 let guard = audio.lock().unwrap();
1998 match guard.as_ref().and_then(|e| e.channels.get(&0)) {
1999 Some(ch) if !ch.player.is_paused() && !ch.player.empty() => 1,
2000 _ => 0,
2001 }
2002 },
2003 )?;
2004
2005 linker.func_wrap(
2006 "oxide",
2007 "api_audio_position",
2008 |caller: Caller<'_, HostState>| -> u64 {
2009 let audio = caller.data().audio.clone();
2010 let guard = audio.lock().unwrap();
2011 guard
2012 .as_ref()
2013 .and_then(|e| e.channels.get(&0))
2014 .map(|ch| ch.player.get_pos().as_millis() as u64)
2015 .unwrap_or(0)
2016 },
2017 )?;
2018
2019 linker.func_wrap(
2020 "oxide",
2021 "api_audio_seek",
2022 |caller: Caller<'_, HostState>, position_ms: u64| -> i32 {
2023 let audio = caller.data().audio.clone();
2024 let guard = audio.lock().unwrap();
2025 match guard.as_ref().and_then(|e| e.channels.get(&0)) {
2026 Some(ch) => {
2027 let pos = Duration::from_millis(position_ms);
2028 match ch.player.try_seek(pos) {
2029 Ok(_) => 0,
2030 Err(e) => {
2031 console_log(
2032 &caller.data().console,
2033 ConsoleLevel::Warn,
2034 format!("[AUDIO] Seek failed: {e}"),
2035 );
2036 -1
2037 }
2038 }
2039 }
2040 None => -1,
2041 }
2042 },
2043 )?;
2044
2045 linker.func_wrap(
2046 "oxide",
2047 "api_audio_duration",
2048 |caller: Caller<'_, HostState>| -> u64 {
2049 let audio = caller.data().audio.clone();
2050 let guard = audio.lock().unwrap();
2051 guard
2052 .as_ref()
2053 .and_then(|e| e.channels.get(&0))
2054 .map(|ch| ch.duration_ms)
2055 .unwrap_or(0)
2056 },
2057 )?;
2058
2059 linker.func_wrap(
2060 "oxide",
2061 "api_audio_set_loop",
2062 |caller: Caller<'_, HostState>, enabled: u32| {
2063 let audio = caller.data().audio.clone();
2064 let mut guard = audio.lock().unwrap();
2065 if guard.is_none() {
2066 *guard = AudioEngine::try_new();
2067 }
2068 if let Some(engine) = guard.as_mut() {
2069 engine.ensure_channel(0).looping = enabled != 0;
2070 }
2071 },
2072 )?;
2073
2074 linker.func_wrap(
2075 "oxide",
2076 "api_audio_channel_play",
2077 |caller: Caller<'_, HostState>, channel: u32, data_ptr: u32, data_len: u32| -> i32 {
2078 let mem = caller.data().memory.expect("memory not set");
2079 let data = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
2080 if data.is_empty() {
2081 return -1;
2082 }
2083
2084 let audio = caller.data().audio.clone();
2085 let mut guard = audio.lock().unwrap();
2086 if guard.is_none() {
2087 *guard = AudioEngine::try_new();
2088 }
2089 match guard.as_mut() {
2090 Some(engine) => {
2091 if engine.play_bytes_on(channel, data) {
2092 console_log(
2093 &caller.data().console,
2094 ConsoleLevel::Log,
2095 format!("[AUDIO] Playing on channel {channel}"),
2096 );
2097 0
2098 } else {
2099 console_log(
2100 &caller.data().console,
2101 ConsoleLevel::Error,
2102 format!("[AUDIO] Failed to decode audio for channel {channel}"),
2103 );
2104 -2
2105 }
2106 }
2107 None => -3,
2108 }
2109 },
2110 )?;
2111
2112 linker.func_wrap(
2113 "oxide",
2114 "api_audio_channel_stop",
2115 |caller: Caller<'_, HostState>, channel: u32| {
2116 let audio = caller.data().audio.clone();
2117 let guard = audio.lock().unwrap();
2118 if let Some(engine) = guard.as_ref() {
2119 if let Some(ch) = engine.channels.get(&channel) {
2120 ch.player.stop();
2121 }
2122 }
2123 },
2124 )?;
2125
2126 linker.func_wrap(
2127 "oxide",
2128 "api_audio_channel_set_volume",
2129 |caller: Caller<'_, HostState>, channel: u32, level: f32| {
2130 let audio = caller.data().audio.clone();
2131 let guard = audio.lock().unwrap();
2132 if let Some(engine) = guard.as_ref() {
2133 if let Some(ch) = engine.channels.get(&channel) {
2134 ch.player.set_volume(level.clamp(0.0, 2.0));
2135 }
2136 }
2137 },
2138 )?;
2139
2140 linker.func_wrap(
2143 "oxide",
2144 "api_ui_button",
2145 |caller: Caller<'_, HostState>,
2146 id: u32,
2147 x: f32,
2148 y: f32,
2149 w: f32,
2150 h: f32,
2151 label_ptr: u32,
2152 label_len: u32|
2153 -> u32 {
2154 let mem = caller.data().memory.expect("memory not set");
2155 let label = read_guest_string(&mem, &caller, label_ptr, label_len).unwrap_or_default();
2156 caller
2157 .data()
2158 .widget_commands
2159 .lock()
2160 .unwrap()
2161 .push(WidgetCommand::Button {
2162 id,
2163 x,
2164 y,
2165 w,
2166 h,
2167 label,
2168 });
2169 if caller.data().widget_clicked.lock().unwrap().contains(&id) {
2170 1
2171 } else {
2172 0
2173 }
2174 },
2175 )?;
2176
2177 linker.func_wrap(
2178 "oxide",
2179 "api_ui_checkbox",
2180 |caller: Caller<'_, HostState>,
2181 id: u32,
2182 x: f32,
2183 y: f32,
2184 label_ptr: u32,
2185 label_len: u32,
2186 initial: u32|
2187 -> u32 {
2188 let mem = caller.data().memory.expect("memory not set");
2189 let label = read_guest_string(&mem, &caller, label_ptr, label_len).unwrap_or_default();
2190 let mut states = caller.data().widget_states.lock().unwrap();
2191 let entry = states
2192 .entry(id)
2193 .or_insert_with(|| WidgetValue::Bool(initial != 0));
2194 let checked = match entry {
2195 WidgetValue::Bool(b) => *b,
2196 _ => initial != 0,
2197 };
2198 drop(states);
2199 caller
2200 .data()
2201 .widget_commands
2202 .lock()
2203 .unwrap()
2204 .push(WidgetCommand::Checkbox { id, x, y, label });
2205 if checked {
2206 1
2207 } else {
2208 0
2209 }
2210 },
2211 )?;
2212
2213 linker.func_wrap(
2214 "oxide",
2215 "api_ui_slider",
2216 |caller: Caller<'_, HostState>,
2217 id: u32,
2218 x: f32,
2219 y: f32,
2220 w: f32,
2221 min: f32,
2222 max: f32,
2223 initial: f32|
2224 -> f32 {
2225 let mut states = caller.data().widget_states.lock().unwrap();
2226 let entry = states
2227 .entry(id)
2228 .or_insert_with(|| WidgetValue::Float(initial));
2229 let value = match entry {
2230 WidgetValue::Float(v) => *v,
2231 _ => initial,
2232 };
2233 drop(states);
2234 caller
2235 .data()
2236 .widget_commands
2237 .lock()
2238 .unwrap()
2239 .push(WidgetCommand::Slider {
2240 id,
2241 x,
2242 y,
2243 w,
2244 min,
2245 max,
2246 });
2247 value
2248 },
2249 )?;
2250
2251 linker.func_wrap(
2252 "oxide",
2253 "api_ui_text_input",
2254 |mut caller: Caller<'_, HostState>,
2255 id: u32,
2256 x: f32,
2257 y: f32,
2258 w: f32,
2259 init_ptr: u32,
2260 init_len: u32,
2261 out_ptr: u32,
2262 out_cap: u32|
2263 -> u32 {
2264 let mem = caller.data().memory.expect("memory not set");
2265 let text = {
2266 let mut states = caller.data().widget_states.lock().unwrap();
2267 let entry = states.entry(id).or_insert_with(|| {
2268 let init =
2269 read_guest_string(&mem, &caller, init_ptr, init_len).unwrap_or_default();
2270 WidgetValue::Text(init)
2271 });
2272 match entry {
2273 WidgetValue::Text(t) => t.clone(),
2274 _ => String::new(),
2275 }
2276 };
2277 caller
2278 .data()
2279 .widget_commands
2280 .lock()
2281 .unwrap()
2282 .push(WidgetCommand::TextInput { id, x, y, w });
2283 let bytes = text.as_bytes();
2284 let write_len = bytes.len().min(out_cap as usize);
2285 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
2286 write_len as u32
2287 },
2288 )?;
2289
2290 Ok(())
2291}
2292
2293fn getrandom(buf: &mut [u8]) {
2294 ::getrandom::getrandom(buf).expect("OS random number generator unavailable");
2295}