diff --git a/Cargo.lock b/Cargo.lock index b9d52de..01a6653 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2455,6 +2455,7 @@ dependencies = [ "xcq-extension", "xcq-extension-core", "xcq-extension-fungibles", + "xcq-primitives", ] [[package]] @@ -4525,6 +4526,7 @@ dependencies = [ "tracing", "xcq-executor", "xcq-extension-procedural", + "xcq-primitives", ] [[package]] @@ -4534,6 +4536,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "xcq-extension", + "xcq-primitives", ] [[package]] @@ -4543,12 +4546,15 @@ dependencies = [ "parity-scale-codec", "scale-info", "xcq-extension", + "xcq-primitives", ] [[package]] name = "xcq-extension-procedural" version = "0.1.0" dependencies = [ + "Inflector", + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.66", @@ -4561,6 +4567,7 @@ version = "0.1.0" dependencies = [ "parity-scale-codec", "scale-info", + "xcq-types", ] [[package]] @@ -4569,18 +4576,6 @@ version = "0.1.0" dependencies = [ "sp-api", "xcq-primitives", - "xcq-runtime-api-procedural", -] - -[[package]] -name = "xcq-runtime-api-procedural" -version = "0.1.0" -dependencies = [ - "Inflector", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.66", ] [[package]] @@ -4596,6 +4591,7 @@ dependencies = [ "xcq-extension", "xcq-extension-core", "xcq-extension-fungibles", + "xcq-primitives", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bf31395..a376063 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,9 +66,11 @@ clap = { version = "4.5.4", features = ["derive"] } env_logger = { version = "0.11.3" } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +impl-trait-for-tuples = "0.2" +fortuples = "0.9" # proc macros -syn = { version = "2", features = ["full", "extra-traits"] } +syn = { version = "2", features = ["full", "visit-mut", "extra-traits"] } quote = "1" proc-macro2 = "1" proc-macro-crate = "3" diff --git a/poc/guests/Cargo.lock b/poc/guests/Cargo.lock index 8abd863..3dc4aac 100644 --- a/poc/guests/Cargo.lock +++ b/poc/guests/Cargo.lock @@ -2,40 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - -[[package]] -name = "byte-slice-cast" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" - -[[package]] -name = "impl-trait-for-tuples" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "parity-scale-codec" -version = "3.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "306800abfa29c7f16596b5970a588435e3d5b3149683d00c12b699cc19f895ee" -dependencies = [ - "arrayvec", - "byte-slice-cast", - "impl-trait-for-tuples", -] - [[package]] name = "poc-guest-pass-custom-type" version = "0.1.0" @@ -54,7 +20,6 @@ dependencies = [ name = "poc-guest-query-balance-fungibles" version = "0.1.0" dependencies = [ - "parity-scale-codec", "polkavm-derive", ] @@ -83,7 +48,7 @@ dependencies = [ "polkavm-common", "proc-macro2", "quote", - "syn 2.0.63", + "syn", ] [[package]] @@ -91,7 +56,7 @@ name = "polkavm-derive-impl-macro" version = "0.10.0" dependencies = [ "polkavm-derive-impl", - "syn 2.0.63", + "syn", ] [[package]] @@ -112,17 +77,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.63" diff --git a/poc/guests/query-balance-fungibles/Cargo.toml b/poc/guests/query-balance-fungibles/Cargo.toml index 402a17e..4f513d9 100644 --- a/poc/guests/query-balance-fungibles/Cargo.toml +++ b/poc/guests/query-balance-fungibles/Cargo.toml @@ -5,5 +5,4 @@ edition = "2021" publish = false [dependencies] -parity-scale-codec = { version = "3.6.12", default-features = false } polkavm-derive = { workspace = true } diff --git a/poc/guests/query-balance-fungibles/src/main.rs b/poc/guests/query-balance-fungibles/src/main.rs index cdc2718..b364657 100644 --- a/poc/guests/query-balance-fungibles/src/main.rs +++ b/poc/guests/query-balance-fungibles/src/main.rs @@ -16,15 +16,16 @@ extern "C" { #[polkavm_derive::polkavm_export] extern "C" fn main(ptr: u32, size: u32) -> u64 { // no variant for this input, since the return type is same for total_supply/balance - let num_query = unsafe { core::ptr::read_volatile(ptr as *const u8) }; - let query_size = (size - 1) / num_query as u32; + let extension_id = unsafe { core::ptr::read_volatile(ptr as *const u64) }; + let num_query = unsafe { core::ptr::read_volatile((ptr + 8) as *const u8) }; + let query_size = (size - 9) / num_query as u32; let mut sum = 0u64; // in this settings, the return type is same for total_supply/balance // otherwise, we need to recognize return type through input data for i in 0..num_query { - let res = unsafe { call(1, ptr + 1 + query_size * i as u32, query_size) }; - let res_ptr = (res >> 32) as *const u8; - let res_len = (res & 0xffffffff) as u32; + let res = unsafe { call(extension_id, ptr + 9 + query_size * i as u32, query_size) }; + let res_len = (res >> 32) as u32; + let res_ptr = (res & 0xffffffff) as *const u8; let res_bytes = unsafe { core::slice::from_raw_parts(res_ptr, res_len as usize) }; sum += u64::from_le_bytes(res_bytes.try_into().unwrap()); } diff --git a/poc/runtime/Cargo.toml b/poc/runtime/Cargo.toml index 8a72f3c..4bdbe05 100644 --- a/poc/runtime/Cargo.toml +++ b/poc/runtime/Cargo.toml @@ -27,6 +27,7 @@ xcq-executor = { workspace = true } xcq-extension = { workspace = true } xcq-extension-core = { workspace = true } xcq-extension-fungibles = { workspace = true } +xcq-primitives = { workspace = true } [dev-dependencies] hex = "0.4" diff --git a/poc/runtime/src/lib.rs b/poc/runtime/src/lib.rs index 239dc5c..1de682b 100644 --- a/poc/runtime/src/lib.rs +++ b/poc/runtime/src/lib.rs @@ -229,6 +229,9 @@ impl_runtime_apis! { fn execute_query(query: Vec, input: Vec) -> xcq::XcqResult { xcq::execute_query(query, input) } + fn metadata() -> Vec { + xcq::metadata().encode() + } } } diff --git a/poc/runtime/src/xcq.rs b/poc/runtime/src/xcq.rs index bb4dd03..6d77248 100644 --- a/poc/runtime/src/xcq.rs +++ b/poc/runtime/src/xcq.rs @@ -7,10 +7,12 @@ pub type XcqResponse = Vec; pub type XcqError = String; pub type XcqResult = Result; -use xcq_extension::{ExtensionsExecutor, Guest, Input, InvokeSource, Method}; +use xcq_extension::{impl_extensions, ExtensionsExecutor, Guest, Input, InvokeSource, Method}; +use xcq_primitives::metadata::Metadata; decl_runtime_apis! { pub trait XcqApi { fn execute_query(query: Vec, input: Vec) -> XcqResult; + fn metadata() -> Vec; } } @@ -21,38 +23,34 @@ impl xcq_extension_core::Config for ExtensionImpl { type ExtensionId = u64; } -impl xcq_extension_core::ExtensionCore for ExtensionImpl { - type Config = ExtensionImpl; - fn has_extension(id: ::ExtensionId) -> bool { - matches!(id, 0 | 1) - } -} - // extension_fungibles impls impl xcq_extension_fungibles::Config for ExtensionImpl { - type AccountId = crate::interface::AccountId; + type AccountId = [u8; 32]; type Balance = crate::interface::Balance; type AssetId = crate::interface::AssetId; } - -impl xcq_extension_fungibles::ExtensionFungibles for ExtensionImpl { - type Config = ExtensionImpl; - fn balance( - asset: xcq_extension_fungibles::AssetIdFor, - who: xcq_extension_fungibles::AccountIdFor, - ) -> xcq_extension_fungibles::BalanceFor { - crate::Assets::balance(asset, who) +impl_extensions! { + impl xcq_extension_core::ExtensionCore for ExtensionImpl { + type Config = ExtensionImpl; + fn has_extension(id: ::ExtensionId) -> bool { + matches!(id, xcq_extension_core::EXTENSION_ID | xcq_extension_fungibles::EXTENSION_ID) + } } - fn total_supply(asset: xcq_extension_fungibles::AssetIdFor) -> xcq_extension_fungibles::BalanceFor { - crate::Assets::total_supply(asset) + + impl xcq_extension_fungibles::ExtensionFungibles for ExtensionImpl { + type Config = ExtensionImpl; + fn balance( + asset: ::AssetId, + who: ::AccountId, + ) -> ::Balance { + crate::Assets::balance(asset, crate::interface::AccountId::from(who)) + } + fn total_supply(asset: ::AssetId) -> ::Balance { + crate::Assets::total_supply(asset) + } } } -type Extensions = ( - xcq_extension_core::Call, - xcq_extension_fungibles::Call, -); - // guest impls pub struct GuestImpl { pub program: Vec, @@ -87,6 +85,10 @@ pub fn execute_query(query: Vec, input: Vec) -> XcqResult { executor.execute_method(guest, input) } +pub fn metadata() -> Metadata { + ExtensionImpl::runtime_metadata().into() +} + #[cfg(test)] mod tests { @@ -97,8 +99,8 @@ mod tests { #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] enum FungiblesMethod { - Balance { asset: AssetId, who: AccountId }, TotalSupply { asset: AssetId }, + Balance { asset: AssetId, who: AccountId }, } #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] @@ -109,7 +111,7 @@ mod tests { fn call_transparent_data_hex() { let raw_blob = include_bytes!("../../../output/poc-guest-transparent-call.polkavm"); // call fungible extension - let mut data = 1u64.encode(); + let mut data = xcq_extension_fungibles::EXTENSION_ID.encode(); let method = FungiblesMethod::TotalSupply { asset: 21 }; data.extend_from_slice(&method.encode()); dbg!(hex::encode((raw_blob.to_vec(), data).encode())); @@ -123,14 +125,15 @@ mod tests { .public(); let alice_account = AccountId::from(alice_public); // query num - let mut data = vec![2u8]; + let mut data = xcq_extension_fungibles::EXTENSION_ID.encode(); + data.extend_from_slice(&vec![2u8]); let method1 = FungiblesMethod::Balance { asset: 21, - who: alice_account.clone(), + who: alice_account.clone().into(), }; let method2 = FungiblesMethod::Balance { asset: 1984, - who: alice_account, + who: alice_account.into(), }; data.extend_from_slice(&method1.encode()); data.extend_from_slice(&method2.encode()); diff --git a/xcq-extension-core/Cargo.toml b/xcq-extension-core/Cargo.toml index b880172..100f8bb 100644 --- a/xcq-extension-core/Cargo.toml +++ b/xcq-extension-core/Cargo.toml @@ -11,6 +11,7 @@ version.workspace = true parity-scale-codec = { workspace = true } xcq-extension = { workspace = true } scale-info = { workspace = true } +xcq-primitives = { workspace = true } [features] default = ["std"] diff --git a/xcq-extension-core/src/lib.rs b/xcq-extension-core/src/lib.rs index 7dad134..fb1630d 100644 --- a/xcq-extension-core/src/lib.rs +++ b/xcq-extension-core/src/lib.rs @@ -1,19 +1,20 @@ #![cfg_attr(not(feature = "std"), no_std)] -use parity_scale_codec::{Decode, Encode}; -use scale_info::prelude::vec::Vec; -use xcq_extension::extension; +use parity_scale_codec::Encode; +use xcq_extension::decl_extensions; -#[extension] -pub trait ExtensionCore { - type Config: Config; - fn has_extension(id: ::ExtensionId) -> bool; - // crypto functions - // fn blake2_64(data: Vec) -> [u8; 8]; - // fn blake2_128(data: Vec) -> [u8; 16]; - // fn blake2_256(data: Vec) -> [u8; 32]; - // fn twox_64(data: Vec) -> [u8; 8]; - // fn read_storage(key: Vec) -> Option>; -} pub trait Config { - type ExtensionId: Decode; + type ExtensionId: Encode; +} + +decl_extensions! { + pub trait ExtensionCore { + type Config: Config; + fn has_extension(id: ::ExtensionId) -> bool; + // crypto functions + // fn blake2_64(data: Vec) -> [u8; 8]; + // fn blake2_128(data: Vec) -> [u8; 16]; + // fn blake2_256(data: Vec) -> [u8; 32]; + // fn twox_64(data: Vec) -> [u8; 8]; + // fn read_storage(key: Vec) -> Option>; + } } diff --git a/xcq-extension-fungibles/Cargo.toml b/xcq-extension-fungibles/Cargo.toml index 9c01c3d..1a9be94 100644 --- a/xcq-extension-fungibles/Cargo.toml +++ b/xcq-extension-fungibles/Cargo.toml @@ -11,6 +11,7 @@ version.workspace = true parity-scale-codec = { workspace = true } xcq-extension = { workspace = true } scale-info = { workspace = true } +xcq-primitives = { workspace = true } [features] default = ["std"] diff --git a/xcq-extension-fungibles/src/lib.rs b/xcq-extension-fungibles/src/lib.rs index 60287d5..c13ed64 100644 --- a/xcq-extension-fungibles/src/lib.rs +++ b/xcq-extension-fungibles/src/lib.rs @@ -1,27 +1,22 @@ #![cfg_attr(not(feature = "std"), no_std)] -use parity_scale_codec::{Decode, Encode}; -use scale_info::prelude::vec::Vec; -use xcq_extension::extension; - -pub type AccountIdFor = <::Config as Config>::AccountId; -pub type BalanceFor = <::Config as Config>::Balance; -pub type AssetIdFor = <::Config as Config>::AssetId; - -#[extension] -pub trait ExtensionFungibles { - type Config: Config; - // fungibles::Inspect (not extensive) - // fn total_inssuance(asset: AssetIdFor) -> BalanceFor; - // fn minimum_balance(asset: AssetIdFor) -> BalanceFor; - fn total_supply(asset: AssetIdFor) -> BalanceFor; - fn balance(asset: AssetIdFor, who: AccountIdFor) -> BalanceFor; - // fungibles::InspectEnumerable - // fn asset_ids() -> Vec>; - // fn account_balances(who: AccountIdFor) -> Vec<(AssetIdFor, BalanceFor)>; -} +use parity_scale_codec::{Codec, Encode}; +use xcq_extension::decl_extensions; pub trait Config { - type AccountId: Decode; - type AssetId: Decode; - type Balance: Encode; + type AssetId: Codec; + type AccountId: Codec; + type Balance: Codec; +} +decl_extensions! { + pub trait ExtensionFungibles { + // fungibles::Inspect (not extensive) + // fn total_inssuance(asset: AssetIdFor) -> BalanceFor; + // fn minimum_balance(asset: AssetIdFor) -> BalanceFor; + type Config: Config; + fn total_supply(asset: ::AssetId) -> ::Balance; + fn balance(asset: ::AssetId, who: ::AccountId) -> ::Balance; + // fungibles::InspectEnumerable + // fn asset_ids() -> Vec>; + // fn account_balances(who: AccountIdFor) -> Vec<(AssetIdFor, BalanceFor)>; + } } diff --git a/xcq-extension/Cargo.toml b/xcq-extension/Cargo.toml index 77f6e62..b9e1714 100644 --- a/xcq-extension/Cargo.toml +++ b/xcq-extension/Cargo.toml @@ -11,10 +11,12 @@ version.workspace = true parity-scale-codec = { workspace = true } scale-info = { workspace = true } xcq-executor = { workspace = true } -impl-trait-for-tuples = "0.2.2" +impl-trait-for-tuples = { workspace = true } tracing = { workspace = true } xcq-extension-procedural = { path = "procedural" } +[dev-dependencies] +xcq-primitives = { workspace = true } [features] default = ["std"] diff --git a/xcq-extension/procedural/Cargo.toml b/xcq-extension/procedural/Cargo.toml index 822a392..a9ac513 100644 --- a/xcq-extension/procedural/Cargo.toml +++ b/xcq-extension/procedural/Cargo.toml @@ -13,4 +13,6 @@ proc-macro = true quote = { workspace = true } syn = { workspace = true } proc-macro2 = { workspace = true } +proc-macro-crate = { workspace = true } +Inflector = { workspace = true } twox-hash = "1.6.3" diff --git a/xcq-extension/procedural/src/decl_extensions.rs b/xcq-extension/procedural/src/decl_extensions.rs new file mode 100644 index 0000000..d19bf76 --- /dev/null +++ b/xcq-extension/procedural/src/decl_extensions.rs @@ -0,0 +1,204 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use std::hash::{Hash, Hasher}; +use syn::{ + parse_macro_input, parse_quote, parse_str, punctuated::Punctuated, spanned::Spanned, token::Comma, Error, ExprCall, + Field, FnArg, Ident, ItemEnum, ItemImpl, ItemTrait, Pat, Result, TraitItem, Type, Variant, +}; + +use crate::utils::{generate_crate_access, generate_mod_name_for_trait}; + +pub fn decl_extensions_impl(input: TokenStream) -> TokenStream { + let item_trait = parse_macro_input!(input as ItemTrait); + decl_extension_inner(&item_trait) + .unwrap_or_else(|e| e.to_compile_error()) + .into() +} + +pub fn decl_extension_inner(item_trait: &ItemTrait) -> Result { + let mod_name = generate_mod_name_for_trait(&item_trait.ident); + + // Assume single config associated type. + let has_config = item_trait + .items + .iter() + .any(|item| matches!(item, syn::TraitItem::Type(_))); + let methods = methods(&item_trait.items)?; + + let call_enum_def = call_enum_def(&item_trait.ident, &methods)?; + let dispatchable_impl = dispatchable_impl(&item_trait.ident, &methods)?; + let extension_id_impl = extension_id_impl(&item_trait.ident, &item_trait.items)?; + + let runtime_metadata = crate::runtime_metadata::generate_decl_metadata(item_trait, has_config)?; + + let expanded = quote! { + #item_trait + #[doc(hidden)] + #[allow(dead_code)] + pub mod #mod_name { + pub use super::*; + #call_enum_def + #dispatchable_impl + #extension_id_impl + #runtime_metadata + } + pub use #mod_name::*; + }; + Ok(expanded) +} +#[derive(Clone)] +struct Method { + /// Function name + pub name: Ident, + /// Information on args: `(name, type)` + pub args: Vec<(Ident, Box)>, +} + +fn call_enum_def(trait_ident: &Ident, methods: &[Method]) -> Result { + let xcq_primitives = generate_crate_access("xcq-primitives")?; + let mut variants = Punctuated::::new(); + for method in methods { + let name = &method.name; + let mut args = Punctuated::::new(); + for (name, ty) in &method.args { + let ty = replace_self_to_impl(ty)?; + args.push(parse_quote! { + #name: #ty + }); + } + variants.push(parse_quote! { + #[allow(non_camel_case_types)] + #name { + #args + } + }); + } + // Add phantom data + variants.push(parse_quote!( + #[doc(hidden)] + __Phantom(core::marker::PhantomData) + )); + Ok(parse_quote!( + #[derive(#xcq_primitives::deps::parity_scale_codec::Decode)] + pub enum Call { + #variants + } + )) +} + +fn dispatchable_impl(trait_ident: &Ident, methods: &[Method]) -> Result { + let xcq_extension = generate_crate_access("xcq-extension")?; + let xcq_primitives = generate_crate_access("xcq-primitives")?; + let mut pats = Vec::::new(); + for method in methods { + let name = &method.name; + let mut args = Punctuated::::new(); + for (ident, _ty) in &method.args { + args.push(parse_quote! { + #ident + }); + } + pats.push(parse_quote! { + Self::#name { + #args + } + }); + } + + let mut method_calls = Vec::::new(); + for method in methods { + let name = &method.name; + let mut args = Punctuated::::new(); + for (ident, _ty) in &method.args { + args.push(parse_quote! { + #ident + }); + } + method_calls.push({ + parse_quote! { + Impl::#name(#args) + } + }); + } + + Ok(parse_quote! { + impl #xcq_extension::Dispatchable for Call { + fn dispatch(self) -> Result<#xcq_primitives::deps::xcq_types::vec::Vec, #xcq_extension::DispatchError> { + match self { + #( #pats => Ok(#method_calls.encode()),)* + Self::__Phantom(_) => unreachable!(), + } + } + } + }) +} + +fn extension_id_impl(trait_ident: &Ident, trait_items: &[TraitItem]) -> Result { + let xcq_extension = generate_crate_access("xcq-extension")?; + let extension_id = calculate_hash(trait_ident, trait_items); + Ok(quote! { + // TODO: check if we need a extension_id trait + impl #xcq_extension::ExtensionId for Call { + const EXTENSION_ID: #xcq_extension::ExtensionIdTy = #extension_id; + } + pub const EXTENSION_ID: #xcq_extension::ExtensionIdTy = #extension_id; + }) +} + +// helper functions +fn methods(trait_items: &[TraitItem]) -> Result> { + let mut methods = vec![]; + for item in trait_items { + if let TraitItem::Fn(method) = item { + let method_name = &method.sig.ident; + let mut method_args = vec![]; + for arg in method.sig.inputs.iter() { + let arg = if let FnArg::Typed(arg) = arg { + arg + } else { + unreachable!("every argument should be typed instead of receiver(self)") + }; + let arg_ident = if let Pat::Ident(pat) = &*arg.pat { + pat.ident.clone() + } else { + let msg = "Invalid call, argument must be ident"; + return Err(Error::new(arg.pat.span(), msg)); + }; + method_args.push((arg_ident, arg.ty.clone())) + } + methods.push(Method { + name: method_name.clone(), + args: method_args, + }); + } + } + Ok(methods) +} + +// TODO: refine this to make it more stable +fn replace_self_to_impl(ty: &Type) -> Result> { + let ty_str = quote!(#ty).to_string(); + + let modified_ty_str = ty_str.replace("Self", "Impl"); + + let modified_ty = parse_str(&modified_ty_str)?; + + Ok(Box::new(modified_ty)) +} + +// TODO: currently we only hash on trait ident and function names, +fn calculate_hash(trait_ident: &Ident, trait_items: &[TraitItem]) -> u64 { + let mut hasher = twox_hash::XxHash64::default(); + // reduce the chance of hash collision + "xcq-ext$".hash(&mut hasher); + trait_ident.hash(&mut hasher); + for trait_item in trait_items { + if let TraitItem::Fn(method) = trait_item { + // reduce the chance of hash collision + "@".hash(&mut hasher); + method.sig.ident.hash(&mut hasher); + } + } + hasher.finish() +} diff --git a/xcq-extension/procedural/src/impl_extensions.rs b/xcq-extension/procedural/src/impl_extensions.rs new file mode 100644 index 0000000..e7f2614 --- /dev/null +++ b/xcq-extension/procedural/src/impl_extensions.rs @@ -0,0 +1,74 @@ +use crate::utils::{extract_impl_trait, generate_mod_name_for_trait, RequireQualifiedTraitPath}; +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, parse_quote, ItemImpl, Result, +}; +pub fn impl_extensions_impl(input: TokenStream) -> TokenStream { + let XcqExtensionImpls { impls } = parse_macro_input!(input as XcqExtensionImpls); + impl_extensions_inner(&impls) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +struct XcqExtensionImpls { + impls: Vec, +} + +impl Parse for XcqExtensionImpls { + fn parse(input: ParseStream) -> Result { + let mut impls = Vec::new(); + while !input.is_empty() { + impls.push(input.parse()?); + } + Ok(Self { impls }) + } +} + +fn impl_extensions_inner(impls: &[ItemImpl]) -> Result { + let runtime_metadata = crate::runtime_metadata::generate_impl_metadata(impls)?; + + let extension_tuple = generate_extension_tuple(impls)?; + + let expanded = quote! { + #(#impls)* + + #runtime_metadata + + #extension_tuple + }; + Ok(expanded) +} + +fn generate_extension_tuple(impls: &[ItemImpl]) -> Result { + let extension_impl_name = &impls + .first() + .expect("Traits should contain at least one implementation; qed") + .self_ty; + let mut extensions = Vec::new(); + for impl_ in impls { + let mut trait_ = extract_impl_trait(impl_, RequireQualifiedTraitPath::Yes)?.clone(); + + let trait_name_ident = &trait_ + .segments + .last() + .as_ref() + .expect("Trait path should always contain at least one item; qed") + .ident; + let mod_name = generate_mod_name_for_trait(trait_name_ident); + // Get absolute path to the `runtime_decl_for_` module by replacing the last segment. + if let Some(segment) = trait_.segments.last_mut() { + *segment = parse_quote!(#mod_name); + } + trait_.segments.push(parse_quote!(Call<#extension_impl_name>)); + extensions.push(trait_); + } + + Ok(quote! { + type Extensions = ( + #(#extensions),* + ); + }) +} diff --git a/xcq-extension/procedural/src/lib.rs b/xcq-extension/procedural/src/lib.rs index 96fc632..6e3f753 100644 --- a/xcq-extension/procedural/src/lib.rs +++ b/xcq-extension/procedural/src/lib.rs @@ -1,182 +1,15 @@ -use proc_macro2::TokenStream; -use quote::quote; -use std::hash::{Hash, Hasher}; -use syn::token::Comma; -use syn::{parse_macro_input, parse_quote, parse_str, spanned::Spanned}; -use syn::{punctuated::Punctuated, ExprCall, Field, Ident, ItemImpl, Pat, TraitItem, Variant}; +use proc_macro::TokenStream; +mod decl_extensions; +mod impl_extensions; +mod runtime_metadata; +mod utils; -#[derive(Clone)] -struct Method { - /// Function name - pub name: Ident, - /// Information on args: `(name, type)` - pub args: Vec<(Ident, Box)>, +#[proc_macro] +pub fn decl_extensions(input: TokenStream) -> TokenStream { + decl_extensions::decl_extensions_impl(input) } -#[proc_macro_attribute] -pub fn extension(_args: proc_macro::TokenStream, input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let input = parse_macro_input!(input as syn::ItemTrait); - - let methods = match methods(&input.items) { - Ok(method) => method, - Err(e) => return e.to_compile_error().into(), - }; - - let call_enum_def = match call_enum_def(&input.ident, &methods) { - Ok(call_enum_def) => call_enum_def, - Err(e) => return e.to_compile_error().into(), - }; - - let dispatchable_impl = dispatchable_impl(&input.ident, &methods); - let extension_id_impl = extension_id_impl(&input.ident, &input.items); - - let expanded = quote! { - #input - #call_enum_def - #dispatchable_impl - #extension_id_impl - }; - expanded.into() -} - -fn call_enum_def(trait_ident: &Ident, methods: &[Method]) -> syn::Result { - let mut variants = Punctuated::::new(); - for method in methods { - let name = &method.name; - let mut args = Punctuated::::new(); - for (name, ty) in &method.args { - let ty = replace_self_to_impl(ty)?; - args.push(parse_quote! { - #name: #ty - }); - } - variants.push(parse_quote! { - #[allow(non_camel_case_types)] - #name { - #args - } - }); - } - // Add phantom data - variants.push(parse_quote!( - #[doc(hidden)] - __Phantom(core::marker::PhantomData) - )); - Ok(parse_quote!( - #[derive(Decode)] - pub enum Call { - #variants - } - )) -} - -fn dispatchable_impl(trait_ident: &Ident, methods: &[Method]) -> TokenStream { - let mut pats = Vec::::new(); - for method in methods { - let name = &method.name; - let mut args = Punctuated::::new(); - for (ident, _ty) in &method.args { - args.push(parse_quote! { - #ident - }); - } - pats.push(parse_quote! { - Self::#name { - #args - } - }); - } - - let mut method_calls = Vec::::new(); - for method in methods { - let name = &method.name; - let mut args = Punctuated::::new(); - for (ident, _ty) in &method.args { - args.push(parse_quote! { - #ident - }); - } - method_calls.push({ - parse_quote! { - Impl::#name(#args) - } - }); - } - - parse_quote! { - impl xcq_extension::Dispatchable for Call { - fn dispatch(self) -> Result, xcq_extension::DispatchError> { - match self { - #( #pats => Ok(#method_calls.encode()),)* - Self::__Phantom(_) => unreachable!(), - } - } - } - } -} - -fn extension_id_impl(trait_ident: &Ident, trait_items: &[TraitItem]) -> ItemImpl { - let extension_id = calculate_hash(trait_ident, trait_items); - parse_quote! { - impl xcq_extension::ExtensionId for Call { - const EXTENSION_ID: xcq_extension::ExtensionIdTy = #extension_id; - } - } -} - -// helper functions -fn methods(trait_items: &[TraitItem]) -> syn::Result> { - let mut methods = vec![]; - for item in trait_items { - if let TraitItem::Fn(method) = item { - let method_name = &method.sig.ident; - let mut method_args = vec![]; - for arg in method.sig.inputs.iter() { - let arg = if let syn::FnArg::Typed(arg) = arg { - arg - } else { - unreachable!("every argument should be typed instead of receiver(self)") - }; - let arg_ident = if let syn::Pat::Ident(pat) = &*arg.pat { - pat.ident.clone() - } else { - let msg = "Invalid call, argument must be ident"; - return Err(syn::Error::new(arg.pat.span(), msg)); - }; - method_args.push((arg_ident, arg.ty.clone())) - } - methods.push(Method { - name: method_name.clone(), - args: method_args, - }); - } - } - Ok(methods) -} - -// TODO: refine this to make it more stable -fn replace_self_to_impl(ty: &syn::Type) -> syn::Result> { - let ty_str = quote!(#ty).to_string(); - - let modified_ty_str = ty_str.replace("Self", "Impl"); - - let modified_ty = parse_str(&modified_ty_str)?; - - Ok(Box::new(modified_ty)) -} - -// TODO: currently we only hash on trait ident and function names, -fn calculate_hash(trait_ident: &Ident, trait_items: &[TraitItem]) -> u64 { - let mut hasher = twox_hash::XxHash64::default(); - // reduce the chance of hash collision - "xcq-ext$".hash(&mut hasher); - trait_ident.hash(&mut hasher); - for trait_item in trait_items { - if let TraitItem::Fn(method) = trait_item { - // reduce the chance of hash collision - "@".hash(&mut hasher); - method.sig.ident.hash(&mut hasher); - } - } - hasher.finish() +#[proc_macro] +pub fn impl_extensions(input: TokenStream) -> TokenStream { + impl_extensions::impl_extensions_impl(input) } diff --git a/xcq-extension/procedural/src/runtime_metadata.rs b/xcq-extension/procedural/src/runtime_metadata.rs new file mode 100644 index 0000000..c3ee461 --- /dev/null +++ b/xcq-extension/procedural/src/runtime_metadata.rs @@ -0,0 +1,250 @@ +use std::collections::HashSet; + +use crate::utils::{extract_impl_trait, generate_crate_access, generate_mod_name_for_trait, RequireQualifiedTraitPath}; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{ + parse_quote, punctuated::Punctuated, visit_mut::VisitMut, GenericParam, Generics, ItemImpl, ItemTrait, Result, + Token, +}; + +/// Generate the runtime metadata of the provided extension trait. +/// +/// The metadata is exposed as a generic function on the hidden module +/// of the trait generated by the `decl_extensions`. +pub fn generate_decl_metadata(decl: &ItemTrait, has_config: bool) -> Result { + let xcq_primitives = generate_crate_access("xcq-primitives")?; + let mut methods = Vec::new(); + + // Convert `::Associated` to `T::Associated` with `T:Config` bound + let mut replacer = AssociatedTypeReplacer { + generic_params: HashSet::new(), + where_predicates: HashSet::new(), + }; + // Adding bounds: `XcqTypeInfo + 'static` for any type parameter in method sigs (generic parameter and associated type). + let mut where_clause = Vec::new(); + + for item in &decl.items { + // Collect metadata for methods only. + let syn::TraitItem::Fn(method) = item else { continue }; + + let mut inputs = Vec::new(); + let signature = &method.sig; + for input in &signature.inputs { + // Exclude `self` from metadata collection. + let syn::FnArg::Typed(typed) = input else { continue }; + + let pat = &typed.pat; + let name = quote!(#pat).to_string(); + let mut ty = typed.ty.clone(); + + if has_config { + replacer.visit_type_mut(&mut ty); + } + + where_clause.push(get_type_param(&ty)); + + inputs.push(quote!( + #xcq_primitives::metadata_ir::MethodParamMetadataIR { + name: #name, + ty: #xcq_primitives::deps::xcq_types::meta_type::<#ty>(), + } + )); + } + + let output = match &signature.output { + syn::ReturnType::Default => quote!(#xcq_primitives::deps::xcq_types::meta_type::<()>()), + syn::ReturnType::Type(_, ty) => { + let mut ty = ty.clone(); + if has_config { + replacer.visit_type_mut(&mut ty); + } + where_clause.push(get_type_param(&ty)); + quote!(#xcq_primitives::deps::xcq_types::meta_type::<#ty>()) + } + }; + + let method_name = signature.ident.to_string(); + + let attrs = &method.attrs; + methods.push(quote!( + #( #attrs )* + #xcq_primitives::metadata_ir::MethodMetadataIR { + name: #method_name, + inputs: #xcq_primitives::deps::xcq_types::vec![ #( #inputs, )* ], + output: #output, + } + )); + } + + let trait_name_ident = &decl.ident; + let trait_name = trait_name_ident.to_string(); + let attrs = &decl.attrs; + + // Assume no generics + // extract associated types + let mut generics = Generics { + lt_token: Some(syn::token::Lt::default()), + params: replacer.generic_params.into_iter().collect(), + gt_token: Some(syn::token::Gt::default()), + where_clause: None, + }; + + for where_predicate in replacer.where_predicates { + generics.make_where_clause().predicates.push(where_predicate); + } + where_clause + .into_iter() + .map(|ty| parse_quote!(#ty: #xcq_primitives::deps::xcq_types::XcqTypeInfo + 'static)) + .for_each(|w| generics.make_where_clause().predicates.push(w)); + + let (impl_generics, _, where_clause) = generics.split_for_impl(); + if has_config { + Ok(quote!( + #( #attrs )* + #[inline(always)] + pub fn runtime_metadata #impl_generics () -> #xcq_primitives::metadata_ir::ExtensionMetadataIR + #where_clause + { + #xcq_primitives::metadata_ir::ExtensionMetadataIR { + name: #trait_name, + methods: #xcq_primitives::deps::xcq_types::vec![ #( #methods, )* ], + } + } + )) + } else { + Ok(quote!( + #(#attrs)* + #[inline(always)] + pub fn runtime_metadata() -> #xcq_primitives::metadata_ir::ExtensionMetadataIR { + #xcq_primitives::metadata_ir::ExtensionMetadataIR { + name: #trait_name, + methods: #xcq_primitives::deps::xcq_types::vec![ #( #methods, )* ], + } + } + )) + } +} + +/// Implement the `runtime_metadata` function on the extensions impl that +/// generates the metadata for the given traits. +/// The metadata of each extension trait is extracted from the generic function +/// exposed by `generate_decl_metadata`. +pub fn generate_impl_metadata(impls: &[ItemImpl]) -> Result { + if impls.is_empty() { + return Ok(quote!()); + } + + let xcq_primitives = generate_crate_access("xcq-primitives")?; + + // Get the name of the runtime for which the traits are implemented. + let extension_impl_name = &impls + .first() + .expect("Traits should contain at least one implementation; qed") + .self_ty; + + let mut metadata = Vec::new(); + + for impl_ in impls { + let mut trait_ = extract_impl_trait(impl_, RequireQualifiedTraitPath::Yes)?.clone(); + + // Implementation traits are always references with a path `impl client::Core ...` + // The trait name is the last segment of this path. + let trait_name_ident = &trait_ + .segments + .last() + .as_ref() + .expect("Trait path should always contain at least one item; qed") + .ident; + + // Convert associated types to generics + let mut generic_params = HashSet::::new(); + for item in &impl_.items { + if let syn::ImplItem::Type(associated_type) = item { + let ty = &associated_type.ty; + generic_params.insert(parse_quote!(#ty)); + } + } + let generics = Generics { + lt_token: Some(syn::token::Lt::default()), + params: generic_params.into_iter().collect(), + gt_token: Some(syn::token::Gt::default()), + where_clause: None, + }; + + // Extract the generics from the trait to pass to the `runtime_metadata` given by `generate_decl_metadata` + // let generics = trait_ + // .segments + // .iter() + // .find_map(|segment| { + // if let syn::PathArguments::AngleBracketed(generics) = &segment.arguments { + // Some(generics.clone()) + // } else { + // None + // } + // }) + // .expect("Trait path should always contain at least one generic parameter; qed"); + + let mod_name = generate_mod_name_for_trait(trait_name_ident); + // Get absolute path to the `runtime_decl_for_` module by replacing the last segment. + if let Some(segment) = trait_.segments.last_mut() { + *segment = parse_quote!(#mod_name); + } + + let attrs = &impl_.attrs; + metadata.push(quote!( + #( #attrs )* + #trait_::runtime_metadata::#generics() + )); + } + + Ok(quote!( + impl #extension_impl_name { + pub fn runtime_metadata() -> #xcq_primitives::metadata_ir::MetadataIR { + #xcq_primitives::metadata_ir::MetadataIR { + extensions: #xcq_primitives::deps::xcq_types::vec![ #( #metadata, )* ], + } + } + } + )) +} + +// Convert associated type to generic type +// i.e `::Associated` to `::Associated` with `Config::Config` bound +struct AssociatedTypeReplacer { + generic_params: HashSet, + where_predicates: HashSet, +} +impl VisitMut for AssociatedTypeReplacer { + fn visit_path_mut(&mut self, path: &mut syn::Path) { + if path.segments.len() == 2 && path.segments[0].ident == "Self" { + let mut new_segments: Punctuated = Punctuated::new(); + let segment = &path.segments[1]; + let generic_param = format_ident!("GenericFor{}", segment.ident); + let new_segment = syn::PathSegment { + ident: generic_param, + arguments: segment.arguments.clone(), + }; + self.generic_params.insert(parse_quote!(#new_segment)); + self.where_predicates.insert(parse_quote!(#new_segment: #segment)); + new_segments.push(new_segment); + path.segments = new_segments; + } + } +} + +/// Instead of returning `&'a AccountId` for the first parameter, this function +/// returns `AccountId` to place bounds around it. +fn get_type_param(ty: &syn::Type) -> syn::Type { + // Remove the lifetime and mutability of the type T to + // place bounds around it. + let ty_elem = match &ty { + syn::Type::Reference(reference) => &reference.elem, + syn::Type::Ptr(ptr) => &ptr.elem, + syn::Type::Slice(slice) => &slice.elem, + syn::Type::Array(arr) => &arr.elem, + _ => ty, + }; + + ty_elem.clone() +} diff --git a/xcq-extension/procedural/src/utils.rs b/xcq-extension/procedural/src/utils.rs new file mode 100644 index 0000000..2edc27d --- /dev/null +++ b/xcq-extension/procedural/src/utils.rs @@ -0,0 +1,52 @@ +use inflector::Inflector; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use proc_macro_crate::{crate_name, FoundCrate}; +use quote::quote; +use syn::{spanned::Spanned, Error, Ident, ItemImpl, Path, Result}; +/// Should a qualified trait path be required? +/// +/// e.g. `path::Trait` is qualified and `Trait` is not. +#[allow(dead_code)] +pub enum RequireQualifiedTraitPath { + Yes, + No, +} +/// Extract the trait that is implemented by the given `ItemImpl`. +pub fn extract_impl_trait(impl_: &ItemImpl, require: RequireQualifiedTraitPath) -> Result<&Path> { + impl_ + .trait_ + .as_ref() + .map(|v| &v.1) + .ok_or_else(|| Error::new(impl_.span(), "Only implementation of traits are supported!")) + .and_then(|p| { + if p.segments.len() > 1 || matches!(require, RequireQualifiedTraitPath::No) { + Ok(p) + } else { + Err(Error::new( + p.span(), + "The implemented trait has to be referenced with a path, \ + e.g. `impl xcq_extension_core::ExtensionCore for Runtime`.", + )) + } + }) +} + +/// Generates the name of the module that contains the trait declaration for the runtime. +pub fn generate_mod_name_for_trait(trait_: &Ident) -> Ident { + Ident::new( + &format!("decl_extension_for_{}", trait_.to_string().to_snake_case()), + Span::call_site(), + ) +} + +pub fn generate_crate_access(def_crate: &str) -> Result { + match crate_name(def_crate) { + Ok(FoundCrate::Itself) => Ok(quote!(crate)), + Ok(FoundCrate::Name(name)) => { + let name = name.replace('-', "_"); + let ident = Ident::new(&name, Span::call_site()); + Ok(quote!(#ident)) + } + Err(e) => Err(Error::new(Span::call_site(), e)), + } +} diff --git a/xcq-extension/src/lib.rs b/xcq-extension/src/lib.rs index 7c289ce..80c93d2 100644 --- a/xcq-extension/src/lib.rs +++ b/xcq-extension/src/lib.rs @@ -18,7 +18,7 @@ pub use extension_id::{ExtensionId, ExtensionIdTy}; mod error; pub use error::ExtensionError; mod macros; -pub use xcq_extension_procedural::extension; +pub use xcq_extension_procedural::{decl_extensions, impl_extensions}; mod perm_controller; pub use perm_controller::{InvokeSource, PermController}; diff --git a/xcq-extension/tests/extension_executor_works.rs b/xcq-extension/tests/extension_executor_works.rs deleted file mode 100644 index 1d9b7b2..0000000 --- a/xcq-extension/tests/extension_executor_works.rs +++ /dev/null @@ -1,148 +0,0 @@ -use parity_scale_codec::{Decode, Encode}; -use xcq_extension::{extension, ExtensionId, ExtensionsExecutor, Guest, Input, InvokeSource, Method}; - -mod extension_core { - use super::*; - #[extension] - pub trait ExtensionCore { - type Config: Config; - fn has_extension(id: ::ExtensionId) -> bool; - // crypto functions - // fn blake2_64(data: Vec) -> [u8; 8]; - // fn blake2_128(data: Vec) -> [u8; 16]; - // fn blake2_256(data: Vec) -> [u8; 32]; - // fn twox_64(data: Vec) -> [u8; 8]; - // fn read_storage(key: Vec) -> Option>; - } - pub trait Config { - type ExtensionId: Decode; - } -} - -mod extension_fungibles { - use super::*; - pub type AccountIdFor = <::Config as Config>::AccountId; - pub type BalanceFor = <::Config as Config>::Balance; - pub type AssetIdFor = <::Config as Config>::AssetId; - #[extension] - pub trait ExtensionFungibles { - type Config: Config; - fn total_supply(asset: AssetIdFor) -> BalanceFor; - fn balance(asset: AssetIdFor, who: AccountIdFor) -> BalanceFor; - } - pub trait Config { - type AccountId: Decode; - type AssetId: Decode; - type Balance: Encode; - } -} - -// extension_core impls -pub struct ExtensionCoreImpl; - -pub struct ExtensionCoreConfigImpl; -impl extension_core::Config for ExtensionCoreConfigImpl { - type ExtensionId = u64; -} - -impl extension_core::ExtensionCore for ExtensionCoreImpl { - type Config = ExtensionCoreConfigImpl; - fn has_extension(id: ::ExtensionId) -> bool { - matches!(id, 0 | 1) - } -} - -// extension_fungibles impls -pub struct ExtensionFungiblesImpl; -pub struct ExtensionFungiblesConfigImpl; - -impl extension_fungibles::Config for ExtensionFungiblesConfigImpl { - type AccountId = [u8; 32]; - type Balance = u32; - type AssetId = u64; -} - -impl extension_fungibles::ExtensionFungibles for ExtensionFungiblesImpl { - type Config = ExtensionFungiblesConfigImpl; - fn balance( - _asset: extension_fungibles::AssetIdFor, - _who: extension_fungibles::AccountIdFor, - ) -> extension_fungibles::BalanceFor { - 0 - } - fn total_supply(_asset: extension_fungibles::AssetIdFor) -> extension_fungibles::BalanceFor { - 100 - } -} - -type Extensions = ( - extension_core::Call, - extension_fungibles::Call, -); - -// guest impls -pub struct GuestImpl { - pub program: Vec, -} - -impl Guest for GuestImpl { - fn program(&self) -> &[u8] { - &self.program - } -} - -pub struct InputImpl { - pub method: Method, - pub args: Vec, -} - -impl Input for InputImpl { - fn method(&self) -> Method { - self.method.clone() - } - fn args(&self) -> &[u8] { - &self.args - } -} - -#[derive(Encode, Decode)] -enum CoreMethod { - HasExtension { id: u64 }, -} - -#[derive(Encode, Decode)] -enum FungiblesMethod { - TotalSupply { asset: u64 }, - Balance { asset: u64, who: [u8; 32] }, -} -#[test] -fn call_core_works() { - let blob = include_bytes!("../../output/poc-guest-transparent-call.polkavm"); - let mut executor = ExtensionsExecutor::::new(InvokeSource::RuntimeAPI); - let guest = GuestImpl { program: blob.to_vec() }; - let method = CoreMethod::HasExtension { id: 0 }; - let mut input_data = as ExtensionId>::EXTENSION_ID.encode(); - input_data.extend_from_slice(&method.encode()); - let input = InputImpl { - method: "main".to_string(), - args: input_data, - }; - let res = executor.execute_method(guest, input).unwrap(); - assert_eq!(res, vec![1]); -} - -#[test] -fn call_fungibles_works() { - let blob = include_bytes!("../../output/poc-guest-transparent-call.polkavm"); - let mut executor = ExtensionsExecutor::::new(InvokeSource::RuntimeAPI); - let guest = GuestImpl { program: blob.to_vec() }; - let method = FungiblesMethod::TotalSupply { asset: 1u64 }; - let mut input_data = as ExtensionId>::EXTENSION_ID.encode(); - input_data.extend_from_slice(&method.encode()); - let input = InputImpl { - method: "main".to_string(), - args: input_data, - }; - let res = executor.execute_method(guest, input).unwrap(); - assert_eq!(res, vec![100u8, 0u8, 0u8, 0u8]); -} diff --git a/xcq-extension/tests/with_associated_types_works.rs b/xcq-extension/tests/with_associated_types_works.rs new file mode 100644 index 0000000..3daa3ca --- /dev/null +++ b/xcq-extension/tests/with_associated_types_works.rs @@ -0,0 +1,198 @@ +use parity_scale_codec::{Codec, Decode, Encode}; +use xcq_extension::{decl_extensions, impl_extensions, ExtensionsExecutor, Guest, Input, InvokeSource, Method}; +use xcq_primitives::deps::xcq_types::{PrimitiveType, XcqType}; +use xcq_primitives::metadata::{ExtensionMetadata, Metadata, MethodMetadata, MethodParamMetadata}; + +mod extension_core { + use super::*; + pub trait Config { + type ExtensionId: Codec; + } + decl_extensions! { + pub trait ExtensionCore { + type Config:Config; + fn has_extension(id: ::ExtensionId) -> bool; + // crypto functions + // fn blake2_64(data: Vec) -> [u8; 8]; + // fn blake2_128(data: Vec) -> [u8; 16]; + // fn blake2_256(data: Vec) -> [u8; 32]; + // fn twox_64(data: Vec) -> [u8; 8]; + // fn read_storage(key: Vec) -> Option>; + } + } +} + +mod extension_fungibles { + use super::*; + pub trait Config { + type AssetId: Codec; + type AccountId: Codec; + type Balance: Codec; + } + decl_extensions! { + pub trait ExtensionFungibles { + type Config:Config; + fn total_supply(asset: ::AssetId) -> ::Balance; + fn balance(asset: ::AssetId, who: ::AccountId) -> ::Balance; + } + } +} + +pub struct ExtensionImpl; + +impl_extensions! { + impl extension_core::ExtensionCore for ExtensionImpl { + type Config = ExtensionImpl; + fn has_extension(id: ::ExtensionId) -> bool { + matches!(id, 0 | 1) + } + } + + impl extension_fungibles::ExtensionFungibles for ExtensionImpl { + type Config = ExtensionImpl; + #[allow(unused_variables)] + fn total_supply(asset: ::AssetId) -> ::Balance { + 100 + } + #[allow(unused_variables)] + fn balance(asset: ::AssetId, who: ::AccountId) -> ::Balance { + 100 + } + } +} +impl extension_core::Config for ExtensionImpl { + type ExtensionId = u64; +} + +impl extension_fungibles::Config for ExtensionImpl { + type AssetId = u32; + type AccountId = [u8; 32]; + type Balance = u64; +} + +// guest impls +pub struct GuestImpl { + pub program: Vec, +} + +impl Guest for GuestImpl { + fn program(&self) -> &[u8] { + &self.program + } +} + +pub struct InputImpl { + pub method: Method, + pub args: Vec, +} + +impl Input for InputImpl { + fn method(&self) -> Method { + self.method.clone() + } + fn args(&self) -> &[u8] { + &self.args + } +} + +#[derive(Encode, Decode)] +enum CoreMethod { + HasExtension { id: u64 }, +} + +#[derive(Encode, Decode)] +enum FungiblesMethod { + TotalSupply { asset: u32 }, + Balance { asset: u32, who: [u8; 32] }, +} +#[test] +fn call_core_works() { + let blob = include_bytes!("../../output/poc-guest-transparent-call.polkavm"); + let mut executor = ExtensionsExecutor::::new(InvokeSource::RuntimeAPI); + let guest = GuestImpl { program: blob.to_vec() }; + let method = CoreMethod::HasExtension { id: 0 }; + let mut input_data = extension_core::EXTENSION_ID.encode(); + input_data.extend_from_slice(&method.encode()); + let input = InputImpl { + method: "main".to_string(), + args: input_data, + }; + let res = executor.execute_method(guest, input).unwrap(); + assert_eq!(res, vec![1]); +} + +#[test] +fn call_fungibles_works() { + let blob = include_bytes!("../../output/poc-guest-query-balance-fungibles.polkavm"); + let mut executor = ExtensionsExecutor::::new(InvokeSource::RuntimeAPI); + let guest = GuestImpl { program: blob.to_vec() }; + let mut input_data = extension_fungibles::EXTENSION_ID.encode(); + input_data.extend_from_slice(&vec![2u8]); + let method1 = FungiblesMethod::Balance { + asset: 1, + who: [0u8; 32], + }; + let method2 = FungiblesMethod::Balance { + asset: 2, + who: [0u8; 32], + }; + input_data.extend_from_slice(&method1.encode()); + input_data.extend_from_slice(&method2.encode()); + let input = InputImpl { + method: "main".to_string(), + args: input_data, + }; + let res = executor.execute_method(guest, input).unwrap(); + assert_eq!(res, vec![200u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8]); +} + +#[test] +fn metadata_works() { + let metadata: Metadata = ExtensionImpl::runtime_metadata().into(); + println!("{:?}", metadata); + assert_eq!( + metadata, + Metadata { + extensions: vec![ + ExtensionMetadata { + name: "ExtensionCore", + methods: vec![MethodMetadata { + name: "has_extension", + inputs: vec![MethodParamMetadata { + name: "id", + ty: XcqType::Primitive(PrimitiveType::U64) + }], + output: XcqType::Primitive(PrimitiveType::Bool) + }] + }, + ExtensionMetadata { + name: "ExtensionFungibles", + methods: vec![ + MethodMetadata { + name: "total_supply", + inputs: vec![MethodParamMetadata { + name: "asset", + ty: XcqType::Primitive(PrimitiveType::U32) + }], + output: XcqType::Primitive(PrimitiveType::U64) + }, + MethodMetadata { + name: "balance", + inputs: vec![ + MethodParamMetadata { + name: "asset", + ty: XcqType::Primitive(PrimitiveType::U32) + }, + MethodParamMetadata { + name: "who", + ty: XcqType::Primitive(PrimitiveType::H256) + } + ], + output: XcqType::Primitive(PrimitiveType::U64) + } + ] + } + ] + } + ) +} diff --git a/xcq-primitives/Cargo.toml b/xcq-primitives/Cargo.toml index 30dcc40..9c65633 100644 --- a/xcq-primitives/Cargo.toml +++ b/xcq-primitives/Cargo.toml @@ -10,7 +10,8 @@ version.workspace = true [dependencies] parity-scale-codec = { workspace = true } scale-info = { workspace = true } +xcq-types = { workspace = true } [features] default = ["std"] -std = ["parity-scale-codec/std", "scale-info/std"] +std = ["parity-scale-codec/std", "scale-info/std", "xcq-types/std"] diff --git a/xcq-primitives/src/lib.rs b/xcq-primitives/src/lib.rs index f0fa12d..ad78d65 100644 --- a/xcq-primitives/src/lib.rs +++ b/xcq-primitives/src/lib.rs @@ -14,3 +14,12 @@ pub enum XcqError { } pub type XcqResult = Result; + +pub mod metadata; +pub mod metadata_ir; + +#[doc(hidden)] +pub mod deps { + pub use parity_scale_codec; + pub use xcq_types; +} diff --git a/xcq-primitives/src/metadata.rs b/xcq-primitives/src/metadata.rs new file mode 100644 index 0000000..52b418f --- /dev/null +++ b/xcq-primitives/src/metadata.rs @@ -0,0 +1,71 @@ +use crate::metadata_ir::{ExtensionMetadataIR, MetadataIR, MethodMetadataIR, MethodParamMetadataIR}; +use parity_scale_codec::Encode; +use xcq_types::{vec::Vec, XcqType}; +/// Metadata of an extension method. +#[derive(Clone, PartialEq, Eq, Debug, Encode)] +pub struct MethodMetadata { + /// Method name. + pub name: &'static str, + /// Method parameters. + pub inputs: Vec, + /// Method output. + pub output: XcqType, +} + +/// Metadata of an method parameter. +#[derive(Clone, PartialEq, Eq, Debug, Encode)] +pub struct MethodParamMetadata { + /// Parameter name. + pub name: &'static str, + /// Parameter type. + pub ty: XcqType, +} + +/// Metadata of an extension +#[derive(Clone, PartialEq, Eq, Debug, Encode)] +pub struct ExtensionMetadata { + pub name: &'static str, + pub methods: Vec, +} + +/// Metadata of extensions +#[derive(Clone, PartialEq, Eq, Debug, Encode)] +pub struct Metadata { + pub extensions: Vec, +} + +impl From for MethodParamMetadata { + fn from(ir: MethodParamMetadataIR) -> Self { + Self { + name: ir.name, + ty: ir.ty.type_info(), + } + } +} + +impl From for MethodMetadata { + fn from(ir: MethodMetadataIR) -> Self { + Self { + name: ir.name, + inputs: ir.inputs.into_iter().map(MethodParamMetadata::from).collect(), + output: ir.output.type_info(), + } + } +} + +impl From for ExtensionMetadata { + fn from(ir: ExtensionMetadataIR) -> Self { + Self { + name: ir.name, + methods: ir.methods.into_iter().map(MethodMetadata::from).collect(), + } + } +} + +impl From for Metadata { + fn from(ir: MetadataIR) -> Self { + Self { + extensions: ir.extensions.into_iter().map(ExtensionMetadata::from).collect(), + } + } +} diff --git a/xcq-primitives/src/metadata_ir.rs b/xcq-primitives/src/metadata_ir.rs new file mode 100644 index 0000000..5ba783d --- /dev/null +++ b/xcq-primitives/src/metadata_ir.rs @@ -0,0 +1,32 @@ +use xcq_types::{vec::Vec, MetaType}; +/// Metadata of a runtime method. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct MethodMetadataIR { + /// Method name. + pub name: &'static str, + /// Method parameters. + pub inputs: Vec, + /// Method output. + pub output: MetaType, +} + +/// Metadata of a runtime method parameter. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct MethodParamMetadataIR { + /// Parameter name. + pub name: &'static str, + /// Parameter type. + pub ty: MetaType, +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct ExtensionMetadataIR { + pub name: &'static str, + pub methods: Vec, +} + +/// Metadata of extensions +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct MetadataIR { + pub extensions: Vec, +} diff --git a/xcq-runtime-api/Cargo.toml b/xcq-runtime-api/Cargo.toml index c52d768..10f9ca0 100644 --- a/xcq-runtime-api/Cargo.toml +++ b/xcq-runtime-api/Cargo.toml @@ -14,7 +14,4 @@ sp-api = { workspace = true } [features] default = ["std"] -std = [ - "xcq-primitives/std", - "sp-api/std", -] +std = ["xcq-primitives/std", "sp-api/std"] diff --git a/xcq-runtime-api/procedural/src/utils.rs b/xcq-runtime-api/procedural/src/utils.rs deleted file mode 100644 index caea63c..0000000 --- a/xcq-runtime-api/procedural/src/utils.rs +++ /dev/null @@ -1,71 +0,0 @@ -use inflector::Inflector; -use proc_macro2::{Span, TokenStream as TokenStream2}; -use proc_macro_crate::{crate_name, FoundCrate}; -use syn::{spanned::Spanned, Error, Ident, ItemImpl, Path, Result}; -/// Should a qualified trait path be required? -/// -/// e.g. `path::Trait` is qualified and `Trait` is not. -pub enum RequireQualifiedTraitPath { - Yes, - No, -} -/// Extract the trait that is implemented by the given `ItemImpl`. -pub fn extract_impl_trait(impl_: &ItemImpl, require: RequireQualifiedTraitPath) -> Result<&Path> { - impl_ - .trait_ - .as_ref() - .map(|v| &v.1) - .ok_or_else(|| Error::new(impl_.span(), "Only implementation of traits are supported!")) - .and_then(|p| { - if p.segments.len() > 1 || matches!(require, RequireQualifiedTraitPath::No) { - Ok(p) - } else { - Err(Error::new( - p.span(), - "The implemented trait has to be referenced with a path, \ - e.g. `impl xcq_extension_core::ExtensionCore for Runtime`.", - )) - } - }) -} - -/// Generates the name of the module that contains the trait declaration for the runtime. -pub fn generate_runtime_mod_name_for_trait(trait_: &Ident) -> Ident { - Ident::new( - &format!("runtime_decl_for_{}", trait_.to_string().to_snake_case()), - Span::call_site(), - ) -} - -/// Generate the crate access for the crate using 2018 syntax. -/// -/// If `frame` is in scope, it will use `polkadot_sdk_frame::deps::`. Else, it will try -/// and find `` directly. -pub fn generate_access_from_frame_or_crate(def_crate: &str) -> Result { - if let Some(path) = get_frame_crate_path(def_crate) { - Ok(path) - } else if let Some(path) = get_sdk_crate_path(def_crate) { - Ok(path) - } else { - let ident = match crate_name(def_crate) { - Ok(FoundCrate::Itself) => { - let name = def_crate.to_string().replace("-", "_"); - Ok(syn::Ident::new(&name, Span::call_site())) - } - Ok(FoundCrate::Name(name)) => Ok(Ident::new(&name, Span::call_site())), - Err(e) => Err(Error::new(Span::call_site(), e)), - }?; - - Ok(syn::Path::from(ident)) - } -} -fn import_xcq_types() -> TokenStream2 { - let found_crate = crate_name("xcq-types").expect("xcq-types not found in Cargo.toml"); - match found_crate { - FoundCrate::Itself => quote! { crate }, - FoundCrate::Name(name) => { - let name = syn::Ident::new(&name, proc_macro2::Span::call_site()); - quote! { ::#name } - } - } -} diff --git a/xcq-runtime-api/src/lib.rs b/xcq-runtime-api/src/lib.rs index d6052e5..d4417de 100644 --- a/xcq-runtime-api/src/lib.rs +++ b/xcq-runtime-api/src/lib.rs @@ -12,5 +12,6 @@ use xcq_primitives::XcqResult; decl_runtime_apis! { pub trait XcqApi { fn execute_query(query: Vec, input: Vec) -> XcqResult; + fn metadata() -> Vec; } } diff --git a/xcq-test-runner/Cargo.toml b/xcq-test-runner/Cargo.toml index 9d88bef..018af19 100644 --- a/xcq-test-runner/Cargo.toml +++ b/xcq-test-runner/Cargo.toml @@ -17,5 +17,6 @@ xcq-executor = { workspace = true, features = ["std"] } xcq-extension = { workspace = true, features = ["std"] } xcq-extension-core = { workspace = true, features = ["std"] } xcq-extension-fungibles = { workspace = true, features = ["std"] } +xcq-primitives = { workspace = true } polkavm = { workspace = true } diff --git a/xcq-test-runner/src/main.rs b/xcq-test-runner/src/main.rs index 46a7c17..519f856 100644 --- a/xcq-test-runner/src/main.rs +++ b/xcq-test-runner/src/main.rs @@ -1,7 +1,7 @@ use clap::Parser; use parity_scale_codec::{Decode, Encode}; use tracing_subscriber::prelude::*; -use xcq_extension::{ExtensionId, ExtensionsExecutor, Guest, Input, InvokeSource, Method}; +use xcq_extension::{impl_extensions, ExtensionId, ExtensionsExecutor, Guest, Input, InvokeSource, Method}; #[derive(Parser, Debug)] #[command(version, about)] @@ -33,7 +33,9 @@ fn main() { program: raw_blob.to_vec(), }; let method = CoreMethod::HasExtension { id: 0 }; - let mut input_data = as ExtensionId>::EXTENSION_ID.encode(); + let mut input_data = + as ExtensionId>::EXTENSION_ID + .encode(); input_data.extend_from_slice(&method.encode()); let input = InputImpl { method: "main".to_string(), @@ -43,43 +45,41 @@ fn main() { tracing::info!("Result: {:?}", res); } -// extension_core impls pub struct ExtensionImpl; + impl xcq_extension_core::Config for ExtensionImpl { type ExtensionId = u64; } -impl xcq_extension_core::ExtensionCore for ExtensionImpl { - type Config = Self; - fn has_extension(id: ::ExtensionId) -> bool { - matches!(id, 0 | 1) - } -} -// extension_fungibles impls impl xcq_extension_fungibles::Config for ExtensionImpl { + type AssetId = u32; type AccountId = [u8; 32]; - type Balance = u32; - type AssetId = u64; + type Balance = u64; } -impl xcq_extension_fungibles::ExtensionFungibles for ExtensionImpl { - type Config = Self; - fn balance( - _asset: xcq_extension_fungibles::AssetIdFor, - _who: xcq_extension_fungibles::AccountIdFor, - ) -> xcq_extension_fungibles::BalanceFor { - 0 +impl_extensions! { + impl xcq_extension_core::ExtensionCore for ExtensionImpl { + type Config = ExtensionImpl; + fn has_extension(id: ::ExtensionId) -> bool { + matches!(id, 0 | 1) + } } - fn total_supply(_asset: xcq_extension_fungibles::AssetIdFor) -> xcq_extension_fungibles::BalanceFor { - 100 + + impl xcq_extension_fungibles::ExtensionFungibles for ExtensionImpl { + type Config = ExtensionImpl; + #[allow(unused_variables)] + fn balance( + asset: ::AssetId, + who: ::AccountId, + ) -> ::Balance { + 100 + } + #[allow(unused_variables)] + fn total_supply(asset: ::AssetId) -> ::Balance { + 0 + } } } - -type Extensions = ( - xcq_extension_core::Call, - xcq_extension_fungibles::Call, -); - // guest impls pub struct GuestImpl { pub program: Vec, diff --git a/xcq-types/Cargo.toml b/xcq-types/Cargo.toml index 31a2a29..02937e2 100644 --- a/xcq-types/Cargo.toml +++ b/xcq-types/Cargo.toml @@ -13,7 +13,7 @@ parity-scale-codec = { workspace = true } serde = { version = "1", default-features = false, optional = true, features = [ "derive", ] } -fortuples = "0.9" +fortuples = { workspace = true } xcq-types-derive = { path = "derive" } [features] diff --git a/xcq-types/src/lib.rs b/xcq-types/src/lib.rs index c79d395..aa2ec7b 100644 --- a/xcq-types/src/lib.rs +++ b/xcq-types/src/lib.rs @@ -9,6 +9,8 @@ pub use ty::*; mod impls; mod prelude; pub use prelude::*; +mod meta_type; +pub use meta_type::MetaType; #[cfg(test)] mod tests; @@ -25,3 +27,11 @@ pub trait XcqTypeInfo { pub trait XcqStaticTypeInfo: XcqTypeInfo + 'static {} impl XcqStaticTypeInfo for T where T: XcqTypeInfo + 'static {} + +/// Returns the runtime bridge to the types compile-time type information. +pub fn meta_type() -> MetaType +where + T: ?Sized + XcqTypeInfo + 'static, +{ + MetaType::new::() +} diff --git a/xcq-types/src/meta_type.rs b/xcq-types/src/meta_type.rs new file mode 100644 index 0000000..c59fda7 --- /dev/null +++ b/xcq-types/src/meta_type.rs @@ -0,0 +1,83 @@ +use crate::prelude::{ + any::TypeId, + cmp::Ordering, + fmt::{Debug, Error as FmtError, Formatter}, + hash::{Hash, Hasher}, +}; + +use crate::{XcqType, XcqTypeInfo}; + +/// A metatype abstraction. +/// +/// Allows to store compile-time type information at runtime. +/// This again allows to derive type ID and type definition from it. +/// +/// This needs a conversion to another representation of types +/// in order to be serializable. +#[derive(Clone, Copy)] +pub struct MetaType { + /// Function pointer to get type information. + fn_type_info: fn() -> XcqType, + // The standard type ID (ab)used in order to provide + // cheap implementations of the standard traits + // such as `PartialEq`, `PartialOrd`, `Debug` and `Hash`. + type_id: TypeId, +} + +impl PartialEq for MetaType { + fn eq(&self, other: &Self) -> bool { + self.type_id == other.type_id + } +} + +impl Eq for MetaType {} + +impl PartialOrd for MetaType { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MetaType { + fn cmp(&self, other: &Self) -> Ordering { + self.type_id.cmp(&other.type_id) + } +} + +impl Hash for MetaType { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + self.type_id.hash(state) + } +} + +impl Debug for MetaType { + fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> { + self.type_id.fmt(f) + } +} + +impl MetaType { + /// Creates a new meta type from the given compile-time known type. + pub fn new() -> Self + where + T: XcqTypeInfo + ?Sized + 'static, + { + Self { + fn_type_info: ::type_info, + type_id: TypeId::of::(), + } + } + + /// Returns the meta type information. + pub fn type_info(&self) -> XcqType { + (self.fn_type_info)() + } + + /// Returns the type identifier provided by `core::any`. + pub fn type_id(&self) -> TypeId { + self.type_id + } +} diff --git a/xcq-types/src/ty/enum.rs b/xcq-types/src/ty/enum.rs index 7c51a0b..063f51a 100644 --- a/xcq-types/src/ty/enum.rs +++ b/xcq-types/src/ty/enum.rs @@ -1,6 +1,7 @@ use crate::{vec::Vec, Field}; +use parity_scale_codec::Encode; -#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Encode)] pub struct Variant { pub ident: Vec, pub fields: Vec, @@ -29,7 +30,7 @@ pub struct Variant { /// } /// ``` /// -#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Encode)] pub struct EnumType { pub ident: Vec, pub variants: Vec, diff --git a/xcq-types/src/ty/field.rs b/xcq-types/src/ty/field.rs index a280122..0b3b0cd 100644 --- a/xcq-types/src/ty/field.rs +++ b/xcq-types/src/ty/field.rs @@ -1,7 +1,8 @@ use crate::vec::Vec; use crate::XcqType; +use parity_scale_codec::Encode; // A Named or Unnamed field in a composite type -#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Encode)] pub struct Field { pub ident: Vec, pub ty: XcqType, diff --git a/xcq-types/src/ty/mod.rs b/xcq-types/src/ty/mod.rs index ad7aaf9..d4888e0 100644 --- a/xcq-types/src/ty/mod.rs +++ b/xcq-types/src/ty/mod.rs @@ -1,3 +1,4 @@ +use parity_scale_codec::Encode; mod r#enum; mod field; mod primitive; @@ -7,7 +8,7 @@ use crate::{boxed::Box, vec::Vec}; pub use self::{field::*, primitive::*, r#enum::*, r#struct::*}; /// Note: no Array Type yet -#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Encode)] pub enum XcqType { Primitive(PrimitiveType), Struct(StructType), diff --git a/xcq-types/src/ty/struct.rs b/xcq-types/src/ty/struct.rs index 98128a5..a0d6743 100644 --- a/xcq-types/src/ty/struct.rs +++ b/xcq-types/src/ty/struct.rs @@ -1,4 +1,5 @@ use crate::{vec::Vec, Field}; +use parity_scale_codec::Encode; /// A struct type, consisting of a named (struct) or unnamed (tuple struct) fields or unit struct. /// Note: in fact, it can represent a @@ -26,7 +27,7 @@ use crate::{vec::Vec, Field}; /// ``` /// struct JustAMarker; /// ``` -#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Encode)] pub struct StructType { pub ident: Vec, pub fields: Vec,