crates/guest-host/src/convention.rs
//! **The calling convention** — the export set a guest speaks and the one packed
//! word both sides pass pointers in. Runtime-free, so a guest SDK compiled for
//! `wasm32-unknown-unknown` can depend on this module for the names.
//!
//! ```text
//! host ─ alloc(len) ─────────────▶ guest (reserve linear memory)
//! host ─ memory.write(ptr, arg) ─▶ guest (the argument, bytes)
//! host ─ plugin_call(op, ptr, len) ▶ guest (one verb, by number)
//! host ◀ u64: ptr << 32 | len ──── guest (the answer, bytes)
//! ```
//!
//! Payload encoding is the ABI's business, not this crate's.
/// Exports every guest must provide. A host names the missing one when a module
/// falls short, so a half-built module is a loud load failure rather than a
/// guest that answers nothing.
pub mod exports {
/// `alloc(size: u32) -> u32` — reserve linear memory the host writes into.
pub const ALLOC: &str = "alloc";
/// `dealloc(ptr: u32, len: u32)` — give a buffer back. A guest whose host
/// makes a fresh store per call may leak; a long-lived guest must not.
pub const DEALLOC: &str = "dealloc";
/// `result_len() -> u32` — byte length of the last pointer-only answer.
pub const RESULT_LEN: &str = "result_len";
/// `plugin_abi() -> u64` — the ABI range the guest speaks, packed
/// `min << 32 | max`.
pub const PLUGIN_ABI: &str = "plugin_abi";
/// `plugin_manifest() -> u32` — a pointer to the manifest bytes;
/// [`RESULT_LEN`] says how many.
pub const MANIFEST: &str = "plugin_manifest";
/// `plugin_call(op: u32, ptr: u32, len: u32) -> u64` — one verb by number,
/// the answer packed `ptr << 32 | len`.
pub const PLUGIN_CALL: &str = "plugin_call";
/// The linear memory, exported under this name.
pub const MEMORY: &str = "memory";
/// Every export of the `plugin_call` convention, in load-check order.
pub const ALL: &[&str] = &[ALLOC, DEALLOC, RESULT_LEN, PLUGIN_ABI, MANIFEST, PLUGIN_CALL];
}
/// The import namespace a guest reaches its host under.
pub const IMPORT_MODULE: &str = "env";
/// `ptr << 32 | len`. A host import answers `0` when the call itself failed —
/// never "absent"; absence is carried inside the payload.
pub const fn pack(ptr: u32, len: u32) -> u64 {
((ptr as u64) << 32) | (len as u64)
}
/// The inverse of [`pack`]: `(ptr, len)`.
pub const fn unpack(packed: u64) -> (u32, u32) {
((packed >> 32) as u32, (packed & 0xffff_ffff) as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pack_and_unpack_round_trip_the_whole_range() {
for (p, l) in [(0, 0), (1, 1), (u32::MAX, 0), (0, u32::MAX), (u32::MAX, u32::MAX), (0x1234_5678, 0x9abc_def0)] {
assert_eq!(unpack(pack(p, l)), (p, l));
}
assert_eq!(pack(0, 0), 0, "the failure word is zero");
}
#[test]
fn the_export_set_is_the_six_names() {
assert_eq!(exports::ALL.len(), 6);
assert!(exports::ALL.contains(&"plugin_call"));
assert!(!exports::ALL.contains(&exports::MEMORY), "memory is checked separately");
}
}