crates/monetize-product-guest/src/guest_export.rs
//! **The wasm exports, generated per module** — the same reason holger's
//! `export_meter!` exists. The macro takes the module's grant list and emits
//! **only the imports that list unlocks**: a guest declaring `grants = [grpc]`
//! contains no `host_http` 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 the host's gate B checks.
//!
//! Everything emitted is `#[cfg(target_arch = "wasm32")]`; on any other target
//! the crate compiles to nothing, so a guest crate is an ordinary workspace
//! member `cargo test --workspace` covers.
/// One grant's `env` imports. Item position, so each expansion is its own
/// `extern` block.
#[doc(hidden)]
#[macro_export]
macro_rules! __product_grant_extern {
(http) => {
#[link(wasm_import_module = "env")]
extern "C" {
fn host_http(ptr: u32, len: u32) -> u64;
}
};
(grpc) => {
#[link(wasm_import_module = "env")]
extern "C" {
fn host_grpc_unary(ptr: u32, len: u32) -> u64;
fn host_grpc_stream_open(ptr: u32, len: u32) -> u64;
fn host_grpc_stream_send(ptr: u32, len: u32) -> u64;
fn host_grpc_stream_close(ptr: u32, len: u32) -> u64;
fn host_grpc_stream_cancel(ptr: u32, len: u32) -> u64;
fn host_grpc_server_stream(ptr: u32, len: u32) -> u64;
fn host_grpc_stream_recv(ptr: u32, len: u32) -> u64;
}
};
(endpoint) => {
#[link(wasm_import_module = "env")]
extern "C" {
fn host_endpoint(ptr: u32, len: u32) -> u64;
}
};
(state) => {
#[link(wasm_import_module = "env")]
extern "C" {
fn host_state_get(ptr: u32, len: u32) -> u64;
fn host_state_put(ptr: u32, len: u32) -> u64;
}
};
(hmac) => {
#[link(wasm_import_module = "env")]
extern "C" {
fn host_hmac_verify(ptr: u32, len: u32) -> u64;
}
};
}
/// One grant's [`crate::Host`] methods. Anything not listed falls through to
/// the trait default, which is a **named refusal**.
#[doc(hidden)]
#[macro_export]
macro_rules! __product_grant_method {
(http) => {
fn http(&self, req: &$crate::wire::HttpRequest) -> ::std::result::Result<$crate::wire::HttpResponse, ::std::string::String> {
let head = $crate::wire::HttpHead { method: req.method.clone(), url: req.url.clone(), headers: req.headers.clone() };
let f = $crate::wire::frame::encode_json(&head, &req.body);
let packed = unsafe { host_http(f.as_ptr() as u32, f.len() as u32) };
let raw = unpack(packed).ok_or_else(|| ::std::string::String::from("host_http failed on the host side"))?;
let (h, body) = $crate::wire::frame::host_answer(&raw)?;
::std::result::Result::Ok($crate::wire::HttpResponse { status: h.status.unwrap_or(0), headers: h.headers, body: body.to_vec() })
}
};
(grpc) => {
fn grpc_unary_at(&self, path: &str, request: &[u8], headers: &[(::std::string::String, ::std::string::String)], endpoint: ::std::option::Option<&str>) -> ::std::result::Result<::std::vec::Vec<u8>, $crate::wire::GrpcError> {
let head = $crate::wire::GrpcHead { path: path.into(), headers: headers.to_vec(), endpoint: endpoint.map(::std::string::String::from) };
let f = $crate::wire::frame::encode_json(&head, request);
let packed = unsafe { host_grpc_unary(f.as_ptr() as u32, f.len() as u32) };
let fail = |m: ::std::string::String| $crate::wire::GrpcError { code: "Unknown".into(), message: m };
let raw = unpack(packed).ok_or_else(|| fail("host_grpc_unary failed on the host side".into()))?;
let (h, body): ($crate::wire::HostHead, &[u8]) = $crate::wire::frame::decode_json(&raw).map_err(fail)?;
if h.ok {
return ::std::result::Result::Ok(body.to_vec());
}
::std::result::Result::Err(h.grpc.unwrap_or_else(|| fail(h.error.unwrap_or_default())))
}
fn grpc_stream_open(&self, path: &str, headers: &[(::std::string::String, ::std::string::String)], expect_secs: u32, endpoint: ::std::option::Option<&str>) -> ::std::result::Result<u64, $crate::wire::GrpcError> {
let head = $crate::wire::StreamOpen { path: path.into(), headers: headers.to_vec(), expect_secs, endpoint: endpoint.map(::std::string::String::from) };
let f = $crate::wire::frame::encode_json(&head, &[]);
let packed = unsafe { host_grpc_stream_open(f.as_ptr() as u32, f.len() as u32) };
let (h, _) = $crate::__stream_answer(unpack(packed), "host_grpc_stream_open")?;
h.stream.ok_or_else(|| $crate::wire::GrpcError { code: "Unknown".into(), message: "host_grpc_stream_open answered no handle".into() })
}
fn grpc_stream_send(&self, stream: u64, chunk: &[u8]) -> ::std::result::Result<(), $crate::wire::GrpcError> {
let f = $crate::wire::frame::encode_json(&$crate::wire::StreamRef { stream }, chunk);
let packed = unsafe { host_grpc_stream_send(f.as_ptr() as u32, f.len() as u32) };
$crate::__stream_answer(unpack(packed), "host_grpc_stream_send").map(|_| ())
}
fn grpc_stream_close(&self, stream: u64) -> ::std::result::Result<::std::vec::Vec<u8>, $crate::wire::GrpcError> {
let f = $crate::wire::frame::encode_json(&$crate::wire::StreamRef { stream }, &[]);
let packed = unsafe { host_grpc_stream_close(f.as_ptr() as u32, f.len() as u32) };
let (_, body) = $crate::__stream_answer(unpack(packed), "host_grpc_stream_close")?;
::std::result::Result::Ok(body)
}
fn grpc_server_stream(&self, path: &str, headers: &[(::std::string::String, ::std::string::String)], request: &[u8], endpoint: ::std::option::Option<&str>) -> ::std::result::Result<u64, $crate::wire::GrpcError> {
let head = $crate::wire::StreamOpen { path: path.into(), headers: headers.to_vec(), expect_secs: 0, endpoint: endpoint.map(::std::string::String::from) };
let f = $crate::wire::frame::encode_json(&head, request);
let packed = unsafe { host_grpc_server_stream(f.as_ptr() as u32, f.len() as u32) };
let (h, _) = $crate::__stream_answer(unpack(packed), "host_grpc_server_stream")?;
h.stream.ok_or_else(|| $crate::wire::GrpcError { code: "Unknown".into(), message: "host_grpc_server_stream answered no handle".into() })
}
fn grpc_stream_recv(&self, stream: u64) -> ::std::result::Result<::std::option::Option<::std::vec::Vec<u8>>, $crate::wire::GrpcError> {
let f = $crate::wire::frame::encode_json(&$crate::wire::StreamRef { stream }, &[]);
let packed = unsafe { host_grpc_stream_recv(f.as_ptr() as u32, f.len() as u32) };
let (h, body) = $crate::__stream_answer(unpack(packed), "host_grpc_stream_recv")?;
if h.end {
return ::std::result::Result::Ok(::std::option::Option::None);
}
::std::result::Result::Ok(::std::option::Option::Some(body))
}
fn grpc_stream_cancel(&self, stream: u64) {
let f = $crate::wire::frame::encode_json(&$crate::wire::StreamRef { stream }, &[]);
let _ = unsafe { host_grpc_stream_cancel(f.as_ptr() as u32, f.len() as u32) };
}
};
(state) => {
fn state_get(&self, key: &str) -> ::std::result::Result<::std::option::Option<::std::vec::Vec<u8>>, ::std::string::String> {
let f = $crate::wire::frame::encode_json(&$crate::wire::StateKey { key: key.into() }, &[]);
let packed = unsafe { host_state_get(f.as_ptr() as u32, f.len() as u32) };
let raw = unpack(packed).ok_or_else(|| ::std::string::String::from("host_state_get failed on the host side"))?;
let (h, body) = $crate::wire::frame::host_answer(&raw)?;
if h.end {
return ::std::result::Result::Ok(::std::option::Option::None);
}
::std::result::Result::Ok(::std::option::Option::Some(body.to_vec()))
}
fn state_put(&self, key: &str, value: &[u8]) -> ::std::result::Result<(), ::std::string::String> {
let f = $crate::wire::frame::encode_json(&$crate::wire::StateKey { key: key.into() }, value);
let packed = unsafe { host_state_put(f.as_ptr() as u32, f.len() as u32) };
let raw = unpack(packed).ok_or_else(|| ::std::string::String::from("host_state_put failed on the host side"))?;
let _ = $crate::wire::frame::host_answer(&raw)?;
::std::result::Result::Ok(())
}
};
(hmac) => {
fn hmac_verify(&self, secret: &str, message: &[u8], candidates_hex: &[::std::string::String]) -> ::std::result::Result<bool, ::std::string::String> {
let head = $crate::wire::HmacHead { secret: secret.into(), algo: "sha256".into(), candidates_hex: candidates_hex.to_vec() };
let f = $crate::wire::frame::encode_json(&head, message);
let packed = unsafe { host_hmac_verify(f.as_ptr() as u32, f.len() as u32) };
let raw = unpack(packed).ok_or_else(|| ::std::string::String::from("host_hmac_verify failed on the host side"))?;
let (h, _) = $crate::wire::frame::host_answer(&raw)?;
match h.value.as_deref() {
::std::option::Option::Some("match") => ::std::result::Result::Ok(true),
::std::option::Option::Some("mismatch") => ::std::result::Result::Ok(false),
other => ::std::result::Result::Err(::std::format!("host_hmac_verify answered {other:?}")),
}
}
fn secret_equals(&self, secret: &str, message: &[u8]) -> ::std::result::Result<bool, ::std::string::String> {
let head = $crate::wire::HmacHead { secret: secret.into(), algo: "eq".into(), candidates_hex: ::std::vec::Vec::new() };
let f = $crate::wire::frame::encode_json(&head, message);
let packed = unsafe { host_hmac_verify(f.as_ptr() as u32, f.len() as u32) };
let raw = unpack(packed).ok_or_else(|| ::std::string::String::from("host_hmac_verify failed on the host side"))?;
let (h, _) = $crate::wire::frame::host_answer(&raw)?;
match h.value.as_deref() {
::std::option::Option::Some("match") => ::std::result::Result::Ok(true),
::std::option::Option::Some("mismatch") => ::std::result::Result::Ok(false),
other => ::std::result::Result::Err(::std::format!("host_hmac_verify answered {other:?}")),
}
}
};
(endpoint) => {
fn endpoint(&self) -> ::std::result::Result<::std::string::String, ::std::string::String> {
let f = $crate::wire::frame::encode(&[], &[]);
let packed = unsafe { host_endpoint(f.as_ptr() as u32, f.len() as u32) };
let raw = unpack(packed).ok_or_else(|| ::std::string::String::from("host_endpoint failed on the host side"))?;
let (h, _) = $crate::wire::frame::host_answer(&raw)?;
::std::result::Result::Ok(h.value.unwrap_or_default())
}
};
}
/// Generate the exports for a [`crate::Guest`].
///
/// ```ignore
/// monetize_product_guest::export_product!(holger_monetize_guest::HolgerGuest, grants = []);
/// ```
///
/// The type must implement `Guest` and `Default`. The grant tokens must match
/// what the type's own manifest declares — the host's gate B refuses the
/// module at load if they do not.
#[macro_export]
macro_rules! export_product {
($guest:ty, grants = [$($grant:ident),* $(,)?] $(,)?) => {
$crate::__export_guest!($guest, $crate::Guest, $crate::dispatch, grants = [$($grant),*]);
};
}
/// **The scaffolding every guest kind shares**: `alloc`/`dealloc`/`result_len`,
/// the host bridge over exactly the granted imports, `plugin_abi`,
/// `plugin_manifest` and `plugin_call` routed to `$dispatch`. A kind (product,
/// vendor, cloud) names its trait and its op switch; nothing else differs, so
/// the ABI cannot drift between kinds. `$trait` must have
/// `fn manifest(&self) -> wire::Manifest`.
#[doc(hidden)]
#[macro_export]
macro_rules! __export_guest {
($guest:ty, $trait:path, $dispatch:path, grants = [$($grant:ident),* $(,)?] $(,)?) => {
$crate::__export_guest!($guest, $trait, $dispatch, sleep = no, grants = [$($grant),*]);
};
// `sleep = yes`: the kind imports `host_sleep` (a cloud, which polls a
// provider). A product and a vendor do not, so a module of theirs still
// instantiates on a server older than the door.
($guest:ty, $trait:path, $dispatch:path, sleep = $sleep:ident, grants = [$($grant:ident),* $(,)?] $(,)?) => {
#[cfg(target_arch = "wasm32")]
const _: () = {
use ::std::cell::{Cell, RefCell};
use ::std::vec::Vec;
use $crate::Host;
::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
})
}
#[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 door. `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`.
let out = unsafe { ::std::slice::from_raw_parts(ptr as *const u8, len) }.to_vec();
dealloc(ptr, len as u32);
::std::option::Option::Some(out)
}
#[link(wasm_import_module = "env")]
extern "C" {
fn host_clock() -> u64;
fn host_log(ptr: u32, len: u32);
}
$crate::__guest_sleep_extern!($sleep);
$( $crate::__product_grant_extern!($grant); )*
/// The host, reached through exactly the imports this module's
/// grants unlock. Everything else falls through to `Host`'s
/// defaults, which refuse by name.
struct HostBridge;
impl Host for HostBridge {
fn clock_unix_ms(&self) -> u64 {
unsafe { host_clock() }
}
fn log(&self, line: &str) {
unsafe { host_log(line.as_ptr() as u32, line.len() as u32) }
}
$crate::__guest_sleep_method!($sleep);
$( $crate::__product_grant_method!($grant); )*
}
/// `'static`, so a kind whose op switch takes `&'static (dyn Host
/// + Sync)` (a cloud: its provider keeps the host inside its HTTP
/// client) can be handed the bridge; a `&dyn Host` switch takes it too.
static BRIDGE: HostBridge = HostBridge;
fn guest() -> $guest {
<$guest as ::std::default::Default>::default()
}
#[no_mangle]
pub extern "C" fn plugin_abi() -> u64 {
let m = <$guest as $trait>::manifest(&guest());
$crate::abi::pack(m.abi_min, m.abi_max)
}
#[no_mangle]
pub extern "C" fn plugin_manifest() -> u32 {
ret(&$crate::wire::encode(&<$guest as $trait>::manifest(&guest())))
}
/// # 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(&guest(), &BRIDGE, op, payload);
let out_ptr = ret(&out);
((out_ptr as u64) << 32) | (out.len() as u64)
}
};
};
}
/// The `host_sleep` import, for a kind that polls a provider (`yes`), or none
/// (`no`).
#[doc(hidden)]
#[macro_export]
macro_rules! __guest_sleep_extern {
(yes) => {
#[link(wasm_import_module = "env")]
extern "C" {
fn host_sleep(ms: u64);
}
};
(no) => {};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __guest_sleep_method {
(yes) => {
fn sleep_ms(&self, ms: u64) {
unsafe { host_sleep(ms) }
}
};
(no) => {};
}