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