use crate::HashMap;
use crate::component::host::{HostContext, HostValue, METHOD_RET_WORDS};
use crate::component::loader::{ComponentBackend, ComponentError};
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock, Mutex};
use veryl_component_sys as sys;
use veryl_component_sys::wasm32::{VALUE_SIZE, VrlValue32};
use wasmtime::{Caller, Engine, Extern, Linker, Memory, Module, Store, TypedFunc};
const EPOCH_TICK: std::time::Duration = std::time::Duration::from_millis(100);
const CALL_DEADLINE_TICKS: u64 = 600;
static ENGINE: LazyLock<Engine> = LazyLock::new(|| {
let mut config = wasmtime::Config::new();
config.epoch_interruption(true);
let engine = Engine::new(&config).expect("wasmtime config is valid");
let ticker = engine.clone();
std::thread::Builder::new()
.name("veryl-wasm-epoch".to_string())
.spawn(move || {
loop {
std::thread::sleep(EPOCH_TICK);
ticker.increment_epoch();
}
})
.expect("epoch ticker thread spawns");
engine
});
fn arm_call_deadline(store: &mut Store<StoreCtx>) {
store.set_epoch_deadline(CALL_DEADLINE_TICKS);
}
static LINKER: LazyLock<Linker<StoreCtx>> = LazyLock::new(|| {
let mut linker = Linker::new(&ENGINE);
add_host_imports(&mut linker).expect("host import signatures are valid");
linker
});
struct StoreCtx {
host: *mut HostContext,
memory: Option<Memory>,
file_allowed: bool,
limits: wasmtime::StoreLimits,
}
unsafe impl Send for StoreCtx {}
const MEMORY_LIMIT: usize = 256 << 20;
fn new_store() -> Store<StoreCtx> {
let mut store = Store::new(
&ENGINE,
StoreCtx {
host: std::ptr::null_mut(),
memory: None,
file_allowed: false,
limits: wasmtime::StoreLimitsBuilder::new()
.memory_size(MEMORY_LIMIT)
.build(),
},
);
store.limiter(|ctx| &mut ctx.limits);
arm_call_deadline(&mut store);
store
}
pub struct WasmLibrary {
path: PathBuf,
module: Module,
}
pub fn get_wasm_library(path: &Path) -> Result<Arc<WasmLibrary>, ComponentError> {
static LIBRARIES: LazyLock<Mutex<HashMap<PathBuf, Arc<WasmLibrary>>>> =
LazyLock::new(|| Mutex::new(HashMap::default()));
let mut libraries = LIBRARIES.lock().unwrap();
if let Some(library) = libraries.get(path) {
return Ok(library.clone());
}
let load_err = |reason: String| ComponentError::LibraryLoad {
path: path.to_path_buf(),
reason,
};
let bytes = std::fs::read(path).map_err(|e| load_err(e.to_string()))?;
let module = Module::new(&ENGINE, &bytes).map_err(|e| load_err(e.to_string()))?;
let library = Arc::new(WasmLibrary {
path: path.to_path_buf(),
module,
});
libraries.insert(path.to_path_buf(), library.clone());
Ok(library)
}
#[derive(Clone)]
struct GuestFuncs {
abi_version: TypedFunc<(), u32>,
kind: TypedFunc<(u32, u32), u32>,
create: TypedFunc<(u32, u32), u32>,
destroy: TypedFunc<u32, ()>,
on_init: TypedFunc<u32, i32>,
on_reset: TypedFunc<u32, i32>,
on_clock: TypedFunc<u32, i32>,
on_finish: TypedFunc<u32, i32>,
call_method: TypedFunc<(u32, u32, u32, u32, u32, u32), i32>,
alloc: TypedFunc<u32, u32>,
free: TypedFunc<(u32, u32), ()>,
}
impl WasmLibrary {
fn instantiate(&self) -> Result<(Store<StoreCtx>, Memory, GuestFuncs), ComponentError> {
let load_err = |reason: String| ComponentError::LibraryLoad {
path: self.path.clone(),
reason,
};
let mut store = new_store();
let instance = LINKER
.instantiate(&mut store, &self.module)
.map_err(|e| load_err(e.to_string()))?;
let memory = instance
.get_memory(&mut store, "memory")
.ok_or_else(|| load_err("guest exports no memory".to_string()))?;
store.data_mut().memory = Some(memory);
fn typed<P, R>(
store: &mut Store<StoreCtx>,
instance: &wasmtime::Instance,
name: &str,
path: &Path,
) -> Result<TypedFunc<P, R>, ComponentError>
where
P: wasmtime::WasmParams,
R: wasmtime::WasmResults,
{
instance
.get_typed_func::<P, R>(store, name)
.map_err(|e| ComponentError::LibraryLoad {
path: path.to_path_buf(),
reason: format!("missing or mistyped export `{name}`: {e}"),
})
}
let funcs = GuestFuncs {
abi_version: typed(
&mut store,
&instance,
"veryl_component_abi_version",
&self.path,
)?,
kind: typed(&mut store, &instance, "veryl_component_kind", &self.path)?,
create: typed(&mut store, &instance, "veryl_component_create", &self.path)?,
destroy: typed(&mut store, &instance, "veryl_component_destroy", &self.path)?,
on_init: typed(&mut store, &instance, "veryl_component_on_init", &self.path)?,
on_reset: typed(
&mut store,
&instance,
"veryl_component_on_reset",
&self.path,
)?,
on_clock: typed(
&mut store,
&instance,
"veryl_component_on_clock",
&self.path,
)?,
on_finish: typed(
&mut store,
&instance,
"veryl_component_on_finish",
&self.path,
)?,
call_method: typed(
&mut store,
&instance,
"veryl_component_call_method",
&self.path,
)?,
alloc: typed(&mut store, &instance, "veryl_component_alloc", &self.path)?,
free: typed(&mut store, &instance, "veryl_component_free", &self.path)?,
};
Ok((store, memory, funcs))
}
}
pub(crate) fn lookup_wasm_component(
path: &Path,
type_name: &str,
) -> Result<ComponentBackend, ComponentError> {
let library = get_wasm_library(path)?;
let (mut store, memory, funcs) = library.instantiate()?;
let load_err = |reason: String| ComponentError::LibraryLoad {
path: path.to_path_buf(),
reason,
};
let abi = funcs
.abi_version
.call(&mut store, ())
.map_err(|e| load_err(format!("abi_version probe trapped: {}", trap_text(&e))))?;
if abi != sys::VRL_COMPONENT_ABI_VERSION {
return Err(ComponentError::AbiMismatch {
name: type_name.to_string(),
found: abi,
expected: sys::VRL_COMPONENT_ABI_VERSION,
});
}
let name = type_name.as_bytes();
let name_ptr = guest_alloc_write(&mut store, memory, &funcs, name)
.map_err(|e| load_err(format!("kind probe failed: {}", trap_text(&e))))?;
let kind = funcs
.kind
.call(&mut store, (name_ptr, name.len() as u32))
.map_err(|e| load_err(format!("kind probe trapped: {}", trap_text(&e))))?;
if kind == u32::MAX {
return Err(ComponentError::UnknownType {
name: type_name.to_string(),
path: Some(path.to_path_buf()),
available: crate::component::loader::library_export_names(path),
});
}
let manifest = match crate::component::loader::library_manifest(path) {
Some(json) => crate::component::loader::parse_library_manifest_json(&json, type_name)
.map_err(load_err)?,
None => None,
};
let mut file_allowed = true;
if let Some(manifest) = manifest {
if manifest.requires.iter().any(|r| r == "native") {
return Err(ComponentError::WasmNativeComponent {
name: type_name.to_string(),
});
}
file_allowed = manifest.requires.iter().any(|r| r == "file");
}
Ok(ComponentBackend::Wasm {
library,
type_name: type_name.to_string(),
kind,
file_allowed,
})
}
fn trap_text(e: &wasmtime::Error) -> String {
match e.downcast_ref::<wasmtime::Trap>() {
Some(trap) => trap.to_string(),
None => e.to_string(),
}
}
fn guest_alloc_write(
store: &mut Store<StoreCtx>,
memory: Memory,
funcs: &GuestFuncs,
bytes: &[u8],
) -> Result<u32, wasmtime::Error> {
arm_call_deadline(store);
let ptr = funcs.alloc.call(&mut *store, bytes.len() as u32)?;
if ptr == 0 {
return Err(wasmtime::Error::msg("guest allocation failed"));
}
memory.write(&mut *store, ptr as usize, bytes)?;
Ok(ptr)
}
pub struct WasmInstance {
store: Store<StoreCtx>,
memory: Memory,
funcs: GuestFuncs,
handle: u32,
kind: u32,
}
impl WasmInstance {
pub(crate) fn create(
library: &WasmLibrary,
type_name: &str,
kind: u32,
file_allowed: bool,
host: &mut HostContext,
) -> Result<Self, ComponentError> {
let (mut store, memory, funcs) = library.instantiate()?;
store.data_mut().file_allowed = file_allowed;
let name = type_name.as_bytes();
let name_ptr = guest_alloc_write(&mut store, memory, &funcs, name).map_err(|e| {
ComponentError::LibraryLoad {
path: library.path.clone(),
reason: trap_text(&e),
}
})?;
store.data_mut().host = host;
arm_call_deadline(&mut store);
let created = funcs.create.call(&mut store, (name_ptr, name.len() as u32));
store.data_mut().host = std::ptr::null_mut();
arm_call_deadline(&mut store);
let _ = funcs.free.call(&mut store, (name_ptr, name.len() as u32));
let handle = match created {
Ok(handle) => handle,
Err(e) => {
let mut messages = host.take_failures();
if messages.is_empty() {
messages.push(format!("component trapped: {}", trap_text(&e)));
}
return Err(ComponentError::CreateFailed {
messages: messages.join("; "),
});
}
};
if handle == 0 {
return Err(ComponentError::CreateFailed {
messages: host.take_failures().join("; "),
});
}
Ok(Self {
store,
memory,
funcs,
handle,
kind,
})
}
pub(crate) fn kind(&self) -> u32 {
self.kind
}
fn call_hook(&mut self, f: TypedFunc<u32, i32>, host: &mut HostContext) -> i32 {
self.store.data_mut().host = host;
arm_call_deadline(&mut self.store);
let result = f.call(&mut self.store, self.handle);
self.store.data_mut().host = std::ptr::null_mut();
match result {
Ok(rc) => rc,
Err(e) => {
host.svc_fail(&format!("component trapped: {}", trap_text(&e)));
1
}
}
}
pub(crate) fn on_init(&mut self, host: &mut HostContext) -> i32 {
self.call_hook(self.funcs.on_init.clone(), host)
}
pub(crate) fn on_reset(&mut self, host: &mut HostContext) -> i32 {
self.call_hook(self.funcs.on_reset.clone(), host)
}
pub(crate) fn on_clock(&mut self, host: &mut HostContext) -> i32 {
self.call_hook(self.funcs.on_clock.clone(), host)
}
pub(crate) fn on_finish(&mut self, host: &mut HostContext) -> i32 {
self.call_hook(self.funcs.on_finish.clone(), host)
}
fn alloc_tracked(
&mut self,
bytes: &[u8],
allocs: &mut Vec<(u32, u32)>,
) -> Result<u32, wasmtime::Error> {
let ptr = guest_alloc_write(&mut self.store, self.memory, &self.funcs, bytes)?;
allocs.push((ptr, bytes.len() as u32));
Ok(ptr)
}
fn free_tracked(&mut self, allocs: &[(u32, u32)]) {
arm_call_deadline(&mut self.store);
for (ptr, size) in allocs {
let _ = self.funcs.free.call(&mut self.store, (*ptr, *size));
}
}
pub(crate) fn call_method(
&mut self,
host: &mut HostContext,
name: &str,
args: &[HostValue],
) -> Option<HostValue> {
let mut allocs = vec![];
let prepared = self.prepare_method_call(name, args, &mut allocs);
let (name_ptr, args_ptr, ret_ptr, ret_words_ptr) = match prepared {
Ok(x) => x,
Err(e) => {
self.free_tracked(&allocs);
host.svc_fail(&format!("method call setup failed: {}", trap_text(&e)));
return None;
}
};
self.store.data_mut().host = host;
arm_call_deadline(&mut self.store);
let result = self.funcs.call_method.call(
&mut self.store,
(
self.handle,
name_ptr,
name.len() as u32,
args_ptr,
args.len() as u32,
ret_ptr,
),
);
self.store.data_mut().host = std::ptr::null_mut();
let value = match result {
Ok(0) => self.decode_return(ret_ptr, ret_words_ptr),
Ok(_) => None,
Err(e) => {
host.svc_fail(&format!("component trapped: {}", trap_text(&e)));
None
}
};
self.free_tracked(&allocs);
value
}
fn prepare_method_call(
&mut self,
name: &str,
args: &[HostValue],
allocs: &mut Vec<(u32, u32)>,
) -> Result<(u32, u32, u32, u32), wasmtime::Error> {
let name_ptr = self.alloc_tracked(name.as_bytes(), allocs)?;
let mut encoded = Vec::with_capacity(args.len() * VALUE_SIZE as usize);
for arg in args {
let v32 = match arg {
HostValue::Bits { words, width } => {
let ptr = self.alloc_tracked(&words_to_bytes(words), allocs)?;
VrlValue32 {
kind: sys::VRL_VALUE_BITS,
width: *width,
words: ptr,
nwords: words.len() as u32,
..Default::default()
}
}
HostValue::Str(s) => {
let ptr = self.alloc_tracked(s.as_bytes(), allocs)?;
VrlValue32 {
kind: sys::VRL_VALUE_STRING,
str_ptr: ptr,
str_len: s.len() as u32,
..Default::default()
}
}
HostValue::Unit => VrlValue32 {
kind: sys::VRL_VALUE_UNIT,
..Default::default()
},
};
encoded.extend_from_slice(&v32.to_le_bytes());
}
let args_ptr = if args.is_empty() {
0
} else {
self.alloc_tracked(&encoded, allocs)?
};
let ret_words = self.alloc_tracked(&[0u8; METHOD_RET_WORDS * 8], allocs)?;
let ret_v32 = VrlValue32 {
kind: sys::VRL_VALUE_UNIT,
words: ret_words,
nwords: METHOD_RET_WORDS as u32,
..Default::default()
};
let ret_ptr = self.alloc_tracked(&ret_v32.to_le_bytes(), allocs)?;
Ok((name_ptr, args_ptr, ret_ptr, ret_words))
}
fn decode_return(&mut self, ret_ptr: u32, ret_words_ptr: u32) -> Option<HostValue> {
let mut bytes = [0u8; VALUE_SIZE as usize];
self.memory
.read(&self.store, ret_ptr as usize, &mut bytes)
.ok()?;
let v32 = VrlValue32::from_le_bytes(&bytes);
match v32.kind {
sys::VRL_VALUE_BITS => {
let nwords = (v32.nwords as usize).min(METHOD_RET_WORDS);
let mut payload = vec![0u8; nwords * 8];
self.memory
.read(&self.store, ret_words_ptr as usize, &mut payload)
.ok()?;
Some(HostValue::Bits {
words: bytes_to_words(&payload),
width: v32.width,
})
}
_ => Some(HostValue::Unit),
}
}
}
impl Drop for WasmInstance {
fn drop(&mut self) {
arm_call_deadline(&mut self.store);
let _ = self.funcs.destroy.call(&mut self.store, self.handle);
}
}
fn words_to_bytes(words: &[u64]) -> Vec<u8> {
words.iter().flat_map(|w| w.to_le_bytes()).collect()
}
fn bytes_to_words(bytes: &[u8]) -> Vec<u64> {
bytes
.chunks(8)
.map(|c| {
let mut b = [0u8; 8];
b[..c.len()].copy_from_slice(c);
u64::from_le_bytes(b)
})
.collect()
}
fn host_of<'a>(caller: &Caller<'_, StoreCtx>) -> Option<&'a mut HostContext> {
let ptr = caller.data().host;
(!ptr.is_null()).then(|| unsafe { &mut *ptr })
}
fn memory_of(caller: &mut Caller<'_, StoreCtx>) -> Result<Memory, wasmtime::Error> {
if let Some(memory) = caller.data().memory {
return Ok(memory);
}
match caller.get_export("memory") {
Some(Extern::Memory(memory)) => Ok(memory),
_ => Err(wasmtime::Error::msg("guest exports no memory")),
}
}
fn check_guest_range(
memory: Memory,
caller: &Caller<'_, StoreCtx>,
ptr: u32,
len: u32,
) -> Result<(), wasmtime::Error> {
let in_range = (ptr as usize)
.checked_add(len as usize)
.is_some_and(|end| end <= memory.data_size(caller));
if in_range {
Ok(())
} else {
Err(wasmtime::Error::msg("guest pointer out of range"))
}
}
fn guest_bytes(
memory: Memory,
caller: &Caller<'_, StoreCtx>,
ptr: u32,
len: u32,
) -> Result<Vec<u8>, wasmtime::Error> {
check_guest_range(memory, caller, ptr, len)?;
let mut buf = vec![0u8; len as usize];
memory.read(caller, ptr as usize, &mut buf)?;
Ok(buf)
}
fn guest_str(
memory: Memory,
caller: &Caller<'_, StoreCtx>,
ptr: u32,
len: u32,
) -> Result<String, wasmtime::Error> {
let bytes = guest_bytes(memory, caller, ptr, len)?;
Ok(String::from_utf8_lossy(&bytes).into_owned())
}
type WResult<T> = Result<T, wasmtime::Error>;
fn add_host_imports(linker: &mut Linker<StoreCtx>) -> WResult<()> {
let m = sys::VRL_WASM_IMPORT_MODULE;
linker.func_wrap(
m,
"port_index",
|mut caller: Caller<'_, StoreCtx>,
name_ptr: u32,
name_len: u32,
dir: u32|
-> WResult<i32> {
let memory = memory_of(&mut caller)?;
let name = guest_str(memory, &caller, name_ptr, name_len)?;
Ok(host_of(&caller).map_or(-1, |h| h.svc_port_index(&name, dir)))
},
)?;
linker.func_wrap(
m,
"port_width",
|caller: Caller<'_, StoreCtx>, idx: u32| -> u32 {
host_of(&caller).map_or(0, |h| h.svc_port_width(idx))
},
)?;
linker.func_wrap(
m,
"read_input",
|mut caller: Caller<'_, StoreCtx>,
idx: u32,
words_ptr: u32,
mask_xz_ptr: u32|
-> WResult<()> {
let memory = memory_of(&mut caller)?;
let bytes = {
let Some(host) = host_of(&caller) else {
return Ok(());
};
let Some(words) = host.svc_input_words(idx) else {
return Ok(());
};
let mask_xz = host.svc_input_mask_xz(idx).unwrap_or(&[]);
(words_to_bytes(words), words_to_bytes(mask_xz))
};
memory.write(&mut caller, words_ptr as usize, &bytes.0)?;
if mask_xz_ptr != 0 {
memory.write(&mut caller, mask_xz_ptr as usize, &bytes.1)?;
}
Ok(())
},
)?;
linker.func_wrap(
m,
"write_output",
|mut caller: Caller<'_, StoreCtx>,
idx: u32,
words_ptr: u32,
mask_xz_ptr: u32|
-> WResult<()> {
let memory = memory_of(&mut caller)?;
let Some(n) = host_of(&caller).and_then(|h| h.svc_port_words_len(idx)) else {
return Ok(());
};
let words = bytes_to_words(&guest_bytes(memory, &caller, words_ptr, (n * 8) as u32)?);
let mask_xz = if mask_xz_ptr == 0 {
None
} else {
Some(bytes_to_words(&guest_bytes(
memory,
&caller,
mask_xz_ptr,
(n * 8) as u32,
)?))
};
if let Some(host) = host_of(&caller) {
host.svc_write_output(idx, &words, mask_xz.as_deref());
}
Ok(())
},
)?;
linker.func_wrap(
m,
"param_get",
|mut caller: Caller<'_, StoreCtx>,
name_ptr: u32,
name_len: u32,
out: u32,
buf: u32,
buf_cap: u32|
-> WResult<i64> {
let memory = memory_of(&mut caller)?;
let name = guest_str(memory, &caller, name_ptr, name_len)?;
let Some(host) = host_of(&caller) else {
return Ok(-1);
};
let Some(value) = host.svc_param(&name) else {
return Ok(-1);
};
let (payload, v32) = match value {
HostValue::Bits { words, width } => (
words_to_bytes(words),
VrlValue32 {
kind: sys::VRL_VALUE_BITS,
width: *width,
words: buf,
nwords: words.len() as u32,
..Default::default()
},
),
HostValue::Str(s) => (
s.clone().into_bytes(),
VrlValue32 {
kind: sys::VRL_VALUE_STRING,
str_ptr: buf,
str_len: s.len() as u32,
..Default::default()
},
),
HostValue::Unit => (
vec![],
VrlValue32 {
kind: sys::VRL_VALUE_UNIT,
..Default::default()
},
),
};
let required = payload.len() as i64;
if required <= buf_cap as i64 {
memory.write(&mut caller, buf as usize, &payload)?;
memory.write(&mut caller, out as usize, &v32.to_le_bytes())?;
}
Ok(required)
},
)?;
linker.func_wrap(
m,
"fail",
|mut caller: Caller<'_, StoreCtx>, msg_ptr: u32, msg_len: u32| -> WResult<()> {
let memory = memory_of(&mut caller)?;
let msg = guest_str(memory, &caller, msg_ptr, msg_len)?;
if let Some(host) = host_of(&caller) {
host.svc_fail(&msg);
}
Ok(())
},
)?;
linker.func_wrap(m, "finish", |caller: Caller<'_, StoreCtx>| {
if let Some(host) = host_of(&caller) {
host.svc_finish();
}
})?;
linker.func_wrap(
m,
"log",
|mut caller: Caller<'_, StoreCtx>, msg_ptr: u32, msg_len: u32| -> WResult<()> {
let memory = memory_of(&mut caller)?;
let msg = guest_str(memory, &caller, msg_ptr, msg_len)?;
if let Some(host) = host_of(&caller) {
host.svc_log(&msg);
}
Ok(())
},
)?;
linker.func_wrap(m, "cycle", |caller: Caller<'_, StoreCtx>| -> u64 {
host_of(&caller).map_or(0, |h| h.cycle)
})?;
linker.func_wrap(m, "sim_time", |caller: Caller<'_, StoreCtx>| -> u64 {
host_of(&caller).map_or(0, |h| h.time)
})?;
linker.func_wrap(m, "seed", |caller: Caller<'_, StoreCtx>| -> u64 {
host_of(&caller).map_or(0, |h| h.seed)
})?;
linker.func_wrap(m, "is_4state", |caller: Caller<'_, StoreCtx>| -> u32 {
host_of(&caller).map_or(0, |h| u32::from(h.use_4state))
})?;
linker.func_wrap(m, "fired_clock", |caller: Caller<'_, StoreCtx>| -> u32 {
host_of(&caller).map_or(0, |h| h.fired_clock)
})?;
linker.func_wrap(
m,
"file_open",
|mut caller: Caller<'_, StoreCtx>,
path_ptr: u32,
path_len: u32,
mode: u32|
-> WResult<i32> {
let memory = memory_of(&mut caller)?;
let path = guest_str(memory, &caller, path_ptr, path_len)?;
let file_allowed = caller.data().file_allowed;
Ok(host_of(&caller).map_or(-1, |h| {
if !file_allowed {
h.svc_fail(
"component performs file I/O but its manifest does not declare `requires(file)`",
);
return -1;
}
h.svc_file_open(&path, mode)
}))
},
)?;
linker.func_wrap(
m,
"file_read",
|mut caller: Caller<'_, StoreCtx>, handle: i32, buf: u32, len: u32| -> WResult<i64> {
let memory = memory_of(&mut caller)?;
check_guest_range(memory, &caller, buf, len)?;
let Some(host) = host_of(&caller) else {
return Ok(-1);
};
let mut tmp = vec![0u8; len as usize];
let n = host.svc_file_read(handle, &mut tmp);
if n > 0 {
memory.write(&mut caller, buf as usize, &tmp[..n as usize])?;
}
Ok(n)
},
)?;
linker.func_wrap(
m,
"file_write",
|mut caller: Caller<'_, StoreCtx>, handle: i32, buf: u32, len: u32| -> WResult<i64> {
let memory = memory_of(&mut caller)?;
let bytes = guest_bytes(memory, &caller, buf, len)?;
Ok(host_of(&caller).map_or(-1, |h| h.svc_file_write(handle, &bytes)))
},
)?;
linker.func_wrap(
m,
"file_seek",
|caller: Caller<'_, StoreCtx>, handle: i32, pos: i64, whence: u32| -> i64 {
host_of(&caller).map_or(-1, |h| h.svc_file_seek(handle, pos, whence))
},
)?;
linker.func_wrap(
m,
"file_close",
|caller: Caller<'_, StoreCtx>, handle: i32| {
if let Some(host) = host_of(&caller) {
host.svc_file_close(handle);
}
},
)?;
linker.func_wrap(
m,
"trace_var",
|mut caller: Caller<'_, StoreCtx>,
name_ptr: u32,
name_len: u32,
width: u32|
-> WResult<i32> {
let memory = memory_of(&mut caller)?;
let name = guest_str(memory, &caller, name_ptr, name_len)?;
Ok(host_of(&caller).map_or(-1, |h| h.svc_trace_var(&name, width)))
},
)?;
linker.func_wrap(
m,
"trace_write",
|mut caller: Caller<'_, StoreCtx>, handle: i32, words_ptr: u32| -> WResult<()> {
let memory = memory_of(&mut caller)?;
let Some(host) = host_of(&caller) else {
return Ok(());
};
let Some(n) = host.svc_trace_words_len(handle) else {
return Ok(());
};
let bytes = guest_bytes(memory, &caller, words_ptr, (n * 8) as u32)?;
host.svc_trace_write(handle, &bytes_to_words(&bytes));
Ok(())
},
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::lookup_wasm_component;
use crate::component::host::{ExternalInstance, HostContext, PortDir};
#[test]
fn component_wasm_transport_preserves_masks_trace_and_files() {
let dir = tempfile::tempdir().unwrap();
let wasm = dir.path().join("component.wasm");
std::fs::write(
&wasm,
br#"(module
(import "veryl" "port_index"
(func $port_index (param i32 i32 i32) (result i32)))
(import "veryl" "write_output"
(func $write_output (param i32 i32 i32)))
(import "veryl" "read_input"
(func $read_input (param i32 i32 i32)))
(import "veryl" "trace_var"
(func $trace_var (param i32 i32 i32) (result i32)))
(import "veryl" "trace_write"
(func $trace_write (param i32 i32)))
(import "veryl" "file_open"
(func $file_open (param i32 i32 i32) (result i32)))
(import "veryl" "file_write"
(func $file_write (param i32 i32 i32) (result i64)))
(import "veryl" "file_close" (func $file_close (param i32)))
(memory (export "memory") 1)
(data (i32.const 0) "outtraceartifact.binokin")
(global $heap (mut i32) (i32.const 1024))
(global $output (mut i32) (i32.const -1))
(global $input (mut i32) (i32.const -1))
(global $trace (mut i32) (i32.const -1))
(func (export "veryl_component_abi_version") (result i32)
i32.const 1)
(func (export "veryl_component_kind") (param i32 i32) (result i32)
i32.const 1)
(func (export "veryl_component_alloc") (param $size i32) (result i32)
(local $old i32)
global.get $heap
local.set $old
global.get $heap
local.get $size
i32.add
global.set $heap
local.get $old)
(func (export "veryl_component_free") (param i32 i32))
(func (export "veryl_component_create") (param i32 i32) (result i32)
i32.const 0
i32.const 3
i32.const 1
call $port_index
global.set $output
i32.const 22
i32.const 2
i32.const 0
call $port_index
global.set $input
i32.const 3
i32.const 5
i32.const 8
call $trace_var
global.set $trace
i32.const 1)
(func (export "veryl_component_destroy") (param i32))
(func (export "veryl_component_on_init") (param i32) (result i32)
(local $file i32)
i32.const 8
i32.const 12
i32.const 1
call $file_open
local.tee $file
i32.const 20
i32.const 2
call $file_write
drop
local.get $file
call $file_close
i32.const 0)
(func (export "veryl_component_on_reset") (param i32) (result i32)
global.get $input
i32.const 80
i32.const 0
call $read_input
global.get $output
i32.const 80
i32.const 0
call $write_output
i32.const 0)
(func (export "veryl_component_on_clock") (param i32) (result i32)
i32.const 64
i64.const 90
i64.store
i32.const 72
i64.const 15
i64.store
global.get $output
i32.const 64
i32.const 72
call $write_output
global.get $trace
i32.const 64
call $trace_write
i32.const 0)
(func (export "veryl_component_on_finish") (param i32) (result i32)
i32.const 0)
(func (export "veryl_component_call_method")
(param i32 i32 i32 i32 i32 i32) (result i32)
i32.const 0)
)"#,
)
.unwrap();
let mut host = HostContext::new();
host.use_4state = true;
host.write_base = Some(dir.path().to_path_buf());
host.add_port("out", PortDir::Output, 8);
let input = host.add_port("in", PortDir::Input, 8);
host.set_input_masked(input, &[0xa5], &[0xff]);
let backend = lookup_wasm_component(&wasm, "fixture").unwrap();
let mut instance = ExternalInstance::create(backend, &mut host).unwrap();
assert_eq!(instance.on_init(&mut host), 0);
assert_eq!(
std::fs::read(dir.path().join("artifact.bin")).unwrap(),
b"ok"
);
assert_eq!(instance.on_clock(&mut host), 0);
assert_eq!(host.output_words("out"), &[0x5a]);
assert_eq!(host.output_mask_xz("out"), &[0x0f]);
assert_eq!(host.trace_vars[0].name, "trace");
assert_eq!(host.trace_vars[0].words, [0x5a]);
assert_eq!(instance.on_reset(&mut host), 0);
assert_eq!(host.output_words("out"), &[0xa5]);
assert_eq!(host.output_mask_xz("out"), &[0]);
}
}