use wasmtime::{Linker, Store, Val};
use crate::backtrace::ThrownError;
use crate::runtime::host::Denial;
use crate::runtime::json::JsonUnknownAllocator;
use crate::runtime::{LinkedPackageModule, StoreData, exec, install_package_modules_async};
#[derive(Clone, Copy, Debug)]
pub struct FinalizerCall<'a> {
pub package: &'a str,
pub key: &'a [u16],
pub value: &'a [u16],
}
#[derive(Debug)]
pub enum FinalizerError {
Unavailable(String),
MalformedArgument(String),
IncompatibleArgument(String),
Denied(Denial),
BudgetExhausted(wasmtime::Error),
Failed(wasmtime::Error),
}
impl std::fmt::Display for FinalizerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unavailable(message) => write!(f, "finalizer unavailable: {message}"),
Self::MalformedArgument(message) => {
write!(f, "malformed finalizer argument: {message}")
}
Self::IncompatibleArgument(message) => {
write!(f, "incompatible finalizer argument: {message}")
}
Self::Denied(denial) => write!(
f,
"finalizer denied `{}` for `{}`",
denial.capability, denial.caller
),
Self::BudgetExhausted(error) => write!(f, "finalizer budget exhausted: {error}"),
Self::Failed(error) => write!(f, "finalizer failed: {error}"),
}
}
}
impl std::error::Error for FinalizerError {}
pub async fn invoke_finalizer_async(
store: &mut Store<StoreData>,
packages: &[LinkedPackageModule<'_>],
call: FinalizerCall<'_>,
) -> Result<(), FinalizerError> {
store.data_mut().finalizing = true;
let declaration = packages
.iter()
.map(|package| package.declaration)
.find(|declaration| declaration.package_name == call.package)
.ok_or_else(|| {
FinalizerError::Unavailable(format!("package `{}` is not linked", call.package))
})?;
if !declaration.values.contains_key("finalizer") {
return Err(FinalizerError::Unavailable(format!(
"package `{}` exports no finalizer",
call.package
)));
}
let value = crate::runtime::json_text::parse(call.value)
.map_err(|error| FinalizerError::MalformedArgument(error.message().to_owned()))?;
let mut linker = Linker::new(store.engine());
crate::runtime::install_runtime_async(&mut linker, store)
.await
.map_err(classify)?;
install_package_modules_async(&mut linker, store, packages)
.await
.map_err(classify)?;
let export = |store: &mut Store<StoreData>, name: &str| match linker.get(
&mut *store,
call.package,
name,
) {
Ok(wasmtime::Extern::Func(function)) => Ok(function),
_ => Err(FinalizerError::Unavailable(format!(
"package `{}` does not export `{name}`; rebuild it",
call.package
))),
};
let check = export(
store,
crate::mangle::finalizer_argument(call.package).as_str(),
)?;
let finalizer = export(
store,
crate::mangle::package_symbol(call.package, "finalizer").as_str(),
)?;
let allocator = JsonUnknownAllocator::new(store).map_err(classify)?;
let unknown = allocator.allocate(store, &value).map_err(classify)?;
let key = allocator
.allocate(
store,
&crate::runtime::json_text::JsonValue::String(call.key.to_vec()),
)
.map_err(classify)?;
let mut argument = [Val::null_any_ref()];
let checked = check
.call_async(&mut *store, &[unknown], &mut argument)
.await;
checked.map_err(|error| match classify(exec::uncaught_error(store, error)) {
FinalizerError::Failed(error) if error.is::<ThrownError>() => {
FinalizerError::IncompatibleArgument(error.to_string())
}
other => other,
})?;
let [argument] = argument;
let mut returned = vec![Val::null_any_ref(); finalizer.ty(&*store).results().len()];
let ran = finalizer
.call_async(&mut *store, &[key, argument], &mut returned)
.await;
ran.map_err(|error| classify(exec::uncaught_error(store, error)))
}
fn classify(error: wasmtime::Error) -> FinalizerError {
if matches!(
error.downcast_ref::<wasmtime::Trap>(),
Some(wasmtime::Trap::OutOfFuel | wasmtime::Trap::Interrupt)
) || crate::runtime::is_memory_exhausted(&error)
{
return FinalizerError::BudgetExhausted(error);
}
if let Some(denial) = error
.downcast_ref::<ThrownError>()
.and_then(|thrown| thrown.denial.clone())
{
return FinalizerError::Denied(denial);
}
FinalizerError::Failed(error)
}