Skip to content
Merged
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
17 changes: 10 additions & 7 deletions program-libs/zero-copy-derive/src/shared/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,18 @@ fn create_unique_type_key(ident: &Ident) -> String {
/// Represents the type of input data (struct or enum)
pub enum InputType<'a> {
Struct(&'a FieldsNamed),
UnitStruct, // Unit struct with no fields (e.g., `struct Foo;`)
Enum(&'a DataEnum),
}

/// Process the derive input to extract the struct information
pub fn process_input(
input: &DeriveInput,
) -> syn::Result<(
&Ident, // Original struct name
proc_macro2::Ident, // Z-struct name
proc_macro2::Ident, // Z-struct meta name
&FieldsNamed, // Struct fields
&Ident, // Original struct name
proc_macro2::Ident, // Z-struct name
proc_macro2::Ident, // Z-struct meta name
Option<&FieldsNamed>, // Struct fields (None for unit structs)
)> {
let name = &input.ident;
let z_struct_name = format_ident!("Z{}", name);
Expand All @@ -44,11 +45,12 @@ pub fn process_input(

let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => fields,
Fields::Named(fields) => Some(fields),
Fields::Unit => None, // Support unit structs (e.g., `struct Foo;`)
_ => {
return Err(syn::Error::new_spanned(
&data.fields,
"ZeroCopy only supports structs with named fields",
"ZeroCopy only supports structs with named fields or unit structs",
))
}
},
Expand Down Expand Up @@ -80,10 +82,11 @@ pub fn process_input_generic(
let input_type = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => InputType::Struct(fields),
Fields::Unit => InputType::UnitStruct, // Support unit structs
_ => {
return Err(syn::Error::new_spanned(
&data.fields,
"ZeroCopy only supports structs with named fields",
"ZeroCopy only supports structs with named fields or unit structs",
))
}
},
Expand Down
31 changes: 31 additions & 0 deletions program-libs/zero-copy-derive/src/zero_copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,37 @@ pub fn derive_zero_copy_impl(input: ProcTokenStream) -> syn::Result<proc_macro2:
#deserialize_impl
})
}
utils::InputType::UnitStruct => {
// Unit struct has no fields - generate minimal implementations
let z_struct_name = z_name;

let zero_copy_struct_inner_impl =
generate_zero_copy_struct_inner::<false>(name, &z_struct_name)?;

// Generate a simple unit ZStruct type alias
let z_struct_def = quote! {
/// Zero-copy reference type for unit struct #name
pub type #z_struct_name<'a> = &'a #name;
};

// Generate minimal deserialize impl for unit struct
let deserialize_impl = quote! {
impl<'a> ::light_zero_copy::traits::ZeroCopyAt<'a> for #name {
type ZeroCopyAt = #z_struct_name<'a>;
fn zero_copy_at(bytes: &'a [u8]) -> ::core::result::Result<(Self::ZeroCopyAt, &'a [u8]), ::light_zero_copy::errors::ZeroCopyError> {
// Unit struct has zero size, return reference to static instance
static UNIT: #name = #name;
Ok((&UNIT, bytes))
}
}
};

Ok(quote! {
#z_struct_def
#zero_copy_struct_inner_impl
#deserialize_impl
})
}
utils::InputType::Enum(enum_data) => {
let z_enum_name = z_name;

Expand Down
22 changes: 21 additions & 1 deletion program-libs/zero-copy-derive/src/zero_copy_eq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,27 @@ pub fn derive_zero_copy_eq_impl(input: ProcTokenStream) -> syn::Result<proc_macr
utils::validate_repr_c_required(&input.attrs, "ZeroCopyEq")?;

// Process the input to extract struct information
let (name, z_struct_name, z_struct_meta_name, fields) = utils::process_input(&input)?;
let (name, z_struct_name, _z_struct_meta_name, fields) = utils::process_input(&input)?;

// Handle unit structs (no fields)
let Some(fields) = fields else {
// Unit struct - all unit structs of the same type are equal
return Ok(quote! {
impl<'a> ::core::cmp::PartialEq<#name> for #z_struct_name<'a> {
fn eq(&self, _other: &#name) -> bool {
true // Unit structs are always equal
}
}

impl<'a> ::core::cmp::PartialEq<#z_struct_name<'a>> for #name {
fn eq(&self, _other: &#z_struct_name<'a>) -> bool {
true // Unit structs are always equal
}
}
});
};

let z_struct_meta_name = quote::format_ident!("Z{}Meta", name);

// Process the fields to separate meta fields and struct fields
let (meta_fields, struct_fields) = utils::process_fields(fields);
Expand Down
71 changes: 71 additions & 0 deletions program-libs/zero-copy-derive/src/zero_copy_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,77 @@ pub fn derive_zero_copy_mut_impl(fn_input: TokenStream) -> syn::Result<proc_macr
// Process the input to extract struct information
let (name, z_struct_name, z_struct_meta_name, fields) = utils::process_input(&input)?;

// Handle unit structs (no fields)
let Some(fields) = fields else {
// Unit struct - generate minimal mutable implementations
let z_struct_name_mut = format_ident!("{}Mut", z_struct_name);

let zero_copy_struct_inner_impl_mut =
zero_copy::generate_zero_copy_struct_inner::<true>(name, &z_struct_name_mut)?;

// Generate a simple unit ZStruct type alias for mut
let z_struct_def_mut = quote::quote! {
/// Zero-copy mutable reference type for unit struct #name
pub type #z_struct_name_mut<'a> = &'a mut #name;
};

// Generate minimal deserialize impl for unit struct
let deserialize_impl_mut = quote::quote! {
impl<'a> ::light_zero_copy::traits::ZeroCopyAtMut<'a> for #name {
type ZeroCopyAtMut = #z_struct_name_mut<'a>;
fn zero_copy_at_mut(bytes: &'a mut [u8]) -> ::core::result::Result<(Self::ZeroCopyAtMut, &'a mut [u8]), ::light_zero_copy::errors::ZeroCopyError> {
// For zero-sized types (ZSTs), Box::new does not allocate heap memory;
// it returns a dangling-but-aligned pointer, so leaking it is safe and
// does not cause a memory leak. This pattern avoids returning a mutable
// reference to a static for ZSTs, which would be unsound.
let unit: &'a mut #name = Box::leak(Box::new(#name));
Ok((unit, bytes))
}
}
};

// Generate unit type config
let config_name = quote::format_ident!("{}Config", name);
let config_struct = quote::quote! {
pub type #config_name = ();
};

// Generate ZeroCopyNew impl for unit struct (specialized version)
let init_mut_impl = quote::quote! {
impl<'a> ::light_zero_copy::traits::ZeroCopyNew<'a> for #name {
type ZeroCopyConfig = #config_name;
type Output = #z_struct_name_mut<'a>;

fn byte_len(_config: &Self::ZeroCopyConfig) -> Result<usize, ::light_zero_copy::errors::ZeroCopyError> {
// Unit struct has 0 bytes
Ok(0)
}

fn new_zero_copy(
bytes: &'a mut [u8],
_config: Self::ZeroCopyConfig,
) -> Result<(Self::Output, &'a mut [u8]), ::light_zero_copy::errors::ZeroCopyError> {
// For zero-sized types (ZSTs), Box::new does not allocate heap memory;
// it returns a dangling-but-aligned pointer, so leaking it is safe and
// does not cause a memory leak.
let unit: &'a mut #name = Box::leak(Box::new(#name));
Ok((unit, bytes))
}
}
};

return Ok(quote::quote! {
#config_struct
#z_struct_def_mut

const _: () = {
#zero_copy_struct_inner_impl_mut
#deserialize_impl_mut
#init_mut_impl
};
});
};

// Process the fields to separate meta fields and struct fields
let (meta_fields, struct_fields) = utils::process_fields(fields);

Expand Down
59 changes: 59 additions & 0 deletions program-tests/zero-copy-derive-test/tests/instruction_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1292,3 +1292,62 @@ impl PartialEq<InstructionDataInvokeCpi> for ZInstructionDataInvokeCpi<'_> {
other.eq(self)
}
}

/// Unit struct for testing ZeroCopyNew derive on empty structs
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(C)]
#[derive(ZeroCopy, ZeroCopyMut)]
pub struct UnitStruct;

#[test]
fn test_unit_struct_zero_copy_new() {
use light_zero_copy::traits::ZeroCopyNew;

// Test byte_len returns 0 for unit struct
let byte_len = UnitStruct::byte_len(&()).unwrap();
assert_eq!(byte_len, 0, "Unit struct should have byte_len of 0");

// Test new_zero_copy works with empty buffer
let mut bytes: [u8; 0] = [];
let (result, remaining) = UnitStruct::new_zero_copy(&mut bytes, ()).unwrap();

// Verify remaining bytes is empty (we consumed nothing)
assert_eq!(remaining.len(), 0, "Should have no remaining bytes");

// Verify we got a valid reference to the unit struct
assert_eq!(*result, UnitStruct, "Should get UnitStruct reference");

// Test new_zero_copy also works with non-empty buffer (should leave all bytes)
let mut bytes_with_extra = [1u8, 2, 3, 4];
let (result2, remaining2) = UnitStruct::new_zero_copy(&mut bytes_with_extra, ()).unwrap();

// Verify all bytes remain (unit struct consumes 0 bytes)
assert_eq!(
remaining2.len(),
4,
"Should have all 4 bytes remaining after unit struct"
);
assert_eq!(*result2, UnitStruct, "Should get UnitStruct reference");
}

#[test]
fn test_unit_struct_zero_copy_at() {
// Test ZeroCopyAt for unit struct
let bytes: [u8; 4] = [1, 2, 3, 4];
let (result, remaining) = UnitStruct::zero_copy_at(&bytes).unwrap();

// Unit struct consumes 0 bytes
assert_eq!(remaining.len(), 4, "Should have all bytes remaining");
assert_eq!(*result, UnitStruct, "Should get UnitStruct reference");
}

#[test]
fn test_unit_struct_zero_copy_at_mut() {
// Test ZeroCopyAtMut for unit struct
let mut bytes: [u8; 4] = [1, 2, 3, 4];
let (result, remaining) = UnitStruct::zero_copy_at_mut(&mut bytes).unwrap();

// Unit struct consumes 0 bytes
assert_eq!(remaining.len(), 4, "Should have all bytes remaining");
assert_eq!(*result, UnitStruct, "Should get UnitStruct reference");
}

This file was deleted.

Loading