use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use js_sys::{Function, Object, Reflect, Uint8Array, WebAssembly};
use uuid::Uuid;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use arora_buffers::serde_uuid::serialize as serialize_value;
use arora_types::module::low::ModuleDefinition;
use super::{Executor, LoadModuleError, UnloadModuleError};
use crate::call::{CallBridge, CallableId};
use crate::engine::EngineRef;
use crate::module::{DispatchError, Module};
pub struct SharedLoader {
engine: Cell<Option<EngineRef>>,
prepared: RefCell<HashMap<Uuid, PreparedInstance>>,
}
pub type SharedLoaderRc = Rc<SharedLoader>;
struct PreparedInstance {
instance: WebAssembly::Instance,
dispatch_cb: Closure<dyn FnMut(u32, u32, u32) -> u32>,
dispatch_indirect_cb: Closure<dyn Fn(i64) -> u32>,
wasi_keepalive: Vec<JsValue>,
late: Rc<RefCell<Option<LateBound>>>,
}
impl SharedLoader {
pub async fn prepare(self: Rc<Self>, id: Uuid, executable: Vec<u8>) -> Result<(), JsValue> {
let engine_ptr = self
.engine
.get()
.ok_or_else(|| JsValue::from_str("BrowserExecutor: set_engine not called"))?;
let (imports, parts) = build_imports(engine_ptr)
.map_err(|e| JsValue::from_str(&format!("building imports failed: {e}")))?;
let result = JsFuture::from(WebAssembly::instantiate_buffer(&executable, &imports)).await?;
let instance: WebAssembly::Instance = Reflect::get(&result, &"instance".into())?
.dyn_into()
.map_err(|_| JsValue::from_str("WebAssembly.instantiate: no instance in result"))?;
self.prepared
.borrow_mut()
.insert(id, parts.with_instance(instance));
Ok(())
}
}
struct ImportParts {
dispatch_cb: Closure<dyn FnMut(u32, u32, u32) -> u32>,
dispatch_indirect_cb: Closure<dyn Fn(i64) -> u32>,
wasi_keepalive: Vec<JsValue>,
late: Rc<RefCell<Option<LateBound>>>,
}
impl ImportParts {
fn with_instance(self, instance: WebAssembly::Instance) -> PreparedInstance {
PreparedInstance {
instance,
dispatch_cb: self.dispatch_cb,
dispatch_indirect_cb: self.dispatch_indirect_cb,
wasi_keepalive: self.wasi_keepalive,
late: self.late,
}
}
}
pub struct BrowserExecutor {
shared: SharedLoaderRc,
}
impl BrowserExecutor {
pub fn new() -> Self {
Self {
shared: Rc::new(SharedLoader {
engine: Cell::new(None),
prepared: RefCell::new(HashMap::new()),
}),
}
}
pub fn shared(&self) -> SharedLoaderRc {
self.shared.clone()
}
}
impl Default for BrowserExecutor {
fn default() -> Self {
Self::new()
}
}
impl Executor for BrowserExecutor {
fn set_engine(&mut self, engine: EngineRef) {
self.shared.engine.set(Some(engine));
}
fn name(&self) -> &'static str {
"wasm"
}
fn load_module(
&mut self,
module_definition: ModuleDefinition,
) -> Result<Box<dyn Module>, LoadModuleError> {
let prepared = self
.shared
.prepared
.borrow_mut()
.remove(&module_definition.header.id);
let prepared = match prepared {
Some(p) => p,
None => {
let engine_ptr = self.shared.engine.get().ok_or_else(|| {
LoadModuleError::Internal("BrowserExecutor: set_engine not called".into())
})?;
let bytes_view = Uint8Array::from(module_definition.executable.as_ref());
let module = WebAssembly::Module::new(&bytes_view.into()).map_err(|e| {
LoadModuleError::Internal(format!("WebAssembly.Module: {:?}", e))
})?;
let (imports, parts) = build_imports(engine_ptr)?;
let instance = WebAssembly::Instance::new(&module, &imports).map_err(|e| {
LoadModuleError::Internal(format!("WebAssembly.Instance: {:?}", e))
})?;
parts.with_instance(instance)
}
};
let PreparedInstance {
instance,
dispatch_cb,
dispatch_indirect_cb,
wasi_keepalive,
late,
} = prepared;
let exports = instance.exports();
let memory: WebAssembly::Memory = Reflect::get(&exports, &"memory".into())
.map_err(js_to_load_err)?
.dyn_into()
.map_err(|_| LoadModuleError::Internal("guest does not export 'memory'".into()))?;
let malloc: Function = Reflect::get(&exports, &"arora_buffer_alloc".into())
.map_err(js_to_load_err)?
.dyn_into()
.map_err(|_| {
LoadModuleError::Internal("guest does not export 'arora_buffer_alloc'".into())
})?;
let free: Function = Reflect::get(&exports, &"arora_buffer_free".into())
.map_err(js_to_load_err)?
.dyn_into()
.map_err(|_| {
LoadModuleError::Internal("guest does not export 'arora_buffer_free'".into())
})?;
let mut arora_functions = HashMap::new();
for export in &module_definition.header.exports {
let id = *export.id();
let symbol = format!("arora_function_{}", id.to_string().replace('-', "_"));
let f: Function = Reflect::get(&exports, &symbol.clone().into())
.map_err(js_to_load_err)?
.dyn_into()
.map_err(|_| {
LoadModuleError::Internal(format!("guest missing export '{}'", symbol))
})?;
arora_functions.insert(id, f);
}
*late.borrow_mut() = Some(LateBound {
memory: memory.clone(),
malloc: malloc.clone(),
});
Ok(Box::new(BrowserModule {
_instance: instance,
memory,
malloc,
free,
arora_functions,
_dispatch_cb: dispatch_cb,
_dispatch_indirect_cb: dispatch_indirect_cb,
_wasi_keepalive: wasi_keepalive,
_late: late,
}))
}
fn unload_module(&mut self, _module_id: Uuid) -> Result<(), UnloadModuleError> {
Ok(())
}
}
struct LateBound {
memory: WebAssembly::Memory,
malloc: Function,
}
struct BrowserModule {
_instance: WebAssembly::Instance,
memory: WebAssembly::Memory,
malloc: Function,
free: Function,
arora_functions: HashMap<Uuid, Function>,
_dispatch_cb: Closure<dyn FnMut(u32, u32, u32) -> u32>,
_dispatch_indirect_cb: Closure<dyn Fn(i64) -> u32>,
_wasi_keepalive: Vec<JsValue>,
_late: Rc<RefCell<Option<LateBound>>>,
}
impl Module for BrowserModule {
fn dispatch(&mut self, function_id: &Uuid, arg: &[u8]) -> Result<Box<[u8]>, DispatchError> {
let arg_size = arg.len() as u32;
let arg_addr = call_u32_u32(&self.malloc, arg_size).map_err(|e| DispatchError::Trap {
message: format!("malloc({arg_size}) failed: {e:?}"),
})?;
write_bytes(&self.memory, arg_addr, arg);
let func =
self.arora_functions
.get(function_id)
.ok_or_else(|| DispatchError::Internal {
message: format!("no exported function {}", function_id),
})?;
let result_addr = func
.call1(&JsValue::NULL, &JsValue::from(arg_addr))
.map_err(|e| DispatchError::Trap {
message: format!("function call failed: {e:?}"),
})?
.as_f64()
.ok_or_else(|| DispatchError::Internal {
message: "function did not return a number".into(),
})? as u32;
self.free
.call1(&JsValue::NULL, &JsValue::from(arg_addr))
.map_err(|e| DispatchError::Trap {
message: format!("free(arg) failed: {e:?}"),
})?;
let mut size_buf = [0u8; 4];
read_bytes(&self.memory, result_addr, &mut size_buf);
let size = u32::from_le_bytes(size_buf);
let mut result_buf = vec![0u8; size as usize];
read_bytes(&self.memory, result_addr, &mut result_buf);
self.free
.call1(&JsValue::NULL, &JsValue::from(result_addr))
.map_err(|e| DispatchError::Trap {
message: format!("free(result) failed: {e:?}"),
})?;
Ok(result_buf.into_boxed_slice())
}
}
fn build_imports(engine_ptr: EngineRef) -> Result<(Object, ImportParts), LoadModuleError> {
let late: Rc<RefCell<Option<LateBound>>> = Rc::new(RefCell::new(None));
let late_d = late.clone();
let dispatch_cb = Closure::<dyn FnMut(u32, u32, u32) -> u32>::new(
move |module_id_ptr, method_id_ptr, arg_ptr| {
let late = late_d.borrow();
let late = late.as_ref().expect("late-bound state not set");
let engine = unsafe { &mut *engine_ptr };
let module_id = read_uuid(&late.memory, module_id_ptr);
let method_id = read_uuid(&late.memory, method_id_ptr);
let arg = read_arora_buffer(&late.memory, arg_ptr);
let result = engine
.dispatch(&module_id, &method_id, arg.as_ref())
.expect("arora_dispatch: engine.dispatch failed");
let result_addr = call_u32_u32(&late.malloc, result.len() as u32)
.expect("arora_dispatch: malloc failed");
write_bytes(&late.memory, result_addr, &result);
result_addr
},
);
let late_di = late.clone();
let dispatch_indirect_cb = Closure::<dyn Fn(i64) -> u32>::new(move |callable_id: i64| {
let late = late_di.borrow();
let late = late.as_ref().expect("late-bound state not set");
let engine = unsafe { &mut *engine_ptr };
let value = engine
.arora_call_indirect(&CallableId {
id: callable_id as u64,
})
.expect("arora_dispatch_indirect: engine call failed");
let buf = serialize_value(&value);
let addr = call_u32_u32(&late.malloc, buf.len() as u32)
.expect("arora_dispatch_indirect: malloc failed");
write_bytes(&late.memory, addr, &buf);
addr
});
let imports = Object::new();
let env = Object::new();
Reflect::set(
&env,
&"arora_dispatch".into(),
dispatch_cb.as_ref().unchecked_ref(),
)
.map_err(js_to_load_err)?;
Reflect::set(
&env,
&"arora_dispatch_indirect".into(),
dispatch_indirect_cb.as_ref().unchecked_ref(),
)
.map_err(js_to_load_err)?;
Reflect::set(&imports, &"env".into(), &env).map_err(js_to_load_err)?;
let (wasi_obj, wasi_keepalive) = build_wasi_stubs();
Reflect::set(&imports, &"wasi_snapshot_preview1".into(), &wasi_obj).map_err(js_to_load_err)?;
Ok((
imports,
ImportParts {
dispatch_cb,
dispatch_indirect_cb,
wasi_keepalive,
late,
},
))
}
fn js_to_load_err(e: JsValue) -> LoadModuleError {
LoadModuleError::Internal(format!("js error: {e:?}"))
}
fn call_u32_u32(f: &Function, arg: u32) -> Result<u32, JsValue> {
let res = f.call1(&JsValue::NULL, &JsValue::from(arg))?;
Ok(res
.as_f64()
.ok_or_else(|| JsValue::from_str("non-numeric return"))? as u32)
}
fn write_bytes(memory: &WebAssembly::Memory, offset: u32, bytes: &[u8]) {
let view = Uint8Array::new(&memory.buffer());
let src = Uint8Array::from(bytes);
view.set(&src, offset);
}
fn read_bytes(memory: &WebAssembly::Memory, offset: u32, out: &mut [u8]) {
let view = Uint8Array::new(&memory.buffer());
let slice = view.subarray(offset, offset + out.len() as u32);
slice.copy_to(out);
}
fn read_uuid(memory: &WebAssembly::Memory, offset: u32) -> Uuid {
let mut buf = [0u8; 16];
read_bytes(memory, offset, &mut buf);
Uuid::from_slice(&buf).expect("16 bytes")
}
fn read_arora_buffer(memory: &WebAssembly::Memory, offset: u32) -> Vec<u8> {
let mut size_buf = [0u8; 4];
read_bytes(memory, offset, &mut size_buf);
let size = u32::from_le_bytes(size_buf);
let mut buf = vec![0u8; size as usize + 4];
read_bytes(memory, offset, &mut buf);
buf
}
fn build_wasi_stubs() -> (Object, Vec<JsValue>) {
let obj = Object::new();
let mut keepalive: Vec<JsValue> = Vec::new();
let proc_exit = Closure::<dyn FnMut(i32)>::new(|code: i32| {
panic!("guest called proc_exit({code})");
});
Reflect::set(
&obj,
&"proc_exit".into(),
proc_exit.as_ref().unchecked_ref(),
)
.unwrap();
keepalive.push(proc_exit.into_js_value());
let fd_write = Closure::<dyn FnMut(i32, i32, i32, i32) -> i32>::new(|_, _, _, _| 0);
Reflect::set(&obj, &"fd_write".into(), fd_write.as_ref().unchecked_ref()).unwrap();
keepalive.push(fd_write.into_js_value());
let fd_close = Closure::<dyn FnMut(i32) -> i32>::new(|_| 0);
Reflect::set(&obj, &"fd_close".into(), fd_close.as_ref().unchecked_ref()).unwrap();
keepalive.push(fd_close.into_js_value());
let fd_seek = Closure::<dyn FnMut(i32, i64, i32, i32) -> i32>::new(|_, _, _, _| 0);
Reflect::set(&obj, &"fd_seek".into(), fd_seek.as_ref().unchecked_ref()).unwrap();
keepalive.push(fd_seek.into_js_value());
let fd_fdstat_get = Closure::<dyn FnMut(i32, i32) -> i32>::new(|_, _| 0);
Reflect::set(
&obj,
&"fd_fdstat_get".into(),
fd_fdstat_get.as_ref().unchecked_ref(),
)
.unwrap();
keepalive.push(fd_fdstat_get.into_js_value());
let fd_prestat_get = Closure::<dyn FnMut(i32, i32) -> i32>::new(|_, _| 8);
Reflect::set(
&obj,
&"fd_prestat_get".into(),
fd_prestat_get.as_ref().unchecked_ref(),
)
.unwrap();
keepalive.push(fd_prestat_get.into_js_value());
let fd_prestat_dir_name = Closure::<dyn FnMut(i32, i32, i32) -> i32>::new(|_, _, _| 0);
Reflect::set(
&obj,
&"fd_prestat_dir_name".into(),
fd_prestat_dir_name.as_ref().unchecked_ref(),
)
.unwrap();
keepalive.push(fd_prestat_dir_name.into_js_value());
let environ_get = Closure::<dyn FnMut(i32, i32) -> i32>::new(|_, _| 0);
Reflect::set(
&obj,
&"environ_get".into(),
environ_get.as_ref().unchecked_ref(),
)
.unwrap();
keepalive.push(environ_get.into_js_value());
let environ_sizes_get = Closure::<dyn FnMut(i32, i32) -> i32>::new(|_, _| 0);
Reflect::set(
&obj,
&"environ_sizes_get".into(),
environ_sizes_get.as_ref().unchecked_ref(),
)
.unwrap();
keepalive.push(environ_sizes_get.into_js_value());
let args_get = Closure::<dyn FnMut(i32, i32) -> i32>::new(|_, _| 0);
Reflect::set(&obj, &"args_get".into(), args_get.as_ref().unchecked_ref()).unwrap();
keepalive.push(args_get.into_js_value());
let args_sizes_get = Closure::<dyn FnMut(i32, i32) -> i32>::new(|_, _| 0);
Reflect::set(
&obj,
&"args_sizes_get".into(),
args_sizes_get.as_ref().unchecked_ref(),
)
.unwrap();
keepalive.push(args_sizes_get.into_js_value());
let clock_time_get = Closure::<dyn FnMut(i32, i64, i32) -> i32>::new(|_, _, _| 0);
Reflect::set(
&obj,
&"clock_time_get".into(),
clock_time_get.as_ref().unchecked_ref(),
)
.unwrap();
keepalive.push(clock_time_get.into_js_value());
let random_get = Closure::<dyn FnMut(i32, i32) -> i32>::new(|_, _| 0);
Reflect::set(
&obj,
&"random_get".into(),
random_get.as_ref().unchecked_ref(),
)
.unwrap();
keepalive.push(random_get.into_js_value());
(obj, keepalive)
}