use std::process::exit;
use vertigo::{
JsJson, JsJsonSerialize,
dev::{
CallbackId, LongPtr, SsrFetchResponse,
command::{CommandForBrowser, CommandForWasm, decode_json},
},
};
use wasmtime::{Caller, Engine, Instance, InstancePre, Linker, Store};
use crate::{
commons::ErrorCode,
serve::{
response_state::ResponseState,
timings::{ENTRY_FUNCTION, HANDLE_URL_FUNCTION, SsrProbe, WASM_COMMAND_FUNCTION},
},
};
use super::{data_context::DataContext, host_state::HostState, message::Message};
const IMPORT_MODULE: &str = "mod";
pub fn build_linker(engine: &Engine) -> Result<Linker<HostState>, ErrorCode> {
fn registration_failed(name: &str, err: impl std::fmt::Debug) -> ErrorCode {
log::error!("WASM host function registration failed: {IMPORT_MODULE}.{name}: {err:?}");
ErrorCode::ServeWasmInstanceFailed
}
let mut linker = Linker::new(engine);
linker
.func_wrap(
IMPORT_MODULE,
"panic_message",
|caller: Caller<'_, HostState>, long_ptr: u64| {
let sender = caller.data().sender.clone();
let mut data_context = DataContext::from_caller(caller);
let long_ptr = LongPtr::from(long_ptr);
let (ptr, offset) = long_ptr.into_parts();
let message = data_context.get_string_from(ptr, offset);
log::error!("wasm panic: {message:?}");
sender.send(Message::Panic(message)).unwrap_or_default();
},
)
.map_err(|err| registration_failed("panic_message", err))?;
linker
.func_wrap(
IMPORT_MODULE,
"dom_access",
|caller: Caller<'_, HostState>, long_ptr: u64| -> u64 {
let state = caller.data();
let probe = state.probe.clone();
let request = state.request.clone();
let handle_command = state.handle_command.clone();
let host_mark = probe.start();
let long_ptr = LongPtr::from(long_ptr);
let mut data_context = DataContext::from_caller(caller);
let value = data_context.get_value_long_ptr(long_ptr);
let result = decode_json::<CommandForBrowser>(value)
.map(|item| handle_command(request, item));
let result = match result {
Ok(result) => data_context.save_value(result).get_long_ptr(),
Err(err) => {
log::error!("import_dom_access -> decode error = {err}");
0
}
};
probe.host_call(host_mark);
result
},
)
.map_err(|err| registration_failed("dom_access", err))?;
Ok(linker)
}
pub struct WasmInstance {
instance: Instance,
store: Store<HostState>,
probe: SsrProbe,
}
impl WasmInstance {
pub fn new(engine: &Engine, instance_pre: &InstancePre<HostState>, state: HostState) -> Self {
let probe = state.probe.clone();
let mut store = Store::new(engine, state);
let instance = match instance_pre.instantiate(&mut store) {
Ok(instance) => instance,
Err(err) => {
log::error!("WASM instantiation error: {err:?}");
exit(ErrorCode::ServeWasmInstanceFailed as i32)
}
};
WasmInstance {
instance,
store,
probe,
}
}
fn call_function<Params: wasmtime::WasmParams, Results: wasmtime::WasmResults>(
&mut self,
name: &'static str,
params: Params,
) -> Result<Results, String> {
let vertigo_entry_function = {
self.instance
.get_typed_func::<Params, Results>(&mut self.store, name)
.map_err(|err| {
log::error!("Error calling function: {err}");
err.to_string()
})?
};
let call_mark = self.probe.start();
let result = vertigo_entry_function
.call(&mut self.store, params)
.map_err(|error| format!("{error}"));
self.probe.wasm_call(name, call_mark);
result
}
pub fn call_vertigo_entry_function(&mut self) {
self.call_function::<(u32, u32), ()>(
ENTRY_FUNCTION,
(super::VERTIGO_VERSION_MAJOR, super::VERTIGO_VERSION_MINOR),
)
.inspect_err(|err| log::error!("Error calling entry function: {err}"))
.unwrap_or_default();
}
pub fn wasm_command(&mut self, command: CommandForWasm) -> JsJson {
let mut data_context = DataContext::from_store(&mut self.store, self.instance);
let params_ptr = data_context.save_value(command.to_json());
let _result = self
.call_function::<u64, u64>(WASM_COMMAND_FUNCTION, params_ptr.get_long_ptr())
.inspect_err(|err| log::error!("Error calling callback: {err}"))
.unwrap_or_default();
JsJson::Null
}
pub fn handle_url(&mut self, url: &str) -> Option<ResponseState> {
let url = JsJson::String(url.to_string());
let params_ptr = {
let mut data_context = DataContext::from_store(&mut self.store, self.instance);
data_context.save_value(url)
};
let result = self
.call_function::<u64, u64>(HANDLE_URL_FUNCTION, params_ptr.get_long_ptr())
.inspect_err(|err| log::error!("Error calling callback: {err}"))
.unwrap_or_default();
let result = {
let mut data_context = DataContext::from_store(&mut self.store, self.instance);
data_context.get_value_long_ptr(LongPtr::from(result))
};
self.decode_response_state(result)
}
fn decode_response_state(&self, value: JsJson) -> Option<ResponseState> {
if let JsJson::Null = value {
return None;
}
let response: Result<ResponseState, vertigo::JsJsonContext> =
decode_json::<ResponseState>(value);
if let Ok(response) = response {
return Some(response);
}
log::error!("decode_response_state: decode error = {response:#?}");
None
}
pub fn send_fetch_response(&mut self, callback: CallbackId, response: SsrFetchResponse) {
let result = self.wasm_command(CommandForWasm::FetchExecResponse { response, callback });
assert_eq!(result, JsJson::Null);
}
}
#[cfg(test)]
mod tests {
use wasmtime::Module;
use super::*;
const PANIC_FIRST: &str = r#"(module
(import "mod" "panic_message" (func (param i64)))
(import "mod" "dom_access" (func (param i64) (result i64))))"#;
const DOM_ACCESS_FIRST: &str = r#"(module
(import "mod" "dom_access" (func (param i64) (result i64)))
(import "mod" "panic_message" (func (param i64))))"#;
fn imports_resolve(wat: &str) -> bool {
let engine = Engine::default();
let Ok(module) = Module::new(&engine, wat) else {
panic!("test WAT does not parse");
};
let Ok(linker) = build_linker(&engine) else {
panic!("build_linker failed");
};
linker.instantiate_pre(&module).is_ok()
}
#[test]
fn imports_resolve_in_either_declared_order() {
assert!(imports_resolve(PANIC_FIRST));
assert!(imports_resolve(DOM_ACCESS_FIRST));
}
#[test]
fn an_unknown_import_does_not_resolve() {
let wat = r#"(module
(import "mod" "dom_access_typo" (func (param i64) (result i64)))
(import "mod" "panic_message" (func (param i64))))"#;
assert!(!imports_resolve(wat));
}
}