Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion soroban-sdk-macros/src/derive_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote};
use syn::{Error, FnArg, Lifetime, Type, TypePath, TypeReference};

use crate::syn_ext;
use crate::{attribute::pass_through_attr_to_gen_code, syn_ext};

pub fn derive_args_type(ty: &str, name: &str) -> TokenStream {
let ty_str = quote!(#ty).to_string();
Expand All @@ -23,6 +23,11 @@ pub fn derive_args_impl(name: &str, fns: &[syn_ext::Fn]) -> TokenStream {
.iter()
.map(|f| {
let fn_ident = &f.ident;
let fn_attrs = f
.attrs
.iter()
.filter(|attr| pass_through_attr_to_gen_code(attr))
.collect::<Vec<_>>();

// Check for the Env argument.
let env_input = f.inputs.first().and_then(|a| match a {
Expand Down Expand Up @@ -78,6 +83,7 @@ pub fn derive_args_impl(name: &str, fns: &[syn_ext::Fn]) -> TokenStream {
.multiunzip();

quote! {
#(#fn_attrs)*
#[inline(always)]
#[allow(clippy::unused_unit)]
pub fn #fn_ident<#fn_input_lifetime>(#(#fn_input_fn_args),*)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ use crate::{
use darling::{ast::NestedMeta, Error, FromMeta};
use proc_macro2::{Ident, TokenStream as TokenStream2};
use quote::quote;
use std::collections::HashSet;
use syn::{ext::IdentExt as _, LitStr, Path, Type};
use syn::{parse_quote, Attribute, LitStr, Path, Type};

// See soroban-sdk/docs/contracttrait.md for documentation on how this works.

Expand Down Expand Up @@ -53,14 +52,40 @@ fn derive(args: &Args) -> Result<TokenStream2, Error> {
let spec_export = args.spec_export.unwrap_or(true);

let trait_default_fns = syn_ext::strs_to_fns(&args.trait_default_fns)?;
let impl_fn_idents: HashSet<_> = args.impl_fns.iter().map(|s| s.value()).collect();
let impl_fns = syn_ext::strs_to_fns(&args.impl_fns)?;

// Filter the list of default fns down to only default fns that have not been redefined /
// overridden in the input fns.
let fns = trait_default_fns
.into_iter()
.filter(|f| !impl_fn_idents.contains(&f.ident.unraw().to_string()))
.collect::<Vec<_>>();
.filter_map(|mut f| {
// No matching impl means keep the default. An unconditional impl drops it.
// Cfg-gated impls only override the default when their cfg is active, so the
// default is kept behind `not(cfg)` to remain available when the impl is absent.
let impl_fns = impl_fns
.iter()
.filter(|impl_fn| impl_fn.ident == f.ident)
.collect::<Vec<_>>();
if impl_fns.is_empty() {
return Some(Ok(f));
}

let mut cfgs = Vec::new();
for impl_fn in impl_fns {
match cfg_condition(&impl_fn.attrs) {
Ok(Some(cfg)) => cfgs.push(cfg),
Ok(None) => return None,
Err(e) => return Some(Err(e)),
}
}
f.attrs
.extend(cfgs.into_iter().map(|cfg| parse_quote!(#[cfg(not(#cfg))])));
Some(Ok(f))
})
.collect::<Result<Vec<_>, Error>>()?;
if fns.is_empty() {
return Ok(quote! {});
}

let mut output = quote! {};
output.extend(derive_pub_fns(
Expand All @@ -81,8 +106,21 @@ fn derive(args: &Args) -> Result<TokenStream2, Error> {
&args.crate_path,
&impl_ty,
Some(trait_ident),
fns.iter().map(|f| &f.ident),
&fns,
));

Ok(output)
}

fn cfg_condition(attrs: &[Attribute]) -> Result<Option<TokenStream2>, Error> {
let cfgs = attrs
.iter()
.filter(|a| a.path().is_ident("cfg"))
.map(|a| a.parse_args::<TokenStream2>().map_err(Error::from))
.collect::<Result<Vec<_>, Error>>()?;
Ok(match cfgs.as_slice() {
[] => None,
[cfg] => Some(quote! { #cfg }),
_ => Some(quote! { all(#(#cfgs),*) }),
})
}
28 changes: 20 additions & 8 deletions soroban-sdk-macros/src/derive_contractimpl_trait_macro.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use crate::{attribute::is_attr_doc, default_crate_path, syn_ext};
use crate::{attribute::pass_through_attr_to_gen_code, default_crate_path, syn_ext};
use darling::{ast::NestedMeta, Error, FromMeta};
use heck::ToSnakeCase;
use proc_macro2::{Ident, TokenStream as TokenStream2};
use quote::ToTokens;
use quote::{format_ident, quote};
use syn::{ext::IdentExt as _, parse2, ImplItemFn, ItemTrait, Path, TraitItem, TraitItemFn, Type};
use syn::{
ext::IdentExt as _, parse2, Attribute, ImplItemFn, ItemTrait, Path, TraitItem, TraitItemFn,
Type,
};

// See soroban-sdk/docs/contracttrait.md for documentation on how this works.

Expand Down Expand Up @@ -49,10 +52,7 @@ fn derive(args: &Args, input: &ItemTrait) -> Result<TokenStream2, Error> {
sig,
attrs,
..
}) => {
let doc_attrs: Vec<_> = attrs.iter().filter(|a| is_attr_doc(a)).collect();
Some(quote!(#(#doc_attrs)* #sig).to_token_stream().to_string())
}
}) => Some(fn_string(sig, attrs)),
_ => None,
});

Expand Down Expand Up @@ -90,6 +90,11 @@ fn derive(args: &Args, input: &ItemTrait) -> Result<TokenStream2, Error> {
Ok(output)
}

fn fn_string(sig: &syn::Signature, attrs: &[Attribute]) -> String {
let attrs = attrs.iter().filter(|a| pass_through_attr_to_gen_code(a));
quote!(#(#attrs)* #sig).to_token_stream().to_string()
}

pub fn generate_call_to_contractimpl_for_trait(
trait_ident: &Path,
impl_ident: &Type,
Expand All @@ -98,19 +103,26 @@ pub fn generate_call_to_contractimpl_for_trait(
args_ident: &str,
spec_ident: &str,
) -> TokenStream2 {
let impl_fn_idents = pub_methods.iter().map(|f| f.sig.ident.unraw().to_string());
let impl_fns = pub_methods.iter().map(impl_fn_string);
quote! {
#trait_ident!(
#trait_ident,
#impl_ident,
[#(#impl_fn_idents),*],
[#(#impl_fns),*],
#client_ident,
#args_ident,
#spec_ident,
);
}
}

fn impl_fn_string(f: &ImplItemFn) -> String {
let attrs = f.attrs.iter().filter(|a| pass_through_attr_to_gen_code(a));
let ident = &f.sig.ident;
// Args and return type are stripped; the receiver only needs attrs and ident for override matching.
quote!(#(#attrs)* fn #ident()).to_token_stream().to_string()
}

fn macro_ident(trait_ident: &Ident) -> Ident {
let lower = trait_ident.unraw().to_string().to_snake_case();
format_ident!("__contractimpl_for_{lower}")
Expand Down
54 changes: 31 additions & 23 deletions soroban-sdk-macros/src/derive_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,21 +238,13 @@ pub fn derive_contract_function_registration_ctor<'a>(
crate_path: &Path,
ty: &Type,
trait_ident: Option<&Path>,
method_idents: impl Iterator<Item = &'a Ident>,
fns: impl IntoIterator<Item = &'a syn_ext::Fn>,
) -> TokenStream2 {
if cfg!(not(feature = "testutils")) {
return quote!();
}

let ty_str = ty_to_safe_ident_str(ty);
let (idents, wrap_idents): (Vec<_>, Vec<_>) = method_idents
.map(|ident| {
let ident_str = ident.unraw().to_string();
let wrap_ident = format_ident!("__{}__{}__invoke_raw_slice", ty_str, ident_str);
(ident_str, wrap_ident)
})
.multiunzip();

let trait_str = trait_ident
.map(|p| {
p.segments
Expand All @@ -262,21 +254,37 @@ pub fn derive_contract_function_registration_ctor<'a>(
.join("_")
})
.unwrap_or_else(|| "".to_string());
let methods_hash = format!("{:x}", Sha256::digest(idents.join(",").as_bytes()));
let ctor_ident = format_ident!("__{ty_str}__{trait_str}__{methods_hash}_ctor");

let ctors = fns
.into_iter()
.map(|f| {
let ident = f.ident.unraw().to_string();
let wrap_ident = format_ident!("__{}__{}__invoke_raw_slice", ty_str, ident);
let attrs = f
.attrs
.iter()
.filter(|attr| pass_through_attr_to_gen_code(attr) && !attr.path().is_ident("doc"))
.collect::<Vec<_>>();
let methods_hash =
format!("{:x}", Sha256::digest(quote!(#ident #(#attrs)*).to_string()));
Comment thread
mootz12 marked this conversation as resolved.
let ctor_ident = format_ident!("__{ty_str}__{trait_str}__{methods_hash}_ctor");
quote! {
#[doc(hidden)]
#(#attrs)*
#[#crate_path::reexports_for_macros::ctor::ctor(crate_path=#crate_path::reexports_for_macros::ctor)]
#[allow(non_snake_case)]
fn #ctor_ident() {
<#ty as #crate_path::testutils::ContractFunctionRegister>::register(
#ident,
#[allow(deprecated)]
&#wrap_ident,
);
}
}
})
.collect::<Vec<_>>();

quote! {
#[doc(hidden)]
#[#crate_path::reexports_for_macros::ctor::ctor(crate_path=#crate_path::reexports_for_macros::ctor)]
#[allow(non_snake_case)]
fn #ctor_ident() {
#(
<#ty as #crate_path::testutils::ContractFunctionRegister>::register(
#idents,
#[allow(deprecated)]
&#wrap_idents,
);
)*
}
#(#ctors)*
}
}
2 changes: 1 addition & 1 deletion soroban-sdk-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ pub fn contractimpl(metadata: TokenStream, input: TokenStream) -> TokenStream {
crate_path,
ty,
trait_ident,
pub_methods.iter().map(|m| &m.sig.ident),
&pub_methods_fns,
);
output.extend(quote! { #cfs });

Expand Down
8 changes: 7 additions & 1 deletion soroban-sdk/docs/contracttrait.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,11 @@ macro_rules! __contractimpl_for_pause {
pub use __contractimpl_for_pause as Pause;
```

The `trait_default_fns` contains stringified signatures (and documentation) of all functions with default implementations. We serialize function signatures and their documentation into strings because the tooling used to parse macros and values doesn't handle raw tokens well as parameters. This information is "remembered" by the declarative macro for later comparison.
The `trait_default_fns` contains stringified signatures and pass-through attributes of all functions with default implementations. We serialize function signatures and supported attributes into strings because the tooling used to parse macros and values doesn't handle raw tokens well as parameters. This information is "remembered" by the declarative macro for later comparison.

Direct `#[cfg(...)]` attributes on default functions are preserved in the generated code. For cross-crate traits, function-level cfgs inside a `#[contracttrait]` are evaluated in the implementing contract crate's configuration context when generating wrappers. Shared trait crates should prefer applying cfg to the whole trait item, or require implementing contracts to define matching features when function-level cfgs are needed.

`cfg_attr` is not interpreted by the bridge. Avoid using `cfg_attr` to conditionally add `cfg` to `#[contracttrait]` default functions or `#[contractimpl]` functions, because generated wrappers may not agree with whether Rust compiles the original function. Use direct `#[cfg(...)]` instead.

### Stage 3: `#[contractimpl(contracttrait)]` calls the trait macro

Expand Down Expand Up @@ -115,6 +119,8 @@ Pause!(
);
```

Direct `#[cfg(...)]` attributes on overriding impl functions are preserved, so a cfg-disabled override does not suppress the trait default function.

### Stage 4: The trait macro calls `contractimpl_trait_default_fns_not_overridden`

The `Pause!` macro expands to call the proc macro `contractimpl_trait_default_fns_not_overridden!` with both:
Expand Down
6 changes: 4 additions & 2 deletions tests-expanded/test_account_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ impl Contract {
}
impl<'a> ContractClient<'a> {}
impl ContractArgs {
#[allow(non_snake_case)]
#[inline(always)]
#[allow(clippy::unused_unit)]
pub fn __check_auth<'i>(
Expand Down Expand Up @@ -408,8 +409,9 @@ pub extern "C" fn __Contract____check_auth__invoke_raw_extern(
}
#[doc(hidden)]
#[allow(non_snake_case)]
#[allow(non_snake_case)]
#[allow(unused)]
fn __Contract__CustomAccountInterface__d465b6861ce11142d9f64c1622e1ad88ae003d910de0a8493889a96a23449736_ctor(
fn __Contract__CustomAccountInterface__1cb152b3d7e456ec2ef6550a76691e16248c4973551c0ab421ef85c852910b2a_ctor(
) {
#[allow(unsafe_code)]
{
Expand All @@ -422,7 +424,7 @@ fn __Contract__CustomAccountInterface__d465b6861ce11142d9f64c1622e1ad88ae003d910
#[allow(non_snake_case)]
extern "C" fn f() -> ::ctor::__support::CtorRetType {
unsafe {
__Contract__CustomAccountInterface__d465b6861ce11142d9f64c1622e1ad88ae003d910de0a8493889a96a23449736_ctor();
__Contract__CustomAccountInterface__1cb152b3d7e456ec2ef6550a76691e16248c4973551c0ab421ef85c852910b2a_ctor();
};
core::default::Default::default()
}
Expand Down
1 change: 1 addition & 0 deletions tests-expanded/test_account_wasm32v1-none.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ impl Contract {
}
impl<'a> ContractClient<'a> {}
impl ContractArgs {
#[allow(non_snake_case)]
#[inline(always)]
#[allow(clippy::unused_unit)]
pub fn __check_auth<'i>(
Expand Down
4 changes: 2 additions & 2 deletions tests-expanded/test_add_i128_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ pub extern "C" fn __Contract__add__invoke_raw_extern(
#[doc(hidden)]
#[allow(non_snake_case)]
#[allow(unused)]
fn __Contract____7e9e5ac30f2216fd0fd6f5faed316f2d5983361a4203c3330cfa46ef65bb4767_ctor() {
fn __Contract____7b8a6f33b43ca26a3f2aa73e408748f9ceb391ac21dfe746c94563016ab72f85_ctor() {
#[allow(unsafe_code)]
{
#[link_section = ".init_array"]
Expand All @@ -322,7 +322,7 @@ fn __Contract____7e9e5ac30f2216fd0fd6f5faed316f2d5983361a4203c3330cfa46ef65bb476
#[allow(non_snake_case)]
extern "C" fn f() -> ::ctor::__support::CtorRetType {
unsafe {
__Contract____7e9e5ac30f2216fd0fd6f5faed316f2d5983361a4203c3330cfa46ef65bb4767_ctor();
__Contract____7b8a6f33b43ca26a3f2aa73e408748f9ceb391ac21dfe746c94563016ab72f85_ctor();
};
core::default::Default::default()
}
Expand Down
4 changes: 2 additions & 2 deletions tests-expanded/test_add_u128_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ pub extern "C" fn __Contract__add__invoke_raw_extern(
#[doc(hidden)]
#[allow(non_snake_case)]
#[allow(unused)]
fn __Contract____7e9e5ac30f2216fd0fd6f5faed316f2d5983361a4203c3330cfa46ef65bb4767_ctor() {
fn __Contract____7b8a6f33b43ca26a3f2aa73e408748f9ceb391ac21dfe746c94563016ab72f85_ctor() {
#[allow(unsafe_code)]
{
#[link_section = ".init_array"]
Expand All @@ -322,7 +322,7 @@ fn __Contract____7e9e5ac30f2216fd0fd6f5faed316f2d5983361a4203c3330cfa46ef65bb476
#[allow(non_snake_case)]
extern "C" fn f() -> ::ctor::__support::CtorRetType {
unsafe {
__Contract____7e9e5ac30f2216fd0fd6f5faed316f2d5983361a4203c3330cfa46ef65bb4767_ctor();
__Contract____7b8a6f33b43ca26a3f2aa73e408748f9ceb391ac21dfe746c94563016ab72f85_ctor();
};
core::default::Default::default()
}
Expand Down
Loading
Loading