plugin/abi/src/guest.rs
//! **The guest side of the ABI, written once and generated per module.**
//!
//! A wasm module has to export `#[no_mangle] extern "C"` symbols, and those
//! cannot come from a generic — so without this macro every `-wasm` crate would
//! carry its own copy of the same ~130 lines of pointer juggling, and two copies
//! of an ABI is exactly how one half drifts from the other. [`export_package_handler!`]
//! emits all eight exports plus the host-backed [`crate::BlobStore`] from one
//! line, so a handler's wasm crate really is glue and nothing else (LAW 5).
//!
//! Everything it emits is `#[cfg(target_arch = "wasm32")]`. On any other target
//! the crate compiles to an empty library — which is what lets a `-wasm` crate be
//! an ordinary workspace member that `cargo check --workspace` covers, without a
//! native build trying to link `env::host_store_get`.
/// Generate the eight ABI exports for a [`crate::PackageHandler`].
///
/// ```ignore
/// holger_plugin_abi::export_package_handler!(holger_handler_skidbladnir::SkidbladnirHandler);
/// ```
///
/// The type must implement `PackageHandler` and be constructible with
/// `Default::default()`. Nothing else is required of the calling crate, and
/// nothing about the handler's *logic* passes through here — this macro moves
/// bytes and calls the handler.
#[macro_export]
macro_rules! export_package_handler {
($handler:ty) => {
#[cfg(target_arch = "wasm32")]
const _: () = {
use ::std::cell::{Cell, RefCell};
use ::std::vec::Vec;
use $crate::{
wire, BlobStore, PackageHandler, WireArtifactId, WireHttpRequest, WireListRequest,
WirePutRequest, Writer,
};
::std::thread_local! {
/// Every buffer handed across the ABI stays owned here for the
/// life of the instance: the host writes into these addresses
/// and reads results back out of them, so freeing one would be a
/// use-after-free across the boundary. The host creates a fresh
/// wasmtime `Store` per call, so this never grows beyond one
/// request's worth.
static ALLOCS: RefCell<Vec<Vec<u8>>> = const { RefCell::new(Vec::new()) };
/// Byte length of the value the last export returned a pointer
/// to, read by the host's `result_len()` immediately after.
static RESULT_LEN: Cell<u32> = const { Cell::new(0) };
}
// `wasm_import_module` MUST be spelled here and must match
// `$crate::imports::MODULE`. Without the attribute `rust-lld`
// treats these as ordinary undefined symbols and refuses to link
// the cdylib, rather than emitting them as wasm imports.
#[link(wasm_import_module = "env")]
extern "C" {
fn host_store_get(key_ptr: u32, key_len: u32) -> u64;
fn host_store_put(key_ptr: u32, key_len: u32, data_ptr: u32, data_len: u32) -> u64;
fn host_store_list(prefix_ptr: u32, prefix_len: u32) -> u64;
fn host_store_size(key_ptr: u32, key_len: u32) -> u64;
}
/// Reserve `size` bytes and return their address.
#[no_mangle]
pub extern "C" fn alloc(size: u32) -> u32 {
ALLOCS.with(|a| {
let mut buf = ::std::vec![0u8; size as usize];
let ptr = buf.as_mut_ptr() as u32;
a.borrow_mut().push(buf);
ptr
})
}
#[no_mangle]
pub extern "C" fn result_len() -> u32 {
RESULT_LEN.with(|l| l.get())
}
/// Copy `bytes` into linear memory, record the length, return the address.
fn ret(bytes: &[u8]) -> u32 {
let ptr = alloc(bytes.len() as u32);
// SAFETY: `ptr` was just returned by `alloc`, which reserved
// exactly `bytes.len()` bytes and keeps the buffer alive in ALLOCS.
unsafe {
::std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr as *mut u8, bytes.len());
}
RESULT_LEN.with(|l| l.set(bytes.len() as u32));
ptr
}
/// Borrow an argument the host wrote into linear memory.
///
/// # Safety
/// `ptr`/`len` must be an address previously returned by `alloc`,
/// holding at least `len` bytes — which is the host's half of the
/// contract for every export below.
unsafe fn arg<'a>(ptr: u32, len: u32) -> &'a [u8] {
::std::slice::from_raw_parts(ptr as *const u8, len as usize)
}
/// Unpack a `ptr << 32 | len` result from a host import.
/// `0` means the host call itself failed — never "absent", which is
/// carried inside the payload as an encoded `Option`.
fn unpack(packed: u64) -> Option<Vec<u8>> {
if packed == 0 {
return None;
}
let ptr = (packed >> 32) as u32;
let len = (packed & 0xffff_ffff) as usize;
// SAFETY: the host allocated this through our own `alloc`, so it
// is inside linear memory and owned by ALLOCS.
Some(unsafe { ::std::slice::from_raw_parts(ptr as *const u8, len) }.to_vec())
}
/// The store, reached through the four host imports. This is the
/// **only** thing that differs from the native backend: the handler
/// above it is the same code.
struct HostStore;
impl BlobStore for HostStore {
fn get(&self, key: &str) -> Result<Option<Vec<u8>>, String> {
let packed = unsafe { host_store_get(key.as_ptr() as u32, key.len() as u32) };
let raw = unpack(packed)
.ok_or_else(|| ::std::format!("host_store_get({key}) failed"))?;
wire::decode_store_get(&raw).map_err(|e| e.message())
}
fn put(&self, key: &str, data: &[u8]) -> Result<(), String> {
let packed = unsafe {
host_store_put(
key.as_ptr() as u32,
key.len() as u32,
data.as_ptr() as u32,
data.len() as u32,
)
};
let raw = unpack(packed)
.ok_or_else(|| ::std::format!("host_store_put({key}) failed"))?;
wire::decode_unit_response(&raw).map_err(|e| e.message())?
}
fn list(&self, prefix: &str) -> Result<Vec<(String, u64)>, String> {
let packed =
unsafe { host_store_list(prefix.as_ptr() as u32, prefix.len() as u32) };
let raw = unpack(packed)
.ok_or_else(|| ::std::format!("host_store_list({prefix}) failed"))?;
wire::decode_store_listing(&raw).map_err(|e| e.message())
}
fn size(&self, key: &str) -> Result<Option<u64>, String> {
let packed = unsafe { host_store_size(key.as_ptr() as u32, key.len() as u32) };
let raw = unpack(packed)
.ok_or_else(|| ::std::format!("host_store_size({key}) failed"))?;
let payload = wire::decode_store_get(&raw).map_err(|e| e.message())?;
match payload {
None => Ok(None),
Some(b) if b.len() == 8 => {
let mut a = [0u8; 8];
a.copy_from_slice(&b);
Ok(Some(u64::from_le_bytes(a)))
}
Some(b) => Err(::std::format!(
"host_store_size({key}) returned {} bytes, expected 8",
b.len()
)),
}
}
}
fn handler() -> $handler {
<$handler as ::std::default::Default>::default()
}
#[no_mangle]
pub extern "C" fn plugin_manifest() -> u32 {
ret(&handler().manifest().to_bytes())
}
/// # Safety
/// `ptr`/`len` must address an encoded `WireArtifactId` written by
/// the host through `alloc`.
#[no_mangle]
pub unsafe extern "C" fn plugin_fetch(ptr: u32, len: u32) -> u32 {
// A malformed argument is reported as an Err on the wire, not as
// "no such artifact": the host must be able to tell an ABI skew
// from a genuine miss.
let out = match WireArtifactId::from_bytes(unsafe { arg(ptr, len) }) {
Ok(id) => handler().fetch(&HostStore, &id),
Err(e) => Err(e.message()),
};
ret(&wire::encode_fetch_response(&out))
}
/// # Safety
/// As [`plugin_fetch`], for an encoded `WirePutRequest`.
#[no_mangle]
pub unsafe extern "C" fn plugin_put(ptr: u32, len: u32) -> u32 {
let out = match WirePutRequest::from_bytes(unsafe { arg(ptr, len) }) {
Ok(req) => handler().put(&HostStore, &req.id, &req.data),
Err(e) => Err(e.message()),
};
ret(&wire::encode_unit_response(&out))
}
/// # Safety
/// As [`plugin_fetch`], for an encoded `WireListRequest`.
#[no_mangle]
pub unsafe extern "C" fn plugin_list(ptr: u32, len: u32) -> u32 {
let out = match WireListRequest::from_bytes(unsafe { arg(ptr, len) }) {
Ok(req) => {
handler().list(&HostStore, req.name_filter.as_deref(), req.limit as usize)
}
Err(e) => Err(e.message()),
};
ret(&wire::encode_list_response(&out))
}
/// # Safety
/// As [`plugin_fetch`], for a length-prefixed suburl string.
#[no_mangle]
pub unsafe extern "C" fn plugin_coordinate_for_path(ptr: u32, len: u32) -> u32 {
let mut r = $crate::Reader::new(unsafe { arg(ptr, len) });
let out = match r
.str("suburl")
.and_then(|s| r.expect_end("suburl").map(|_| s))
{
Ok(suburl) => handler().coordinate_for_path(&suburl),
// This verb's return type has no error arm — it is the
// quarantine gate's lookup, whose `None` means "not gated by
// coordinate". A skewed argument therefore loses a gate
// check; it can never serve bytes that should not have been.
Err(_) => None,
};
ret(&wire::encode_coordinate_response(&out))
}
/// # Safety
/// As [`plugin_fetch`], for an encoded `WireHttpRequest`.
#[no_mangle]
pub unsafe extern "C" fn plugin_http(ptr: u32, len: u32) -> u32 {
let out = match WireHttpRequest::from_bytes(unsafe { arg(ptr, len) }) {
Ok(req) => handler().http(&HostStore, &req),
Err(e) => Err(e.message()),
};
ret(&wire::encode_http_response(&out))
}
// Silence "unused" for the one import a handler may not reach.
#[allow(dead_code)]
fn _writer_is_used() -> Writer {
Writer::new()
}
};
};
}