Skip to content

Commit cd5faad

Browse files
committed
introduce StableAbi extension to frozen-abi
1 parent 5390fa9 commit cd5faad

File tree

5 files changed

+158
-53
lines changed

5 files changed

+158
-53
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frozen-abi-macro/src/lib.rs

Lines changed: 143 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,44 @@ pub fn derive_abi_enum_visitor(_item: TokenStream) -> TokenStream {
2323
"".parse().unwrap()
2424
}
2525

26+
#[cfg(not(feature = "frozen-abi"))]
27+
#[proc_macro_derive(StableAbi)]
28+
pub fn derive_stable_abi(_item: TokenStream) -> TokenStream {
29+
"".parse().unwrap()
30+
}
31+
32+
#[cfg(feature = "frozen-abi")]
33+
#[proc_macro_derive(StableAbi)]
34+
pub fn derive_stable_abi(item: TokenStream) -> TokenStream {
35+
use {
36+
quote::quote,
37+
syn::{parse_macro_input, Error, Item},
38+
};
39+
40+
let item = parse_macro_input!(item as Item);
41+
42+
let ident = match item {
43+
Item::Struct(ref s) => &s.ident,
44+
Item::Enum(ref e) => &e.ident,
45+
Item::Type(ref t) => &t.ident,
46+
_ => {
47+
return Error::new_spanned(
48+
item,
49+
"StableAbi can only be derived for struct, enum, or type alias",
50+
)
51+
.to_compile_error()
52+
.into();
53+
}
54+
};
55+
56+
let expanded = quote! {
57+
#[automatically_derived]
58+
impl ::solana_frozen_abi::stable_abi::StableAbi for #ident {}
59+
};
60+
61+
expanded.into()
62+
}
63+
2664
#[cfg(feature = "frozen-abi")]
2765
use proc_macro2::{Span, TokenStream as TokenStream2, TokenTree};
2866
#[cfg(feature = "frozen-abi")]
@@ -156,8 +194,8 @@ fn derive_abi_sample_enum_type(input: ItemEnum) -> TokenStream {
156194
#[cfg(feature = "frozen-abi")]
157195
fn derive_abi_sample_struct_type(input: ItemStruct) -> TokenStream {
158196
let type_name = &input.ident;
159-
let mut sample_fields = quote! {};
160197
let fields = &input.fields;
198+
let mut sample_fields = quote! {};
161199

162200
match fields {
163201
Fields::Named(_) => {
@@ -167,19 +205,15 @@ fn derive_abi_sample_struct_type(input: ItemStruct) -> TokenStream {
167205
#field_name: AbiExample::example(),
168206
});
169207
}
170-
sample_fields = quote! {
171-
{ #sample_fields }
172-
}
208+
sample_fields = quote! {{ #sample_fields }};
173209
}
174210
Fields::Unnamed(_) => {
175211
for _ in fields {
176212
sample_fields.extend(quote! {
177213
AbiExample::example(),
178214
});
179215
}
180-
sample_fields = quote! {
181-
( #sample_fields )
182-
}
216+
sample_fields = quote! {( #sample_fields )};
183217
}
184218
_ => unimplemented!("fields: {:?}", fields),
185219
}
@@ -195,7 +229,7 @@ fn derive_abi_sample_struct_type(input: ItemStruct) -> TokenStream {
195229
impl #impl_generics ::solana_frozen_abi::abi_example::AbiExample for #type_name #ty_generics #where_clause {
196230
fn example() -> Self {
197231
::std::println!(
198-
"AbiExample for struct: {}",
232+
"AbiExample::example for struct: {}",
199233
std::any::type_name::<#type_name #ty_generics>()
200234
);
201235
use ::solana_frozen_abi::abi_example::AbiExample;
@@ -277,39 +311,61 @@ pub fn derive_abi_enum_visitor(item: TokenStream) -> TokenStream {
277311
fn quote_for_test(
278312
test_mod_ident: &Ident,
279313
type_name: &Ident,
280-
expected_digest: &str,
314+
expected_api_digest: Option<&str>,
315+
expected_abi_digest: Option<&str>,
281316
) -> TokenStream2 {
282-
quote! {
283-
#[cfg(test)]
284-
mod #test_mod_ident {
285-
use super::*;
286-
use ::solana_frozen_abi::abi_example::{AbiExample, AbiEnumVisitor};
287-
317+
let test_api = if expected_api_digest.is_some() {
318+
quote! {
288319
#[test]
289-
fn test_abi_digest() {
320+
fn test_api_digest() {
321+
use ::solana_frozen_abi::abi_example::{AbiExample, AbiEnumVisitor};
290322
let mut digester = ::solana_frozen_abi::abi_digester::AbiDigester::create();
291323
let example = <#type_name>::example();
292324
let result = <_>::visit_for_abi(&&example, &mut digester);
293-
let mut hash = digester.finalize();
294-
if result.is_err() {
295-
::std::eprintln!("Error: digest error: {:#?}", result);
296-
}
297325
result.unwrap();
298-
let actual_digest = ::std::format!("{}", hash);
299-
if ::std::env::var("SOLANA_ABI_BULK_UPDATE").is_ok() {
300-
if #expected_digest != actual_digest {
301-
::std::eprintln!("sed -i -e 's/{}/{}/g' $(git grep --files-with-matches frozen_abi)", #expected_digest, hash);
302-
}
303-
::std::eprintln!("Warning: Not testing the abi digest under SOLANA_ABI_BULK_UPDATE!");
304-
} else {
305-
if let Ok(dir) = ::std::env::var("SOLANA_ABI_DUMP_DIR") {
306-
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);
307-
} else {
308-
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!");
309-
}
326+
let hash = digester.finalize();
327+
assert_eq!(#expected_api_digest, ::std::format!("{}", hash));
328+
}
329+
}
330+
} else {
331+
TokenStream2::new()
332+
};
333+
334+
let test_abi = if expected_abi_digest.is_some() {
335+
quote! {
336+
#[test]
337+
fn test_abi_digest() {
338+
use ::rand::{SeedableRng, RngCore};
339+
use ::rand_chacha::ChaCha8Rng;
340+
use ::bincode;
341+
use ::solana_frozen_abi::stable_abi::StableAbi;
342+
343+
let mut rng = ChaCha8Rng::seed_from_u64(20666175621446498);
344+
let mut digester = ::solana_frozen_abi::hash::Hasher::default();
345+
346+
for _ in 0..100_000 {
347+
let val = <#type_name>::random(&mut rng);
348+
digester.hash(&bincode::serialize(&val).unwrap());
310349
}
350+
assert_eq!(#expected_abi_digest, ::std::format!("{}", digester.result()));
311351
}
312352
}
353+
} else {
354+
TokenStream2::new()
355+
};
356+
357+
let no_tests = test_api.is_empty() && test_abi.is_empty();
358+
if no_tests {
359+
return TokenStream2::new();
360+
}
361+
362+
quote! {
363+
#[cfg(test)]
364+
mod #test_mod_ident {
365+
use super::*;
366+
#test_api
367+
#test_abi
368+
}
313369
}
314370
}
315371

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

321377
#[cfg(feature = "frozen-abi")]
322-
fn frozen_abi_type_alias(input: ItemType, expected_digest: &str) -> TokenStream {
378+
fn frozen_abi_type_alias(
379+
input: ItemType,
380+
expected_api_digest: Option<&str>,
381+
expected_abi_digest: Option<&str>,
382+
) -> TokenStream {
323383
let type_name = &input.ident;
324-
let test = quote_for_test(&test_mod_name(type_name), type_name, expected_digest);
384+
let test = quote_for_test(
385+
&test_mod_name(type_name),
386+
type_name,
387+
expected_api_digest,
388+
expected_abi_digest,
389+
);
325390
let result = quote! {
326391
#input
327392
#test
@@ -330,9 +395,18 @@ fn frozen_abi_type_alias(input: ItemType, expected_digest: &str) -> TokenStream
330395
}
331396

332397
#[cfg(feature = "frozen-abi")]
333-
fn frozen_abi_struct_type(input: ItemStruct, expected_digest: &str) -> TokenStream {
398+
fn frozen_abi_struct_type(
399+
input: ItemStruct,
400+
expected_api_digest: Option<&str>,
401+
expected_abi_digest: Option<&str>,
402+
) -> TokenStream {
334403
let type_name = &input.ident;
335-
let test = quote_for_test(&test_mod_name(type_name), type_name, expected_digest);
404+
let test = quote_for_test(
405+
&test_mod_name(type_name),
406+
type_name,
407+
expected_api_digest,
408+
expected_abi_digest,
409+
);
336410
let result = quote! {
337411
#input
338412
#test
@@ -387,9 +461,18 @@ fn quote_sample_variant(
387461
}
388462

389463
#[cfg(feature = "frozen-abi")]
390-
fn frozen_abi_enum_type(input: ItemEnum, expected_digest: &str) -> TokenStream {
464+
fn frozen_abi_enum_type(
465+
input: ItemEnum,
466+
expected_api_digest: Option<&str>,
467+
expected_abi_digest: Option<&str>,
468+
) -> TokenStream {
391469
let type_name = &input.ident;
392-
let test = quote_for_test(&test_mod_name(type_name), type_name, expected_digest);
470+
let test = quote_for_test(
471+
&test_mod_name(type_name),
472+
type_name,
473+
expected_api_digest,
474+
expected_abi_digest,
475+
);
393476
let result = quote! {
394477
#input
395478
#test
@@ -400,31 +483,39 @@ fn frozen_abi_enum_type(input: ItemEnum, expected_digest: &str) -> TokenStream {
400483
#[cfg(feature = "frozen-abi")]
401484
#[proc_macro_attribute]
402485
pub fn frozen_abi(attrs: TokenStream, item: TokenStream) -> TokenStream {
403-
let mut expected_digest: Option<String> = None;
486+
let mut api_expected_digest: Option<String> = None;
487+
let mut abi_expected_digest: Option<String> = None;
488+
404489
let attrs_parser = syn::meta::parser(|meta| {
405490
if meta.path.is_ident("digest") {
406-
expected_digest = Some(meta.value()?.parse::<LitStr>()?.value());
491+
api_expected_digest = Some(meta.value()?.parse::<LitStr>()?.value());
492+
Ok(())
493+
} else if meta.path.is_ident("abi_digest") {
494+
abi_expected_digest = Some(meta.value()?.parse::<LitStr>()?.value());
407495
Ok(())
408496
} else {
409497
Err(meta.error("unsupported \"frozen_abi\" property"))
410498
}
411499
});
412500
parse_macro_input!(attrs with attrs_parser);
413501

414-
let Some(expected_digest) = expected_digest else {
415-
return Error::new_spanned(
416-
TokenStream2::from(item),
417-
"the required \"digest\" = ... attribute is missing.",
418-
)
419-
.to_compile_error()
420-
.into();
421-
};
422-
423502
let item = parse_macro_input!(item as Item);
424503
match item {
425-
Item::Struct(input) => frozen_abi_struct_type(input, &expected_digest),
426-
Item::Enum(input) => frozen_abi_enum_type(input, &expected_digest),
427-
Item::Type(input) => frozen_abi_type_alias(input, &expected_digest),
504+
Item::Struct(input) => frozen_abi_struct_type(
505+
input,
506+
api_expected_digest.as_deref(),
507+
abi_expected_digest.as_deref(),
508+
),
509+
Item::Enum(input) => frozen_abi_enum_type(
510+
input,
511+
api_expected_digest.as_deref(),
512+
abi_expected_digest.as_deref(),
513+
),
514+
Item::Type(input) => frozen_abi_type_alias(
515+
input,
516+
api_expected_digest.as_deref(),
517+
abi_expected_digest.as_deref(),
518+
),
428519
_ => Error::new_spanned(
429520
item,
430521
"frozen_abi isn't applicable; only for struct, enum and type",

frozen-abi/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ bv = { workspace = true, features = ["serde"] }
2727
bytes = { workspace = true }
2828
dashmap = { workspace = true }
2929
log = { workspace = true, features = ["std"] }
30+
rand = { workspace = true }
3031
serde = { workspace = true, features = ["rc"] }
3132
serde_derive = { workspace = true }
3233
serde_with = { workspace = true }

frozen-abi/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ pub mod abi_digester;
1010
#[cfg(feature = "frozen-abi")]
1111
pub mod abi_example;
1212
#[cfg(feature = "frozen-abi")]
13-
mod hash;
13+
pub mod hash;
14+
#[cfg(feature = "frozen-abi")]
15+
pub mod stable_abi;
1416

1517
#[cfg(feature = "frozen-abi")]
1618
#[macro_use]

frozen-abi/src/stable_abi.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use rand::{distributions::Standard, Rng, RngCore};
2+
3+
pub trait StableAbi: Sized {
4+
fn random(rng: &mut impl RngCore) -> Self
5+
where
6+
Standard: rand::distributions::Distribution<Self>,
7+
{
8+
rng.gen::<Self>()
9+
}
10+
}

0 commit comments

Comments
 (0)