diff --git a/program-libs/zero-copy-derive/src/shared/utils.rs b/program-libs/zero-copy-derive/src/shared/utils.rs index b1dee887ce..7646f177ca 100644 --- a/program-libs/zero-copy-derive/src/shared/utils.rs +++ b/program-libs/zero-copy-derive/src/shared/utils.rs @@ -23,6 +23,7 @@ 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), } @@ -30,10 +31,10 @@ pub enum InputType<'a> { 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); @@ -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", )) } }, @@ -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", )) } }, diff --git a/program-libs/zero-copy-derive/src/zero_copy.rs b/program-libs/zero-copy-derive/src/zero_copy.rs index 059c9e8778..bb484b769c 100644 --- a/program-libs/zero-copy-derive/src/zero_copy.rs +++ b/program-libs/zero-copy-derive/src/zero_copy.rs @@ -300,6 +300,37 @@ pub fn derive_zero_copy_impl(input: ProcTokenStream) -> syn::Result { + // 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::(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; diff --git a/program-libs/zero-copy-derive/src/zero_copy_eq.rs b/program-libs/zero-copy-derive/src/zero_copy_eq.rs index bd095e9d6e..53ca4ca31b 100644 --- a/program-libs/zero-copy-derive/src/zero_copy_eq.rs +++ b/program-libs/zero-copy-derive/src/zero_copy_eq.rs @@ -276,7 +276,27 @@ pub fn derive_zero_copy_eq_impl(input: ProcTokenStream) -> syn::Result ::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); diff --git a/program-libs/zero-copy-derive/src/zero_copy_mut.rs b/program-libs/zero-copy-derive/src/zero_copy_mut.rs index ee7e901b90..3767349d60 100644 --- a/program-libs/zero-copy-derive/src/zero_copy_mut.rs +++ b/program-libs/zero-copy-derive/src/zero_copy_mut.rs @@ -23,6 +23,77 @@ pub fn derive_zero_copy_mut_impl(fn_input: TokenStream) -> syn::Result(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 { + // 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); diff --git a/program-tests/zero-copy-derive-test/tests/instruction_data.rs b/program-tests/zero-copy-derive-test/tests/instruction_data.rs index 9518cd9bac..5f0fbfbdac 100644 --- a/program-tests/zero-copy-derive-test/tests/instruction_data.rs +++ b/program-tests/zero-copy-derive-test/tests/instruction_data.rs @@ -1292,3 +1292,62 @@ impl PartialEq 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"); +} diff --git a/program-tests/zero-copy-derive-test/tests/ui/fail/01_empty_struct.stderr b/program-tests/zero-copy-derive-test/tests/ui/fail/01_empty_struct.stderr deleted file mode 100644 index f3d5bf094b..0000000000 --- a/program-tests/zero-copy-derive-test/tests/ui/fail/01_empty_struct.stderr +++ /dev/null @@ -1,15 +0,0 @@ -error: ZeroCopy only supports structs with named fields - --> tests/ui/fail/01_empty_struct.rs:5:10 - | -5 | #[derive(ZeroCopy, ZeroCopyMut)] - | ^^^^^^^^ - | - = note: this error originates in the derive macro `ZeroCopy` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: ZeroCopy only supports structs with named fields - --> tests/ui/fail/01_empty_struct.rs:5:20 - | -5 | #[derive(ZeroCopy, ZeroCopyMut)] - | ^^^^^^^^^^^ - | - = note: this error originates in the derive macro `ZeroCopyMut` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/program-tests/zero-copy-derive-test/tests/ui/fail/01_empty_struct.rs b/program-tests/zero-copy-derive-test/tests/ui/pass/01_empty_struct.rs similarity index 100% rename from program-tests/zero-copy-derive-test/tests/ui/fail/01_empty_struct.rs rename to program-tests/zero-copy-derive-test/tests/ui/pass/01_empty_struct.rs