diff --git a/crates/Cargo.lock b/crates/Cargo.lock index cdaceb3ae9..871b6bab8a 100644 --- a/crates/Cargo.lock +++ b/crates/Cargo.lock @@ -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" diff --git a/crates/xrpl-host-functions-macros/Cargo.toml b/crates/xrpl-host-functions-macros/Cargo.toml index ad785f2543..e5efeda8ea 100644 --- a/crates/xrpl-host-functions-macros/Cargo.toml +++ b/crates/xrpl-host-functions-macros/Cargo.toml @@ -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" diff --git a/crates/xrpl-host-functions-macros/src/lib.rs b/crates/xrpl-host-functions-macros/src/lib.rs index b93cf3ffd9..6d40738670 100644 --- a/crates/xrpl-host-functions-macros/src/lib.rs +++ b/crates/xrpl-host-functions-macros/src/lib.rs @@ -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 { + 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, +} + +impl Parse for HostFunctionsInput { + fn parse(input: ParseStream) -> syn::Result { + 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, + signature: Signature, +} + +impl ParsedHostFunction { + const GAS_PATH: &str = "gas"; + const WASM_NAME_PATH: &str = "wasm_name"; + + fn parse(value: TraitItemFn) -> Result { + 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 { + match value { + Expr::Lit(ExprLit { + lit: Lit::Int(i), .. + }) => i.base10_parse::(), + other => Err(syn::Error::new_spanned( + other, + "expected an integer literal", + )), + } + } + + fn parse_string(value: &Expr) -> Result { + 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()); } } diff --git a/crates/xrpl-host-functions/Cargo.toml b/crates/xrpl-host-functions/Cargo.toml index 67052a9fc2..c08bb7d62f 100644 --- a/crates/xrpl-host-functions/Cargo.toml +++ b/crates/xrpl-host-functions/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition.workspace = true [dependencies] +xrpl-host-functions-macros.path = "../xrpl-host-functions-macros" diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index b93cf3ffd9..73a2deecbc 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -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 = Result; + +/// 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; + + #[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); +}