rust: qemu-api-macros: add automatic TryFrom/TryInto derivation

This is going to be fairly common. Using a custom procedural macro
provides better error messages and automatically finds the right
type.

Note that this is different from the same-named macro in the
derive_more crate.  That one provides conversion from e.g. tuples
to enums with tuple variants, not from integers to enums.

Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
This commit is contained in:
Paolo Bonzini 2024-12-04 08:58:46 +01:00
parent a3b620fff7
commit 809c703a60
2 changed files with 73 additions and 29 deletions

View file

@ -5,8 +5,8 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse_macro_input, parse_quote, punctuated::Punctuated, token::Comma, Data, DeriveInput, Field,
Fields, Ident, Type, Visibility,
parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, Data,
DeriveInput, Field, Fields, Ident, Meta, Path, Token, Type, Variant, Visibility,
};
mod utils;
@ -98,3 +98,73 @@ pub fn derive_offsets(input: TokenStream) -> TokenStream {
TokenStream::from(expanded)
}
#[allow(non_snake_case)]
fn get_repr_uN(input: &DeriveInput, msg: &str) -> Result<Path, MacroError> {
let repr = input.attrs.iter().find(|attr| attr.path().is_ident("repr"));
if let Some(repr) = repr {
let nested = repr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)?;
for meta in nested {
match meta {
Meta::Path(path) if path.is_ident("u8") => return Ok(path),
Meta::Path(path) if path.is_ident("u16") => return Ok(path),
Meta::Path(path) if path.is_ident("u32") => return Ok(path),
Meta::Path(path) if path.is_ident("u64") => return Ok(path),
_ => {}
}
}
}
Err(MacroError::Message(
format!("#[repr(u8/u16/u32/u64) required for {}", msg),
input.ident.span(),
))
}
fn get_variants(input: &DeriveInput) -> Result<&Punctuated<Variant, Comma>, MacroError> {
if let Data::Enum(e) = &input.data {
if let Some(v) = e.variants.iter().find(|v| v.fields != Fields::Unit) {
return Err(MacroError::Message(
"Cannot derive TryInto for enum with non-unit variants.".to_string(),
v.fields.span(),
));
}
Ok(&e.variants)
} else {
Err(MacroError::Message(
"Cannot derive TryInto for union or struct.".to_string(),
input.ident.span(),
))
}
}
#[rustfmt::skip::macros(quote)]
fn derive_tryinto_or_error(input: DeriveInput) -> Result<proc_macro2::TokenStream, MacroError> {
let repr = get_repr_uN(&input, "#[derive(TryInto)]")?;
let name = &input.ident;
let variants = get_variants(&input)?;
let discriminants: Vec<&Ident> = variants.iter().map(|f| &f.ident).collect();
Ok(quote! {
impl core::convert::TryFrom<#repr> for #name {
type Error = #repr;
fn try_from(value: #repr) -> Result<Self, Self::Error> {
#(const #discriminants: #repr = #name::#discriminants as #repr;)*;
match value {
#(#discriminants => Ok(Self::#discriminants),)*
_ => Err(value),
}
}
}
})
}
#[proc_macro_derive(TryInto)]
pub fn derive_tryinto(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let expanded = derive_tryinto_or_error(input).unwrap_or_else(Into::into);
TokenStream::from(expanded)
}