Skip to content

feat: zero copy macro #1851

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ jobs:
cargo test -p light-account-checks --all-features
cargo test -p light-verifier --all-features
cargo test -p light-merkle-tree-metadata --all-features
cargo test -p light-zero-copy --features std
cargo test -p light-zero-copy --features "std, mut, derive"
cargo test -p light-zero-copy-derive --features "mut"
cargo test -p light-hash-set --all-features
- name: program-libs-slow
packages: light-bloom-filter light-indexed-merkle-tree light-batched-merkle-tree
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,5 @@ output1.txt
.zed

**/.claude/**/*

expand.rs
44 changes: 44 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ members = [
"program-libs/hash-set",
"program-libs/indexed-merkle-tree",
"program-libs/indexed-array",
"program-libs/zero-copy-derive",
"programs/account-compression",
"programs/system",
"programs/compressed-token",
Expand Down Expand Up @@ -167,6 +168,7 @@ light-compressed-account = { path = "program-libs/compressed-account", version =
light-account-checks = { path = "program-libs/account-checks", version = "0.3.0" }
light-verifier = { path = "program-libs/verifier", version = "2.1.0" }
light-zero-copy = { path = "program-libs/zero-copy", version = "0.2.0" }
light-zero-copy-derive = { path = "program-libs/zero-copy-derive", version = "0.1.0" }
photon-api = { path = "sdk-libs/photon-api", version = "0.51.0" }
forester-utils = { path = "forester-utils", version = "2.0.0" }
account-compression = { path = "programs/account-compression", version = "2.0.0", features = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,9 +399,13 @@ impl<'a> Deserialize<'a> for InstructionDataInvokeCpiWithAccountInfo {
let (account_infos, bytes) = {
let (num_slices, mut bytes) = Ref::<&[u8], U32>::from_prefix(bytes)?;
let num_slices = u32::from(*num_slices) as usize;
// TODO: add check that remaining data is enough to read num_slices
// This prevents agains invalid data allocating a lot of heap memory
let mut slices = Vec::with_capacity(num_slices);
if bytes.len() < num_slices {
return Err(ZeroCopyError::InsufficientMemoryAllocated(
bytes.len(),
num_slices,
));
}
for _ in 0..num_slices {
let (slice, _bytes) = CompressedAccountInfo::zero_copy_at_with_owner(
bytes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,14 @@ impl<'a> Deserialize<'a> for InstructionDataInvokeCpiWithReadOnly {
let (input_compressed_accounts, bytes) = {
let (num_slices, mut bytes) = Ref::<&[u8], U32>::from_prefix(bytes)?;
let num_slices = u32::from(*num_slices) as usize;
// TODO: add check that remaining data is enough to read num_slices
// This prevents agains invalid data allocating a lot of heap memory
// Prevent heap exhaustion attacks by checking if num_slices is reasonable
// Each element needs at least 1 byte when serialized
if bytes.len() < num_slices {
return Err(ZeroCopyError::InsufficientMemoryAllocated(
bytes.len(),
num_slices,
));
}
let mut slices = Vec::with_capacity(num_slices);
for _ in 0..num_slices {
let (slice, _bytes) =
Expand Down
26 changes: 26 additions & 0 deletions program-libs/zero-copy-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "light-zero-copy-derive"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
description = "Proc macro for zero-copy deserialization"

[features]
default = []
mut = []

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full", "extra-traits"] }
lazy_static = "1.4"

[dev-dependencies]
trybuild = "1.0"
rand = "0.8"
borsh = { workspace = true }
light-zero-copy = { workspace = true, features = ["std", "derive"] }
zerocopy = { workspace = true, features = ["derive"] }
103 changes: 103 additions & 0 deletions program-libs/zero-copy-derive/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Light-Zero-Copy-Derive

A procedural macro for deriving zero-copy deserialization for Rust structs used with Solana programs.

## Features

This crate provides two key derive macros:

1. `#[derive(ZeroCopy)]` - Implements zero-copy deserialization with:
- The `zero_copy_at` and `zero_copy_at_mut` methods for deserialization
- Full Borsh compatibility for serialization/deserialization
- Efficient memory representation with no copying of data
- `From<Z<StructName>>` and `From<Z<StructName>Mut>` implementations for easy conversion back to the original struct

2. `#[derive(ZeroCopyEq)]` - Adds equality comparison support:
- Compare zero-copy instances with regular struct instances
- Can be used alongside `ZeroCopy` for complete functionality
- Derivation for Options<struct> is not robust and may not compile.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Clarify the Option type limitation with specific examples.

The documentation mentions that "Derivation for Options is not robust and may not compile" but doesn't explain what patterns fail or why. Please provide specific examples of what works and what doesn't, along with any workarounds.

Consider expanding this line:

-   - Derivation for Options<struct> is not robust and may not compile.
+   - Derivation for Options<struct> has limitations:
+     - Option<CustomStruct> may fail to compile due to dereference pattern issues
+     - Option<T> where T implements Copy typically works correctly
+     - Workaround: Consider using a different representation or manual implementation
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Derivation for Options<struct> is not robust and may not compile.
- Derivation for Options<struct> has limitations:
- Option<CustomStruct> may fail to compile due to dereference pattern issues
- Option<T> where T implements Copy typically works correctly
- Workaround: Consider using a different representation or manual implementation
🤖 Prompt for AI Agents
In program-libs/zero-copy-derive/README.md at line 18, the note about the
limitation of deriving for Options<struct> lacks clarity and examples. Expand
this section by adding specific examples showing which Option patterns compile
successfully and which fail, explaining the reasons behind these failures. Also,
include any known workarounds or alternative approaches to handle these cases to
improve user understanding.


## Rules for Zero-Copy Deserialization

The macro follows these rules when generating code:

1. Creates a `ZStruct` for your struct that follows zero-copy principles
1. Fields are extracted into a meta struct until reaching a `Vec`, `Option` or non-`Copy` type
2. Vectors are represented as `ZeroCopySlice` and not included in the meta struct
3. Integer types are replaced with their zerocopy equivalents (e.g., `u16` → `U16`)
4. Fields after the first vector are directly included in the `ZStruct` and deserialized one by one
5. If a vector contains a nested vector (non-`Copy` type), it must implement `Deserialize`
6. Elements in an `Option` must implement `Deserialize`
7. Types that don't implement `Copy` must implement `Deserialize` and are deserialized one by one

## Usage

### Basic Usage

```rust
use borsh::{BorshDeserialize, BorshSerialize};
use light_zero_copy_derive::ZeroCopy;
use light_zero_copy::{borsh::Deserialize, borsh_mut::DeserializeMut};

#[repr(C)]
#[derive(Debug, PartialEq, BorshSerialize, BorshDeserialize, ZeroCopy)]
pub struct MyStruct {
pub a: u8,
pub b: u16,
pub vec: Vec<u8>,
pub c: u64,
}
let my_struct = MyStruct {
a: 1,
b: 2,
vec: vec![1u8; 32],
c: 3,
};
// Use the struct with zero-copy deserialization
let mut bytes = my_struct.try_to_vec().unwrap();

// Immutable zero-copy deserialization
let (zero_copy, _remaining) = MyStruct::zero_copy_at(&bytes).unwrap();

// Convert back to original struct using From implementation
let converted: MyStruct = zero_copy.clone().into();
assert_eq!(converted, my_struct);

// Mutable zero-copy deserialization with modification
let (mut zero_copy_mut, _remaining) = MyStruct::zero_copy_at_mut(&mut bytes).unwrap();
zero_copy_mut.a = 42;

// The change is reflected when we convert back to the original struct
let modified: MyStruct = zero_copy_mut.into();
assert_eq!(modified.a, 42);

// And also when we deserialize directly from the modified bytes
let borsh = MyStruct::try_from_slice(&bytes).unwrap();
assert_eq!(borsh.a, 42u8);
```

### With Equality Comparison

```rust
use borsh::{BorshDeserialize, BorshSerialize};
use light_zero_copy_derive::ZeroCopy;

#[repr(C)]
#[derive(Debug, PartialEq, BorshSerialize, BorshDeserialize, ZeroCopy)]
pub struct MyStruct {
pub a: u8,
pub b: u16,
pub vec: Vec<u8>,
pub c: u64,
}
let my_struct = MyStruct {
a: 1,
b: 2,
vec: vec![1u8; 32],
c: 3,
};
// Use the struct with zero-copy deserialization
let mut bytes = my_struct.try_to_vec().unwrap();
let (zero_copy, _remaining) = MyStruct::zero_copy_at(&bytes).unwrap();
assert_eq!(zero_copy, my_struct);
```
Comment on lines +79 to +103
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix the equality comparison example to actually demonstrate ZeroCopyEq.

The example titled "With Equality Comparison" doesn't actually demonstrate the ZeroCopyEq derive macro. It's missing the import and derive attribute for ZeroCopyEq.

Apply this diff to fix the example:

 use borsh::{BorshDeserialize, BorshSerialize};
-use light_zero_copy_derive::ZeroCopy;
+use light_zero_copy_derive::{ZeroCopy, ZeroCopyEq};

 #[repr(C)]
-#[derive(Debug, PartialEq, BorshSerialize, BorshDeserialize, ZeroCopy)]
+#[derive(Debug, PartialEq, BorshSerialize, BorshDeserialize, ZeroCopy, ZeroCopyEq)]
 pub struct MyStruct {
     pub a: u8,
     pub b: u16,
     pub vec: Vec<u8>,
     pub c: u64,
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### With Equality Comparison
```rust
use borsh::{BorshDeserialize, BorshSerialize};
use light_zero_copy_derive::ZeroCopy;
#[repr(C)]
#[derive(Debug, PartialEq, BorshSerialize, BorshDeserialize, ZeroCopy)]
pub struct MyStruct {
pub a: u8,
pub b: u16,
pub vec: Vec<u8>,
pub c: u64,
}
let my_struct = MyStruct {
a: 1,
b: 2,
vec: vec![1u8; 32],
c: 3,
};
// Use the struct with zero-copy deserialization
let mut bytes = my_struct.try_to_vec().unwrap();
let (zero_copy, _remaining) = MyStruct::zero_copy_at(&bytes).unwrap();
assert_eq!(zero_copy, my_struct);
```
use borsh::{BorshDeserialize, BorshSerialize};
use light_zero_copy_derive::{ZeroCopy, ZeroCopyEq};
#[repr(C)]
#[derive(Debug, PartialEq, BorshSerialize, BorshDeserialize, ZeroCopy, ZeroCopyEq)]
pub struct MyStruct {
pub a: u8,
pub b: u16,
pub vec: Vec<u8>,
pub c: u64,
}
let my_struct = MyStruct {
a: 1,
b: 2,
vec: vec![1u8; 32],
c: 3,
};
// Use the struct with zero-copy deserialization
let mut bytes = my_struct.try_to_vec().unwrap();
let (zero_copy, _remaining) = MyStruct::zero_copy_at(&bytes).unwrap();
assert_eq!(zero_copy, my_struct);
🤖 Prompt for AI Agents
In program-libs/zero-copy-derive/README.md between lines 79 and 103, the example
titled "With Equality Comparison" does not demonstrate the ZeroCopyEq derive
macro properly. To fix this, add the import for ZeroCopyEq from
light_zero_copy_derive and include ZeroCopyEq in the derive attribute list for
MyStruct. This will correctly show how to use the ZeroCopyEq macro for equality
comparison in the example.

Loading
Loading