From 2cc8b87c871d9d9b310e6c6cbebfed15c43c939d Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 29 Jul 2026 10:59:00 +0100 Subject: [PATCH] Add self to host functions trat --- crates/xrpl-host-functions-macros/src/lib.rs | 48 +++--- .../src/parsed_host_function.rs | 143 ++++++++++++------ crates/xrpl-host-functions/src/lib.rs | 10 +- .../tests/generated_abi.rs | 40 +++-- docs/claude/redesign_impl.md | 15 +- 5 files changed, 168 insertions(+), 88 deletions(-) diff --git a/crates/xrpl-host-functions-macros/src/lib.rs b/crates/xrpl-host-functions-macros/src/lib.rs index 6ce649396b..738a84d8ea 100644 --- a/crates/xrpl-host-functions-macros/src/lib.rs +++ b/crates/xrpl-host-functions-macros/src/lib.rs @@ -15,9 +15,9 @@ use parsed_host_function::ParsedHostFunction; /// Declares the wasm host ABI once, and generates everything that follows from it. /// -/// The input is a block of bare `fn` declarations, each carrying the gas cost the -/// host charges before the call and the name the guest imports it under. Doc -/// comments are kept and appear on the generated items. +/// The input is a block of `fn` declarations, each carrying the gas cost the host +/// charges before the call and the name the guest imports it under. Doc comments +/// are kept and appear on the generated items. /// /// ``` /// use xrpl_host_functions_macros::host_functions; @@ -26,19 +26,19 @@ use parsed_host_function::ParsedHostFunction; /// /// The sequence number of the ledger being built. /// #[gas = 60] /// #[wasm_name = "ldgr_index"] -/// fn get_ledger_sqn() -> [u8; 4]; +/// fn get_ledger_sqn(&self) -> [u8; 4]; /// /// /// Writes `msg` to the trace log. /// #[gas = 500] /// #[wasm_name = "trace_num"] -/// fn trace_num(msg: &str, number: i64); +/// fn trace_num(&self, msg: &str, number: i64); /// } /// -/// // A `HostFunctions` trait, with a `&mut self` receiver added: +/// // A `HostFunctions` trait, holding the declarations verbatim: /// struct Host; /// impl HostFunctions for Host { -/// fn get_ledger_sqn(&mut self) -> [u8; 4] { 7u32.to_le_bytes() } -/// fn trace_num(&mut self, _msg: &str, _number: i64) {} +/// fn get_ledger_sqn(&self) -> [u8; 4] { 7u32.to_le_bytes() } +/// fn trace_num(&self, _msg: &str, _number: i64) {} /// } /// /// // A `HostFunctionSpec` enum carrying the ABI metadata as a `const` table: @@ -47,9 +47,9 @@ use parsed_host_function::ParsedHostFunction; /// assert_eq!(HostFunctionSpec::ALL.len(), 2); /// ``` /// -/// A declaration must be a plain `fn` with no receiver, no body and no generics: -/// it maps to exactly one wasm import signature. Two declarations may not share a -/// `wasm_name`, nor collapse to the same PascalCase variant. +/// A declaration must be a plain `fn` taking `&self`, with no body and no +/// generics: it maps to exactly one wasm import signature. Two declarations may +/// not share a `wasm_name`, nor collapse to the same PascalCase variant. #[proc_macro] pub fn host_functions(input: proc_macro::TokenStream) -> proc_macro::TokenStream { expand(input.into()) @@ -123,9 +123,9 @@ fn generate(functions: &[ParsedHostFunction]) -> TokenStream { /// /// Implement it once per execution environment — the ledger host, a test /// double, a benchmark fake — and a guest module cannot tell them apart. - /// Each method is a declaration from the `host_functions!` block with a - /// `&mut self` receiver added; the receiver is not part of the ABI the - /// guest sees. + /// Each method is one declaration from the `host_functions!` block, as + /// written; its `&self` receiver is not part of the ABI the guest sees, + /// so a host that must mutate does so behind interior mutability. pub trait HostFunctions { #(#trait_methods)* } @@ -217,10 +217,10 @@ mod tests { fn reports_mistakes_from_every_function() { let error = expand(quote! { #[wasm_name = "ldgr_index"] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; #[gas = 2000] - fn sha512_half(data: &[u8]) -> [u8; 32]; + fn sha512_half(&self, data: &[u8]) -> [u8; 32]; }) .expect_err("expected parsing to fail"); @@ -249,19 +249,19 @@ mod tests { let generated = expand(quote! { #[gas = 60] #[wasm_name = "ldgr_index"] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; #[gas = 500] #[wasm_name = "trace_num"] - fn trace_num(msg: &str, number: i64); + fn trace_num(&self, msg: &str, number: i64); }) .unwrap() .to_string(); for expected in [ "pub trait HostFunctions", - "fn get_ledger_sqn (& mut self) -> [u8 ; 4] ;", - "fn trace_num (& mut self , msg : & str , number : i64) ;", + "fn get_ledger_sqn (& self) -> [u8 ; 4] ;", + "fn trace_num (& self , msg : & str , number : i64) ;", "pub struct HostFnSpec", "pub name : & 'static str", "pub gas : u64", @@ -279,11 +279,11 @@ mod tests { let messages = messages(quote! { #[gas = 60] #[wasm_name = "trace"] - fn trace(msg: &str); + fn trace(&self, msg: &str); #[gas = 70] #[wasm_name = "trace"] - fn trace_num(msg: &str, number: i64); + fn trace_num(&self, msg: &str, number: i64); }); assert_eq!(messages.len(), 1, "{messages:?}"); @@ -299,11 +299,11 @@ mod tests { let messages = messages(quote! { #[gas = 60] #[wasm_name = "a"] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; #[gas = 70] #[wasm_name = "b"] - fn get_ledger__sqn() -> [u8; 4]; + fn get_ledger__sqn(&self) -> [u8; 4]; }); assert_eq!(messages.len(), 1, "{messages:?}"); diff --git a/crates/xrpl-host-functions-macros/src/parsed_host_function.rs b/crates/xrpl-host-functions-macros/src/parsed_host_function.rs index a8612aa459..de50141ce5 100644 --- a/crates/xrpl-host-functions-macros/src/parsed_host_function.rs +++ b/crates/xrpl-host-functions-macros/src/parsed_host_function.rs @@ -1,7 +1,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{ - Attribute, Expr, ExprLit, Ident, Lit, LitStr, Safety, Signature, TraitItemFn, parse_quote, + Attribute, Expr, ExprLit, Ident, Lit, LitStr, ReceiverKind, Safety, Signature, TraitItemFn, }; use crate::errors; @@ -27,14 +27,12 @@ pub(crate) struct ParsedHostFunction { } impl ParsedHostFunction { - /// `#[doc …] fn get_ledger_sqn(&mut self) -> [u8; 4];` + /// `#[doc …] fn get_ledger_sqn(&self) -> [u8; 4];` pub(crate) fn trait_method(&self) -> TokenStream { let docs = &self.docs; - - // The receiver is not part of the wasm ABI, so declarations omit it and - // only the trait needs one. - let mut signature = self.signature.clone(); - signature.inputs.insert(0, parse_quote!(&self)); + // The declaration is already a trait method: emitted verbatim, so what + // the block reads like is what the trait is. + let signature = &self.signature; quote! { #(#docs)* @@ -124,12 +122,7 @@ impl ParsedHostFunction { "a host function must not be generic: it maps to one wasm import signature", )); } - if let Some(receiver) = function.sig.receiver() { - errors.push(syn::Error::new_spanned( - receiver, - "the receiver is added by the macro; declare only the wasm parameters", - )); - } + errors.extend(check_receiver(&function.sig).err()); if let Some(name) = &wasm_name { errors.extend(check_wasm_name(name).err()); } @@ -163,6 +156,35 @@ impl ParsedHostFunction { } } +/// Every declaration carries a receiver, and it is always `&self`. +/// +/// `&self` is the only receiver that can work: the VM reaches the host through a +/// shared `&dyn HostFunctions` stored in the wasmi `Store`, and a host that needs +/// to mutate does so behind interior mutability. The receiver is not part of the +/// wasm ABI — the guest passes no `self` — so it is uniform across the block. +fn check_receiver(signature: &Signature) -> syn::Result<()> { + let Some(receiver) = signature.receiver() else { + return Err(syn::Error::new_spanned( + &signature.ident, + format!( + "a host function must declare its receiver: `fn {}(&self, ...)`", + signature.ident + ), + )); + }; + + // `&self` and nothing else: not `&mut self`, not `self`/`mut self`, not a + // typed `self: Box`, and not a spelled-out lifetime. + if !matches!(receiver.kind, ReceiverKind::Reference(_, None, None)) { + return Err(syn::Error::new_spanned( + receiver, + "a host function's receiver must be exactly `&self`: the VM calls the host \ + through a shared `&dyn HostFunctions`", + )); + } + Ok(()) +} + /// `const`, `async`, `unsafe`/`safe` and `extern "…"` have no meaning in the /// wasm ABI, and would otherwise pass silently into the generated trait. fn reject_modifiers(signature: &Signature, errors: &mut Vec) { @@ -339,7 +361,7 @@ mod tests { let parsed = ParsedHostFunction::parse(parse_quote! { #[gas = 60] #[wasm_name = "ldgr_index"] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; }) .unwrap(); @@ -378,7 +400,7 @@ mod tests { let messages = messages(parse_quote! { #[gas = 60] #[wasm_name = "two_factor"] - fn _2fa(); + fn _2fa(&self); }); assert_eq!(messages.len(), 1, "{messages:?}"); @@ -410,7 +432,7 @@ mod tests { let messages = messages(parse_quote! { #[gas = -5] #[wasm_name = "ldgr_index"] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; }); assert_eq!(messages.len(), 1, "{messages:?}"); @@ -422,7 +444,7 @@ mod tests { let empty = messages(parse_quote! { #[gas = 60] #[wasm_name = ""] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; }); assert_eq!(empty.len(), 1, "{empty:?}"); assert_eq!(empty[0], "the wasm name must not be empty"); @@ -430,7 +452,7 @@ mod tests { let spaced = messages(parse_quote! { #[gas = 60] #[wasm_name = "ldgr index"] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; }); assert_eq!(spaced.len(), 1, "{spaced:?}"); assert!(spaced[0].contains("may only contain"), "{spaced:?}"); @@ -439,10 +461,10 @@ mod tests { #[test] fn rejects_signature_modifiers() { for declaration in [ - quote! { unsafe fn get_ledger_sqn() -> [u8; 4]; }, - quote! { async fn get_ledger_sqn() -> [u8; 4]; }, - quote! { const fn get_ledger_sqn() -> [u8; 4]; }, - quote! { extern "C" fn get_ledger_sqn() -> [u8; 4]; }, + quote! { unsafe fn get_ledger_sqn(&self) -> [u8; 4]; }, + quote! { async fn get_ledger_sqn(&self) -> [u8; 4]; }, + quote! { const fn get_ledger_sqn(&self) -> [u8; 4]; }, + quote! { extern "C" fn get_ledger_sqn(&self) -> [u8; 4]; }, ] { let function: TraitItemFn = syn::parse2(quote! { #[gas = 60] @@ -458,12 +480,12 @@ mod tests { } #[test] - fn trait_method_takes_a_receiver_and_ends_in_a_semicolon() { + fn trait_method_keeps_the_declared_receiver_and_ends_in_a_semicolon() { let parsed = ParsedHostFunction::parse(parse_quote! { /// Hashes `data`. #[gas = 2000] #[wasm_name = "sha512_half"] - fn sha512_half(data: &[u8]) -> [u8; 32]; + fn sha512_half(&self, data: &[u8]) -> [u8; 32]; }) .unwrap(); @@ -475,7 +497,7 @@ mod tests { "{method}" ); assert!( - method.contains("fn sha512_half (& mut self , data : & [u8]) -> [u8 ; 32] ;"), + method.contains("fn sha512_half (& self , data : & [u8]) -> [u8 ; 32] ;"), "{method}" ); } @@ -485,7 +507,7 @@ mod tests { let parsed = ParsedHostFunction::parse(parse_quote! { #[gas = 60] #[wasm_name = "ldgr_index"] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; }) .unwrap(); @@ -503,7 +525,7 @@ mod tests { /// Third line. #[gas = 60] #[wasm_name = "ldgr_index"] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; }) .unwrap(); @@ -516,16 +538,17 @@ mod tests { let traced = ParsedHostFunction::parse(parse_quote! { #[gas = 500] #[wasm_name = "trace"] - fn trace(msg: &str, data: &[u8], as_hex: bool); + fn trace(&self, msg: &str, data: &[u8], as_hex: bool); }) .unwrap(); - assert_eq!(traced.signature.inputs.len(), 3); + // The receiver is `inputs[0]`; the three wasm parameters follow it. + assert_eq!(traced.signature.inputs.len(), 4); assert!(matches!(traced.signature.output, syn::ReturnType::Default)); let hashed = ParsedHostFunction::parse(parse_quote! { #[gas = 2000] #[wasm_name = "sha512_half"] - fn sha512_half(data: &[u8]) -> [u8; 32]; + fn sha512_half(&self, data: &[u8]) -> [u8; 32]; }) .unwrap(); assert!(matches!(hashed.signature.output, syn::ReturnType::Type(..))); @@ -534,7 +557,7 @@ mod tests { #[test] fn reports_both_missing_attributes_at_once() { let messages = messages(parse_quote! { - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; }); assert_eq!(messages.len(), 2); @@ -547,7 +570,7 @@ mod tests { let messages = messages(parse_quote! { #[gas = 60] #[wsam_name = "typo"] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; }); // The typo'd attribute, plus the `wasm_name` it failed to be. @@ -563,7 +586,7 @@ mod tests { let gas = messages(parse_quote! { #[gas = "60"] #[wasm_name = "ldgr_index"] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; }); assert_eq!(gas.len(), 1, "{gas:?}"); assert!( @@ -574,7 +597,7 @@ mod tests { let name = messages(parse_quote! { #[gas = 60] #[wasm_name = 7] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; }); assert_eq!(name.len(), 1, "{name:?}"); assert!( @@ -588,7 +611,7 @@ mod tests { let messages = messages(parse_quote! { #[gas = 99999999999999999999999] #[wasm_name = "ldgr_index"] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; }); assert_eq!(messages.len(), 1, "{messages:?}"); @@ -600,7 +623,7 @@ mod tests { let bare = messages(parse_quote! { #[gas] #[wasm_name = "ldgr_index"] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; }); assert_eq!(bare.len(), 1, "{bare:?}"); assert!(bare[0].contains("gas = ..."), "{bare:?}"); @@ -608,7 +631,7 @@ mod tests { let list = messages(parse_quote! { #[gas(60)] #[wasm_name = "ldgr_index"] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; }); assert_eq!(list.len(), 1, "{list:?}"); } @@ -620,7 +643,7 @@ mod tests { #[gas = 70] #[wasm_name = "ldgr_index"] #[wasm_name = "ldgr_index"] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; }); assert_eq!(messages.len(), 2, "{messages:?}"); @@ -637,7 +660,7 @@ mod tests { let messages = messages(parse_quote! { #[gas = "60"] #[wasm_name = 7] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; }); assert_eq!(messages.len(), 2, "{messages:?}"); @@ -652,7 +675,7 @@ mod tests { let messages = messages(parse_quote! { #[gas = 60] #[wasm_name = "ldgr_index"] - fn get_ledger_sqn() -> [u8; 4] { [0; 4] } + fn get_ledger_sqn(&self) -> [u8; 4] { [0; 4] } }); assert_eq!(messages.len(), 1, "{messages:?}"); @@ -664,7 +687,7 @@ mod tests { let parameter = messages(parse_quote! { #[gas = 60] #[wasm_name = "ldgr_index"] - fn get_ledger_sqn() -> T; + fn get_ledger_sqn(&self) -> T; }); assert_eq!(parameter.len(), 1, "{parameter:?}"); assert!( @@ -675,20 +698,50 @@ mod tests { let clause = messages(parse_quote! { #[gas = 60] #[wasm_name = "ldgr_index"] - fn get_ledger_sqn() -> [u8; 4] where Self: Sized; + fn get_ledger_sqn(&self) -> [u8; 4] where Self: Sized; }); assert_eq!(clause.len(), 1, "{clause:?}"); } #[test] - fn rejects_an_explicit_receiver() { + fn requires_a_receiver() { let messages = messages(parse_quote! { #[gas = 60] #[wasm_name = "ldgr_index"] - fn get_ledger_sqn(&self) -> [u8; 4]; + fn get_ledger_sqn() -> [u8; 4]; }); assert_eq!(messages.len(), 1, "{messages:?}"); - assert!(messages[0].contains("receiver"), "{messages:?}"); + assert!( + messages[0].contains("must declare its receiver: `fn get_ledger_sqn(&self, ...)`"), + "{messages:?}" + ); + } + + /// Anything but `&self` would need a host the VM cannot hand out: it holds + /// one shared `&dyn HostFunctions` for the whole run. + #[test] + fn rejects_receivers_other_than_shared_self() { + for receiver in [ + quote! { &mut self }, + quote! { self }, + quote! { mut self }, + quote! { self: Box }, + quote! { &'a self }, + ] { + let function: TraitItemFn = syn::parse2(quote! { + #[gas = 60] + #[wasm_name = "ldgr_index"] + fn get_ledger_sqn(#receiver) -> [u8; 4]; + }) + .unwrap_or_else(|_| panic!("`{receiver}` should parse")); + + let messages = messages(function); + assert_eq!(messages.len(), 1, "`{receiver}`: {messages:?}"); + assert!( + messages[0].contains("must be exactly `&self`"), + "`{receiver}`: {messages:?}" + ); + } } } diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 99d30e99e9..e123bf04fc 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -87,21 +87,21 @@ pub const HASH_LEN: usize = 32; host_functions! { #[gas = 60] #[wasm_name = "ldgr_index"] - fn get_ledger_sqn() -> [u8; 4]; + fn get_ledger_sqn(&self) -> [u8; 4]; #[gas = 70] #[wasm_name = "home_le_field"] - fn get_current_ledger_obj_field(field: i32) -> Vec; + fn get_current_ledger_obj_field(&self, field: i32) -> Vec; #[gas = 2000] #[wasm_name = "sha512_half"] - fn sha512_half(data: &[u8]) -> [u8; 32]; + fn sha512_half(&self, data: &[u8]) -> [u8; 32]; #[gas = 500] #[wasm_name = "trace"] - fn trace(msg: &str, data: &[u8], as_hex: bool); + fn trace(&self, msg: &str, data: &[u8], as_hex: bool); #[gas = 500] #[wasm_name = "trace_num"] - fn trace_num(msg: &str, number: i64); + fn trace_num(&self, msg: &str, number: i64); } diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index a2e85606cf..5e0bf564ca 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -1,41 +1,48 @@ //! Exercises what `host_functions!` generates: the trait is implementable and //! the spec table agrees with the declarations in `src/lib.rs`. +use std::cell::RefCell; + use xrpl_host_functions::{HASH_LEN, HostFnSpec, HostFunctionSpec, HostFunctions}; /// Records what it was asked to do; enough to prove the trait is usable. +/// +/// Every method takes `&self`, so a host that records anything keeps it behind +/// interior mutability. #[derive(Default)] struct FakeHost { - traced: Vec, + traced: RefCell>, } impl HostFunctions for FakeHost { - fn get_ledger_sqn(&mut self) -> [u8; 4] { + fn get_ledger_sqn(&self) -> [u8; 4] { 7u32.to_le_bytes() } - fn get_current_ledger_obj_field(&mut self, field: i32) -> Vec { + fn get_current_ledger_obj_field(&self, field: i32) -> Vec { vec![field as u8] } - fn sha512_half(&mut self, data: &[u8]) -> [u8; HASH_LEN] { + fn sha512_half(&self, data: &[u8]) -> [u8; HASH_LEN] { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; digest } - fn trace(&mut self, msg: &str, data: &[u8], as_hex: bool) { - self.traced.push(format!("{msg}/{}/{as_hex}", data.len())); + fn trace(&self, msg: &str, data: &[u8], as_hex: bool) { + self.traced + .borrow_mut() + .push(format!("{msg}/{}/{as_hex}", data.len())); } - fn trace_num(&mut self, msg: &str, number: i64) { - self.traced.push(format!("{msg}={number}")); + fn trace_num(&self, msg: &str, number: i64) { + self.traced.borrow_mut().push(format!("{msg}={number}")); } } #[test] fn the_trait_is_implementable() { - let mut host = FakeHost::default(); + let host = FakeHost::default(); assert_eq!(host.get_ledger_sqn(), [7, 0, 0, 0]); assert_eq!(host.get_current_ledger_obj_field(3), vec![3]); @@ -43,7 +50,20 @@ fn the_trait_is_implementable() { host.trace("hello", b"xy", true); host.trace_num("count", -1); - assert_eq!(host.traced, ["hello/2/true", "count=-1"]); + assert_eq!(*host.traced.borrow(), ["hello/2/true", "count=-1"]); +} + +/// The VM reaches the host as one shared trait object held in the wasmi `Store`, +/// which is what the `&self` receivers are for. +#[test] +fn the_trait_is_callable_through_a_shared_trait_object() { + let fake = FakeHost::default(); + let host: &dyn HostFunctions = &fake; + + assert_eq!(host.get_ledger_sqn(), [7, 0, 0, 0]); + host.trace_num("count", 1); + + assert_eq!(*fake.traced.borrow(), ["count=1"]); } #[test] diff --git a/docs/claude/redesign_impl.md b/docs/claude/redesign_impl.md index 9ec4f202c2..9d44506154 100644 --- a/docs/claude/redesign_impl.md +++ b/docs/claude/redesign_impl.md @@ -25,6 +25,10 @@ only for reference. Anything we need about the old semantics is recoverable with `host_functions! { ... }` generates the `HostFunctions` trait + the `HostFunctionSpec` enum (wasm import name + gas per function). Also `HostError`. **This crate is the single source of truth for the ABI.** + Each declaration spells its receiver — always `&self`, checked by the macro, so + a declaration reads exactly as the trait method it becomes. `&self` is what lets + the VM hold the host as one shared `&dyn HostFunctions` in the wasmi `Store`; a + host that needs to mutate uses interior mutability. - `crates/xrpl-host-functions-macros/` — the `host_functions!` proc macro. - `crates/xrpl-wasm-vm/` — the wasmi wrapper: `vm.rs` (engine/store/run), `abi.rs` (gas + transfer-limit + guest-memory marshaling), `register.rs` @@ -129,6 +133,7 @@ That is the entire gap. ``` params: + &self -> nothing (receiver, not part of the ABI) i32, bool -> i32 (bool: nonzero = true) i64 -> i64 &[u8], &str -> i32 ptr, i32 len const uint8_t*, int32_t @@ -338,10 +343,12 @@ useful for comparison and for the gas assertions in `Wasm_test.cpp` — not gosp ## Current state (2026-07-29) `crates/` does **not** compile: the macro-generated trait (value-returning, -infallible) and the uncommitted VM code (fill-caller's-buffer, `HostResult`, -`&dyn`) are two different ABI shapes. Every call site in `register.rs` is affected, -as are the macro's own doctests and `xrpl-host-functions/tests/generated_abi.rs` -(which still use `&mut self` and value returns). +infallible) and the VM code (fill-caller's-buffer, `HostResult`) are two +different ABI shapes. The 8 errors are all in `xrpl-wasm-vm` — every byte-returning +call site in `register.rs`, plus `?` on the infallible `trace`/`trace_num`. + +`xrpl-host-functions` and `xrpl-host-functions-macros` are green (`cargo test` + +`clippy`): 28 macro tests, 5 ABI tests, 1 doctest. Resolving that shape is the immediate work. Per the mechanism note above, the value-returning direction (plus `HostResult`) is the one that composes with