use std::sync::OnceLock;
use anyhow::Result;
use derive_more::{Display, Error, From};
use uuid::Uuid;
use wasmtime::component::{bindgen, Component, HasData, Linker};
use wasmtime::{Config, Engine as WasmEngine, Store};
use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
use arora_types::module::low::{ExportSymbol, ModuleDefinition};
use super::{Executor, LoadModuleError, UnloadModuleError};
use crate::call::{CallBridge, CallableId};
use crate::compiled::{Code, CompiledModule};
use crate::engine::EngineRef;
use crate::load::module_definition_from_parts;
use crate::module::{guest_error, DispatchError, Module as AroraModule};
bindgen!({
world: "module",
path: "wit",
});
#[derive(Debug, Error, Display, From)]
pub enum InitializationError {
Internal(#[error(not(source))] anyhow::Error),
}
struct EnginePtr(EngineRef);
unsafe impl Send for EnginePtr {}
struct HostState {
wasi: WasiCtx,
table: ResourceTable,
engine: EnginePtr,
}
impl WasiView for HostState {
fn ctx(&mut self) -> WasiCtxView<'_> {
WasiCtxView {
ctx: &mut self.wasi,
table: &mut self.table,
}
}
}
fn uuid_to_id(uuid: &Uuid) -> Id {
let (hi, lo) = uuid.as_u64_pair();
Id { hi, lo }
}
fn id_to_uuid(id: Id) -> Uuid {
Uuid::from_u64_pair(id.hi, id.lo)
}
impl arora::module::types::Host for HostState {}
impl arora::module::host::Host for HostState {
fn dispatch(&mut self, module: Id, method: Id, arg: Vec<u8>) -> Result<Vec<u8>, String> {
let engine = unsafe { &mut *self.engine.0 };
engine
.dispatch(&id_to_uuid(module), &id_to_uuid(method), &arg)
.map(Vec::from)
.map_err(|e| format!("{e}"))
}
fn dispatch_indirect(&mut self, callable: u64) -> Result<Vec<u8>, String> {
let bridge: &mut dyn CallBridge = unsafe { &mut *self.engine.0 };
let value = bridge
.arora_call_indirect(&CallableId { id: callable })
.map_err(|e| format!("{e}"))?;
Ok(arora_buffers::serde_uuid::serialize(&value).into())
}
}
pub struct ComponentExecutor {
linker: Linker<HostState>,
arora_engine: Option<EngineRef>,
}
fn config() -> Config {
let mut config = Config::new();
config.debug_info(cfg!(debug_assertions));
config.cranelift_opt_level(wasmtime::OptLevel::Speed);
config.concurrency_support(false);
config
}
fn engine() -> Result<&'static WasmEngine, LoadModuleError> {
static ENGINE: OnceLock<Result<WasmEngine, String>> = OnceLock::new();
ENGINE
.get_or_init(|| WasmEngine::new(&config()).map_err(|e| format!("{e:#}")))
.as_ref()
.map_err(|e| {
LoadModuleError::Internal(format!("failed to create the component engine: {e}"))
})
}
pub(crate) fn compile(executable: &[u8]) -> Result<Component, LoadModuleError> {
Component::new(engine()?, executable).map_err(|_| LoadModuleError::MalformedExecutable)
}
struct HasIo;
impl HasData for HasIo {
type Data<'a> = &'a mut ResourceTable;
}
fn add_wasi_to_linker(linker: &mut Linker<HostState>) -> wasmtime::Result<()> {
use wasmtime_wasi::cli::{WasiCli, WasiCliView};
use wasmtime_wasi::clocks::{WasiClocks, WasiClocksView};
use wasmtime_wasi::filesystem::{WasiFilesystem, WasiFilesystemView};
use wasmtime_wasi::p2::bindings::{cli, clocks, filesystem, io, random, sync};
use wasmtime_wasi::random::WasiRandom;
let l = linker;
io::error::add_to_linker::<_, HasIo>(l, |t| t.ctx().table)?;
sync::io::poll::add_to_linker::<_, HasIo>(l, |t| t.ctx().table)?;
sync::io::streams::add_to_linker::<_, HasIo>(l, |t| t.ctx().table)?;
clocks::wall_clock::add_to_linker::<_, WasiClocks>(l, HostState::clocks)?;
clocks::monotonic_clock::add_to_linker::<_, WasiClocks>(l, HostState::clocks)?;
filesystem::preopens::add_to_linker::<_, WasiFilesystem>(l, HostState::filesystem)?;
sync::filesystem::types::add_to_linker::<_, WasiFilesystem>(l, HostState::filesystem)?;
random::random::add_to_linker::<_, WasiRandom>(l, |t| t.ctx().ctx.random())?;
random::insecure::add_to_linker::<_, WasiRandom>(l, |t| t.ctx().ctx.random())?;
random::insecure_seed::add_to_linker::<_, WasiRandom>(l, |t| t.ctx().ctx.random())?;
cli::exit::add_to_linker::<_, WasiCli>(l, HostState::cli)?;
cli::environment::add_to_linker::<_, WasiCli>(l, HostState::cli)?;
cli::stdin::add_to_linker::<_, WasiCli>(l, HostState::cli)?;
cli::stdout::add_to_linker::<_, WasiCli>(l, HostState::cli)?;
cli::stderr::add_to_linker::<_, WasiCli>(l, HostState::cli)?;
cli::terminal_input::add_to_linker::<_, WasiCli>(l, HostState::cli)?;
cli::terminal_output::add_to_linker::<_, WasiCli>(l, HostState::cli)?;
cli::terminal_stdin::add_to_linker::<_, WasiCli>(l, HostState::cli)?;
cli::terminal_stdout::add_to_linker::<_, WasiCli>(l, HostState::cli)?;
cli::terminal_stderr::add_to_linker::<_, WasiCli>(l, HostState::cli)?;
Ok(())
}
impl ComponentExecutor {
pub fn new() -> Result<Self, InitializationError> {
let engine = engine().map_err(|e| anyhow::anyhow!("{e}"))?;
let mut linker: Linker<HostState> = Linker::new(engine);
add_wasi_to_linker(&mut linker).map_err(anyhow::Error::from)?;
Module::add_to_linker::<_, wasmtime::component::HasSelf<_>>(&mut linker, |s| s)
.map_err(anyhow::Error::from)?;
Ok(Self {
linker,
arora_engine: None,
})
}
fn instantiate(
&self,
exports: &[ExportSymbol],
component: &Component,
) -> Result<Box<dyn AroraModule>, LoadModuleError> {
let state = HostState {
wasi: WasiCtxBuilder::new().inherit_stdio().build(),
table: ResourceTable::new(),
engine: EnginePtr(self.arora_engine.ok_or_else(|| {
LoadModuleError::Internal("ComponentExecutor: set_engine not called".into())
})?),
};
let mut store = Store::new(component.engine(), state);
let bindings = Module::instantiate(&mut store, component, &self.linker)
.map_err(|e| LoadModuleError::Internal(format!("instantiate: {e:#}")))?;
let served = bindings
.call_functions(&mut store)
.map_err(|e| LoadModuleError::Internal(format!("functions trapped: {e:#}")))?;
let function_ids: Vec<Uuid> = exports.iter().map(|e| *e.id()).collect();
if let Some(missing) = function_ids
.iter()
.find(|id| !served.iter().any(|served| id_to_uuid(*served) == **id))
{
return Err(LoadModuleError::Internal(format!(
"failed to find function export {missing}: the component does not serve it"
)));
}
Ok(Box::new(ComponentModule {
bindings,
store,
function_ids,
trapped: None,
}))
}
}
impl Executor for ComponentExecutor {
fn set_engine(&mut self, engine: EngineRef) {
self.arora_engine = Some(engine);
}
fn name(&self) -> &'static str {
"wasm-component"
}
fn load_module(
&mut self,
module_definition: ModuleDefinition,
) -> Result<Box<dyn AroraModule>, LoadModuleError> {
let component = compile(&module_definition.executable)?;
self.instantiate(&module_definition.header.exports, &component)
}
fn load_compiled_module(
&mut self,
module: &CompiledModule,
) -> Result<Box<dyn AroraModule>, LoadModuleError> {
match &module.code {
Code::Component(component) => self.instantiate(&module.header.exports, component),
Code::Executable(executable) => self.load_module(module_definition_from_parts(
module.header.clone(),
executable.as_ref().into(),
)),
_ => Err(LoadModuleError::Internal(
"ComponentExecutor: the module was compiled for another executor".into(),
)),
}
}
fn unload_module(&mut self, _: Uuid) -> Result<(), UnloadModuleError> {
Ok(())
}
}
struct ComponentModule {
bindings: Module,
store: Store<HostState>,
function_ids: Vec<Uuid>,
trapped: Option<String>,
}
impl AroraModule for ComponentModule {
fn dispatch(&mut self, method_id: &Uuid, arg: &[u8]) -> Result<Box<[u8]>, DispatchError> {
if !self.function_ids.contains(method_id) {
return Err(DispatchError::FunctionNotFound { id: *method_id });
}
if let Some(trap) = &self.trapped {
return Err(DispatchError::Trap {
message: format!("{TRAPPED} ({trap})"),
});
}
let result = match self
.bindings
.call_dispatch(&mut self.store, uuid_to_id(method_id), arg)
{
Ok(result) => result,
Err(e) => {
let message = format!("dispatch trapped: {e:#?}");
self.trapped = Some(message.clone());
return Err(DispatchError::Trap { message });
}
};
match result {
Ok(bytes) => match guest_error(&bytes) {
Some(message) => Err(DispatchError::Guest { message }),
None => Ok(bytes.into_boxed_slice()),
},
Err(message) => Err(DispatchError::Guest { message }),
}
}
}
pub(crate) const TRAPPED: &str =
"the module trapped in an earlier call and runs no more calls until it is reloaded";
#[cfg(test)]
mod tests {
use super::*;
fn component(kind: &str, lift_options: &str) -> String {
format!(
r#"(component
(core module $m
(func (export "f") (result i32) i32.const 0)
(func (export "cb") (param i32 i32 i32) (result i32) i32.const 0))
(core instance $i (instantiate $m))
(func (export "f") {kind} (result u32)
(canon lift (core func $i "f") {lift_options})))"#
)
}
#[test]
fn a_component_using_the_async_abi_is_refused() {
let engine = WasmEngine::new(&config()).unwrap();
let async_lifted = component("async", r#"async (callback (core func $i "cb"))"#);
let error = Component::new(&engine, async_lifted).err().unwrap();
assert!(
format!("{error:#}").contains("require the component model async feature"),
"{error:#}"
);
}
#[test]
fn a_component_lifting_synchronously_compiles() {
let engine = WasmEngine::new(&config()).unwrap();
Component::new(&engine, component("", "")).unwrap();
}
}