Skip to content

Commit f5870e8

Browse files
committed
feat: zero-copy-derive
fix: light-zero-copy tests comment derive mut commented byte len fix: derive macro for non mut pre bytelen refactor: detach bytelen trait stash adding config simple config derive works stash stash new at stash new at Compressed Account stash man InstructionDataInvoke new_zero_copy works stash simple config zero_copy_new tests stash refactor fixed lifetime issue stash instruction data tests work move byte_len to init_mut added randomized tests stash got failing random tests fixed u8 and bool remove bytelen renamed trait fix lint fix tests apply feedback meta_struct use syn to parse options instead of strings primitive types Replace string-based type comparisons with proper syn AST matching replace parse_str with parse_quote replace empty quote with unreachable! add byte len check borsh_vec_u8_as_slice_mut converted unimplemented to panic cleanup redundant as u64 etc fix docs cleanup cleanup commtend code cleanup mut conditionals remove bytelen derive cleanup refactor: replace duplicate code with generate_deserialize_call refactor detecting copy moved to internal refactor: add error handling cleanup cleanup file structure stash wip transform all primitive types to zero copy types simplify analyze_struct_fields fix empty meta struct generation stash zero copy changes unified some with Deserialize::Output unified integer field type enum renam VecNonStaticZeroCopy -> VecDynamicZeroCopy
1 parent 5fb5198 commit f5870e8

31 files changed

+8787
-15
lines changed

.github/workflows/rust.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ jobs:
5353
cargo test -p light-account-checks --all-features
5454
cargo test -p light-verifier --all-features
5555
cargo test -p light-merkle-tree-metadata --all-features
56-
cargo test -p light-zero-copy --features std
56+
cargo test -p light-zero-copy --features "std, mut, derive"
57+
cargo test -p light-zero-copy-derive --features "mut"
5758
cargo test -p light-hash-set --all-features
5859
- name: program-libs-slow
5960
packages: light-bloom-filter light-indexed-merkle-tree light-batched-merkle-tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,5 @@ output1.txt
8686
.zed
8787

8888
**/.claude/**/*
89+
90+
expand.rs

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 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",
@@ -167,6 +168,7 @@ light-compressed-account = { path = "program-libs/compressed-account", version =
167168
light-account-checks = { path = "program-libs/account-checks", version = "0.3.0" }
168169
light-verifier = { path = "program-libs/verifier", version = "2.1.0" }
169170
light-zero-copy = { path = "program-libs/zero-copy", version = "0.2.0" }
171+
light-zero-copy-derive = { path = "program-libs/zero-copy-derive", version = "0.1.0" }
170172
photon-api = { path = "sdk-libs/photon-api", version = "0.51.0" }
171173
forester-utils = { path = "forester-utils", version = "2.0.0" }
172174
account-compression = { path = "programs/account-compression", version = "2.0.0", features = [
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
[features]
9+
default = []
10+
mut = []
11+
12+
[lib]
13+
proc-macro = true
14+
15+
[dependencies]
16+
proc-macro2 = "1.0"
17+
quote = "1.0"
18+
syn = { version = "2.0", features = ["full", "extra-traits"] }
19+
lazy_static = "1.4"
20+
21+
[dev-dependencies]
22+
trybuild = "1.0"
23+
rand = "0.8"
24+
borsh = { workspace = true }
25+
light-zero-copy = { workspace = true, features = ["std", "derive"] }
26+
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+
```

0 commit comments

Comments
 (0)