ethercrab_wire_derive/lib.rs
1//! Derive attributes for [`ethercrab-wire`].
2//!
3//! # Experimental
4//!
5//! This crate is in its early stages and may contain bugs or publish breaking changes at any time.
6//! It is in use by [`ethercrab`] and is well exercised there, but please use with caution in your
7//! own code.
8//!
9//! These derives support both structs with bit- and (multi)byte-sized fields for structs, as well
10//! as enums with optional catch-all variant.
11//!
12//! # Supported attributes
13//!
14//! ## Structs
15//!
16//! - `#[wire(bits = N)]` OR `#[wire(bytes = N)]`
17//!
18//! The size of this struct when packed on the wire. These attributes may not be present at the
19//! same time.
20//!
21//! ## Struct fields
22//!
23//! - `#[wire(bits = N)]` OR `#[wire(bytes = N)]`
24//!
25//! How many bytes this field consumes on the wire. These attributes may not be present at the
26//! same time.
27//!
28//! All builtin `u*` and `i*` types do not need this attribute by default; their size is computed
29//! automatically if it's omitted. If a different bit width is desired, the `#[wire]` attribute is
30//! required.
31//!
32//! - `#[wire(pre_skip = N)]` OR `#[wire(pre_skip_bytes = N)]`
33//!
34//! Skip one or more whole bytes before or after this field in the packed representation.
35//!
36//! - `#[wire(post_skip = N)]` OR `#[wire(post_skip_bytes = N)]`
37//!
38//! How many bits or bytes to skip in the raw data **after** this field.
39//!
40//! These attributes are only applicable to fields that are less than 8 bits wide.
41//!
42//! ## Enums
43//!
44//! Enums must have a `#[repr()]` attribute, as well as implement the `Copy` trait.
45//!
46//! ## Enum discriminants
47//!
48//! Enum discriminants may not contain fields.
49//!
50//! - `#[wire(alternatives = [])]`
51//!
52//! A discriminant with this attribute will be parsed successfully if either its direct value or
53//! any of the listed alternatives are found in the input data.
54//!
55//! The discriminant value is used when packing _to_ the wire.
56//!
57//! - `#[wire(catch_all)]`
58//!
59//! Apply this once to a discriminant with a single unnamed field the same type as the enum's
60//! `#[repr()]` to catch any unrecognised values.
61//!
62//! # Examples
63//!
64//! ## A struct with both bit fields and multi-byte fields.
65//!
66//! ```rust
67//! #[derive(ethercrab_wire::EtherCrabWireReadWrite)]
68//! #[wire(bytes = 4)]
69//! struct Mixed {
70//! #[wire(bits = 1)]
71//! one_bit: u8,
72//! #[wire(bits = 2)]
73//! two_bits: u8,
74//!
75//! // Fields that are 8 bits or larger must be byte aligned, so we skip the two remaining bits
76//! // of the previous byte with `post_skip`.
77//! #[wire(bits = 3, post_skip = 2)]
78//! three_bits: u8,
79//!
80//! /// Whole `u8`
81//! // #[wire(bytes = 1)] Attribute not required as it is the default width of a `u8`
82//! one_byte: u8,
83//!
84//! /// Whole `u16`
85//! // #[wire(bytes = 2)] Attribute not required as it is the default width of a `u16`
86//! one_word: u16,
87//! }
88//! ```
89//!
90//! ## Generic structs
91//!
92//! Structs can hold generic fields as long as the generic parameters implement one or more
93//! `EtherCrabWire*` traits as appropriate.
94//!
95//! <section class="warning">
96//!
97//! It is the end user's responsibility to ensure the generic is of the correct size compared to the
98//! `#[wire(bits|bytes)]` attribute on the containing struct.
99//!
100//! </section>
101//!
102//! ```rust
103//! // Example value abstraction. NOTE: This byte size MUST be the same as the `T` definition in
104//! // `AnalogInput`
105//! #[derive(ethercrab_wire::EtherCrabWireRead)]
106//! #[wire(bytes = 2)]
107//! struct Value {
108//! #[wire(bytes = 2)]
109//! raw: u16
110//! };
111//!
112//! /// Example: Status word for Beckhoff EL31xx devices and others.
113//! #[derive(ethercrab_wire::EtherCrabWireRead)]
114//! #[wire(bytes = 4)]
115//! pub struct AnalogInput<T: ethercrab_wire::EtherCrabWireRead> {
116//! #[wire(bits = 1)]
117//! underrange: bool,
118//! #[wire(bits = 1)]
119//! overrange: bool,
120//! #[wire(bits = 2)]
121//! limit1: u8,
122//! #[wire(bits = 2)]
123//! limit2: u8,
124//! #[wire(bits = 1)]
125//! error: bool,
126//! #[wire(pre_skip = 6, bits = 1)]
127//! sync_error: bool,
128//! #[wire(bits = 1)]
129//! tx_pdo_bad: bool,
130//! #[wire(bits = 1)]
131//! tx_pdo_toggle: bool,
132//! #[wire(bits = 16)]
133//! value: T, // <-- generic field
134//! }
135//!
136//! type IntegerAnalog = AnalogInput<Value>;
137//! ```
138//!
139//! ## Enum with catch all discriminant and alternatives
140//!
141//! ```rust
142//! # use ethercrab_wire::EtherCrabWireRead;
143//! # #[derive(PartialEq, Debug)]
144//! #[derive(Copy, Clone, ethercrab_wire::EtherCrabWireReadWrite)]
145//! #[repr(u8)]
146//! enum OneByte {
147//! Foo = 0x01,
148//! #[wire(alternatives = [ 3, 4, 5, 6 ])]
149//! Bar = 0x02,
150//! Baz = 0x07,
151//! Quux = 0xab,
152//! #[wire(catch_all)]
153//! Unknown(u8),
154//! }
155//!
156//! // Normal discriminant
157//! assert_eq!(OneByte::unpack_from_slice(&[0x07]), Ok(OneByte::Baz));
158//!
159//! // Alternative value for `Bar`
160//! assert_eq!(OneByte::unpack_from_slice(&[0x05]), Ok(OneByte::Bar));
161//!
162//! // Catch all
163//! assert_eq!(OneByte::unpack_from_slice(&[0xaa]), Ok(OneByte::Unknown(0xaa)));
164//! ```
165//!
166//! # Struct field alignment
167//!
168//! Struct fields of 1 byte or more MUST be byte-aligned. For example, the following struct will be
169//! rejected due to `bar` being 5 bits "early":
170//!
171//! ```rust,compile_fail
172//! #[derive(ethercrab_wire::EtherCrabWireReadWrite)]
173//! #[wire(bytes = 2)]
174//! struct Broken {
175//! #[wire(bits = 3)]
176//! foo: u8,
177//!
178//! // There are 5 bits here unaccounted for
179//!
180//! #[wire(bytes = 1)]
181//! bar: u8,
182//! }
183//! ```
184//!
185//! This can easily be fixed by using the `pre_skip` or `post_skip` attributes to realign the next
186//! field to 8 bits (or skip whole bytes of the input data):
187//!
188//! ```rust
189//! #[derive(ethercrab_wire::EtherCrabWireReadWrite)]
190//! #[wire(bytes = 2)]
191//! struct Fixed {
192//! #[wire(bits = 3, post_skip = 5)]
193//! foo: u8,
194//! #[wire(bytes = 1)]
195//! bar: u8,
196//! }
197//! ```
198//!
199//! A field in the middle of a byte can be written as such, maintaining 8 bit alignment:
200//!
201//! ```rust
202//! #[derive(ethercrab_wire::EtherCrabWireReadWrite)]
203//! #[wire(bytes = 1)]
204//! struct Middle {
205//! #[wire(pre_skip = 2, bits = 3, post_skip = 3)]
206//! foo: u8,
207//! }
208//! ```
209//!
210//! [`ethercrab`]: https://docs.rs/ethercrab
211//! [`ethercrab-wire`]: https://docs.rs/ethercrab-wire
212
213#![deny(missing_docs)]
214#![deny(missing_copy_implementations)]
215#![deny(trivial_casts)]
216#![deny(trivial_numeric_casts)]
217#![deny(unused_import_braces)]
218#![deny(unused_qualifications)]
219#![deny(rustdoc::broken_intra_doc_links)]
220#![deny(rustdoc::private_intra_doc_links)]
221
222mod generate_enum;
223mod generate_struct;
224mod help;
225mod parse_enum;
226mod parse_struct;
227
228use generate_enum::{generate_enum_read, generate_enum_write};
229use generate_struct::{generate_sized_impl, generate_struct_read, generate_struct_write};
230use parse_enum::parse_enum;
231use parse_struct::parse_struct;
232use proc_macro::TokenStream;
233use syn::{parse_macro_input, Data, DeriveInput};
234
235/// Items that can be written to and read from the wire.
236///
237/// Please see the [crate documentation](index.html) for examples and supported
238/// attributes.
239///
240/// For write-only items, see [`EtherCrabWireWrite`]. For read-only items, see
241/// [`EtherCrabWireRead`].
242#[proc_macro_derive(EtherCrabWireReadWrite, attributes(wire))]
243pub fn ether_crab_wire(input: TokenStream) -> TokenStream {
244 let input = parse_macro_input!(input as DeriveInput);
245
246 let res = match input.clone().data {
247 Data::Enum(e) => parse_enum(e, input.clone()).map(|parsed| {
248 let mut tokens = generate_enum_write(parsed.clone(), &input, false);
249
250 tokens.extend(generate_enum_read(parsed, &input));
251
252 tokens
253 }),
254 Data::Struct(s) => parse_struct(s, input.clone()).map(|parsed| {
255 let mut tokens = generate_struct_write(&parsed, &input);
256
257 tokens.extend(generate_struct_read(&parsed, &input));
258
259 tokens.extend(generate_sized_impl(&parsed, &input));
260
261 tokens
262 }),
263 Data::Union(_) => Err(syn::Error::new(
264 input.ident.span(),
265 "Unions are not supported",
266 )),
267 };
268
269 let res = match res {
270 Ok(res) => res,
271 Err(e) => return e.to_compile_error().into(),
272 };
273
274 TokenStream::from(res)
275}
276
277/// Items that can only be read from the wire.
278///
279/// Please see the [crate documentation](index.html) for examples and supported attributes.
280///
281/// For read/write items, see [`EtherCrabWireReadWrite`]. For write-only items, see
282/// [`EtherCrabWireWrite`].
283#[proc_macro_derive(EtherCrabWireRead, attributes(wire))]
284pub fn ether_crab_wire_read(input: TokenStream) -> TokenStream {
285 let input = parse_macro_input!(input as DeriveInput);
286
287 let res = match input.clone().data {
288 Data::Enum(e) => {
289 parse_enum(e, input.clone()).map(|parsed| generate_enum_read(parsed, &input))
290 }
291 Data::Struct(s) => parse_struct(s, input.clone()).map(|parsed| {
292 let mut tokens = generate_struct_read(&parsed, &input);
293
294 tokens.extend(generate_sized_impl(&parsed, &input));
295
296 tokens
297 }),
298 Data::Union(_) => Err(syn::Error::new(
299 input.ident.span(),
300 "Unions are not supported",
301 )),
302 };
303
304 let res = match res {
305 Ok(res) => res,
306 Err(e) => return e.to_compile_error().into(),
307 };
308
309 TokenStream::from(res)
310}
311
312/// Items that can only be written to the wire.
313///
314/// Please see the [crate documentation](index.html) for examples and supported attributes.
315///
316/// For read/write items, see [`EtherCrabWireReadWrite`]. For read-only items, see
317/// [`EtherCrabWireRead`].
318#[proc_macro_derive(EtherCrabWireWrite, attributes(wire))]
319pub fn ether_crab_wire_write(input: TokenStream) -> TokenStream {
320 let input = parse_macro_input!(input as DeriveInput);
321
322 let res = match input.clone().data {
323 Data::Enum(e) => {
324 parse_enum(e, input.clone()).map(|parsed| generate_enum_write(parsed, &input, true))
325 }
326 Data::Struct(s) => parse_struct(s, input.clone()).map(|parsed| {
327 let mut tokens = generate_struct_write(&parsed, &input);
328
329 tokens.extend(generate_sized_impl(&parsed, &input));
330
331 tokens
332 }),
333 Data::Union(_) => Err(syn::Error::new(
334 input.ident.span(),
335 "Unions are not supported",
336 )),
337 };
338
339 let res = match res {
340 Ok(res) => res,
341 Err(e) => return e.to_compile_error().into(),
342 };
343
344 TokenStream::from(res)
345}
346
347#[cfg(test)]
348mod tests {
349 #[test]
350 fn trybuild_cases() {
351 let t = trybuild::TestCases::new();
352
353 t.compile_fail("ui/*.rs");
354 }
355}