Skip to content

Commit 1513a4c

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

File tree

5 files changed

+169
-53
lines changed

5 files changed

+169
-53
lines changed

Cargo.lock

Lines changed: 14 additions & 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: 141 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,42 @@ 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 quote::quote;
36+
use syn::{parse_macro_input, Error, Item};
37+
38+
let item = parse_macro_input!(item as Item);
39+
40+
let ident = match item {
41+
Item::Struct(ref s) => &s.ident,
42+
Item::Enum(ref e) => &e.ident,
43+
Item::Type(ref t) => &t.ident,
44+
_ => {
45+
return Error::new_spanned(
46+
item,
47+
"StableAbi can only be derived for struct, enum, or type alias",
48+
)
49+
.to_compile_error()
50+
.into();
51+
}
52+
};
53+
54+
let expanded = quote! {
55+
#[automatically_derived]
56+
impl ::solana_frozen_abi::stable_abi::StableAbi for #ident {}
57+
};
58+
59+
expanded.into()
60+
}
61+
2662
#[cfg(feature = "frozen-abi")]
2763
use proc_macro2::{Span, TokenStream as TokenStream2, TokenTree};
2864
#[cfg(feature = "frozen-abi")]
@@ -156,8 +192,8 @@ fn derive_abi_sample_enum_type(input: ItemEnum) -> TokenStream {
156192
#[cfg(feature = "frozen-abi")]
157193
fn derive_abi_sample_struct_type(input: ItemStruct) -> TokenStream {
158194
let type_name = &input.ident;
159-
let mut sample_fields = quote! {};
160195
let fields = &input.fields;
196+
let mut sample_fields = quote! {};
161197

162198
match fields {
163199
Fields::Named(_) => {
@@ -167,19 +203,15 @@ fn derive_abi_sample_struct_type(input: ItemStruct) -> TokenStream {
167203
#field_name: AbiExample::example(),
168204
});
169205
}
170-
sample_fields = quote! {
171-
{ #sample_fields }
172-
}
206+
sample_fields = quote! {{ #sample_fields }};
173207
}
174208
Fields::Unnamed(_) => {
175209
for _ in fields {
176210
sample_fields.extend(quote! {
177211
AbiExample::example(),
178212
});
179213
}
180-
sample_fields = quote! {
181-
( #sample_fields )
182-
}
214+
sample_fields = quote! {( #sample_fields )};
183215
}
184216
_ => unimplemented!("fields: {:?}", fields),
185217
}
@@ -195,7 +227,7 @@ fn derive_abi_sample_struct_type(input: ItemStruct) -> TokenStream {
195227
impl #impl_generics ::solana_frozen_abi::abi_example::AbiExample for #type_name #ty_generics #where_clause {
196228
fn example() -> Self {
197229
::std::println!(
198-
"AbiExample for struct: {}",
230+
"AbiExample::example for struct: {}",
199231
std::any::type_name::<#type_name #ty_generics>()
200232
);
201233
use ::solana_frozen_abi::abi_example::AbiExample;
@@ -277,39 +309,61 @@ pub fn derive_abi_enum_visitor(item: TokenStream) -> TokenStream {
277309
fn quote_for_test(
278310
test_mod_ident: &Ident,
279311
type_name: &Ident,
280-
expected_digest: &str,
312+
expected_api_digest: Option<&str>,
313+
expected_abi_digest: Option<&str>,
281314
) -> TokenStream2 {
282-
quote! {
283-
#[cfg(test)]
284-
mod #test_mod_ident {
285-
use super::*;
286-
use ::solana_frozen_abi::abi_example::{AbiExample, AbiEnumVisitor};
287-
315+
let test_api = if expected_api_digest.is_some() {
316+
quote! {
288317
#[test]
289-
fn test_abi_digest() {
318+
fn test_api_digest() {
319+
use ::solana_frozen_abi::abi_example::{AbiExample, AbiEnumVisitor};
290320
let mut digester = ::solana_frozen_abi::abi_digester::AbiDigester::create();
291321
let example = <#type_name>::example();
292322
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-
}
297323
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-
}
324+
let hash = digester.finalize();
325+
assert_eq!(#expected_api_digest, format!("{}", hash));
326+
}
327+
}
328+
} else {
329+
TokenStream2::new()
330+
};
331+
332+
let test_abi = if expected_abi_digest.is_some() {
333+
quote! {
334+
#[test]
335+
fn test_abi_digest() {
336+
use ::rand::{SeedableRng, RngCore};
337+
use ::rand_chacha::ChaCha8Rng;
338+
use ::bincode;
339+
use ::solana_frozen_abi::stable_abi::StableAbi;
340+
341+
let mut rng = ChaCha8Rng::seed_from_u64(20666175621446498);
342+
let mut digester = ::solana_frozen_abi::hash::Hasher::default();
343+
344+
for _ in 0..100_000 {
345+
let val = <#type_name>::random(&mut rng);
346+
digester.hash(&bincode::serialize(&val).unwrap());
310347
}
348+
assert_eq!(#expected_abi_digest, format!("{}", digester.result()));
311349
}
312350
}
351+
} else {
352+
TokenStream2::new()
353+
};
354+
355+
let no_tests = test_api.is_empty() && test_abi.is_empty();
356+
if no_tests {
357+
return TokenStream2::new();
358+
}
359+
360+
quote! {
361+
#[cfg(test)]
362+
mod #test_mod_ident {
363+
use super::*;
364+
#test_api
365+
#test_abi
366+
}
313367
}
314368
}
315369

@@ -319,9 +373,18 @@ fn test_mod_name(type_name: &Ident) -> Ident {
319373
}
320374

321375
#[cfg(feature = "frozen-abi")]
322-
fn frozen_abi_type_alias(input: ItemType, expected_digest: &str) -> TokenStream {
376+
fn frozen_abi_type_alias(
377+
input: ItemType,
378+
expected_api_digest: Option<&str>,
379+
expected_abi_digest: Option<&str>,
380+
) -> TokenStream {
323381
let type_name = &input.ident;
324-
let test = quote_for_test(&test_mod_name(type_name), type_name, expected_digest);
382+
let test = quote_for_test(
383+
&test_mod_name(type_name),
384+
type_name,
385+
expected_api_digest,
386+
expected_abi_digest,
387+
);
325388
let result = quote! {
326389
#input
327390
#test
@@ -330,9 +393,18 @@ fn frozen_abi_type_alias(input: ItemType, expected_digest: &str) -> TokenStream
330393
}
331394

332395
#[cfg(feature = "frozen-abi")]
333-
fn frozen_abi_struct_type(input: ItemStruct, expected_digest: &str) -> TokenStream {
396+
fn frozen_abi_struct_type(
397+
input: ItemStruct,
398+
expected_api_digest: Option<&str>,
399+
expected_abi_digest: Option<&str>,
400+
) -> TokenStream {
334401
let type_name = &input.ident;
335-
let test = quote_for_test(&test_mod_name(type_name), type_name, expected_digest);
402+
let test = quote_for_test(
403+
&test_mod_name(type_name),
404+
type_name,
405+
expected_api_digest,
406+
expected_abi_digest,
407+
);
336408
let result = quote! {
337409
#input
338410
#test
@@ -387,9 +459,18 @@ fn quote_sample_variant(
387459
}
388460

389461
#[cfg(feature = "frozen-abi")]
390-
fn frozen_abi_enum_type(input: ItemEnum, expected_digest: &str) -> TokenStream {
462+
fn frozen_abi_enum_type(
463+
input: ItemEnum,
464+
expected_api_digest: Option<&str>,
465+
expected_abi_digest: Option<&str>,
466+
) -> TokenStream {
391467
let type_name = &input.ident;
392-
let test = quote_for_test(&test_mod_name(type_name), type_name, expected_digest);
468+
let test = quote_for_test(
469+
&test_mod_name(type_name),
470+
type_name,
471+
expected_api_digest,
472+
expected_abi_digest,
473+
);
393474
let result = quote! {
394475
#input
395476
#test
@@ -400,31 +481,39 @@ fn frozen_abi_enum_type(input: ItemEnum, expected_digest: &str) -> TokenStream {
400481
#[cfg(feature = "frozen-abi")]
401482
#[proc_macro_attribute]
402483
pub fn frozen_abi(attrs: TokenStream, item: TokenStream) -> TokenStream {
403-
let mut expected_digest: Option<String> = None;
484+
let mut api_expected_digest: Option<String> = None;
485+
let mut abi_expected_digest: Option<String> = None;
486+
404487
let attrs_parser = syn::meta::parser(|meta| {
405488
if meta.path.is_ident("digest") {
406-
expected_digest = Some(meta.value()?.parse::<LitStr>()?.value());
489+
api_expected_digest = Some(meta.value()?.parse::<LitStr>()?.value());
490+
Ok(())
491+
} else if meta.path.is_ident("abi_digest") {
492+
abi_expected_digest = Some(meta.value()?.parse::<LitStr>()?.value());
407493
Ok(())
408494
} else {
409495
Err(meta.error("unsupported \"frozen_abi\" property"))
410496
}
411497
});
412498
parse_macro_input!(attrs with attrs_parser);
413499

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-
423500
let item = parse_macro_input!(item as Item);
424501
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),
502+
Item::Struct(input) => frozen_abi_struct_type(
503+
input,
504+
api_expected_digest.as_deref(),
505+
abi_expected_digest.as_deref(),
506+
),
507+
Item::Enum(input) => frozen_abi_enum_type(
508+
input,
509+
api_expected_digest.as_deref(),
510+
abi_expected_digest.as_deref(),
511+
),
512+
Item::Type(input) => frozen_abi_type_alias(
513+
input,
514+
api_expected_digest.as_deref(),
515+
abi_expected_digest.as_deref(),
516+
),
428517
_ => Error::new_spanned(
429518
item,
430519
"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
@@ -33,6 +33,7 @@ serde_with = { workspace = true }
3333
sha2 = { workspace = true }
3434
solana-frozen-abi-macro = { workspace = true }
3535
thiserror = { workspace = true }
36+
rand = { workspace = true }
3637

3738
[target.'cfg(not(target_os = "solana"))'.dependencies]
3839
im = { workspace = true, features = ["rayon", "serde"] }

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)