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

nordisk/holger

plugin/abi/src/guest_meter.rs

//! **The guest side of ABI v2 for a Meter**, written once and generated per
//! module — the same reason [`crate::export_package_handler`] exists.
//!
//! One thing here is not cosmetic. The macro takes the module's capability list
//! and emits **only the imports that list unlocks**. A guest declaring
//! `caps = [usage_read, store_list]` contains no `host_audit_write` import at
//! all, so the manifest it publishes and the import list a host reads out of its
//! `.wasm` are the same statement made twice — which is exactly what gate B
//! ([`crate::v2::check_grants`]) checks. A macro that emitted every import would
//! have made every guest ask for every capability.
//!
//! Everything emitted is `#[cfg(target_arch = "wasm32")]`; on any other target
//! the crate compiles to nothing, so a meter's `-wasm` crate is an ordinary
//! workspace member that `cargo check --workspace` covers.

/// One capability's `env` imports. Item position, so each expansion is its own
/// `extern` block.
#[doc(hidden)]
#[macro_export]
macro_rules! __meter_cap_extern {
    (store_read) => {
        #[link(wasm_import_module = "env")]
        extern "C" {
            fn host_store_get(key_ptr: u32, key_len: u32) -> u64;
            fn host_store_size(key_ptr: u32, key_len: u32) -> u64;
        }
    };
    (store_write) => {
        #[link(wasm_import_module = "env")]
        extern "C" {
            fn host_store_put(key_ptr: u32, key_len: u32, data_ptr: u32, data_len: u32) -> u64;
        }
    };
    (store_list) => {
        #[link(wasm_import_module = "env")]
        extern "C" {
            fn host_store_list(prefix_ptr: u32, prefix_len: u32) -> u64;
        }
    };
    (usage_read) => {
        #[link(wasm_import_module = "env")]
        extern "C" {
            fn host_usage_read(repo_ptr: u32, repo_len: u32) -> u64;
        }
    };
    (audit_write) => {
        #[link(wasm_import_module = "env")]
        extern "C" {
            fn host_audit_write(ptr: u32, len: u32) -> u64;
        }
    };
    (clock_read) => {
        #[link(wasm_import_module = "env")]
        extern "C" {
            fn host_clock() -> u64;
        }
    };
}

/// One capability's [`crate::v2::MeterHost`] methods. Anything not listed falls
/// through to the trait default, which is a **named refusal** — never a stub
/// that answers zero.
#[doc(hidden)]
#[macro_export]
macro_rules! __meter_cap_method {
    (store_read) => {
        fn store_get(&self, key: &str) -> ::std::result::Result<::std::option::Option<::std::vec::Vec<u8>>, ::std::string::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"))?;
            $crate::wire::decode_store_get(&raw).map_err(|e| e.message())
        }
        fn store_size(&self, key: &str) -> ::std::result::Result<::std::option::Option<u64>, ::std::string::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"))?;
            match $crate::wire::decode_store_get(&raw).map_err(|e| e.message())? {
                ::std::option::Option::None => ::std::result::Result::Ok(::std::option::Option::None),
                ::std::option::Option::Some(b) if b.len() == 8 => {
                    let mut a = [0u8; 8];
                    a.copy_from_slice(&b);
                    ::std::result::Result::Ok(::std::option::Option::Some(u64::from_le_bytes(a)))
                }
                ::std::option::Option::Some(b) => ::std::result::Result::Err(::std::format!(
                    "host_store_size({key}) returned {} bytes, expected 8",
                    b.len()
                )),
            }
        }
    };
    (store_write) => {
        fn store_put(&self, key: &str, data: &[u8]) -> ::std::result::Result<(), ::std::string::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"))?;
            $crate::wire::decode_unit_response(&raw).map_err(|e| e.message())?
        }
    };
    (store_list) => {
        fn store_list(&self, prefix: &str) -> ::std::result::Result<::std::vec::Vec<(::std::string::String, u64)>, ::std::string::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"))?;
            $crate::wire::decode_store_listing(&raw).map_err(|e| e.message())
        }
    };
    (usage_read) => {
        fn usage_read(&self, repository: &str) -> ::std::result::Result<::std::vec::Vec<$crate::v2::WireMeterReading>, ::std::string::String> {
            let packed = unsafe { host_usage_read(repository.as_ptr() as u32, repository.len() as u32) };
            let raw = unpack(packed).ok_or_else(|| ::std::format!("host_usage_read({repository}) failed"))?;
            $crate::v2::payload::decode_usage(&raw).map_err(|e| e.message())?
        }
    };
    (audit_write) => {
        fn audit_write(&self, record: &$crate::v2::WireAuditRecord) -> ::std::result::Result<(), ::std::string::String> {
            let bytes = record.to_bytes();
            let packed = unsafe { host_audit_write(bytes.as_ptr() as u32, bytes.len() as u32) };
            let raw = unpack(packed).ok_or_else(|| ::std::string::String::from("host_audit_write failed"))?;
            $crate::wire::decode_unit_response(&raw).map_err(|e| e.message())?
        }
    };
    (clock_read) => {
        fn clock(&self) -> ::std::result::Result<$crate::v2::WireInstant, ::std::string::String> {
            let packed = unsafe { host_clock() };
            let raw = unpack(packed).ok_or_else(|| ::std::string::String::from("host_clock failed"))?;
            $crate::v2::payload::decode_clock(&raw).map_err(|e| e.message())?
        }
    };
}

/// Generate the six ABI v2 exports for a [`crate::v2::Meter`].
///
/// ```ignore
/// holger_plugin_abi::export_meter!(
///     holger_plugin_allocation::AllocationMeter,
///     caps = [usage_read, store_list],
/// );
/// ```
///
/// The type must implement `Meter` and be constructible with
/// `Default::default()`. The capability tokens must match what the type's own
/// manifest declares — a test in the guest crate asserts they do, and gate B
/// would refuse the module at load if they did not.
#[macro_export]
macro_rules! export_meter {
    ($meter:ty, caps = [$($cap:ident),* $(,)?] $(,)?) => {
        #[cfg(target_arch = "wasm32")]
        const _: () = {
            use ::std::cell::{Cell, RefCell};
            use ::std::vec::Vec;

            use $crate::v2::{dispatch_meter, AbiRange, Meter, MeterHost};

            ::std::thread_local! {
                static ALLOCS: RefCell<Vec<Vec<u8>>> = const { RefCell::new(Vec::new()) };
                static RESULT_LEN: Cell<u32> = const { Cell::new(0) };
            }

            #[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
                })
            }

            /// v1 leaks by design — a fresh `Store` per call makes that sound.
            /// A meter guest is long-lived, so it has to be able to give a
            /// buffer back.
            #[no_mangle]
            pub extern "C" fn dealloc(ptr: u32, len: u32) {
                ALLOCS.with(|a| {
                    let mut v = a.borrow_mut();
                    if let Some(i) = v
                        .iter()
                        .position(|b| b.as_ptr() as u32 == ptr && b.len() == len as usize)
                    {
                        v.swap_remove(i);
                    }
                });
            }

            #[no_mangle]
            pub extern "C" fn result_len() -> u32 {
                RESULT_LEN.with(|l| l.get())
            }

            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.
                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
            }

            /// # Safety
            /// `ptr`/`len` must be an address previously returned by `alloc`.
            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".
            #[allow(dead_code)]
            fn unpack(packed: u64) -> ::std::option::Option<Vec<u8>> {
                if packed == 0 {
                    return ::std::option::Option::None;
                }
                let ptr = (packed >> 32) as u32;
                let len = (packed & 0xffff_ffff) as usize;
                // SAFETY: the host allocated this through our own `alloc`.
                ::std::option::Option::Some(
                    unsafe { ::std::slice::from_raw_parts(ptr as *const u8, len) }.to_vec(),
                )
            }

            $( $crate::__meter_cap_extern!($cap); )*

            /// The host, reached through exactly the imports this module's
            /// capabilities unlock. Everything else falls through to
            /// `MeterHost`'s defaults, which refuse by name.
            struct HostBridge;

            impl MeterHost for HostBridge {
                $( $crate::__meter_cap_method!($cap); )*
            }

            fn meter() -> $meter {
                <$meter as ::std::default::Default>::default()
            }

            #[no_mangle]
            pub extern "C" fn plugin_abi() -> u64 {
                meter().manifest().abi.pack()
            }

            #[no_mangle]
            pub extern "C" fn plugin_manifest() -> u32 {
                ret(&meter().manifest().to_bytes())
            }

            /// # Safety
            /// `ptr`/`len` must address a payload the host wrote through `alloc`.
            #[no_mangle]
            pub unsafe extern "C" fn plugin_call(op: u32, ptr: u32, len: u32) -> u64 {
                let payload = if len == 0 { &[][..] } else { unsafe { arg(ptr, len) } };
                let out = dispatch_meter(&meter(), &HostBridge, op, payload);
                let out_ptr = ret(&out);
                ((out_ptr as u64) << 32) | (out.len() as u64)
            }

            #[allow(dead_code)]
            fn _abi_range_is_used() -> AbiRange {
                AbiRange::host()
            }
        };
    };
}