Skip to content

Commit

Permalink
Add ContractArgs for building function args (#1382)
Browse files Browse the repository at this point in the history
### What
Add ContractArgs for building function args.

### Usage

```diff
-let contract_id = env.register(Contract, (100_u32, 1000_i64));
+let contract_id = env.register(Contract, ContractArgs::__constructor(&100_u32, &1000_i64));
```

### Diff of generated code for constructor test vector

```diff
@@ -7,6 +7,8 @@ extern crate core;
 extern crate compiler_builtins as _;
 use soroban_sdk::{contract, contractimpl, contracttype, Env};
 pub struct Contract;
+///ContractArgs is a type for building arg lists for functions defined in "Contract".
+pub struct ContractArgs;
 ///ContractClient is a client for calling the contract defined in "Contract".
 pub struct ContractClient<'a> {
     pub env: soroban_sdk::Env,
@@ -210,6 +212,17 @@ impl<'a> ContractClient<'a> {
         res
     }
 }
+impl ContractArgs {
+    pub fn __constructor<'i>(
+        init_key: &'i u32,
+        init_value: &'i i64,
+    ) -> (&'i u32, &'i i64) {
+        (init_key, init_value)
+    }
+    pub fn get_data<'i>(key: &'i DataKey) -> (&'i DataKey,) {
+        (key,)
+    }
+}
 #[doc(hidden)]
 pub mod ____constructor {
     use super::*;
```

### Why

To provide a way to make specifying args as concrete.

Dependent on:
- stellar/rs-soroban-env#1479

Close #1348

### Note

This is still very much an experiment.
  • Loading branch information
leighmcculloch authored Nov 1, 2024
1 parent b7b8255 commit e59dcc6
Show file tree
Hide file tree
Showing 13 changed files with 217 additions and 48 deletions.
26 changes: 9 additions & 17 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,18 @@ soroban-token-sdk = { version = "22.0.0-rc.3", path = "soroban-token-sdk" }

[workspace.dependencies.soroban-env-common]
version = "=22.0.0-rc.3"
#git = "https://github.com/stellar/rs-soroban-env"
#rev = "8911531dfb9a4331bc308ff8934c2223bc4e81ed"
git = "https://github.com/stellar/rs-soroban-env"
rev = "bd0c80a1fe171e75f8d745f17975a73927d44ecd"

[workspace.dependencies.soroban-env-guest]
version = "=22.0.0-rc.3"
#git = "https://github.com/stellar/rs-soroban-env"
#rev = "8911531dfb9a4331bc308ff8934c2223bc4e81ed"
git = "https://github.com/stellar/rs-soroban-env"
rev = "bd0c80a1fe171e75f8d745f17975a73927d44ecd"

[workspace.dependencies.soroban-env-host]
version = "=22.0.0-rc.3"
#git = "https://github.com/stellar/rs-soroban-env"
#rev = "8911531dfb9a4331bc308ff8934c2223bc4e81ed"
git = "https://github.com/stellar/rs-soroban-env"
rev = "bd0c80a1fe171e75f8d745f17975a73927d44ecd"

[workspace.dependencies.stellar-strkey]
version = "=0.0.9"
Expand All @@ -48,7 +48,7 @@ features = ["curr"]
#git = "https://github.com/stellar/rs-stellar-xdr"
#rev = "67be5955a15f1d3a4df83fe86e6ae107f687141b"

#[patch."https://github.com/stellar/rs-soroban-env"]
#[patch.crates-io]
#soroban-env-common = { path = "../rs-soroban-env/soroban-env-common" }
#soroban-env-guest = { path = "../rs-soroban-env/soroban-env-guest" }
#soroban-env-host = { path = "../rs-soroban-env/soroban-env-host" }
Expand Down
104 changes: 104 additions & 0 deletions soroban-sdk-macros/src/derive_args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use itertools::MultiUnzip;
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote};
use syn::{Error, FnArg, Lifetime, Type, TypePath, TypeReference};

use crate::syn_ext;

pub fn derive_args_type(ty: &str, name: &str) -> TokenStream {
let ty_str = quote!(#ty).to_string();
let args_doc =
format!("{name} is a type for building arg lists for functions defined in {ty_str}.");
let args_ident = format_ident!("{name}");
quote! {
#[doc = #args_doc]
pub struct #args_ident;
}
}

pub fn derive_args_impl(name: &str, fns: &[syn_ext::Fn]) -> TokenStream {
// Map the traits methods to methods for the Args.
let mut errors = Vec::<Error>::new();
let fns: Vec<_> = fns
.iter()
.map(|f| {
let fn_ident = &f.ident;

// Check for the Env argument.
let env_input = f.inputs.first().and_then(|a| match a {
FnArg::Typed(pat_type) => {
let mut ty = &*pat_type.ty;
if let Type::Reference(TypeReference { elem, .. }) = ty {
ty = elem;
}
if let Type::Path(TypePath {
path: syn::Path { segments, .. },
..
}) = ty
{
if segments.last().map_or(false, |s| s.ident == "Env") {
Some(())
} else {
None
}
} else {
None
}
}
FnArg::Receiver(_) => None,
});

// Map all remaining inputs.
let fn_input_lifetime = Lifetime::new("'i", Span::call_site());
let (fn_input_names, fn_input_types, fn_input_fn_args): (Vec<_>, Vec<_>, Vec<_>) = f
.inputs
.iter()
.skip(if env_input.is_some() { 1 } else { 0 })
.map(|t| {
let ident = match syn_ext::fn_arg_ident(t) {
Ok(ident) => ident,
Err(e) => {
errors.push(e);
format_ident!("_")
}
};
let ty = match syn_ext::fn_arg_ref_type(t, Some(&fn_input_lifetime)) {
Ok(ty) => Some(ty),
Err(e) => {
errors.push(e);
None
}
};
(
ident,
ty,
syn_ext::fn_arg_make_ref(t, Some(&fn_input_lifetime)),
)
})
.multiunzip();

quote! {
#[inline(always)]
pub fn #fn_ident<#fn_input_lifetime>(#(#fn_input_fn_args),*)
-> (#(#fn_input_types,)*)
{
(#(#fn_input_names,)*)
}
}
})
.collect();

// If errors have occurred, render them instead.
if !errors.is_empty() {
let compile_errors = errors.iter().map(Error::to_compile_error);
return quote! { #(#compile_errors)* };
}

// Render the Client.
let args_ident = format_ident!("{}", name);
quote! {
impl #args_ident {
#(#fns)*
}
}
}
2 changes: 1 addition & 1 deletion soroban-sdk-macros/src/derive_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ pub fn derive_client_impl(crate_path: &Path, name: &str, fns: &[syn_ext::Fn]) ->
format_ident!("_")
}
};
(ident, syn_ext::fn_arg_make_ref(t))
(ident, syn_ext::fn_arg_make_ref(t, None))
})
.unzip();
let fn_output = f.output();
Expand Down
50 changes: 50 additions & 0 deletions soroban-sdk-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
extern crate proc_macro;

mod arbitrary;
mod derive_args;
mod derive_client;
mod derive_enum;
mod derive_enum_int;
Expand All @@ -15,6 +16,7 @@ mod path;
mod symbol;
mod syn_ext;

use derive_args::{derive_args_impl, derive_args_type};
use derive_client::{derive_client_impl, derive_client_type};
use derive_enum::derive_type_enum;
use derive_enum_int::derive_type_enum_int;
Expand Down Expand Up @@ -136,8 +138,11 @@ pub fn contract(metadata: TokenStream, input: TokenStream) -> TokenStream {
let fn_set_registry_ident = format_ident!("__{}_fn_set_registry", ty_str.to_lowercase());
let crate_path = &args.crate_path;
let client = derive_client_type(&args.crate_path, &ty_str, &client_ident);
let args_ident = format!("{ty_str}Args");
let contract_args = derive_args_type(&ty_str, &args_ident);
let mut output = quote! {
#input2
#contract_args
#client
};
if cfg!(feature = "testutils") {
Expand Down Expand Up @@ -206,6 +211,18 @@ pub fn contractimpl(metadata: TokenStream, input: TokenStream) -> TokenStream {
let ty = &imp.self_ty;
let ty_str = quote!(#ty).to_string();

// TODO: Use imp.trait_ in generating the args ident, to create a unique
// args for each trait impl for a contract, to avoid conflicts.
let args_ident = if let Type::Path(path) = &**ty {
path.path
.segments
.last()
.map(|name| format!("{}Args", name.ident))
} else {
None
}
.unwrap_or_else(|| "Args".to_string());

// TODO: Use imp.trait_ in generating the client ident, to create a unique
// client for each trait impl for a contract, to avoid conflicts.
let client_ident = if let Type::Path(path) = &**ty {
Expand Down Expand Up @@ -239,6 +256,7 @@ pub fn contractimpl(metadata: TokenStream, input: TokenStream) -> TokenStream {
match derived {
Ok(derived_ok) => {
let mut output = quote! {
#[#crate_path::contractargs(name = #args_ident, impl_only = true)]
#[#crate_path::contractclient(crate_path = #crate_path_str, name = #client_ident, impl_only = true)]
#[#crate_path::contractspecfn(name = #ty_str)]
#imp
Expand Down Expand Up @@ -533,6 +551,38 @@ pub fn contractfile(metadata: TokenStream) -> TokenStream {
quote! { #contents_lit }.into()
}

#[derive(Debug, FromMeta)]
struct ContractArgsArgs {
name: String,
#[darling(default)]
impl_only: bool,
}

#[proc_macro_attribute]
pub fn contractargs(metadata: TokenStream, input: TokenStream) -> TokenStream {
let args = match NestedMeta::parse_meta_list(metadata.into()) {
Ok(v) => v,
Err(e) => {
return TokenStream::from(darling::Error::from(e).write_errors());
}
};
let args = match ContractArgsArgs::from_list(&args) {
Ok(v) => v,
Err(e) => return e.write_errors().into(),
};
let input2: TokenStream2 = input.clone().into();
let item = parse_macro_input!(input as HasFnsItem);
let methods: Vec<_> = item.fns();
let args_type = (!args.impl_only).then(|| derive_args_type(&item.name(), &args.name));
let args_impl = derive_args_impl(&args.name, &methods);
quote! {
#input2
#args_type
#args_impl
}
.into()
}

#[derive(Debug, FromMeta)]
struct ContractClientArgs {
#[darling(default = "default_crate_path")]
Expand Down
27 changes: 24 additions & 3 deletions soroban-sdk-macros/src/syn_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use syn::{
};
use syn::{
spanned::Spanned, token::And, Error, FnArg, Ident, ImplItem, ImplItemFn, ItemImpl, ItemTrait,
Pat, PatType, TraitItem, TraitItemFn, Type, TypeReference, Visibility,
Lifetime, Pat, PatType, TraitItem, TraitItemFn, Type, TypeReference, Visibility,
};

/// Gets methods from the implementation that have public visibility. For
Expand Down Expand Up @@ -47,9 +47,30 @@ pub fn fn_arg_ident(arg: &FnArg) -> Result<Ident, Error> {
))
}

/// Returns a clone of the type from the FnArg.
pub fn fn_arg_ref_type(arg: &FnArg, lifetime: Option<&Lifetime>) -> Result<Type, Error> {
if let FnArg::Typed(pat_type) = arg {
if !matches!(*pat_type.ty, Type::Reference(_)) {
Ok(Type::Reference(TypeReference {
and_token: And::default(),
lifetime: lifetime.cloned(),
mutability: None,
elem: pat_type.ty.clone(),
}))
} else {
Ok((*pat_type.ty).clone())
}
} else {
Err(Error::new(
arg.span(),
"argument in this form is not supported, use simple named arguments only",
))
}
}

/// Returns a clone of FnArg with the type as a reference if the arg is a typed
/// arg and its type is not already a reference.
pub fn fn_arg_make_ref(arg: &FnArg) -> FnArg {
pub fn fn_arg_make_ref(arg: &FnArg, lifetime: Option<&Lifetime>) -> FnArg {
if let FnArg::Typed(pat_type) = arg {
if !matches!(*pat_type.ty, Type::Reference(_)) {
return FnArg::Typed(PatType {
Expand All @@ -58,7 +79,7 @@ pub fn fn_arg_make_ref(arg: &FnArg) -> FnArg {
colon_token: pat_type.colon_token,
ty: Box::new(Type::Reference(TypeReference {
and_token: And::default(),
lifetime: None,
lifetime: lifetime.cloned(),
mutability: None,
elem: pat_type.ty.clone(),
})),
Expand Down
7 changes: 5 additions & 2 deletions soroban-sdk/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,10 @@ impl Env {
/// and is being registered natively. Pass the contract wasm bytes when the
/// contract has been loaded as wasm.
///
/// Pass the arguments for the contract's constructor, or `()` if none.
/// Pass the arguments for the contract's constructor, or `()` if none. For
/// contracts with a constructor, use the contract's generated `Args` type
/// to construct the arguments with the appropropriate types for invoking
/// the constructor during registration.
///
/// Returns the address of the registered contract that is the same as the
/// contract id passed in.
Expand Down Expand Up @@ -610,7 +613,7 @@ impl Env {
/// # }
/// # fn main() {
/// let env = Env::default();
/// let contract_id = env.register(Contract, (123_u32,));
/// let contract_id = env.register(Contract, ContractArgs::__constructor(&123,));
/// }
/// ```
/// Register a contract wasm, by specifying the wasm bytes:
Expand Down
Loading

0 comments on commit e59dcc6

Please sign in to comment.