Implementing macro

This commit is contained in:
Sergey Kuznetsov
2026-07-24 17:35:44 +01:00
parent b7059deb9f
commit 7ebd92ad3e
5 changed files with 307 additions and 14 deletions

8
crates/Cargo.lock generated
View File

@@ -303,10 +303,18 @@ dependencies = [
[[package]]
name = "xrpl-host-functions"
version = "0.1.0"
dependencies = [
"xrpl-host-functions-macros",
]
[[package]]
name = "xrpl-host-functions-macros"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "xrpl-wasm-vm"

View File

@@ -3,4 +3,10 @@ name = "xrpl-host-functions-macros"
version = "0.1.0"
edition.workspace = true
[lib]
proc-macro = true
[dependencies]
syn = { version = "3", features = ["full"] }
quote = "1"
proc-macro2 = "1"

View File

@@ -1,5 +1,170 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
use proc_macro::TokenStream;
use quote::quote;
use syn::{
Attribute, Expr, ExprLit, Lit, Signature, TraitItemFn,
parse::{Parse, ParseStream},
parse2,
};
#[proc_macro]
pub fn host_functions(input: TokenStream) -> TokenStream {
expand(input.into())
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
fn expand(input: proc_macro2::TokenStream) -> syn::Result<proc_macro2::TokenStream> {
let HostFunctionsInput { functions } = parse2(input)?;
// let mut errors = Vec::new();
for f in functions {}
Ok(quote! {
trait HostFunctions {
}
enum HostFunctionSpec {
}
impl HostFunctionSpec
}
.into())
}
struct HostFunctionsInput {
functions: Vec<TraitItemFn>,
}
impl Parse for HostFunctionsInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut functions = Vec::new();
while !input.is_empty() {
functions.push(input.parse()?);
}
Ok(HostFunctionsInput { functions })
}
}
struct ParsedHostFunction {
gas: usize,
wasm_name: String,
docs: Vec<Attribute>,
signature: Signature,
}
impl ParsedHostFunction {
const GAS_PATH: &str = "gas";
const WASM_NAME_PATH: &str = "wasm_name";
fn parse(value: TraitItemFn) -> Result<Self, syn::Error> {
let mut gas = None;
let mut wasm_name = None;
let mut docs = Vec::new();
let mut errors = Vec::new();
for attr in &value.attrs {
let named = match attr.meta.require_name_value() {
Ok(n) => n,
Err(e) => {
errors.push(e);
continue;
}
};
match &named.path {
p if p.is_ident(Self::GAS_PATH) => {
let parsed_value = match Self::parse_number(&named.value) {
Ok(n) => n,
Err(e) => {
errors.push(e);
continue;
}
};
if gas.replace(parsed_value).is_some() {
errors.push(syn::Error::new_spanned(
named,
format!("duplicated {} attribute", Self::GAS_PATH),
));
}
}
p if p.is_ident(Self::WASM_NAME_PATH) => {
let parsed_value = match Self::parse_string(&named.value) {
Ok(n) => n,
Err(e) => {
errors.push(e);
continue;
}
};
if wasm_name.replace(parsed_value).is_some() {
errors.push(syn::Error::new_spanned(
named,
format!("duplicated {} attribute", Self::WASM_NAME_PATH),
));
}
}
p if p.is_ident("doc") => {
docs.push(attr.clone());
}
_ => {
errors.push(syn::Error::new_spanned(named, "unexpected attribute"));
}
}
}
if !errors.is_empty() {
return Err(errors
.into_iter()
.reduce(|mut l, r| {
l.combine(r);
l
})
.unwrap());
}
if gas.is_none() {
return Err(syn::Error::new_spanned(
&value.sig,
format!("missing {} attribute", Self::GAS_PATH),
));
}
if wasm_name.is_none() {
return Err(syn::Error::new_spanned(
&value.sig,
format!("missing {} attribute", Self::WASM_NAME_PATH),
));
}
Ok(Self {
gas: gas.unwrap(),
wasm_name: wasm_name.unwrap(),
docs,
signature: value.sig,
})
}
fn parse_number(value: &Expr) -> Result<usize, syn::Error> {
match value {
Expr::Lit(ExprLit {
lit: Lit::Int(i), ..
}) => i.base10_parse::<usize>(),
other => Err(syn::Error::new_spanned(
other,
"expected an integer literal",
)),
}
}
fn parse_string(value: &Expr) -> Result<String, syn::Error> {
match value {
Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) => Ok(s.value()),
other => Err(syn::Error::new_spanned(other, "expected string literal")),
}
}
}
#[cfg(test)]
@@ -7,8 +172,25 @@ mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
fn reads_gas_and_wasm_name() {
let f: TraitItemFn = syn::parse_quote! {
/// some comment
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn() -> [u8; 4];
};
let p = ParsedHostFunction::parse(f).unwrap();
assert_eq!(p.gas, 60);
assert_eq!(p.wasm_name, "ldgr_index");
}
#[test]
fn rejects_unknown_attribute() {
let f: TraitItemFn = syn::parse_quote! {
#[gas = 60]
#[wsam_name = "typo"]
fn get_ledger_sqn() -> [u8; 4];
};
assert!(ParsedHostFunction::parse(f).is_err());
}
}

View File

@@ -4,3 +4,4 @@ version = "0.1.0"
edition.workspace = true
[dependencies]
xrpl-host-functions-macros.path = "../xrpl-host-functions-macros"

View File

@@ -1,14 +1,110 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
#![no_std]
use xrpl_host_functions_macros::host_abi;
/// Error codes a host function may return.
///
/// The discriminants mirror `HostFunctionError` in
/// `include/xrpl/tx/wasm/WasmCommon.h`, so a negative `i32` crossing the wasm
/// boundary means the same thing to the guest, the Rust host, and the existing
/// C++ code. The full set is kept (not just the ones the PoC uses today) to
/// preserve that shared meaning.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum HostError {
Internal = -1,
FieldNotFound = -2,
BufferTooSmall = -3,
NoArray = -4,
NotLeafField = -5,
LocatorMalformed = -6,
SlotOutRange = -7,
SlotsFull = -8,
EmptySlot = -9,
LedgerObjNotFound = -10,
Decoding = -11,
DataFieldTooLarge = -12,
PointerOutOfBounds = -13,
NoMemExported = -14,
InvalidParams = -15,
InvalidAccount = -16,
InvalidField = -17,
IndexOutOfBounds = -18,
FloatInputMalformed = -19,
FloatComputationError = -20,
NoRuntime = -21,
OutOfGas = -22,
OutOfTransferLimit = -23,
}
#[cfg(test)]
mod tests {
use super::*;
impl HostError {
/// The negative wire value the guest sees as the function's return code.
#[inline]
pub const fn code(self) -> i32 {
self as i32
}
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
/// Reconstruct a `HostError` from its wire code; unknown/positive values map to `Internal`.
pub const fn from_code(code: i32) -> HostError {
match code {
-1 => HostError::Internal,
-2 => HostError::FieldNotFound,
-3 => HostError::BufferTooSmall,
-4 => HostError::NoArray,
-5 => HostError::NotLeafField,
-6 => HostError::LocatorMalformed,
-7 => HostError::SlotOutRange,
-8 => HostError::SlotsFull,
-9 => HostError::EmptySlot,
-10 => HostError::LedgerObjNotFound,
-11 => HostError::Decoding,
-12 => HostError::DataFieldTooLarge,
-13 => HostError::PointerOutOfBounds,
-14 => HostError::NoMemExported,
-15 => HostError::InvalidParams,
-16 => HostError::InvalidAccount,
-17 => HostError::InvalidField,
-18 => HostError::IndexOutOfBounds,
-19 => HostError::FloatInputMalformed,
-20 => HostError::FloatComputationError,
-21 => HostError::NoRuntime,
-22 => HostError::OutOfGas,
-23 => HostError::OutOfTransferLimit,
_ => HostError::Internal,
}
}
}
/// Convenience alias for the trait's fallible returns.
pub type HostResult<T> = Result<T, HostError>;
/// A `sha512Half` digest: the first 32 bytes of a SHA-512, as XRPL uses it.
pub const HASH_LEN: usize = 32;
/// Per-function ABI metadata: the wasm import name and the consensus-fixed base gas cost.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HostFnSpec {
pub name: &'static str,
pub base_gas: u64,
}
host_functions! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn() -> [u8; 4];
#[gas = 70]
#[wasm_name = "home_le_field"]
fn get_current_ledger_obj_field(field: i32) -> Vec<u8>;
#[gas = 2000]
#[wasm_name = "sha512_half"]
fn sha512_half(data: &[u8]) -> [u8; 32];
#[gas = 500]
#[wasm_name = "trace"]
fn trace(msg: &str, data: &[u8], as_hex: bool);
#[gas = 500]
#[wasm_name = "trace_num"]
fn trace_num(msg: &str, number: i64);
}