From 5b9b45e87d913c4ec2e5ca7e813d1cc8a4339415 Mon Sep 17 00:00:00 2001 From: marcfir Date: Tue, 26 Aug 2025 12:46:47 +0000 Subject: [PATCH 1/3] feat(ethercrab-wire-derive): Support #[repr(c,packed)] --- .../benches/derive-struct.rs | 58 ++++++++++++++++++- ethercrab-wire-derive/src/generate_struct.rs | 17 ++++-- ethercrab-wire-derive/src/help.rs | 21 +++++++ ethercrab-wire-derive/src/parse_struct.rs | 5 +- 4 files changed, 94 insertions(+), 7 deletions(-) diff --git a/ethercrab-wire-derive/benches/derive-struct.rs b/ethercrab-wire-derive/benches/derive-struct.rs index b2818f03..82210014 100644 --- a/ethercrab-wire-derive/benches/derive-struct.rs +++ b/ethercrab-wire-derive/benches/derive-struct.rs @@ -1,6 +1,56 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireWrite, EtherCrabWireWriteSized}; +pub fn normal_types_repr_packed(c: &mut Criterion) { + #[derive(ethercrab_wire::EtherCrabWireReadWrite)] + #[wire(bytes = 16)] + #[repr(C, packed)] + struct NormalTypes { + #[wire(bytes = 4)] + foo: u32, + + #[wire(bytes = 2)] + bar: u16, + + #[wire(bytes = 2)] + baz: u16, + + #[wire(bytes = 8)] + huge: u64, + } + + let input_data = [ + 0xbf, 0x7e, 0xc0, 0x07, 0xab, 0xa1, 0xc2, 0x22, 0x45, 0x23, 0xaa, 0x68, 0x47, 0xbf, 0xff, + 0xea, + ]; + + c.bench_function("u* [repr(C,packed)] struct unpack", |b| { + b.iter(|| NormalTypes::unpack_from_slice(black_box(&input_data))) + }); + + let instance = NormalTypes::unpack_from_slice(&input_data).unwrap(); + + c.bench_function("u* [repr(C,packed)] struct pack array", |b| { + b.iter(|| black_box(instance.pack())) + }); + + c.bench_function("u* [repr(C,packed)] struct pack slice unchecked", |b| { + b.iter(|| { + let mut buf = [0u8; 16]; + + instance.pack_to_slice_unchecked(black_box(&mut buf)); + }) + }); + + c.bench_function("u* [repr(C,packed)] struct pack slice checked", |b| { + b.iter(|| { + let mut buf = [0u8; 16]; + + let _ = instance.pack_to_slice(black_box(&mut buf)); + }) + }); +} + pub fn normal_types(c: &mut Criterion) { #[derive(ethercrab_wire::EtherCrabWireReadWrite)] #[wire(bytes = 16)] @@ -157,5 +207,11 @@ pub fn packed(c: &mut Criterion) { }); } -criterion_group!(structs, normal_types, packed_and_normal, packed); +criterion_group!( + structs, + normal_types, + normal_types_repr_packed, + packed_and_normal, + packed +); criterion_main!(structs); diff --git a/ethercrab-wire-derive/src/generate_struct.rs b/ethercrab-wire-derive/src/generate_struct.rs index 73daf53e..9d5ac77d 100644 --- a/ethercrab-wire-derive/src/generate_struct.rs +++ b/ethercrab-wire-derive/src/generate_struct.rs @@ -22,23 +22,31 @@ pub fn generate_struct_write(parsed: &StructMeta, input: &DeriveInput) -> proc_m .ty_name .unwrap_or_else(|| Ident::new("UnknownTypeStopLookingAtMe", Span::call_site())); + let field_access = if parsed.repr_packed { + quote! {{ + unsafe { core::ptr::read_unaligned(&raw const self.#name) } + }} + } else { + quote! {self.#name} + }; + // Small optimisation if ty_name == "u8" || ty_name == "bool" { let mask = (2u16.pow(field.bits.len() as u32) - 1) << bit_start; let mask = proc_macro2::TokenStream::from_str(&format!("{:#010b}", mask)).unwrap(); quote! { - buf[#byte_start] |= ((self.#name as u8) << #bit_start) & #mask; + buf[#byte_start] |= ((#field_access as u8) << #bit_start) & #mask; } } // Single byte fields need merging into the other data else if field.bytes.len() == 1 { let mask = (2u16.pow(field.bits.len() as u32) - 1) << bit_start; let mask = proc_macro2::TokenStream::from_str(&format!("{:#010b}", mask)).unwrap(); - + quote! { let mut field_buf = [0u8; 1]; - let res = <#field_ty as ::ethercrab_wire::EtherCrabWireWrite>::pack_to_slice_unchecked(&self.#name, &mut field_buf)[0]; + let res = <#field_ty as ::ethercrab_wire::EtherCrabWireWrite>::pack_to_slice_unchecked(&#field_access, &mut field_buf)[0]; buf[#byte_start] |= (res << #bit_start) & #mask; } @@ -46,9 +54,8 @@ pub fn generate_struct_write(parsed: &StructMeta, input: &DeriveInput) -> proc_m // Assumption: multi-byte fields are byte-aligned. This should be validated during parse. else { let byte_end = field.bytes.end; - quote! { - <#field_ty as ::ethercrab_wire::EtherCrabWireWrite>::pack_to_slice_unchecked(&self.#name, &mut buf[#byte_start..#byte_end]); + <#field_ty as ::ethercrab_wire::EtherCrabWireWrite>::pack_to_slice_unchecked(&#field_access, &mut buf[#byte_start..#byte_end]); } } }); diff --git a/ethercrab-wire-derive/src/help.rs b/ethercrab-wire-derive/src/help.rs index ce7b4806..0c86cdfb 100644 --- a/ethercrab-wire-derive/src/help.rs +++ b/ethercrab-wire-derive/src/help.rs @@ -113,6 +113,27 @@ pub fn attr_exists(attrs: &[syn::Attribute], search: &str) -> bool { false } +pub fn has_repr_packed(attrs: &[syn::Attribute]) -> bool { + for attr in attrs { + match attr.meta.clone() { + Meta::List(l) if l.path.is_ident("repr") => { + let mut has_packed = false; + let _ = l.parse_nested_meta(|meta| { + if meta.path.is_ident("packed") { + has_packed = true; + } + Ok(()) + }); + if has_packed { + return true; + } + } + _ => (), + } + } + false +} + // pub fn field_is_enum_attr(attrs: &[syn::Attribute]) -> Result { // for attr in my_attributes(attrs) { // let Ok(nested) = attr.parse_args_with(Punctuated::::parse_terminated) diff --git a/ethercrab-wire-derive/src/parse_struct.rs b/ethercrab-wire-derive/src/parse_struct.rs index b99baafa..f8062448 100644 --- a/ethercrab-wire-derive/src/parse_struct.rs +++ b/ethercrab-wire-derive/src/parse_struct.rs @@ -1,4 +1,4 @@ -use crate::help::{all_valid_attrs, attr_exists, bit_width_attr, usize_attr}; +use crate::help::{all_valid_attrs, attr_exists, bit_width_attr, has_repr_packed, usize_attr}; use std::ops::Range; use syn::{DataStruct, DeriveInput, Fields, FieldsNamed, Ident, Type, Visibility}; @@ -6,6 +6,7 @@ use syn::{DataStruct, DeriveInput, Fields, FieldsNamed, Ident, Type, Visibility} pub struct StructMeta { /// Width in bits on the wire. pub width_bits: usize, + pub repr_packed: bool, pub fields: Vec, } @@ -47,6 +48,7 @@ pub fn parse_struct( // --- Struct attributes all_valid_attrs(&attrs, &["bits", "bytes"])?; + let repr_packed = has_repr_packed(&attrs); let width = bit_width_attr(&attrs)?; @@ -182,6 +184,7 @@ pub fn parse_struct( } Ok(StructMeta { + repr_packed, width_bits: width, fields: field_meta, }) From eec0c50668d7f1c134e0784097fa5e6694d8cc6c Mon Sep 17 00:00:00 2001 From: James Waples Date: Sat, 15 Nov 2025 14:40:51 -0800 Subject: [PATCH 2/3] Fix formatting --- ethercrab-wire-derive/src/generate_struct.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ethercrab-wire-derive/src/generate_struct.rs b/ethercrab-wire-derive/src/generate_struct.rs index 9d5ac77d..afa188ec 100644 --- a/ethercrab-wire-derive/src/generate_struct.rs +++ b/ethercrab-wire-derive/src/generate_struct.rs @@ -29,7 +29,7 @@ pub fn generate_struct_write(parsed: &StructMeta, input: &DeriveInput) -> proc_m } else { quote! {self.#name} }; - + // Small optimisation if ty_name == "u8" || ty_name == "bool" { let mask = (2u16.pow(field.bits.len() as u32) - 1) << bit_start; @@ -43,7 +43,7 @@ pub fn generate_struct_write(parsed: &StructMeta, input: &DeriveInput) -> proc_m else if field.bytes.len() == 1 { let mask = (2u16.pow(field.bits.len() as u32) - 1) << bit_start; let mask = proc_macro2::TokenStream::from_str(&format!("{:#010b}", mask)).unwrap(); - + quote! { let mut field_buf = [0u8; 1]; let res = <#field_ty as ::ethercrab_wire::EtherCrabWireWrite>::pack_to_slice_unchecked(&#field_access, &mut field_buf)[0]; From a58180c489a8f6f6f5feaef59dd5a1e1613acc12 Mon Sep 17 00:00:00 2001 From: James Waples Date: Sat, 15 Nov 2025 15:02:34 -0800 Subject: [PATCH 3/3] Changelog entry --- ethercrab-wire-derive/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ethercrab-wire-derive/CHANGELOG.md b/ethercrab-wire-derive/CHANGELOG.md index b1adb169..d1ca7013 100644 --- a/ethercrab-wire-derive/CHANGELOG.md +++ b/ethercrab-wire-derive/CHANGELOG.md @@ -6,6 +6,11 @@ Derives for `ethercrab`. ## [Unreleased] - ReleaseDate +## Added + +- [#343](https://github.com/ethercrab-rs/ethercrab/pull/343) (@marcfir) Added support for + `#[repr(C, packed)]` on structs + ### Changed - **(breaking)** [#230](https://github.com/ethercrab-rs/ethercrab/pull/230) Increase MSRV from 1.77