orio_derive/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::{Literal, Span, TokenStream as TokenStream2};
3use quote::{quote, ToTokens};
4use syn::*;
5
6#[proc_macro_derive(Io, attributes(io))]
7pub fn derive_io(input: TokenStream) -> TokenStream {
8	let DeriveInput { ident, data, .. } = parse_macro_input!(input);
9	derive_io_inner(ident, data).into()
10}
11
12fn derive_io_inner(ident: Ident, data: Data) -> TokenStream2 {
13	let (read, write, size) = match data {
14		Data::Struct(data) => (
15			read_fields(quote! { Self }, &data.fields),
16			write_struct_fields(&data.fields),
17			struct_field_sizes(&data.fields)
18		),
19		Data::Enum(data) => {
20			// TODO: support #[repr(u8)] annotations and stuff
21			let variant_type = match data.variants.len() {
22				0 => return Error::new(ident.span(), "Empty enums are not supported, use () instead.").to_compile_error(),
23				1..=255 => quote! { u8 },
24				256..=65535 => quote! { u16 },
25				_ => quote! { u32 }
26			};
27			// TODO: support enum discriminants
28			let read_variants = data.variants.iter()
29				.enumerate()
30				.map(|(i, variant)| {
31					let i = Literal::usize_unsuffixed(i);
32					let ident = &variant.ident;
33					let read_fields = read_fields(quote! { Self::#ident }, &variant.fields);
34					quote! {
35						#i => #read_fields,
36					}
37				});
38			// writing variant is done first since every enum has it, save the write calls and just make it a single one
39			let match_variants = data.variants.iter()
40				.enumerate()
41				.map(|(i, variant)| {
42					// need to correctly discard fields when getting variant
43					let i = Literal::usize_unsuffixed(i);
44					let ident = &variant.ident;
45					let matcher = match &variant.fields {
46						Fields::Named(_) => quote! { {..} },
47						Fields::Unnamed(_) => quote! { (..) },
48						Fields::Unit => quote! {}
49					};
50					quote! {
51						Self::#ident #matcher => #i
52					}
53				});
54			let write_variants = data.variants.iter()
55				.filter_map(|variant| {
56					let ident = &variant.ident;
57					let matcher = enum_matcher(&variant.fields)?;
58					let writes = enum_field_idents(&variant.fields, "write", |func, ident| quote! {
59						#func(#ident, w)?;
60					});
61					Some(quote! {
62						Self::#ident #matcher => { #(#writes)* }
63					})
64				});
65			let size_variants = data.variants.iter()
66				.filter_map(|variant| {
67					let ident = &variant.ident;
68					let matcher = enum_matcher(&variant.fields)?;
69					let sizes = enum_field_idents(&variant.fields, "size", |func, ident| quote! {
70						#func(#ident)
71					});
72					Some(quote! {
73						Self::#ident #matcher => #(#sizes)+*
74					})
75				});
76			(quote! {
77				// block needed since its inside Ok()
78				{
79					let variant = #variant_type::read(r)?;
80					match variant {
81						#(#read_variants)*
82						_ => return Err(derive_enum_variant_error(variant.into(), stringify!(#ident)))
83					}
84				}
85			}, quote! {
86				let variant: #variant_type = match self {
87					#(#match_variants),*
88				};
89				variant.write(w)?;
90				match self {
91					#(#write_variants,)*
92					_ => {} // for fields that are ignored or have no data
93				}
94			}, quote! {
95				1 + match self {
96					#(#size_variants,)*
97					_ => 0
98				}
99			})
100		},
101		Data::Union(_) => return Error::new(ident.span(), "Unions are not supported").to_compile_error()
102	};
103
104	quote! {
105		#[automatically_derived]
106		impl Io for #ident {
107			fn read(r: &mut Reader) -> ::std::io::Result<Self> {
108				Ok(#read)
109			}
110
111			fn write(&self, w: &mut Writer) -> ::std::io::Result<()> {
112				#write
113				Ok(())
114			}
115
116			fn size(&self) -> usize {
117				#size
118			}
119		}
120	}
121}
122
123enum FieldOptions {
124	None,
125	Ignored,
126	Len(Type)
127}
128
129impl FieldOptions {
130	fn is_some(&self) -> bool {
131		!matches!(self, Self::None)
132	}
133
134	fn is_ignored(&self) -> bool {
135		matches!(self, Self::Ignored)
136	}
137}
138
139fn field_options(field: &Field) -> FieldOptions {
140	let mut options = FieldOptions::None;
141	for attr in field.attrs.iter() {
142		// check for #[io] attribute
143		if !attr.path().is_ident("io") {
144			continue;
145		}
146
147		// find #[io(ignored)]
148		if let Err(e) = attr.parse_nested_meta(|meta| {
149			if options.is_some() {
150				return Err(meta.error("duplicate io attributes used"));
151			}
152
153			if meta.path.is_ident("ignore") {
154				options = FieldOptions::Ignored;
155				return Ok(());
156			}
157
158			if meta.path.is_ident("len") {
159				let content;
160				parenthesized!(content in meta.input);
161				let len = content.parse()?;
162				options = FieldOptions::Len(len);
163				return Ok(());
164			}
165
166			Err(meta.error(format!("unknown io attribute '{}' used", meta.path.to_token_stream())))
167		}) {
168			panic!("Failed to parse attribute for field '{}': {e}", field.ident.as_ref().unwrap());
169		}
170	}
171
172	options
173}
174
175
176fn is_ignored(field: &Field) -> bool {
177	field_options(field).is_ignored()
178}
179
180fn read_fields(base: TokenStream2, fields: &Fields) -> TokenStream2 {
181	match fields {
182		Fields::Named(fields) => {
183			let read_fields = fields.named.iter()
184				.map(|field| {
185					let ident = &field.ident;
186					let read = read_field(&field);
187					quote! {
188						#ident: #read
189					}
190				});
191			quote! {
192				#base {
193					#(#read_fields),*
194				}
195			}
196		},
197		Fields::Unnamed(fields) => {
198			let read_fields = fields.unnamed.iter()
199				.map(|field| read_field(&field));
200			quote! {
201				#base(#(#read_fields),*)
202			}
203		},
204		Fields::Unit => base
205	}
206}
207
208// part of reading a field common to structs and enums
209fn read_field(field: &Field) -> TokenStream2 {
210	let ty = &field.ty;
211	let options = field_options(field);
212	match options {
213		FieldOptions::Ignored => {
214			quote! {
215				<#ty as ::std::default::Default>::default()
216			}
217		},
218		FieldOptions::None => {
219			quote! {
220				<#ty as Io>::read(r)?
221			}
222		},
223		FieldOptions::Len(len) => {
224			quote! {
225				<#ty as LenIo>::read::<#len>(r)?
226			}
227		}
228	}
229}
230
231fn write_struct_fields(fields: &Fields) -> TokenStream2 {
232	match fields {
233		Fields::Named(fields) => {
234			let fields = fields.named.iter()
235				.filter_map(|field| {
236					let options = field_options(field);
237					let ident = &field.ident;
238					Some(match options {
239						FieldOptions::Ignored => return None,
240						FieldOptions::None => quote! {
241							Io::write(&self.#ident, w)?;
242						},
243						FieldOptions::Len(len) => quote! {
244							LenIo::write::<#len>(&self.#ident, w)?;
245						}
246					})
247				});
248			quote! {
249				#(#fields)*
250			}
251		},
252		Fields::Unnamed(fields) => {
253			// this is why this cant be reused for enums
254			// named fields are ok since "&self." can be removed, but this would need #i to
255			// be replaced with a name generated from a-z, which is a bit much.
256			let fields = fields.unnamed.iter()
257				.enumerate()
258				.filter_map(|(i, field)| {
259					let options = field_options(field);
260					let i: Index = i.into();
261					Some(match options {
262						FieldOptions::Ignored => return None,
263						FieldOptions::None => quote! {
264							Io::write(&self.#i, w)?;
265						},
266						FieldOptions::Len(len) => quote! {
267							LenIo::write::<#len>(&self.#i, w)?;
268						}
269					})
270				});
271			quote! {
272				#(#fields)*
273			}
274		},
275		Fields::Unit => quote! {}
276	}
277}
278
279fn struct_field_sizes(fields: &Fields) -> TokenStream2 {
280	match fields {
281		Fields::Named(fields) => {
282			let fields = fields.named.iter()
283				.filter_map(|field| {
284					let options = field_options(field);
285					let ident = &field.ident;
286					Some(match options {
287						FieldOptions::Ignored => return None,
288						FieldOptions::None => quote! {
289							Io::size(&self.#ident)
290						},
291						FieldOptions::Len(len) => quote! {
292							LenIo::size::<#len>(&self.#ident)
293						}
294					})
295				});
296			quote! {
297				#(#fields)+*
298			}
299		},
300		Fields::Unnamed(fields) => {
301			let fields = fields.unnamed.iter()
302				.enumerate()
303				.filter_map(|(i, field)| {
304					let options = field_options(field);
305					let i: Index = i.into();
306					Some(match options {
307						FieldOptions::Ignored => return None,
308						FieldOptions::None => quote! {
309							Io::size(&self.#i)
310						},
311						FieldOptions::Len(len) => quote! {
312							LenIo::size::<#len>(&self.#i)
313						}
314					})
315				});
316			quote! {
317				#(#fields)+*
318			}
319		},
320		Fields::Unit => quote! {}
321	}
322}
323
324fn enum_matcher(fields: &Fields) -> Option<TokenStream2> {
325	Some(match fields {
326		Fields::Named(fields) => {
327			let fields = fields.named.iter()
328				.filter(|field| !is_ignored(field))
329				.filter_map(|field| field.ident.clone());
330			quote! {
331				{ #(#fields),* }
332			}
333		},
334		Fields::Unnamed(fields) => {
335			let fields = fields.unnamed.iter()
336				.enumerate()
337				.filter(|(_, field)| !is_ignored(field))
338				.map(|(i, _)| name_unnamed_field(i));
339			quote! {
340				(#(#fields),*)
341			}
342		},
343		Fields::Unit => return None
344	})
345}
346
347fn enum_field_idents(fields: &Fields, name: &'static str, f: impl Fn(TokenStream2, Ident) -> TokenStream2) -> Vec<TokenStream2> {
348	let name = Ident::new(name, Span::call_site());
349	match fields {
350		Fields::Named(fields) => fields.named.iter()
351			.filter_map(|field| {
352				let ident = field.ident.clone().unwrap();
353				map_trait(&name, field, ident)
354			})
355			.map(|(a, b)| f(a, b))
356			.collect(),
357		Fields::Unnamed(fields) => fields.unnamed.iter()
358			.enumerate()
359			.filter_map(|(i, field)| {
360				let ident = name_unnamed_field(i);
361				map_trait(&name, field, ident)
362			})
363			.map(|(a, b)| f(a, b))
364			.collect(),
365		Fields::Unit => vec![]
366	}
367}
368
369fn map_trait(name: &Ident, field: &Field, ident: Ident) -> Option<(TokenStream2, Ident)> {
370	let options = field_options(field);
371	Some((match options {
372		FieldOptions::Ignored => return None,
373		FieldOptions::None => quote! {
374			Io::#name
375		},
376		FieldOptions::Len(len) => quote! {
377			LenIo::#name::<#len>
378		}
379	}, ident))
380}
381
382// not allowed to match Variant(1, 2, 3) so have to assign them letters
383fn name_unnamed_field(i: usize) -> Ident {
384	let c = (b'a' + (i as u8)) as char;
385	let s = c.to_string();
386	Ident::new(&s, Span::call_site())
387}