diff options
Diffstat (limited to 'rust/qemu-macros')
-rw-r--r-- | rust/qemu-macros/Cargo.toml | 25 | ||||
-rw-r--r-- | rust/qemu-macros/meson.build | 23 | ||||
-rw-r--r-- | rust/qemu-macros/src/bits.rs | 213 | ||||
-rw-r--r-- | rust/qemu-macros/src/lib.rs | 502 | ||||
-rw-r--r-- | rust/qemu-macros/src/migration_state.rs | 298 | ||||
-rw-r--r-- | rust/qemu-macros/src/tests.rs | 456 |
6 files changed, 1517 insertions, 0 deletions
diff --git a/rust/qemu-macros/Cargo.toml b/rust/qemu-macros/Cargo.toml new file mode 100644 index 0000000..c25b6c0 --- /dev/null +++ b/rust/qemu-macros/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "qemu_macros" +version = "0.1.0" +authors = ["Manos Pitsidianakis <manos.pitsidianakis@linaro.org>"] +description = "Rust bindings for QEMU - Utility macros" +resolver = "2" +publish = false + +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lib] +proc-macro = true + +[dependencies] +attrs = "0.2.9" +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["extra-traits"] } + +[lints] +workspace = true diff --git a/rust/qemu-macros/meson.build b/rust/qemu-macros/meson.build new file mode 100644 index 0000000..0f27e0d --- /dev/null +++ b/rust/qemu-macros/meson.build @@ -0,0 +1,23 @@ +_qemu_macros_rs = rust.proc_macro( + 'qemu_macros', + files('src/lib.rs'), + override_options: ['rust_std=2021', 'build.rust_std=2021'], + rust_args: [ + '--cfg', 'use_fallback', + '--cfg', 'feature="syn-error"', + '--cfg', 'feature="proc-macro"', + ], + dependencies: [ + attrs_rs_native, + proc_macro2_rs_native, + quote_rs_native, + syn_rs_native, + ], +) + +qemu_macros = declare_dependency( + link_with: _qemu_macros_rs, +) + +rust.test('rust-qemu-macros-tests', _qemu_macros_rs, + suite: ['unit', 'rust']) diff --git a/rust/qemu-macros/src/bits.rs b/rust/qemu-macros/src/bits.rs new file mode 100644 index 0000000..a80a3b9 --- /dev/null +++ b/rust/qemu-macros/src/bits.rs @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: MIT or Apache-2.0 or GPL-2.0-or-later + +// shadowing is useful together with "if let" +#![allow(clippy::shadow_unrelated)] + +use proc_macro2::{ + Delimiter, Group, Ident, Punct, Spacing, Span, TokenStream, TokenTree, TokenTree as TT, +}; +use syn::Error; + +pub struct BitsConstInternal { + typ: TokenTree, +} + +fn paren(ts: TokenStream) -> TokenTree { + TT::Group(Group::new(Delimiter::Parenthesis, ts)) +} + +fn ident(s: &'static str) -> TokenTree { + TT::Ident(Ident::new(s, Span::call_site())) +} + +fn punct(ch: char) -> TokenTree { + TT::Punct(Punct::new(ch, Spacing::Alone)) +} + +/// Implements a recursive-descent parser that translates Boolean expressions on +/// bitmasks to invocations of `const` functions defined by the `bits!` macro. +impl BitsConstInternal { + // primary ::= '(' or ')' + // | ident + // | '!' ident + fn parse_primary( + &self, + tok: TokenTree, + it: &mut dyn Iterator<Item = TokenTree>, + out: &mut TokenStream, + ) -> Result<Option<TokenTree>, Error> { + let next = match tok { + TT::Group(ref g) => { + if g.delimiter() != Delimiter::Parenthesis && g.delimiter() != Delimiter::None { + return Err(Error::new(g.span(), "expected parenthesis")); + } + let mut stream = g.stream().into_iter(); + let Some(first_tok) = stream.next() else { + return Err(Error::new(g.span(), "expected operand, found ')'")); + }; + let mut output = TokenStream::new(); + // start from the lowest precedence + let next = self.parse_or(first_tok, &mut stream, &mut output)?; + if let Some(tok) = next { + return Err(Error::new(tok.span(), format!("unexpected token {tok}"))); + } + out.extend(Some(paren(output))); + it.next() + } + TT::Ident(_) => { + let mut output = TokenStream::new(); + output.extend([ + self.typ.clone(), + TT::Punct(Punct::new(':', Spacing::Joint)), + TT::Punct(Punct::new(':', Spacing::Joint)), + tok, + ]); + out.extend(Some(paren(output))); + it.next() + } + TT::Punct(ref p) => { + if p.as_char() != '!' { + return Err(Error::new(p.span(), "expected operand")); + } + let Some(rhs_tok) = it.next() else { + return Err(Error::new(p.span(), "expected operand at end of input")); + }; + let next = self.parse_primary(rhs_tok, it, out)?; + out.extend([punct('.'), ident("invert"), paren(TokenStream::new())]); + next + } + _ => { + return Err(Error::new(tok.span(), "unexpected literal")); + } + }; + Ok(next) + } + + fn parse_binop< + F: Fn( + &Self, + TokenTree, + &mut dyn Iterator<Item = TokenTree>, + &mut TokenStream, + ) -> Result<Option<TokenTree>, Error>, + >( + &self, + tok: TokenTree, + it: &mut dyn Iterator<Item = TokenTree>, + out: &mut TokenStream, + ch: char, + f: F, + method: &'static str, + ) -> Result<Option<TokenTree>, Error> { + let mut next = f(self, tok, it, out)?; + while next.is_some() { + let op = next.as_ref().unwrap(); + let TT::Punct(ref p) = op else { break }; + if p.as_char() != ch { + break; + } + + let Some(rhs_tok) = it.next() else { + return Err(Error::new(p.span(), "expected operand at end of input")); + }; + let mut rhs = TokenStream::new(); + next = f(self, rhs_tok, it, &mut rhs)?; + out.extend([punct('.'), ident(method), paren(rhs)]); + } + Ok(next) + } + + // sub ::= primary ('-' primary)* + pub fn parse_sub( + &self, + tok: TokenTree, + it: &mut dyn Iterator<Item = TokenTree>, + out: &mut TokenStream, + ) -> Result<Option<TokenTree>, Error> { + self.parse_binop(tok, it, out, '-', Self::parse_primary, "difference") + } + + // and ::= sub ('&' sub)* + fn parse_and( + &self, + tok: TokenTree, + it: &mut dyn Iterator<Item = TokenTree>, + out: &mut TokenStream, + ) -> Result<Option<TokenTree>, Error> { + self.parse_binop(tok, it, out, '&', Self::parse_sub, "intersection") + } + + // xor ::= and ('&' and)* + fn parse_xor( + &self, + tok: TokenTree, + it: &mut dyn Iterator<Item = TokenTree>, + out: &mut TokenStream, + ) -> Result<Option<TokenTree>, Error> { + self.parse_binop(tok, it, out, '^', Self::parse_and, "symmetric_difference") + } + + // or ::= xor ('|' xor)* + pub fn parse_or( + &self, + tok: TokenTree, + it: &mut dyn Iterator<Item = TokenTree>, + out: &mut TokenStream, + ) -> Result<Option<TokenTree>, Error> { + self.parse_binop(tok, it, out, '|', Self::parse_xor, "union") + } + + pub fn parse( + it: &mut dyn Iterator<Item = TokenTree>, + ) -> Result<proc_macro2::TokenStream, Error> { + let mut pos = Span::call_site(); + let mut typ = proc_macro2::TokenStream::new(); + + // Gobble everything up to an `@` sign, which is followed by a + // parenthesized expression; that is, all token trees except the + // last two form the type. + let next = loop { + let tok = it.next(); + if let Some(ref t) = tok { + pos = t.span(); + } + match tok { + None => break None, + Some(TT::Punct(ref p)) if p.as_char() == '@' => { + let tok = it.next(); + if let Some(ref t) = tok { + pos = t.span(); + } + break tok; + } + Some(x) => typ.extend(Some(x)), + } + }; + + let Some(tok) = next else { + return Err(Error::new( + pos, + "expected expression, do not call this macro directly", + )); + }; + let TT::Group(ref _group) = tok else { + return Err(Error::new( + tok.span(), + "expected parenthesis, do not call this macro directly", + )); + }; + let mut out = TokenStream::new(); + let state = Self { + typ: TT::Group(Group::new(Delimiter::None, typ)), + }; + + let next = state.parse_primary(tok, it, &mut out)?; + + // A parenthesized expression is a single production of the grammar, + // so the input must have reached the last token. + if let Some(tok) = next { + return Err(Error::new(tok.span(), format!("unexpected token {tok}"))); + } + Ok(out) + } +} diff --git a/rust/qemu-macros/src/lib.rs b/rust/qemu-macros/src/lib.rs new file mode 100644 index 0000000..50239f2 --- /dev/null +++ b/rust/qemu-macros/src/lib.rs @@ -0,0 +1,502 @@ +// Copyright 2024, Linaro Limited +// Author(s): Manos Pitsidianakis <manos.pitsidianakis@linaro.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +use proc_macro::TokenStream; +use quote::{quote, quote_spanned}; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, parse_quote, + punctuated::Punctuated, + spanned::Spanned, + token::Comma, + Attribute, Data, DeriveInput, Error, Field, Fields, FieldsUnnamed, Ident, Meta, Path, Token, + Variant, +}; + +mod bits; +use bits::BitsConstInternal; + +mod migration_state; +use migration_state::MigrationStateDerive; + +#[cfg(test)] +mod tests; + +fn get_fields<'a>( + input: &'a DeriveInput, + msg: &str, +) -> Result<&'a Punctuated<Field, Comma>, Error> { + let Data::Struct(ref s) = &input.data else { + return Err(Error::new( + input.ident.span(), + format!("Struct required for {msg}"), + )); + }; + let Fields::Named(ref fs) = &s.fields else { + return Err(Error::new( + input.ident.span(), + format!("Named fields required for {msg}"), + )); + }; + Ok(&fs.named) +} + +fn get_unnamed_field<'a>(input: &'a DeriveInput, msg: &str) -> Result<&'a Field, Error> { + let Data::Struct(ref s) = &input.data else { + return Err(Error::new( + input.ident.span(), + format!("Struct required for {msg}"), + )); + }; + let Fields::Unnamed(FieldsUnnamed { ref unnamed, .. }) = &s.fields else { + return Err(Error::new( + s.fields.span(), + format!("Tuple struct required for {msg}"), + )); + }; + if unnamed.len() != 1 { + return Err(Error::new( + s.fields.span(), + format!("A single field is required for {msg}"), + )); + } + Ok(&unnamed[0]) +} + +fn is_c_repr(input: &DeriveInput, msg: &str) -> Result<(), Error> { + let expected = parse_quote! { #[repr(C)] }; + + if input.attrs.iter().any(|attr| attr == &expected) { + Ok(()) + } else { + Err(Error::new( + input.ident.span(), + format!("#[repr(C)] required for {msg}"), + )) + } +} + +fn is_transparent_repr(input: &DeriveInput, msg: &str) -> Result<(), Error> { + let expected = parse_quote! { #[repr(transparent)] }; + + if input.attrs.iter().any(|attr| attr == &expected) { + Ok(()) + } else { + Err(Error::new( + input.ident.span(), + format!("#[repr(transparent)] required for {msg}"), + )) + } +} + +fn derive_object_or_error(input: DeriveInput) -> Result<proc_macro2::TokenStream, Error> { + is_c_repr(&input, "#[derive(Object)]")?; + + let name = &input.ident; + let parent = &get_fields(&input, "#[derive(Object)]")? + .get(0) + .ok_or_else(|| { + Error::new( + input.ident.span(), + "#[derive(Object)] requires a parent field", + ) + })? + .ident; + + Ok(quote! { + ::common::assert_field_type!(#name, #parent, + ::qom::ParentField<<#name as ::qom::ObjectImpl>::ParentType>); + + ::util::module_init! { + MODULE_INIT_QOM => unsafe { + ::qom::type_register_static(&<#name as ::qom::ObjectImpl>::TYPE_INFO); + } + } + }) +} + +#[proc_macro_derive(Object)] +pub fn derive_object(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + derive_object_or_error(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +fn derive_opaque_or_error(input: DeriveInput) -> Result<proc_macro2::TokenStream, Error> { + is_transparent_repr(&input, "#[derive(Wrapper)]")?; + + let name = &input.ident; + let field = &get_unnamed_field(&input, "#[derive(Wrapper)]")?; + let typ = &field.ty; + + Ok(quote! { + unsafe impl ::common::opaque::Wrapper for #name { + type Wrapped = <#typ as ::common::opaque::Wrapper>::Wrapped; + } + impl #name { + pub unsafe fn from_raw<'a>(ptr: *mut <Self as ::common::opaque::Wrapper>::Wrapped) -> &'a Self { + let ptr = ::std::ptr::NonNull::new(ptr).unwrap().cast::<Self>(); + unsafe { ptr.as_ref() } + } + + pub const fn as_mut_ptr(&self) -> *mut <Self as ::common::opaque::Wrapper>::Wrapped { + self.0.as_mut_ptr() + } + + pub const fn as_ptr(&self) -> *const <Self as ::common::opaque::Wrapper>::Wrapped { + self.0.as_ptr() + } + + pub const fn as_void_ptr(&self) -> *mut ::core::ffi::c_void { + self.0.as_void_ptr() + } + + pub const fn raw_get(slot: *mut Self) -> *mut <Self as ::common::opaque::Wrapper>::Wrapped { + slot.cast() + } + } + }) +} + +#[derive(Debug)] +enum DevicePropertyName { + CStr(syn::LitCStr), + Str(syn::LitStr), +} + +impl Parse for DevicePropertyName { + fn parse(input: ParseStream<'_>) -> syn::Result<Self> { + let lo = input.lookahead1(); + if lo.peek(syn::LitStr) { + Ok(Self::Str(input.parse()?)) + } else if lo.peek(syn::LitCStr) { + Ok(Self::CStr(input.parse()?)) + } else { + Err(lo.error()) + } + } +} + +#[derive(Default, Debug)] +struct DeviceProperty { + rename: Option<DevicePropertyName>, + bitnr: Option<syn::Expr>, + defval: Option<syn::Expr>, +} + +impl DeviceProperty { + fn parse_from(&mut self, a: &Attribute) -> syn::Result<()> { + use attrs::{set, with, Attrs}; + let mut parser = Attrs::new(); + parser.once("rename", with::eq(set::parse(&mut self.rename))); + parser.once("bit", with::eq(set::parse(&mut self.bitnr))); + parser.once("default", with::eq(set::parse(&mut self.defval))); + a.parse_args_with(&mut parser) + } + + fn parse(a: &Attribute) -> syn::Result<Self> { + let mut retval = Self::default(); + retval.parse_from(a)?; + Ok(retval) + } +} + +#[proc_macro_derive(Device, attributes(property))] +pub fn derive_device(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + derive_device_or_error(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +fn derive_device_or_error(input: DeriveInput) -> Result<proc_macro2::TokenStream, Error> { + is_c_repr(&input, "#[derive(Device)]")?; + let properties: Vec<(syn::Field, DeviceProperty)> = get_fields(&input, "#[derive(Device)]")? + .iter() + .flat_map(|f| { + f.attrs + .iter() + .filter(|a| a.path().is_ident("property")) + .map(|a| Ok((f.clone(), DeviceProperty::parse(a)?))) + }) + .collect::<Result<Vec<_>, Error>>()?; + let name = &input.ident; + let mut properties_expanded = vec![]; + + for (field, prop) in properties { + let DeviceProperty { + rename, + bitnr, + defval, + } = prop; + let field_name = field.ident.unwrap(); + macro_rules! str_to_c_str { + ($value:expr, $span:expr) => {{ + let (value, span) = ($value, $span); + let cstr = std::ffi::CString::new(value.as_str()).map_err(|err| { + Error::new( + span, + format!( + "Property name `{value}` cannot be represented as a C string: {err}" + ), + ) + })?; + let cstr_lit = syn::LitCStr::new(&cstr, span); + Ok(quote! { #cstr_lit }) + }}; + } + + let prop_name = rename.map_or_else( + || str_to_c_str!(field_name.to_string(), field_name.span()), + |prop_rename| -> Result<proc_macro2::TokenStream, Error> { + match prop_rename { + DevicePropertyName::CStr(cstr_lit) => Ok(quote! { #cstr_lit }), + DevicePropertyName::Str(str_lit) => { + str_to_c_str!(str_lit.value(), str_lit.span()) + } + } + }, + )?; + let field_ty = field.ty.clone(); + let qdev_prop = if bitnr.is_none() { + quote! { <#field_ty as ::hwcore::QDevProp>::BASE_INFO } + } else { + quote! { <#field_ty as ::hwcore::QDevProp>::BIT_INFO } + }; + let bitnr = bitnr.unwrap_or(syn::Expr::Verbatim(quote! { 0 })); + let set_default = defval.is_some(); + let defval = defval.unwrap_or(syn::Expr::Verbatim(quote! { 0 })); + properties_expanded.push(quote! { + ::hwcore::bindings::Property { + name: ::std::ffi::CStr::as_ptr(#prop_name), + info: #qdev_prop, + offset: ::core::mem::offset_of!(#name, #field_name) as isize, + bitnr: #bitnr, + set_default: #set_default, + defval: ::hwcore::bindings::Property__bindgen_ty_1 { u: #defval as u64 }, + ..::common::Zeroable::ZERO + } + }); + } + + Ok(quote_spanned! {input.span() => + unsafe impl ::hwcore::DevicePropertiesImpl for #name { + const PROPERTIES: &'static [::hwcore::bindings::Property] = &[ + #(#properties_expanded),* + ]; + } + }) +} + +#[proc_macro_derive(Wrapper)] +pub fn derive_opaque(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + derive_opaque_or_error(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[allow(non_snake_case)] +fn get_repr_uN(input: &DeriveInput, msg: &str) -> Result<Path, Error> { + 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(Error::new( + input.ident.span(), + format!("#[repr(u8/u16/u32/u64) required for {msg}"), + )) +} + +fn get_variants(input: &DeriveInput) -> Result<&Punctuated<Variant, Comma>, Error> { + let Data::Enum(ref e) = &input.data else { + return Err(Error::new( + input.ident.span(), + "Cannot derive TryInto for union or struct.", + )); + }; + if let Some(v) = e.variants.iter().find(|v| v.fields != Fields::Unit) { + return Err(Error::new( + v.fields.span(), + "Cannot derive TryInto for enum with non-unit variants.", + )); + } + Ok(&e.variants) +} + +#[rustfmt::skip::macros(quote)] +fn derive_tryinto_body( + name: &Ident, + variants: &Punctuated<Variant, Comma>, + repr: &Path, +) -> Result<proc_macro2::TokenStream, Error> { + let discriminants: Vec<&Ident> = variants.iter().map(|f| &f.ident).collect(); + + Ok(quote! { + #(const #discriminants: #repr = #name::#discriminants as #repr;)* + match value { + #(#discriminants => core::result::Result::Ok(#name::#discriminants),)* + _ => core::result::Result::Err(value), + } + }) +} + +#[rustfmt::skip::macros(quote)] +fn derive_tryinto_or_error(input: DeriveInput) -> Result<proc_macro2::TokenStream, Error> { + let repr = get_repr_uN(&input, "#[derive(TryInto)]")?; + let name = &input.ident; + let body = derive_tryinto_body(name, get_variants(&input)?, &repr)?; + let errmsg = format!("invalid value for {name}"); + + Ok(quote! { + impl #name { + #[allow(dead_code)] + pub const fn into_bits(self) -> #repr { + self as #repr + } + + #[allow(dead_code)] + pub const fn from_bits(value: #repr) -> Self { + match ({ + #body + }) { + Ok(x) => x, + Err(_) => panic!(#errmsg), + } + } + } + impl core::convert::TryFrom<#repr> for #name { + type Error = #repr; + + #[allow(ambiguous_associated_items)] + fn try_from(value: #repr) -> Result<Self, #repr> { + #body + } + } + }) +} + +#[proc_macro_derive(TryInto)] +pub fn derive_tryinto(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + derive_tryinto_or_error(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[proc_macro] +pub fn bits_const_internal(ts: TokenStream) -> TokenStream { + let ts = proc_macro2::TokenStream::from(ts); + let mut it = ts.into_iter(); + + let out = BitsConstInternal::parse(&mut it).unwrap_or_else(syn::Error::into_compile_error); + + // https://github.com/rust-lang/rust-clippy/issues/15852 + quote! { + { + #[allow(clippy::double_parens)] + #out + } + } + .into() +} + +/// Derive macro for generating migration state structures and trait +/// implementations. +/// +/// This macro generates a migration state struct and implements the +/// `ToMigrationState` trait for the annotated struct, enabling state +/// serialization and restoration. Note that defining a `VMStateDescription` +/// for the migration state struct is left to the user. +/// +/// # Container attributes +/// +/// The following attributes can be applied to the struct: +/// +/// - `#[migration_state(rename = CustomName)]` - Customizes the name of the +/// generated migration struct. By default, the generated struct is named +/// `{OriginalName}Migration`. +/// +/// # Field attributes +/// +/// The following attributes can be applied to individual fields: +/// +/// - `#[migration_state(omit)]` - Excludes the field from the migration state +/// entirely. +/// +/// - `#[migration_state(into(Type))]` - Converts the field using `.into()` +/// during both serialization and restoration. +/// +/// - `#[migration_state(try_into(Type))]` - Converts the field using +/// `.try_into()` during both serialization and restoration. Returns +/// `InvalidError` on conversion failure. +/// +/// - `#[migration_state(clone)]` - Clones the field value. +/// +/// Fields without any attributes use `ToMigrationState` recursively; note that +/// this is a simple copy for types that implement `Copy`. +/// +/// # Attribute compatibility +/// +/// - `omit` cannot be used with any other attributes +/// - only one of `into(Type)`, `try_into(Type)` can be used, but they can be +/// coupled with `clone`. +/// +/// # Examples +/// +/// Basic usage: +/// ```ignore +/// #[derive(ToMigrationState)] +/// struct MyStruct { +/// field1: u32, +/// field2: Timer, +/// } +/// ``` +/// +/// With attributes: +/// ```ignore +/// #[derive(ToMigrationState)] +/// #[migration_state(rename = CustomMigration)] +/// struct MyStruct { +/// #[migration_state(omit)] +/// runtime_field: u32, +/// +/// #[migration_state(clone)] +/// shared_data: String, +/// +/// #[migration_state(into(Cow<'static, str>), clone)] +/// converted_field: String, +/// +/// #[migration_state(try_into(i8))] +/// fallible_field: u32, +/// +/// // Default: use ToMigrationState trait recursively +/// nested_field: NestedStruct, +/// +/// // Primitive types have a default implementation of ToMigrationState +/// simple_field: u32, +/// } +/// ``` +#[proc_macro_derive(ToMigrationState, attributes(migration_state))] +pub fn derive_to_migration_state(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + MigrationStateDerive::expand(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} diff --git a/rust/qemu-macros/src/migration_state.rs b/rust/qemu-macros/src/migration_state.rs new file mode 100644 index 0000000..5edf0ef --- /dev/null +++ b/rust/qemu-macros/src/migration_state.rs @@ -0,0 +1,298 @@ +use std::borrow::Cow; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use syn::{spanned::Spanned, DeriveInput, Error, Field, Ident, Result, Type}; + +use crate::get_fields; + +#[derive(Debug, Default)] +enum ConversionMode { + #[default] + None, + Omit, + Into(Type), + TryInto(Type), + ToMigrationState, +} + +impl ConversionMode { + fn target_type(&self, original_type: &Type) -> TokenStream { + match self { + ConversionMode::Into(ty) | ConversionMode::TryInto(ty) => ty.to_token_stream(), + ConversionMode::ToMigrationState => { + quote! { <#original_type as ToMigrationState>::Migrated } + } + _ => original_type.to_token_stream(), + } + } +} + +#[derive(Debug, Default)] +struct ContainerAttrs { + rename: Option<Ident>, +} + +impl ContainerAttrs { + fn parse_from(&mut self, attrs: &[syn::Attribute]) -> Result<()> { + use attrs::{set, with, Attrs}; + Attrs::new() + .once("rename", with::eq(set::parse(&mut self.rename))) + .parse_attrs("migration_state", attrs)?; + Ok(()) + } + + fn parse(attrs: &[syn::Attribute]) -> Result<Self> { + let mut container_attrs = Self::default(); + container_attrs.parse_from(attrs)?; + Ok(container_attrs) + } +} + +#[derive(Debug, Default)] +struct FieldAttrs { + conversion: ConversionMode, + clone: bool, +} + +impl FieldAttrs { + fn parse_from(&mut self, attrs: &[syn::Attribute]) -> Result<()> { + let mut omit_flag = false; + let mut into_type: Option<Type> = None; + let mut try_into_type: Option<Type> = None; + + use attrs::{set, with, Attrs}; + Attrs::new() + .once("omit", set::flag(&mut omit_flag)) + .once("into", with::paren(set::parse(&mut into_type))) + .once("try_into", with::paren(set::parse(&mut try_into_type))) + .once("clone", set::flag(&mut self.clone)) + .parse_attrs("migration_state", attrs)?; + + self.conversion = match (omit_flag, into_type, try_into_type, self.clone) { + // Valid combinations of attributes first... + (true, None, None, false) => ConversionMode::Omit, + (false, Some(ty), None, _) => ConversionMode::Into(ty), + (false, None, Some(ty), _) => ConversionMode::TryInto(ty), + (false, None, None, true) => ConversionMode::None, // clone without conversion + (false, None, None, false) => ConversionMode::ToMigrationState, // default behavior + + // ... then the error cases + (true, _, _, _) => { + return Err(Error::new( + attrs[0].span(), + "ToMigrationState: omit cannot be used with other attributes", + )); + } + (_, Some(_), Some(_), _) => { + return Err(Error::new( + attrs[0].span(), + "ToMigrationState: into and try_into attributes cannot be used together", + )); + } + }; + + Ok(()) + } + + fn parse(attrs: &[syn::Attribute]) -> Result<Self> { + let mut field_attrs = Self::default(); + field_attrs.parse_from(attrs)?; + Ok(field_attrs) + } +} + +#[derive(Debug)] +struct MigrationStateField { + name: Ident, + original_type: Type, + attrs: FieldAttrs, +} + +impl MigrationStateField { + fn maybe_clone(&self, mut value: TokenStream) -> TokenStream { + if self.attrs.clone { + value = quote! { #value.clone() }; + } + value + } + + fn generate_migration_state_field(&self) -> TokenStream { + let name = &self.name; + let field_type = self.attrs.conversion.target_type(&self.original_type); + + quote! { + pub #name: #field_type, + } + } + + fn generate_snapshot_field(&self) -> TokenStream { + let name = &self.name; + let value = self.maybe_clone(quote! { self.#name }); + + match &self.attrs.conversion { + ConversionMode::Omit => { + unreachable!("Omitted fields are filtered out during processing") + } + ConversionMode::None => quote! { + target.#name = #value; + }, + ConversionMode::Into(_) => quote! { + target.#name = #value.into(); + }, + ConversionMode::TryInto(_) => quote! { + target.#name = #value.try_into().map_err(|_| migration::InvalidError)?; + }, + ConversionMode::ToMigrationState => quote! { + self.#name.snapshot_migration_state(&mut target.#name)?; + }, + } + } + + fn generate_restore_field(&self) -> TokenStream { + let name = &self.name; + + match &self.attrs.conversion { + ConversionMode::Omit => { + unreachable!("Omitted fields are filtered out during processing") + } + ConversionMode::None => quote! { + self.#name = #name; + }, + ConversionMode::Into(_) => quote! { + self.#name = #name.into(); + }, + ConversionMode::TryInto(_) => quote! { + self.#name = #name.try_into().map_err(|_| migration::InvalidError)?; + }, + ConversionMode::ToMigrationState => quote! { + self.#name.restore_migrated_state_mut(#name, _version_id)?; + }, + } + } +} + +#[derive(Debug)] +pub struct MigrationStateDerive { + input: DeriveInput, + fields: Vec<MigrationStateField>, + container_attrs: ContainerAttrs, +} + +impl MigrationStateDerive { + fn parse(input: DeriveInput) -> Result<Self> { + let container_attrs = ContainerAttrs::parse(&input.attrs)?; + let fields = get_fields(&input, "ToMigrationState")?; + let fields = Self::process_fields(fields)?; + + Ok(Self { + input, + fields, + container_attrs, + }) + } + + fn process_fields( + fields: &syn::punctuated::Punctuated<Field, syn::token::Comma>, + ) -> Result<Vec<MigrationStateField>> { + let processed = fields + .iter() + .map(|field| { + let attrs = FieldAttrs::parse(&field.attrs)?; + Ok((field, attrs)) + }) + .collect::<Result<Vec<_>>>()? + .into_iter() + .filter(|(_, attrs)| !matches!(attrs.conversion, ConversionMode::Omit)) + .map(|(field, attrs)| MigrationStateField { + name: field.ident.as_ref().unwrap().clone(), + original_type: field.ty.clone(), + attrs, + }) + .collect(); + + Ok(processed) + } + + fn migration_state_name(&self) -> Cow<'_, Ident> { + match &self.container_attrs.rename { + Some(rename) => Cow::Borrowed(rename), + None => Cow::Owned(format_ident!("{}Migration", &self.input.ident)), + } + } + + fn generate_migration_state_struct(&self) -> TokenStream { + let name = self.migration_state_name(); + let fields = self + .fields + .iter() + .map(MigrationStateField::generate_migration_state_field); + + quote! { + #[derive(Default)] + pub struct #name { + #(#fields)* + } + } + } + + fn generate_snapshot_migration_state(&self) -> TokenStream { + let fields = self + .fields + .iter() + .map(MigrationStateField::generate_snapshot_field); + + quote! { + fn snapshot_migration_state(&self, target: &mut Self::Migrated) -> Result<(), migration::InvalidError> { + #(#fields)* + Ok(()) + } + } + } + + fn generate_restore_migrated_state(&self) -> TokenStream { + let names: Vec<_> = self.fields.iter().map(|f| &f.name).collect(); + let fields = self + .fields + .iter() + .map(MigrationStateField::generate_restore_field); + + // version_id could be used or not depending on conversion attributes + quote! { + #[allow(clippy::used_underscore_binding)] + fn restore_migrated_state_mut(&mut self, source: Self::Migrated, _version_id: u8) -> Result<(), migration::InvalidError> { + let Self::Migrated { #(#names),* } = source; + #(#fields)* + Ok(()) + } + } + } + + fn generate(&self) -> TokenStream { + let struct_name = &self.input.ident; + let generics = &self.input.generics; + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let name = self.migration_state_name(); + let migration_state_struct = self.generate_migration_state_struct(); + let snapshot_impl = self.generate_snapshot_migration_state(); + let restore_impl = self.generate_restore_migrated_state(); + + quote! { + #migration_state_struct + + impl #impl_generics ToMigrationState for #struct_name #ty_generics #where_clause { + type Migrated = #name; + + #snapshot_impl + + #restore_impl + } + } + } + + pub fn expand(input: DeriveInput) -> Result<TokenStream> { + let tokens = Self::parse(input)?.generate(); + Ok(tokens) + } +} diff --git a/rust/qemu-macros/src/tests.rs b/rust/qemu-macros/src/tests.rs new file mode 100644 index 0000000..6569141 --- /dev/null +++ b/rust/qemu-macros/src/tests.rs @@ -0,0 +1,456 @@ +// Copyright 2025, Linaro Limited +// Author(s): Manos Pitsidianakis <manos.pitsidianakis@linaro.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +use quote::quote; + +use super::*; + +macro_rules! derive_compile_fail { + ($derive_fn:path, $input:expr, $($error_msg:expr),+ $(,)?) => {{ + let input: proc_macro2::TokenStream = $input; + let error_msg = &[$( quote! { ::core::compile_error! { $error_msg } } ),*]; + let derive_fn: fn(input: syn::DeriveInput) -> Result<proc_macro2::TokenStream, syn::Error> = + $derive_fn; + + let input: syn::DeriveInput = syn::parse2(input).unwrap(); + let result = derive_fn(input); + let err = result.unwrap_err().into_compile_error(); + assert_eq!( + err.to_string(), + quote! { #(#error_msg)* }.to_string() + ); + }}; +} + +macro_rules! derive_compile { + ($derive_fn:path, $input:expr, $($expected:tt)*) => {{ + let input: proc_macro2::TokenStream = $input; + let expected: proc_macro2::TokenStream = $($expected)*; + let derive_fn: fn(input: syn::DeriveInput) -> Result<proc_macro2::TokenStream, syn::Error> = + $derive_fn; + + let input: syn::DeriveInput = syn::parse2(input).unwrap(); + let result = derive_fn(input).unwrap(); + assert_eq!(result.to_string(), expected.to_string()); + }}; +} + +#[test] +fn test_derive_device() { + // Check that repr(C) is used + derive_compile_fail!( + derive_device_or_error, + quote! { + #[derive(Device)] + struct Foo { + _unused: [u8; 0], + } + }, + "#[repr(C)] required for #[derive(Device)]" + ); + // Check that invalid/misspelled attributes raise an error + derive_compile_fail!( + derive_device_or_error, + quote! { + #[repr(C)] + #[derive(Device)] + struct DummyState { + #[property(defalt = true)] + migrate_clock: bool, + } + }, + "Expected one of `bit`, `default` or `rename`" + ); + // Check that repeated attributes are not allowed: + derive_compile_fail!( + derive_device_or_error, + quote! { + #[repr(C)] + #[derive(Device)] + struct DummyState { + #[property(rename = "migrate-clk", rename = "migrate-clk", default = true)] + migrate_clock: bool, + } + }, + "Duplicate argument", + "Already used here", + ); + derive_compile_fail!( + derive_device_or_error, + quote! { + #[repr(C)] + #[derive(Device)] + struct DummyState { + #[property(default = true, default = true)] + migrate_clock: bool, + } + }, + "Duplicate argument", + "Already used here", + ); + derive_compile_fail!( + derive_device_or_error, + quote! { + #[repr(C)] + #[derive(Device)] + struct DummyState { + #[property(bit = 0, bit = 1)] + flags: u32, + } + }, + "Duplicate argument", + "Already used here", + ); + // Check that the field name is preserved when `rename` isn't used: + derive_compile!( + derive_device_or_error, + quote! { + #[repr(C)] + #[derive(Device)] + pub struct DummyState { + parent: ParentField<DeviceState>, + #[property(default = true)] + migrate_clock: bool, + } + }, + quote! { + unsafe impl ::hwcore::DevicePropertiesImpl for DummyState { + const PROPERTIES: &'static [::hwcore::bindings::Property] = &[ + ::hwcore::bindings::Property { + name: ::std::ffi::CStr::as_ptr(c"migrate_clock"), + info: <bool as ::hwcore::QDevProp>::BASE_INFO, + offset: ::core::mem::offset_of!(DummyState, migrate_clock) as isize, + bitnr: 0, + set_default: true, + defval: ::hwcore::bindings::Property__bindgen_ty_1 { u: true as u64 }, + ..::common::Zeroable::ZERO + } + ]; + } + } + ); + // Check that `rename` value is used for the property name when used: + derive_compile!( + derive_device_or_error, + quote! { + #[repr(C)] + #[derive(Device)] + pub struct DummyState { + parent: ParentField<DeviceState>, + #[property(rename = "migrate-clk", default = true)] + migrate_clock: bool, + } + }, + quote! { + unsafe impl ::hwcore::DevicePropertiesImpl for DummyState { + const PROPERTIES: &'static [::hwcore::bindings::Property] = &[ + ::hwcore::bindings::Property { + name: ::std::ffi::CStr::as_ptr(c"migrate-clk"), + info: <bool as ::hwcore::QDevProp>::BASE_INFO, + offset: ::core::mem::offset_of!(DummyState, migrate_clock) as isize, + bitnr: 0, + set_default: true, + defval: ::hwcore::bindings::Property__bindgen_ty_1 { u: true as u64 }, + ..::common::Zeroable::ZERO + } + ]; + } + } + ); + // Check that `bit` value is used for the bit property without default + // value (note: though C macro (e.g., DEFINE_PROP_BIT) always requires + // default value, Rust side allows to default this field to "0"): + derive_compile!( + derive_device_or_error, + quote! { + #[repr(C)] + #[derive(Device)] + pub struct DummyState { + parent: ParentField<DeviceState>, + #[property(bit = 3)] + flags: u32, + } + }, + quote! { + unsafe impl ::hwcore::DevicePropertiesImpl for DummyState { + const PROPERTIES: &'static [::hwcore::bindings::Property] = &[ + ::hwcore::bindings::Property { + name: ::std::ffi::CStr::as_ptr(c"flags"), + info: <u32 as ::hwcore::QDevProp>::BIT_INFO, + offset: ::core::mem::offset_of!(DummyState, flags) as isize, + bitnr: 3, + set_default: false, + defval: ::hwcore::bindings::Property__bindgen_ty_1 { u: 0 as u64 }, + ..::common::Zeroable::ZERO + } + ]; + } + } + ); + // Check that `bit` value is used for the bit property when used: + derive_compile!( + derive_device_or_error, + quote! { + #[repr(C)] + #[derive(Device)] + pub struct DummyState { + parent: ParentField<DeviceState>, + #[property(bit = 3, default = true)] + flags: u32, + } + }, + quote! { + unsafe impl ::hwcore::DevicePropertiesImpl for DummyState { + const PROPERTIES: &'static [::hwcore::bindings::Property] = &[ + ::hwcore::bindings::Property { + name: ::std::ffi::CStr::as_ptr(c"flags"), + info: <u32 as ::hwcore::QDevProp>::BIT_INFO, + offset: ::core::mem::offset_of!(DummyState, flags) as isize, + bitnr: 3, + set_default: true, + defval: ::hwcore::bindings::Property__bindgen_ty_1 { u: true as u64 }, + ..::common::Zeroable::ZERO + } + ]; + } + } + ); + // Check that `bit` value is used for the bit property with rename when used: + derive_compile!( + derive_device_or_error, + quote! { + #[repr(C)] + #[derive(Device)] + pub struct DummyState { + parent: ParentField<DeviceState>, + #[property(rename = "msi", bit = 3, default = false)] + flags: u64, + } + }, + quote! { + unsafe impl ::hwcore::DevicePropertiesImpl for DummyState { + const PROPERTIES: &'static [::hwcore::bindings::Property] = &[ + ::hwcore::bindings::Property { + name: ::std::ffi::CStr::as_ptr(c"msi"), + info: <u64 as ::hwcore::QDevProp>::BIT_INFO, + offset: ::core::mem::offset_of!(DummyState, flags) as isize, + bitnr: 3, + set_default: true, + defval: ::hwcore::bindings::Property__bindgen_ty_1 { u: false as u64 }, + ..::common::Zeroable::ZERO + } + ]; + } + } + ); +} + +#[test] +fn test_derive_object() { + derive_compile_fail!( + derive_object_or_error, + quote! { + #[derive(Object)] + struct Foo { + _unused: [u8; 0], + } + }, + "#[repr(C)] required for #[derive(Object)]" + ); + derive_compile!( + derive_object_or_error, + quote! { + #[derive(Object)] + #[repr(C)] + struct Foo { + _unused: [u8; 0], + } + }, + quote! { + ::common::assert_field_type!( + Foo, + _unused, + ::qom::ParentField<<Foo as ::qom::ObjectImpl>::ParentType> + ); + ::util::module_init! { + MODULE_INIT_QOM => unsafe { + ::qom::type_register_static(&<Foo as ::qom::ObjectImpl>::TYPE_INFO); + } + } + } + ); +} + +#[test] +fn test_derive_tryinto() { + derive_compile_fail!( + derive_tryinto_or_error, + quote! { + #[derive(TryInto)] + struct Foo { + _unused: [u8; 0], + } + }, + "#[repr(u8/u16/u32/u64) required for #[derive(TryInto)]" + ); + derive_compile!( + derive_tryinto_or_error, + quote! { + #[derive(TryInto)] + #[repr(u8)] + enum Foo { + First = 0, + Second, + } + }, + quote! { + impl Foo { + #[allow(dead_code)] + pub const fn into_bits(self) -> u8 { + self as u8 + } + + #[allow(dead_code)] + pub const fn from_bits(value: u8) -> Self { + match ({ + const First: u8 = Foo::First as u8; + const Second: u8 = Foo::Second as u8; + match value { + First => core::result::Result::Ok(Foo::First), + Second => core::result::Result::Ok(Foo::Second), + _ => core::result::Result::Err(value), + } + }) { + Ok(x) => x, + Err(_) => panic!("invalid value for Foo"), + } + } + } + + impl core::convert::TryFrom<u8> for Foo { + type Error = u8; + + #[allow(ambiguous_associated_items)] + fn try_from(value: u8) -> Result<Self, u8> { + const First: u8 = Foo::First as u8; + const Second: u8 = Foo::Second as u8; + match value { + First => core::result::Result::Ok(Foo::First), + Second => core::result::Result::Ok(Foo::Second), + _ => core::result::Result::Err(value), + } + } + } + } + ); +} + +#[test] +fn test_derive_to_migration_state() { + derive_compile_fail!( + MigrationStateDerive::expand, + quote! { + struct MyStruct { + #[migration_state(omit, clone)] + bad: u32, + } + }, + "ToMigrationState: omit cannot be used with other attributes" + ); + derive_compile_fail!( + MigrationStateDerive::expand, + quote! { + struct MyStruct { + #[migration_state(into)] + bad: u32, + } + }, + "unexpected end of input, expected parentheses" + ); + derive_compile_fail!( + MigrationStateDerive::expand, + quote! { + struct MyStruct { + #[migration_state(into(String), try_into(String))] + bad: &'static str, + } + }, + "ToMigrationState: into and try_into attributes cannot be used together" + ); + derive_compile!( + MigrationStateDerive::expand, + quote! { + #[migration_state(rename = CustomMigration)] + struct MyStruct { + #[migration_state(omit)] + runtime_field: u32, + + #[migration_state(clone)] + shared_data: String, + + #[migration_state(into(Cow<'static, str>), clone)] + converted_field: String, + + #[migration_state(try_into(i8))] + fallible_field: u32, + + nested_field: NestedStruct, + simple_field: u32, + } + }, + quote! { + #[derive(Default)] + pub struct CustomMigration { + pub shared_data: String, + pub converted_field: Cow<'static, str>, + pub fallible_field: i8, + pub nested_field: <NestedStruct as ToMigrationState>::Migrated, + pub simple_field: <u32 as ToMigrationState>::Migrated, + } + impl ToMigrationState for MyStruct { + type Migrated = CustomMigration; + fn snapshot_migration_state( + &self, + target: &mut Self::Migrated + ) -> Result<(), migration::InvalidError> { + target.shared_data = self.shared_data.clone(); + target.converted_field = self.converted_field.clone().into(); + target.fallible_field = self + .fallible_field + .try_into() + .map_err(|_| migration::InvalidError)?; + self.nested_field + .snapshot_migration_state(&mut target.nested_field)?; + self.simple_field + .snapshot_migration_state(&mut target.simple_field)?; + Ok(()) + } + #[allow(clippy::used_underscore_binding)] + fn restore_migrated_state_mut( + &mut self, + source: Self::Migrated, + _version_id: u8 + ) -> Result<(), migration::InvalidError> { + let Self::Migrated { + shared_data, + converted_field, + fallible_field, + nested_field, + simple_field + } = source; + self.shared_data = shared_data; + self.converted_field = converted_field.into(); + self.fallible_field = fallible_field + .try_into() + .map_err(|_| migration::InvalidError)?; + self.nested_field + .restore_migrated_state_mut(nested_field, _version_id)?; + self.simple_field + .restore_migrated_state_mut(simple_field, _version_id)?; + Ok(()) + } + } + } + ); +} |