Skip to content

Commit 1a77ac8

Browse files
committed
feat: zero-copy-derive
1 parent 5fb5198 commit 1a77ac8

27 files changed

+5231
-76
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ members = [
1313
"program-libs/hash-set",
1414
"program-libs/indexed-merkle-tree",
1515
"program-libs/indexed-array",
16+
"program-libs/zero-copy-derive",
1617
"programs/account-compression",
1718
"programs/system",
1819
"programs/compressed-token",
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "light-zero-copy-derive"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "Apache-2.0"
6+
description = "Proc macro for zero-copy deserialization"
7+
8+
[lib]
9+
proc-macro = true
10+
11+
[dependencies]
12+
proc-macro2 = "1.0"
13+
quote = "1.0"
14+
syn = { version = "2.0", features = ["full", "extra-traits"] }
15+
lazy_static = "1.4"
16+
17+
[dev-dependencies]
18+
trybuild = "1.0"
19+
rand = "0.8"
20+
borsh = { workspace = true }
21+
light-zero-copy = { workspace = true, features = ["std"] }
22+
zerocopy = { workspace = true, features = ["derive"] }
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Light-Zero-Copy-Derive
2+
3+
A procedural macro for deriving zero-copy deserialization for Rust structs used with Solana programs.
4+
5+
## Features
6+
7+
This crate provides two key derive macros:
8+
9+
1. `#[derive(ZeroCopy)]` - Implements zero-copy deserialization with:
10+
- The `zero_copy_at` and `zero_copy_at_mut` methods for deserialization
11+
- Full Borsh compatibility for serialization/deserialization
12+
- Efficient memory representation with no copying of data
13+
- `From<Z<StructName>>` and `From<Z<StructName>Mut>` implementations for easy conversion back to the original struct
14+
15+
2. `#[derive(ZeroCopyEq)]` - Adds equality comparison support:
16+
- Compare zero-copy instances with regular struct instances
17+
- Can be used alongside `ZeroCopy` for complete functionality
18+
- Derivation for Options<struct> is not robust and may not compile.
19+
20+
## Rules for Zero-Copy Deserialization
21+
22+
The macro follows these rules when generating code:
23+
24+
1. Creates a `ZStruct` for your struct that follows zero-copy principles
25+
1. Fields are extracted into a meta struct until reaching a `Vec`, `Option` or non-`Copy` type
26+
2. Vectors are represented as `ZeroCopySlice` and not included in the meta struct
27+
3. Integer types are replaced with their zerocopy equivalents (e.g., `u16``U16`)
28+
4. Fields after the first vector are directly included in the `ZStruct` and deserialized one by one
29+
5. If a vector contains a nested vector (non-`Copy` type), it must implement `Deserialize`
30+
6. Elements in an `Option` must implement `Deserialize`
31+
7. Types that don't implement `Copy` must implement `Deserialize` and are deserialized one by one
32+
33+
## Usage
34+
35+
### Basic Usage
36+
37+
```rust
38+
use borsh::{BorshDeserialize, BorshSerialize};
39+
use light_zero_copy_derive::ZeroCopy;
40+
use light_zero_copy::{borsh::Deserialize, borsh_mut::DeserializeMut};
41+
42+
#[repr(C)]
43+
#[derive(Debug, PartialEq, BorshSerialize, BorshDeserialize, ZeroCopy)]
44+
pub struct MyStruct {
45+
pub a: u8,
46+
pub b: u16,
47+
pub vec: Vec<u8>,
48+
pub c: u64,
49+
}
50+
let my_struct = MyStruct {
51+
a: 1,
52+
b: 2,
53+
vec: vec![1u8; 32],
54+
c: 3,
55+
};
56+
// Use the struct with zero-copy deserialization
57+
let mut bytes = my_struct.try_to_vec().unwrap();
58+
59+
// Immutable zero-copy deserialization
60+
let (zero_copy, _remaining) = MyStruct::zero_copy_at(&bytes).unwrap();
61+
62+
// Convert back to original struct using From implementation
63+
let converted: MyStruct = zero_copy.clone().into();
64+
assert_eq!(converted, my_struct);
65+
66+
// Mutable zero-copy deserialization with modification
67+
let (mut zero_copy_mut, _remaining) = MyStruct::zero_copy_at_mut(&mut bytes).unwrap();
68+
zero_copy_mut.a = 42;
69+
70+
// The change is reflected when we convert back to the original struct
71+
let modified: MyStruct = zero_copy_mut.into();
72+
assert_eq!(modified.a, 42);
73+
74+
// And also when we deserialize directly from the modified bytes
75+
let borsh = MyStruct::try_from_slice(&bytes).unwrap();
76+
assert_eq!(borsh.a, 42u8);
77+
```
78+
79+
### With Equality Comparison
80+
81+
```rust
82+
use borsh::{BorshDeserialize, BorshSerialize};
83+
use light_zero_copy_derive::ZeroCopy;
84+
85+
#[repr(C)]
86+
#[derive(Debug, PartialEq, BorshSerialize, BorshDeserialize, ZeroCopy)]
87+
pub struct MyStruct {
88+
pub a: u8,
89+
pub b: u16,
90+
pub vec: Vec<u8>,
91+
pub c: u64,
92+
}
93+
let my_struct = MyStruct {
94+
a: 1,
95+
b: 2,
96+
vec: vec![1u8; 32],
97+
c: 3,
98+
};
99+
// Use the struct with zero-copy deserialization
100+
let mut bytes = my_struct.try_to_vec().unwrap();
101+
let (zero_copy, _remaining) = MyStruct::zero_copy_at(&bytes).unwrap();
102+
assert_eq!(zero_copy, my_struct);
103+
```
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
use proc_macro2::TokenStream;
2+
use quote::quote;
3+
use syn::{Field, Ident};
4+
5+
use crate::{
6+
utils,
7+
z_struct::{analyze_struct_fields, FieldType},
8+
};
9+
10+
/// Generates byte_len implementation for DeserializeMut trait
11+
///
12+
/// RULES AND EXCEPTIONS FROM borsh_mut.rs:
13+
///
14+
/// DEFAULT RULE: Call byte_len() on each field and sum the results
15+
///
16+
/// EXCEPTIONS:
17+
/// 1. Boolean fields: Use core::mem::size_of::<u8>() (1 byte) instead of byte_len()
18+
/// * See line 97 where booleans use a special case
19+
///
20+
/// NOTES ON TYPE-SPECIFIC IMPLEMENTATIONS:
21+
/// * Primitive types: self.field.byte_len() delegates to size_of::<T>()
22+
/// - u8, u16, u32, u64, etc. all use size_of::<T>() in their implementations
23+
/// - See implementations in lines 88-90, 146-148, and macro in lines 135-151
24+
///
25+
/// * Arrays [T; N]: use size_of::<Self>() in implementation (line 41)
26+
///
27+
/// * Vec<T>: 4 bytes for length prefix + sum of byte_len() for each element
28+
/// - The Vec implementation in line 131 is: 4 + self.iter().map(|t| t.byte_len()).sum::<usize>()
29+
/// - Special case in Struct4 (line 650-657): explicitly sums the byte_len of each item
30+
///
31+
/// * VecU8<T>: Uses 1 byte for length prefix instead of regular Vec's 4 bytes
32+
/// - Implementation in line 205 shows: 1 + size_of::<T>()
33+
///
34+
/// * Option<T>: 1 byte for discriminator + value's byte_len if Some, or just 1 byte if None
35+
/// - See implementation in lines 66-72
36+
///
37+
/// * Fixed-size types: Generally implement as their own fixed size
38+
/// - Pubkey (line 45-46): hard-coded as 32 bytes
39+
pub fn generate_byte_len_impl<'a>(
40+
_name: &Ident,
41+
meta_fields: &'a [&'a Field],
42+
struct_fields: &'a [&'a Field],
43+
) -> TokenStream {
44+
let field_types = analyze_struct_fields(struct_fields);
45+
46+
// Generate statements for calculating byte_len for each field
47+
let meta_byte_len = if !meta_fields.is_empty() {
48+
meta_fields
49+
.iter()
50+
.map(|field| {
51+
let field_name = &field.ident;
52+
// Handle boolean fields specially by using size_of instead of byte_len
53+
if utils::is_bool_type(&field.ty) {
54+
quote! { core::mem::size_of::<u8>() }
55+
} else {
56+
quote! { self.#field_name.byte_len() }
57+
}
58+
})
59+
.reduce(|acc, item| {
60+
quote! { #acc + #item }
61+
})
62+
} else {
63+
None
64+
};
65+
66+
// Generate byte_len calculations for struct fields
67+
// Default rule: Use self.field.byte_len() for all fields
68+
// Exception: Use core::mem::size_of::<u8>() for boolean fields
69+
let struct_byte_len = field_types.into_iter().map(|field_type| {
70+
match field_type {
71+
// Exception 1: Booleans use size_of::<u8>() directly
72+
FieldType::Bool(_) | FieldType::CopyU8Bool(_) => {
73+
quote! { core::mem::size_of::<u8>() }
74+
}
75+
// All other types delegate to their own byte_len implementation
76+
FieldType::VecU8(field_name)
77+
| FieldType::VecCopy(field_name, _)
78+
| FieldType::VecNonCopy(field_name, _)
79+
| FieldType::Array(field_name, _)
80+
| FieldType::Option(field_name, _)
81+
| FieldType::Pubkey(field_name)
82+
| FieldType::IntegerU64(field_name)
83+
| FieldType::IntegerU32(field_name)
84+
| FieldType::IntegerU16(field_name)
85+
| FieldType::IntegerU8(field_name)
86+
| FieldType::Copy(field_name, _)
87+
| FieldType::NonCopy(field_name, _) => {
88+
quote! { self.#field_name.byte_len() }
89+
}
90+
}
91+
});
92+
93+
// Combine meta fields and struct fields for total byte_len calculation
94+
let combined_byte_len = match meta_byte_len {
95+
Some(meta) => {
96+
let struct_bytes = struct_byte_len.fold(quote!(), |acc, item| {
97+
if acc.is_empty() {
98+
item
99+
} else {
100+
quote! { #acc + #item }
101+
}
102+
});
103+
104+
if struct_bytes.is_empty() {
105+
meta
106+
} else {
107+
quote! { #meta + #struct_bytes }
108+
}
109+
}
110+
None => struct_byte_len.fold(quote!(), |acc, item| {
111+
if acc.is_empty() {
112+
item
113+
} else {
114+
quote! { #acc + #item }
115+
}
116+
}),
117+
};
118+
119+
// Generate the final implementation
120+
quote! {
121+
fn byte_len(&self) -> usize {
122+
#combined_byte_len
123+
}
124+
}
125+
}
126+
127+
#[cfg(test)]
128+
mod tests {
129+
use quote::format_ident;
130+
use syn::parse_quote;
131+
132+
use super::*;
133+
134+
#[test]
135+
fn test_generate_byte_len_simple() {
136+
let name = format_ident!("TestStruct");
137+
138+
let field1: Field = parse_quote!(pub a: u8);
139+
let field2: Field = parse_quote!(pub b: u16);
140+
141+
let meta_fields = vec![&field1, &field2];
142+
let struct_fields: Vec<&Field> = vec![];
143+
144+
let result = generate_byte_len_impl(&name, &meta_fields, &struct_fields);
145+
let result_str = result.to_string();
146+
147+
assert!(result_str.contains("fn byte_len (& self) -> usize"));
148+
assert!(result_str.contains("self . a . byte_len () + self . b . byte_len ()"));
149+
}
150+
151+
#[test]
152+
fn test_generate_byte_len_with_vec() {
153+
let name = format_ident!("TestStruct");
154+
155+
let field1: Field = parse_quote!(pub a: u8);
156+
let field2: Field = parse_quote!(pub vec: Vec<u8>);
157+
let field3: Field = parse_quote!(pub c: u32);
158+
159+
let meta_fields = vec![&field1];
160+
let struct_fields = vec![&field2, &field3];
161+
162+
let result = generate_byte_len_impl(&name, &meta_fields, &struct_fields);
163+
let result_str = result.to_string();
164+
165+
assert!(result_str.contains("fn byte_len (& self) -> usize"));
166+
assert!(result_str.contains(
167+
"self . a . byte_len () + self . vec . byte_len () + self . c . byte_len ()"
168+
));
169+
}
170+
171+
#[test]
172+
fn test_generate_byte_len_with_option() {
173+
let name = format_ident!("TestStruct");
174+
175+
let field1: Field = parse_quote!(pub a: u8);
176+
let field2: Field = parse_quote!(pub option: Option<u32>);
177+
178+
let meta_fields = vec![&field1];
179+
let struct_fields = vec![&field2];
180+
181+
let result = generate_byte_len_impl(&name, &meta_fields, &struct_fields);
182+
let result_str = result.to_string();
183+
184+
assert!(result_str.contains("fn byte_len (& self) -> usize"));
185+
assert!(result_str.contains("self . a . byte_len () + self . option . byte_len ()"));
186+
}
187+
188+
#[test]
189+
fn test_generate_byte_len_with_bool() {
190+
let name = format_ident!("TestStruct");
191+
192+
let field1: Field = parse_quote!(pub a: u8);
193+
let field2: Field = parse_quote!(pub b: bool);
194+
195+
let meta_fields = vec![&field1, &field2];
196+
let struct_fields: Vec<&Field> = vec![];
197+
198+
let result = generate_byte_len_impl(&name, &meta_fields, &struct_fields);
199+
let result_str = result.to_string();
200+
201+
assert!(result_str.contains("fn byte_len (& self) -> usize"));
202+
assert!(result_str.contains("self . a . byte_len () + core :: mem :: size_of :: < u8 > ()"));
203+
}
204+
}

0 commit comments

Comments
 (0)