From af5b645a9171b3086d2391aae9d04506627b5483 Mon Sep 17 00:00:00 2001 From: Fletcher Porter Date: Wed, 16 Jul 2025 14:50:54 +0300 Subject: [PATCH] Support generic structs in ethercat-wire-derive By using generics for e.g. measurement values, users can separate their network logic which touch ethercrab and the fieldbus from their control logic which will be concerned with how the value should be gained, scaled, and otherwise manipulated. --- ethercrab-wire-derive/CHANGELOG.md | 1 + ethercrab-wire-derive/Cargo.toml | 1 + ethercrab-wire-derive/README.md | 34 ++++++++++ ethercrab-wire-derive/src/generate_struct.rs | 66 ++++++++++++++++++-- ethercrab-wire-derive/src/lib.rs | 34 ++++++++++ ethercrab-wire-derive/src/parse_struct.rs | 11 +++- 6 files changed, 141 insertions(+), 6 deletions(-) diff --git a/ethercrab-wire-derive/CHANGELOG.md b/ethercrab-wire-derive/CHANGELOG.md index b1adb169..aca68446 100644 --- a/ethercrab-wire-derive/CHANGELOG.md +++ b/ethercrab-wire-derive/CHANGELOG.md @@ -10,6 +10,7 @@ Derives for `ethercrab`. - **(breaking)** [#230](https://github.com/ethercrab-rs/ethercrab/pull/230) Increase MSRV from 1.77 to 1.79. +- Support generic parameters for structs. ## [0.2.0] - 2024-07-28 diff --git a/ethercrab-wire-derive/Cargo.toml b/ethercrab-wire-derive/Cargo.toml index 704b85f9..409b1d5f 100644 --- a/ethercrab-wire-derive/Cargo.toml +++ b/ethercrab-wire-derive/Cargo.toml @@ -27,6 +27,7 @@ syn = { version = "2.0.44", features = ["full"] } trybuild = "1.0.86" ethercrab-wire = { path = "../ethercrab-wire" } syn = { version = "2.0.44", features = ["full", "extra-traits"] } +pretty_assertions = "1.4.1" [[bench]] name = "derive-struct" diff --git a/ethercrab-wire-derive/README.md b/ethercrab-wire-derive/README.md index 7e9b1270..23421ab6 100644 --- a/ethercrab-wire-derive/README.md +++ b/ethercrab-wire-derive/README.md @@ -156,6 +156,40 @@ struct Middle { } ``` +## Generic structs + +Structs can take generic parameters so long as those parameters are `EtherCrabWireRead/Write` as relevant. +It's up to the user to ensure that the generic type can be constructed with the number of bits given. + +```rust +/// Status word for Beckhoff EL31xx devices and others. +#[derive(ethercrab_wire::EtherCrabWireRead)] +#[wire(bytes = 4)] +pub struct AnalogInput +where + Value: ethercrab_wire::EtherCrabWireRead, +{ + #[wire(bits = 1)] + underrange: bool, + #[wire(bits = 1)] + overrange: bool, + #[wire(bits = 2)] + limit1: u8, + #[wire(bits = 2)] + limit2: u8, + #[wire(bits = 1)] + error: bool, + #[wire(pre_skip = 6, bits = 1)] + sync_error: bool, + #[wire(bits = 1)] + tx_pdo_bad: bool, + #[wire(bits = 1)] + tx_pdo_toggle: bool, + #[wire(bits = 16)] + value: Value, // <-- generic field +} +``` + [`ethercrab`]: https://docs.rs/ethercrab [`ethercrab-wire`]: https://docs.rs/ethercrab-wire diff --git a/ethercrab-wire-derive/src/generate_struct.rs b/ethercrab-wire-derive/src/generate_struct.rs index 73daf53e..ddb0a7f2 100644 --- a/ethercrab-wire-derive/src/generate_struct.rs +++ b/ethercrab-wire-derive/src/generate_struct.rs @@ -53,8 +53,10 @@ pub fn generate_struct_write(parsed: &StructMeta, input: &DeriveInput) -> proc_m } }); + let (impl_generics, type_generics, where_clause) = parsed.generics.split_for_impl(); quote! { - impl ::ethercrab_wire::EtherCrabWireWrite for #name { + impl #impl_generics ::ethercrab_wire::EtherCrabWireWrite for #name #type_generics + #where_clause { fn pack_to_slice_unchecked<'buf>(&self, buf: &'buf mut [u8]) -> &'buf [u8] { let buf = match buf.get_mut(0..#size_bytes) { Some(buf) => buf, @@ -75,7 +77,8 @@ pub fn generate_struct_write(parsed: &StructMeta, input: &DeriveInput) -> proc_m } } - impl ::ethercrab_wire::EtherCrabWireWriteSized for #name { + impl #impl_generics ::ethercrab_wire::EtherCrabWireWriteSized for #name #type_generics + #where_clause { fn pack(&self) -> Self::Buffer { let mut buf = [0u8; #size_bytes]; @@ -144,8 +147,10 @@ pub fn generate_struct_read(parsed: &StructMeta, input: &DeriveInput) -> proc_ma } }); + let (impl_generics, type_generics, where_clause) = parsed.generics.split_for_impl(); quote! { - impl ::ethercrab_wire::EtherCrabWireRead for #name { + impl #impl_generics ::ethercrab_wire::EtherCrabWireRead for #name #type_generics + #where_clause { fn unpack_from_slice(buf: &[u8]) -> Result { let buf = buf.get(0..#size_bytes).ok_or(::ethercrab_wire::WireError::ReadBufferTooShort)?; @@ -161,8 +166,10 @@ pub fn generate_sized_impl(parsed: &StructMeta, input: &DeriveInput) -> proc_mac let name = input.ident.clone(); let size_bytes = parsed.width_bits.div_ceil(8); + let (impl_generics, type_generics, where_clause) = parsed.generics.split_for_impl(); quote! { - impl ::ethercrab_wire::EtherCrabWireSized for #name { + impl #impl_generics ::ethercrab_wire::EtherCrabWireSized for #name #type_generics + #where_clause { const PACKED_LEN: usize = #size_bytes; type Buffer = [u8; #size_bytes]; @@ -173,3 +180,54 @@ pub fn generate_sized_impl(parsed: &StructMeta, input: &DeriveInput) -> proc_mac } } } + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use ethercrab_wire::{EtherCrabWireRead, EtherCrabWireReadWrite, EtherCrabWireWrite}; + + #[test] + fn generic_struct() { + #[derive(EtherCrabWireReadWrite, PartialEq, Debug)] + #[wire(bytes = 8)] + struct TestTypeGeneric { + #[wire(bits = 32)] + a: i32, + #[wire(bits = 32)] + b: T, + } + let test_type_generic = TestTypeGeneric:: { + a: -16, + b: u32::MAX, + }; + let mut slice = [0u8; 8]; + test_type_generic.pack_to_slice(&mut slice).unwrap(); + assert_eq!( + Ok(test_type_generic), + TestTypeGeneric::::unpack_from_slice(&slice) + ); + + #[derive(EtherCrabWireReadWrite, PartialEq, Debug)] + #[wire(bytes = 8)] + struct TestWhereClause + where + T: EtherCrabWireReadWrite, + { + #[wire(bits = 32)] + a: i32, + #[wire(bits = 32)] + b: T, + } + let test_where_clause = TestWhereClause:: { + a: -16, + b: u32::MAX, + }; + let mut slice = [0u8; 8]; + test_where_clause.pack_to_slice(&mut slice).unwrap(); + assert_eq!( + Ok(test_where_clause), + TestWhereClause::::unpack_from_slice(&slice) + ); + } +} diff --git a/ethercrab-wire-derive/src/lib.rs b/ethercrab-wire-derive/src/lib.rs index b6cfc2e5..530f46a7 100644 --- a/ethercrab-wire-derive/src/lib.rs +++ b/ethercrab-wire-derive/src/lib.rs @@ -154,6 +154,40 @@ //! } //! ``` //! +//! ## Generic structs +//! +//! Structs can take generic parameters so long as those parameters are `EtherCrabWireRead/Write` as relevant. +//! It's up to the user to ensure that the generic type can be constructed with the number of bits given. +//! +//! ```rust +//! /// Status word for Beckhoff EL31xx devices and others. +//! #[derive(ethercrab_wire::EtherCrabWireRead)] +//! #[wire(bytes = 4)] +//! pub struct AnalogInput +//! where +//! Value: ethercrab_wire::EtherCrabWireRead, +//! { +//! #[wire(bits = 1)] +//! underrange: bool, +//! #[wire(bits = 1)] +//! overrange: bool, +//! #[wire(bits = 2)] +//! limit1: u8, +//! #[wire(bits = 2)] +//! limit2: u8, +//! #[wire(bits = 1)] +//! error: bool, +//! #[wire(pre_skip = 6, bits = 1)] +//! sync_error: bool, +//! #[wire(bits = 1)] +//! tx_pdo_bad: bool, +//! #[wire(bits = 1)] +//! tx_pdo_toggle: bool, +//! #[wire(bits = 16)] +//! value: Value, // <-- generic field +//! } +//! ``` +//! //! [`ethercrab`]: https://docs.rs/ethercrab //! [`ethercrab-wire`]: https://docs.rs/ethercrab-wire diff --git a/ethercrab-wire-derive/src/parse_struct.rs b/ethercrab-wire-derive/src/parse_struct.rs index b99baafa..12d4fb6e 100644 --- a/ethercrab-wire-derive/src/parse_struct.rs +++ b/ethercrab-wire-derive/src/parse_struct.rs @@ -1,6 +1,6 @@ use crate::help::{all_valid_attrs, attr_exists, bit_width_attr, usize_attr}; use std::ops::Range; -use syn::{DataStruct, DeriveInput, Fields, FieldsNamed, Ident, Type, Visibility}; +use syn::{DataStruct, DeriveInput, Fields, FieldsNamed, Generics, Ident, Type, Visibility}; #[derive(Clone)] pub struct StructMeta { @@ -8,6 +8,7 @@ pub struct StructMeta { pub width_bits: usize, pub fields: Vec, + pub generics: Generics, } #[derive(Clone)] @@ -42,7 +43,12 @@ pub struct FieldMeta { pub fn parse_struct( s: DataStruct, - DeriveInput { attrs, ident, .. }: DeriveInput, + DeriveInput { + attrs, + ident, + generics, + .. + }: DeriveInput, ) -> syn::Result { // --- Struct attributes @@ -184,5 +190,6 @@ pub fn parse_struct( Ok(StructMeta { width_bits: width, fields: field_meta, + generics, }) }