use crate::abi::{self, RawOutcome};
use crate::engine::{self, HostState};
use faucet_core::FaucetError;
use wasmtime::{Instance, Memory, Store, TypedFunc};
pub(crate) enum Outcome {
Emit(Vec<u8>),
Drop,
Error(String),
}
pub(crate) struct RunResult {
pub(crate) outcome: Outcome,
pub(crate) fuel_consumed: u64,
}
pub(crate) struct WasmInstance {
store: Store<HostState>,
memory: Memory,
alloc: TypedFunc<i32, i32>,
transform: TypedFunc<(i32, i32), i64>,
free: Option<TypedFunc<(i32, i32), ()>>,
error_ptr: Option<TypedFunc<(), i32>>,
error_len: Option<TypedFunc<(), i32>>,
fuel_limit: u64,
peak_memory: u64,
}
impl WasmInstance {
pub(crate) fn new(
mut store: Store<HostState>,
instance: Instance,
function: &str,
fuel_limit: u64,
) -> Result<Self, FaucetError> {
let memory = engine::memory_export(&mut store, &instance)?;
let alloc = instance
.get_typed_func::<i32, i32>(&mut store, "alloc")
.map_err(|e| {
FaucetError::Config(format!(
"wasm transform: missing or invalid 'alloc(i32) -> i32' export: {e}"
))
})?;
let transform = instance
.get_typed_func::<(i32, i32), i64>(&mut store, function)
.map_err(|e| {
FaucetError::Config(format!(
"wasm transform: missing or invalid '{function}(i32, i32) -> i64' export: {e}"
))
})?;
let free = instance
.get_typed_func::<(i32, i32), ()>(&mut store, "free")
.ok();
let error_ptr = instance
.get_typed_func::<(), i32>(&mut store, "error_ptr")
.ok();
let error_len = instance
.get_typed_func::<(), i32>(&mut store, "error_len")
.ok();
Ok(Self {
store,
memory,
alloc,
transform,
free,
error_ptr,
error_len,
fuel_limit,
peak_memory: 0,
})
}
pub(crate) fn peak_memory(&self) -> u64 {
self.peak_memory
}
pub(crate) fn run(&mut self, input: &[u8]) -> Result<RunResult, FaucetError> {
self.store.set_fuel(self.fuel_limit).map_err(|e| {
FaucetError::Transform(format!("wasm transform: could not set fuel: {e}"))
})?;
let len = match i32::try_from(input.len()) {
Ok(l) => l,
Err(_) => {
return Ok(self.finish(Outcome::Error(
"wasm transform: record exceeds the wasm32 4 GiB address space".to_owned(),
)));
}
};
let in_ptr = match self.alloc.call(&mut self.store, len) {
Ok(p) => p,
Err(e) => {
return Ok(self.finish(Outcome::Error(trap_msg("alloc", &e))));
}
};
if let Err(e) = self.memory.write(&mut self.store, in_ptr as usize, input) {
self.free_buf(in_ptr, len);
return Ok(self.finish(Outcome::Error(format!(
"wasm transform: failed to write input at offset {in_ptr}: {e}"
))));
}
let ret = match self.transform.call(&mut self.store, (in_ptr, len)) {
Ok(r) => r,
Err(e) => {
self.free_buf(in_ptr, len);
return Ok(self.finish(Outcome::Error(trap_msg("transform", &e))));
}
};
let outcome = match abi::classify(ret as u64) {
RawOutcome::Drop => {
self.free_buf(in_ptr, len);
Outcome::Drop
}
RawOutcome::Error => {
let msg = self.read_error();
self.free_buf(in_ptr, len);
Outcome::Error(msg)
}
RawOutcome::Emit { ptr, len: out_len } => match self.read_output(ptr, out_len) {
Ok(bytes) => {
self.free_out(ptr, out_len);
self.free_buf(in_ptr, len);
Outcome::Emit(bytes)
}
Err(msg) => {
self.free_buf(in_ptr, len);
Outcome::Error(msg)
}
},
};
Ok(self.finish(outcome))
}
fn finish(&mut self, outcome: Outcome) -> RunResult {
let fuel_consumed = self
.fuel_limit
.saturating_sub(self.store.get_fuel().unwrap_or(0));
let memory_bytes = self.memory.data_size(&self.store) as u64;
self.peak_memory = self.peak_memory.max(memory_bytes);
RunResult {
outcome,
fuel_consumed,
}
}
fn read_output(&self, ptr: u32, len: u32) -> Result<Vec<u8>, String> {
let data = self.memory.data(&self.store);
let start = ptr as usize;
let end = start.saturating_add(len as usize);
data.get(start..end).map(<[u8]>::to_vec).ok_or_else(|| {
format!(
"wasm transform: output slice [{start}, {end}) is out of bounds (memory is {} bytes)",
data.len()
)
})
}
fn read_error(&mut self) -> String {
let (Some(ep), Some(el)) = (&self.error_ptr, &self.error_len) else {
return "wasm transform: module signalled an error (no error_ptr/error_len exports)"
.to_owned();
};
let ptr = ep.call(&mut self.store, ()).unwrap_or(0);
let len = el.call(&mut self.store, ()).unwrap_or(0);
let data = self.memory.data(&self.store);
let start = ptr as usize;
let end = start.saturating_add(len as usize);
match data.get(start..end) {
Some(slice) => format!(
"wasm transform: module error: {}",
String::from_utf8_lossy(slice)
),
None => {
"wasm transform: module signalled an error (error pointer out of bounds)".to_owned()
}
}
}
fn free_buf(&mut self, ptr: i32, len: i32) {
if let Some(free) = &self.free {
let _ = free.call(&mut self.store, (ptr, len));
}
}
fn free_out(&mut self, ptr: u32, len: u32) {
if let (Some(free), Ok(p), Ok(l)) = (&self.free, i32::try_from(ptr), i32::try_from(len)) {
let _ = free.call(&mut self.store, (p, l));
}
}
}
fn trap_msg(ctx: &str, err: &wasmtime::Error) -> String {
format!("wasm transform: {ctx} failed: {err:?}")
}