holger-plugin-abi 0.1.0

holger package-handler plugin ABI: the wire contract a handler speaks whether it is linked in or loaded from a .wasm
Documentation
//! **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()
            }
        };
    };
}