Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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");
    }
}