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//! - `#[wire(pre_skip = N)]` OR `#[wire(pre_skip_bytes = N)]`
29//!
30//! Skip one or more whole bytes before or after this field in the packed representation.
31//!
32//! - `#[wire(post_skip = N)]` OR `#[wire(post_skip_bytes = N)]`
33//!
34//! How many bits or bytes to skip in the raw data **after** this field.
35//!
36//! These attributes are only applicable to fields that are less than 8 bits wide.
37//!
38//! ## Enums
39//!
40//! Enums must have a `#[repr()]` attribute, as well as implement the `Copy` trait.
41//!
42//! ## Enum discriminants
43//!
44//! Enum discriminants may not contain fields.
45//!
46//! - `#[wire(alternatives = [])]`
47//!
48//! A discriminant with this attribute will be parsed successfully if either its direct value or
49//! any of the listed alternatives are found in the input data.
50//!
51//! The discriminant value is used when packing _to_ the wire.
52//!
53//! - `#[wire(catch_all)]`
54//!
55//! Apply this once to a discriminant with a single unnamed field the same type as the enum's
56//! `#[repr()]` to catch any unrecognised values.
57//!
58//! # Examples
59//!
60//! ## A struct with both bit fields and multi-byte fields.
61//!
62//! ```rust
63//! #[derive(ethercrab_wire::EtherCrabWireReadWrite)]
64//! #[wire(bytes = 4)]
65//! struct Mixed {
66//! #[wire(bits = 1)]
67//! one_bit: u8,
68//! #[wire(bits = 2)]
69//! two_bits: u8,
70//!
71//! // Fields that are 8 bits or larger must be byte aligned, so we skip the two remaining bits
72//! // of the previous byte with `post_skip`.
73//! #[wire(bits = 3, post_skip = 2)]
74//! three_bits: u8,
75//!
76//! /// Whole `u8`
77//! #[wire(bytes = 1)]
78//! one_byte: u8,
79//!
80//! /// Whole `u16`
81//! #[wire(bytes = 2)]
82//! one_word: u16,
83//! }
84//! ```
85//!
86//! ## Enum with catch all discriminant and alternatives
87//!
88//! ```rust
89//! # use ethercrab_wire::EtherCrabWireRead;
90//! # #[derive(PartialEq, Debug)]
91//! #[derive(Copy, Clone, ethercrab_wire::EtherCrabWireReadWrite)]
92//! #[repr(u8)]
93//! enum OneByte {
94//! Foo = 0x01,
95//! #[wire(alternatives = [ 3, 4, 5, 6 ])]
96//! Bar = 0x02,
97//! Baz = 0x07,
98//! Quux = 0xab,
99//! #[wire(catch_all)]
100//! Unknown(u8),
101//! }
102//!
103//! // Normal discriminant
104//! assert_eq!(OneByte::unpack_from_slice(&[0x07]), Ok(OneByte::Baz));
105//!
106//! // Alternative value for `Bar`
107//! assert_eq!(OneByte::unpack_from_slice(&[0x05]), Ok(OneByte::Bar));
108//!
109//! // Catch all
110//! assert_eq!(OneByte::unpack_from_slice(&[0xaa]), Ok(OneByte::Unknown(0xaa)));
111//! ```
112//!
113//! # Struct field alignment
114//!
115//! Struct fields of 1 byte or more MUST be byte-aligned. For example, the following struct will be
116//! rejected due to `bar` being 5 bits "early":
117//!
118//! ```rust,compile_fail
119//! #[derive(ethercrab_wire::EtherCrabWireReadWrite)]
120//! #[wire(bytes = 2)]
121//! struct Broken {
122//! #[wire(bits = 3)]
123//! foo: u8,
124//!
125//! // There are 5 bits here unaccounted for
126//!
127//! #[wire(bytes = 1)]
128//! bar: u8,
129//! }
130//! ```
131//!
132//! This can easily be fixed by using the `pre_skip` or `post_skip` attributes to realign the next
133//! field to 8 bits (or skip whole bytes of the input data):
134//!
135//! ```rust
136//! #[derive(ethercrab_wire::EtherCrabWireReadWrite)]
137//! #[wire(bytes = 2)]
138//! struct Fixed {
139//! #[wire(bits = 3, post_skip = 5)]
140//! foo: u8,
141//! #[wire(bytes = 1)]
142//! bar: u8,
143//! }
144//! ```
145//!
146//! A field in the middle of a byte can be written as such, maintaining 8 bit alignment:
147//!
148//! ```rust
149//! #[derive(ethercrab_wire::EtherCrabWireReadWrite)]
150//! #[wire(bytes = 1)]
151//! struct Middle {
152//! #[wire(pre_skip = 2, bits = 3, post_skip = 3)]
153//! foo: u8,
154//! }
155//! ```
156//!
157//! [`ethercrab`]: https://docs.rs/ethercrab
158//! [`ethercrab-wire`]: https://docs.rs/ethercrab-wire
159
160#![deny(missing_docs)]
161#![deny(missing_copy_implementations)]
162#![deny(trivial_casts)]
163#![deny(trivial_numeric_casts)]
164#![deny(unused_import_braces)]
165#![deny(unused_qualifications)]
166#![deny(rustdoc::broken_intra_doc_links)]
167#![deny(rustdoc::private_intra_doc_links)]
168
169mod generate_enum;
170mod generate_struct;
171mod help;
172mod parse_enum;
173mod parse_struct;
174
175use generate_enum::{generate_enum_read, generate_enum_write};
176use generate_struct::{generate_sized_impl, generate_struct_read, generate_struct_write};
177use parse_enum::parse_enum;
178use parse_struct::parse_struct;
179use proc_macro::TokenStream;
180use syn::{parse_macro_input, Data, DeriveInput};
181
182/// Items that can be written to and read from the wire.
183///
184/// Please see the [crate documentation](index.html) for examples and supported
185/// attributes.
186///
187/// For write-only items, see [`EtherCrabWireWrite`]. For read-only items, see
188/// [`EtherCrabWireRead`].
189#[proc_macro_derive(EtherCrabWireReadWrite, attributes(wire))]
190pub fn ether_crab_wire(input: TokenStream) -> TokenStream {
191 let input = parse_macro_input!(input as DeriveInput);
192
193 let res = match input.clone().data {
194 Data::Enum(e) => parse_enum(e, input.clone()).map(|parsed| {
195 let mut tokens = generate_enum_write(parsed.clone(), &input, false);
196
197 tokens.extend(generate_enum_read(parsed, &input));
198
199 tokens
200 }),
201 Data::Struct(s) => parse_struct(s, input.clone()).map(|parsed| {
202 let mut tokens = generate_struct_write(&parsed, &input);
203
204 tokens.extend(generate_struct_read(&parsed, &input));
205
206 tokens.extend(generate_sized_impl(&parsed, &input));
207
208 tokens
209 }),
210 Data::Union(_) => Err(syn::Error::new(
211 input.ident.span(),
212 "Unions are not supported",
213 )),
214 };
215
216 let res = match res {
217 Ok(res) => res,
218 Err(e) => return e.to_compile_error().into(),
219 };
220
221 TokenStream::from(res)
222}
223
224/// Items that can only be read from the wire.
225///
226/// Please see the [crate documentation](index.html) for examples and supported attributes.
227///
228/// For read/write items, see [`EtherCrabWireReadWrite`]. For write-only items, see
229/// [`EtherCrabWireWrite`].
230#[proc_macro_derive(EtherCrabWireRead, attributes(wire))]
231pub fn ether_crab_wire_read(input: TokenStream) -> TokenStream {
232 let input = parse_macro_input!(input as DeriveInput);
233
234 let res = match input.clone().data {
235 Data::Enum(e) => {
236 parse_enum(e, input.clone()).map(|parsed| generate_enum_read(parsed, &input))
237 }
238 Data::Struct(s) => parse_struct(s, input.clone()).map(|parsed| {
239 let mut tokens = generate_struct_read(&parsed, &input);
240
241 tokens.extend(generate_sized_impl(&parsed, &input));
242
243 tokens
244 }),
245 Data::Union(_) => Err(syn::Error::new(
246 input.ident.span(),
247 "Unions are not supported",
248 )),
249 };
250
251 let res = match res {
252 Ok(res) => res,
253 Err(e) => return e.to_compile_error().into(),
254 };
255
256 TokenStream::from(res)
257}
258
259/// Items that can only be written to the wire.
260///
261/// Please see the [crate documentation](index.html) for examples and supported attributes.
262///
263/// For read/write items, see [`EtherCrabWireReadWrite`]. For read-only items, see
264/// [`EtherCrabWireRead`].
265#[proc_macro_derive(EtherCrabWireWrite, attributes(wire))]
266pub fn ether_crab_wire_write(input: TokenStream) -> TokenStream {
267 let input = parse_macro_input!(input as DeriveInput);
268
269 let res = match input.clone().data {
270 Data::Enum(e) => {
271 parse_enum(e, input.clone()).map(|parsed| generate_enum_write(parsed, &input, true))
272 }
273 Data::Struct(s) => parse_struct(s, input.clone()).map(|parsed| {
274 let mut tokens = generate_struct_write(&parsed, &input);
275
276 tokens.extend(generate_sized_impl(&parsed, &input));
277
278 tokens
279 }),
280 Data::Union(_) => Err(syn::Error::new(
281 input.ident.span(),
282 "Unions are not supported",
283 )),
284 };
285
286 let res = match res {
287 Ok(res) => res,
288 Err(e) => return e.to_compile_error().into(),
289 };
290
291 TokenStream::from(res)
292}
293
294#[cfg(test)]
295mod tests {
296 #[test]
297 fn trybuild_cases() {
298 let t = trybuild::TestCases::new();
299
300 t.compile_fail("ui/*.rs");
301 }
302}