use crate::config::WasmTransformConfig;
use crate::instance::WasmInstance;
use crate::metrics;
use faucet_core::FaucetError;
use std::path::PathBuf;
use std::time::{Instant, SystemTime};
use wasmtime::{
Caller, Config, Engine, Extern, Linker, Memory, Module, Store, StoreLimits, StoreLimitsBuilder,
};
pub(crate) struct HostState {
pub(crate) limits: StoreLimits,
pub(crate) epoch_base: Instant,
}
pub(crate) struct WasmEngine {
engine: Engine,
module: Module,
linker: Linker<HostState>,
pub(crate) function: String,
pub(crate) memory_bytes: usize,
pub(crate) fuel_limit: u64,
pub(crate) module_label: String,
path: PathBuf,
mtime: Option<SystemTime>,
reload_on_change: bool,
}
impl WasmEngine {
pub(crate) fn compile(cfg: &WasmTransformConfig) -> Result<Self, FaucetError> {
let path = PathBuf::from(&cfg.module);
let module_label = cfg.module_label();
let bytes = std::fs::read(&path).map_err(|e| {
FaucetError::Config(format!(
"wasm transform: cannot read module '{}': {e}",
path.display()
))
})?;
let mtime = std::fs::metadata(&path).and_then(|m| m.modified()).ok();
let mut config = Config::new();
config.consume_fuel(true);
let engine = Engine::new(&config)
.map_err(|e| FaucetError::Config(format!("wasm transform: engine init failed: {e}")))?;
let compile_start = Instant::now();
let module = Module::new(&engine, &bytes).map_err(|e| {
FaucetError::Config(format!(
"wasm transform: failed to compile '{}': {e}",
path.display()
))
})?;
metrics::compile_duration(&module_label, compile_start.elapsed().as_secs_f64());
let linker = build_linker(&engine)?;
let memory_bytes = (cfg.memory_limit_mb as usize) * 1024 * 1024;
let engine_wrap = Self {
engine,
module,
linker,
function: cfg.function.clone(),
memory_bytes,
fuel_limit: cfg.fuel_limit,
module_label,
path,
mtime,
reload_on_change: cfg.reload_on_change,
};
let _probe = engine_wrap.new_page_instance()?;
Ok(engine_wrap)
}
pub(crate) fn reload_if_changed(&mut self) {
if !self.reload_on_change {
return;
}
let cur = std::fs::metadata(&self.path)
.and_then(|m| m.modified())
.ok();
if cur == self.mtime {
return;
}
match std::fs::read(&self.path) {
Ok(bytes) => {
let start = Instant::now();
match Module::new(&self.engine, &bytes) {
Ok(module) => {
metrics::compile_duration(
&self.module_label,
start.elapsed().as_secs_f64(),
);
self.module = module;
self.mtime = cur;
tracing::info!(
target: "faucet::transform::wasm",
module = %self.module_label,
"reloaded changed wasm module"
);
}
Err(e) => {
tracing::warn!(
target: "faucet::transform::wasm",
module = %self.module_label,
error = %e,
"wasm module changed but failed to recompile; keeping previous module"
);
}
}
}
Err(e) => {
tracing::warn!(
target: "faucet::transform::wasm",
module = %self.module_label,
error = %e,
"wasm module changed but became unreadable; keeping previous module"
);
}
}
}
pub(crate) fn new_page_instance(&self) -> Result<WasmInstance, FaucetError> {
let limits = StoreLimitsBuilder::new()
.memory_size(self.memory_bytes)
.trap_on_grow_failure(true)
.build();
let mut store = Store::new(
&self.engine,
HostState {
limits,
epoch_base: Instant::now(),
},
);
store.limiter(|state| &mut state.limits);
store.set_fuel(self.fuel_limit).map_err(|e| {
FaucetError::Config(format!("wasm transform: could not enable fuel: {e}"))
})?;
let instance = self
.linker
.instantiate(&mut store, &self.module)
.map_err(|e| {
FaucetError::Transform(format!(
"wasm transform: instantiation failed for '{}': {e}",
self.module_label
))
})?;
WasmInstance::new(store, instance, &self.function, self.fuel_limit)
}
}
fn build_linker(engine: &Engine) -> Result<Linker<HostState>, FaucetError> {
let mut linker = Linker::new(engine);
linker
.func_wrap(
"faucet_v1",
"log",
|mut caller: Caller<'_, HostState>, level: i32, ptr: i32, len: i32| {
let msg = read_host_string(&mut caller, ptr, len);
emit_log(level, &msg);
},
)
.map_err(|e| FaucetError::Config(format!("wasm transform: linker log: {e}")))?;
linker
.func_wrap(
"faucet_v1",
"now_ns",
|caller: Caller<'_, HostState>| -> i64 {
caller.data().epoch_base.elapsed().as_nanos() as i64
},
)
.map_err(|e| FaucetError::Config(format!("wasm transform: linker now_ns: {e}")))?;
Ok(linker)
}
fn emit_log(level: i32, msg: &str) {
match level {
0 => tracing::trace!(target: "faucet::transform::wasm::module", "{msg}"),
1 => tracing::debug!(target: "faucet::transform::wasm::module", "{msg}"),
2 => tracing::info!(target: "faucet::transform::wasm::module", "{msg}"),
3 => tracing::warn!(target: "faucet::transform::wasm::module", "{msg}"),
_ => tracing::error!(target: "faucet::transform::wasm::module", "{msg}"),
}
}
fn read_host_string(caller: &mut Caller<'_, HostState>, ptr: i32, len: i32) -> String {
let Some(Extern::Memory(mem)) = caller.get_export("memory") else {
return String::new();
};
let (ptr, len) = (ptr as usize, len as usize);
let data = mem.data(&caller);
match data.get(ptr..ptr.saturating_add(len)) {
Some(slice) => String::from_utf8_lossy(slice).into_owned(),
None => String::new(),
}
}
pub(crate) fn memory_export(
store: &mut Store<HostState>,
instance: &wasmtime::Instance,
) -> Result<Memory, FaucetError> {
instance.get_memory(store, "memory").ok_or_else(|| {
FaucetError::Config(
"wasm transform: module does not export a linear memory named 'memory'".to_owned(),
)
})
}