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
1 change: 1 addition & 0 deletions Cargo.lock

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

195 changes: 143 additions & 52 deletions frozen-abi-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,44 @@ pub fn derive_abi_enum_visitor(_item: TokenStream) -> TokenStream {
"".parse().unwrap()
}

#[cfg(not(feature = "frozen-abi"))]
#[proc_macro_derive(StableAbi)]
pub fn derive_stable_abi(_item: TokenStream) -> TokenStream {
"".parse().unwrap()
}

#[cfg(feature = "frozen-abi")]
#[proc_macro_derive(StableAbi)]
pub fn derive_stable_abi(item: TokenStream) -> TokenStream {
use {
quote::quote,
syn::{parse_macro_input, Error, Item},
};

let item = parse_macro_input!(item as Item);

let ident = match item {
Item::Struct(ref s) => &s.ident,
Item::Enum(ref e) => &e.ident,
Item::Type(ref t) => &t.ident,
_ => {
return Error::new_spanned(
item,
"StableAbi can only be derived for struct, enum, or type alias",
)
.to_compile_error()
.into();
}
};

let expanded = quote! {
#[automatically_derived]
impl ::solana_frozen_abi::stable_abi::StableAbi for #ident {}
};

expanded.into()
}

#[cfg(feature = "frozen-abi")]
use proc_macro2::{Span, TokenStream as TokenStream2, TokenTree};
#[cfg(feature = "frozen-abi")]
Expand Down Expand Up @@ -156,8 +194,8 @@ fn derive_abi_sample_enum_type(input: ItemEnum) -> TokenStream {
#[cfg(feature = "frozen-abi")]
fn derive_abi_sample_struct_type(input: ItemStruct) -> TokenStream {
let type_name = &input.ident;
let mut sample_fields = quote! {};
let fields = &input.fields;
let mut sample_fields = quote! {};

match fields {
Fields::Named(_) => {
Expand All @@ -167,19 +205,15 @@ fn derive_abi_sample_struct_type(input: ItemStruct) -> TokenStream {
#field_name: AbiExample::example(),
});
}
sample_fields = quote! {
{ #sample_fields }
}
sample_fields = quote! {{ #sample_fields }};
}
Fields::Unnamed(_) => {
for _ in fields {
sample_fields.extend(quote! {
AbiExample::example(),
});
}
sample_fields = quote! {
( #sample_fields )
}
sample_fields = quote! {( #sample_fields )};
}
_ => unimplemented!("fields: {:?}", fields),
}
Expand All @@ -195,7 +229,7 @@ fn derive_abi_sample_struct_type(input: ItemStruct) -> TokenStream {
impl #impl_generics ::solana_frozen_abi::abi_example::AbiExample for #type_name #ty_generics #where_clause {
fn example() -> Self {
::std::println!(
"AbiExample for struct: {}",
"AbiExample::example for struct: {}",
std::any::type_name::<#type_name #ty_generics>()
);
use ::solana_frozen_abi::abi_example::AbiExample;
Expand Down Expand Up @@ -277,39 +311,61 @@ pub fn derive_abi_enum_visitor(item: TokenStream) -> TokenStream {
fn quote_for_test(
test_mod_ident: &Ident,
type_name: &Ident,
expected_digest: &str,
expected_api_digest: Option<&str>,
expected_abi_digest: Option<&str>,
) -> TokenStream2 {
quote! {
#[cfg(test)]
mod #test_mod_ident {
use super::*;
use ::solana_frozen_abi::abi_example::{AbiExample, AbiEnumVisitor};

let test_api = if expected_api_digest.is_some() {
quote! {
#[test]
fn test_abi_digest() {
fn test_api_digest() {
use ::solana_frozen_abi::abi_example::{AbiExample, AbiEnumVisitor};
let mut digester = ::solana_frozen_abi::abi_digester::AbiDigester::create();
let example = <#type_name>::example();
let result = <_>::visit_for_abi(&&example, &mut digester);
let mut hash = digester.finalize();
if result.is_err() {
::std::eprintln!("Error: digest error: {:#?}", result);
}
result.unwrap();
let actual_digest = ::std::format!("{}", hash);
if ::std::env::var("SOLANA_ABI_BULK_UPDATE").is_ok() {
if #expected_digest != actual_digest {
::std::eprintln!("sed -i -e 's/{}/{}/g' $(git grep --files-with-matches frozen_abi)", #expected_digest, hash);
}
::std::eprintln!("Warning: Not testing the abi digest under SOLANA_ABI_BULK_UPDATE!");
} else {
if let Ok(dir) = ::std::env::var("SOLANA_ABI_DUMP_DIR") {
assert_eq!(#expected_digest, actual_digest, "Possibly ABI changed? Examine the diff in SOLANA_ABI_DUMP_DIR!: \n$ diff -u {}/*{}* {}/*{}*", dir, #expected_digest, dir, actual_digest);
} else {
assert_eq!(#expected_digest, actual_digest, "Possibly ABI changed? Confirm the diff by rerunning before and after this test failed with SOLANA_ABI_DUMP_DIR!");
}
let hash = digester.finalize();
assert_eq!(#expected_api_digest, ::std::format!("{}", hash));
}
}
} else {
TokenStream2::new()
};

let test_abi = if expected_abi_digest.is_some() {
quote! {
#[test]
fn test_abi_digest() {
use ::rand::{SeedableRng, RngCore};
use ::rand_chacha::ChaCha8Rng;
use ::bincode;
use ::solana_frozen_abi::stable_abi::StableAbi;

let mut rng = ChaCha8Rng::seed_from_u64(20666175621446498);
let mut digester = ::solana_frozen_abi::hash::Hasher::default();

for _ in 0..100_000 {
let val = <#type_name>::random(&mut rng);
digester.hash(&bincode::serialize(&val).unwrap());
}
assert_eq!(#expected_abi_digest, ::std::format!("{}", digester.result()));
}
}
} else {
TokenStream2::new()
};

let no_tests = test_api.is_empty() && test_abi.is_empty();
if no_tests {
return TokenStream2::new();
}

quote! {
#[cfg(test)]
mod #test_mod_ident {
use super::*;
#test_api
#test_abi
}
}
}

Expand All @@ -319,9 +375,18 @@ fn test_mod_name(type_name: &Ident) -> Ident {
}

#[cfg(feature = "frozen-abi")]
fn frozen_abi_type_alias(input: ItemType, expected_digest: &str) -> TokenStream {
fn frozen_abi_type_alias(
input: ItemType,
expected_api_digest: Option<&str>,
expected_abi_digest: Option<&str>,
) -> TokenStream {
let type_name = &input.ident;
let test = quote_for_test(&test_mod_name(type_name), type_name, expected_digest);
let test = quote_for_test(
&test_mod_name(type_name),
type_name,
expected_api_digest,
expected_abi_digest,
);
let result = quote! {
#input
#test
Expand All @@ -330,9 +395,18 @@ fn frozen_abi_type_alias(input: ItemType, expected_digest: &str) -> TokenStream
}

#[cfg(feature = "frozen-abi")]
fn frozen_abi_struct_type(input: ItemStruct, expected_digest: &str) -> TokenStream {
fn frozen_abi_struct_type(
input: ItemStruct,
expected_api_digest: Option<&str>,
expected_abi_digest: Option<&str>,
) -> TokenStream {
let type_name = &input.ident;
let test = quote_for_test(&test_mod_name(type_name), type_name, expected_digest);
let test = quote_for_test(
&test_mod_name(type_name),
type_name,
expected_api_digest,
expected_abi_digest,
);
let result = quote! {
#input
#test
Expand Down Expand Up @@ -387,9 +461,18 @@ fn quote_sample_variant(
}

#[cfg(feature = "frozen-abi")]
fn frozen_abi_enum_type(input: ItemEnum, expected_digest: &str) -> TokenStream {
fn frozen_abi_enum_type(
input: ItemEnum,
expected_api_digest: Option<&str>,
expected_abi_digest: Option<&str>,
) -> TokenStream {
let type_name = &input.ident;
let test = quote_for_test(&test_mod_name(type_name), type_name, expected_digest);
let test = quote_for_test(
&test_mod_name(type_name),
type_name,
expected_api_digest,
expected_abi_digest,
);
let result = quote! {
#input
#test
Expand All @@ -400,31 +483,39 @@ fn frozen_abi_enum_type(input: ItemEnum, expected_digest: &str) -> TokenStream {
#[cfg(feature = "frozen-abi")]
#[proc_macro_attribute]
pub fn frozen_abi(attrs: TokenStream, item: TokenStream) -> TokenStream {
let mut expected_digest: Option<String> = None;
let mut api_expected_digest: Option<String> = None;
let mut abi_expected_digest: Option<String> = None;

let attrs_parser = syn::meta::parser(|meta| {
if meta.path.is_ident("digest") {
expected_digest = Some(meta.value()?.parse::<LitStr>()?.value());
api_expected_digest = Some(meta.value()?.parse::<LitStr>()?.value());
Ok(())
} else if meta.path.is_ident("abi_digest") {
abi_expected_digest = Some(meta.value()?.parse::<LitStr>()?.value());
Ok(())
} else {
Err(meta.error("unsupported \"frozen_abi\" property"))
}
});
parse_macro_input!(attrs with attrs_parser);

let Some(expected_digest) = expected_digest else {
return Error::new_spanned(
TokenStream2::from(item),
"the required \"digest\" = ... attribute is missing.",
)
.to_compile_error()
.into();
};

let item = parse_macro_input!(item as Item);
match item {
Item::Struct(input) => frozen_abi_struct_type(input, &expected_digest),
Item::Enum(input) => frozen_abi_enum_type(input, &expected_digest),
Item::Type(input) => frozen_abi_type_alias(input, &expected_digest),
Item::Struct(input) => frozen_abi_struct_type(
input,
api_expected_digest.as_deref(),
abi_expected_digest.as_deref(),
),
Item::Enum(input) => frozen_abi_enum_type(
input,
api_expected_digest.as_deref(),
abi_expected_digest.as_deref(),
),
Item::Type(input) => frozen_abi_type_alias(
input,
api_expected_digest.as_deref(),
abi_expected_digest.as_deref(),
),
_ => Error::new_spanned(
item,
"frozen_abi isn't applicable; only for struct, enum and type",
Expand Down
1 change: 1 addition & 0 deletions frozen-abi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ bv = { workspace = true, features = ["serde"] }
bytes = { workspace = true }
dashmap = { workspace = true }
log = { workspace = true, features = ["std"] }
rand = { workspace = true }
serde = { workspace = true, features = ["rc"] }
serde_derive = { workspace = true }
serde_with = { workspace = true }
Expand Down
4 changes: 3 additions & 1 deletion frozen-abi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ pub mod abi_digester;
#[cfg(feature = "frozen-abi")]
pub mod abi_example;
#[cfg(feature = "frozen-abi")]
mod hash;
pub mod hash;
#[cfg(feature = "frozen-abi")]
pub mod stable_abi;

#[cfg(feature = "frozen-abi")]
#[macro_use]
Expand Down
10 changes: 10 additions & 0 deletions frozen-abi/src/stable_abi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use rand::{distributions::Standard, Rng, RngCore};

pub trait StableAbi: Sized {
fn random(rng: &mut impl RngCore) -> Self
where
Standard: rand::distributions::Distribution<Self>,
{
rng.gen::<Self>()
}
}
Loading