Skip to content

Commit 57cfd4f

Browse files
authored
feat: zero copy macro (#1851)
* feat: zero-copy-derive * remove commented tests
1 parent aad94a4 commit 57cfd4f

32 files changed

+8614
-21
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
@@ -87,3 +87,5 @@ output1.txt
8787

8888
**/.claude/**/*
8989
**/~/
90+
91+
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 = [

program-libs/compressed-account/src/instruction_data/with_account_info.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,9 +399,13 @@ impl<'a> Deserialize<'a> for InstructionDataInvokeCpiWithAccountInfo {
399399
let (account_infos, bytes) = {
400400
let (num_slices, mut bytes) = Ref::<&[u8], U32>::from_prefix(bytes)?;
401401
let num_slices = u32::from(*num_slices) as usize;
402-
// TODO: add check that remaining data is enough to read num_slices
403-
// This prevents agains invalid data allocating a lot of heap memory
404402
let mut slices = Vec::with_capacity(num_slices);
403+
if bytes.len() < num_slices {
404+
return Err(ZeroCopyError::InsufficientMemoryAllocated(
405+
bytes.len(),
406+
num_slices,
407+
));
408+
}
405409
for _ in 0..num_slices {
406410
let (slice, _bytes) = CompressedAccountInfo::zero_copy_at_with_owner(
407411
bytes,

program-libs/compressed-account/src/instruction_data/with_readonly.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,8 +347,14 @@ impl<'a> Deserialize<'a> for InstructionDataInvokeCpiWithReadOnly {
347347
let (input_compressed_accounts, bytes) = {
348348
let (num_slices, mut bytes) = Ref::<&[u8], U32>::from_prefix(bytes)?;
349349
let num_slices = u32::from(*num_slices) as usize;
350-
// TODO: add check that remaining data is enough to read num_slices
351-
// This prevents agains invalid data allocating a lot of heap memory
350+
// Prevent heap exhaustion attacks by checking if num_slices is reasonable
351+
// Each element needs at least 1 byte when serialized
352+
if bytes.len() < num_slices {
353+
return Err(ZeroCopyError::InsufficientMemoryAllocated(
354+
bytes.len(),
355+
num_slices,
356+
));
357+
}
352358
let mut slices = Vec::with_capacity(num_slices);
353359
for _ in 0..num_slices {
354360
let (slice, _bytes) =
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)