* [PATCH RFC v2] rust: add qdev DeviceProperties derive macro
@ 2025-07-03 14:37 Manos Pitsidianakis
2025-07-08 9:47 ` Paolo Bonzini
0 siblings, 1 reply; 6+ messages in thread
From: Manos Pitsidianakis @ 2025-07-03 14:37 UTC (permalink / raw)
To: qemu-devel
Cc: qemu-rust, Alex Bennée, Paolo Bonzini, Zhao Liu,
Manos Pitsidianakis
Add derive macro for declaring qdev properties directly above the field
definitions. To do this, we split DeviceImpl::properties method on a
separate trait so we can implement only that part in the derive macro
expansion (we cannot partially implement the DeviceImpl trait).
Adding a `property` attribute above the field declaration will generate
a `qemu_api::bindings::Property` array member in the device's property
list.
Signed-off-by: Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
---
TODOs:
- Update hpet code to use the derive macro
- Change MacroError use to syn::Error use if changed in upstream too
Changes in v2:
- Rewrite to take advantage of const_refs_to_static feature, we still
need to update to a newer MSRV.
- Use existing get_fields function (Paolo)
- return errors instead of panicking (Paolo)
- Link to v1: https://lore.kernel.org/qemu-devel/20250522-rust-qdev-properties-v1-1-5b18b218bad1@linaro.org
---
rust/hw/char/pl011/src/device.rs | 13 +-
rust/hw/char/pl011/src/device_class.rs | 26 +---
rust/hw/timer/hpet/src/device.rs | 4 +-
rust/qemu-api-macros/src/lib.rs | 217 ++++++++++++++++++++++++++++++++-
rust/qemu-api/src/qdev.rs | 57 +++++++--
rust/qemu-api/tests/tests.rs | 9 +-
6 files changed, 282 insertions(+), 44 deletions(-)
diff --git a/rust/hw/char/pl011/src/device.rs b/rust/hw/char/pl011/src/device.rs
index 5b53f2649f161287f40f79075afba47db6d9315c..b2b8dcdfeb6797286918a5ec3e94e1c254e176fe 100644
--- a/rust/hw/char/pl011/src/device.rs
+++ b/rust/hw/char/pl011/src/device.rs
@@ -12,7 +12,10 @@
log_mask_ln,
memory::{hwaddr, MemoryRegion, MemoryRegionOps, MemoryRegionOpsBuilder},
prelude::*,
- qdev::{Clock, ClockEvent, DeviceImpl, DeviceState, Property, ResetType, ResettablePhasesImpl},
+ qdev::{
+ Clock, ClockEvent, DeviceImpl, DevicePropertiesImpl, DeviceState, ResetType,
+ ResettablePhasesImpl,
+ },
qom::{ObjectImpl, Owned, ParentField, ParentInit},
static_assert,
sysbus::{SysBusDevice, SysBusDeviceImpl},
@@ -101,12 +104,13 @@ pub struct PL011Registers {
}
#[repr(C)]
-#[derive(qemu_api_macros::Object)]
+#[derive(qemu_api_macros::Object, qemu_api_macros::DeviceProperties)]
/// PL011 Device Model in QEMU
pub struct PL011State {
pub parent_obj: ParentField<SysBusDevice>,
pub iomem: MemoryRegion,
#[doc(alias = "chr")]
+ #[property(rename = "chardev")]
pub char_backend: CharBackend,
pub regs: BqlRefCell<PL011Registers>,
/// QEMU interrupts
@@ -125,6 +129,7 @@ pub struct PL011State {
#[doc(alias = "clk")]
pub clock: Owned<Clock>,
#[doc(alias = "migrate_clk")]
+ #[property(rename = "migrate-clk", default = true)]
pub migrate_clock: bool,
}
@@ -172,9 +177,6 @@ impl ObjectImpl for PL011State {
}
impl DeviceImpl for PL011State {
- fn properties() -> &'static [Property] {
- &device_class::PL011_PROPERTIES
- }
fn vmsd() -> Option<&'static VMStateDescription> {
Some(&device_class::VMSTATE_PL011)
}
@@ -709,6 +711,7 @@ impl PL011Impl for PL011Luminary {
const DEVICE_ID: DeviceId = DeviceId(&[0x11, 0x00, 0x18, 0x01, 0x0d, 0xf0, 0x05, 0xb1]);
}
+impl DevicePropertiesImpl for PL011Luminary {}
impl DeviceImpl for PL011Luminary {}
impl ResettablePhasesImpl for PL011Luminary {}
impl SysBusDeviceImpl for PL011Luminary {}
diff --git a/rust/hw/char/pl011/src/device_class.rs b/rust/hw/char/pl011/src/device_class.rs
index d328d846323f6080a9573053767e51481eb32941..83d70d7d82aac4a3252a0b4cb24af705b01d3635 100644
--- a/rust/hw/char/pl011/src/device_class.rs
+++ b/rust/hw/char/pl011/src/device_class.rs
@@ -8,11 +8,8 @@
};
use qemu_api::{
- bindings::{qdev_prop_bool, qdev_prop_chr},
- prelude::*,
- vmstate::VMStateDescription,
- vmstate_clock, vmstate_fields, vmstate_of, vmstate_struct, vmstate_subsections, vmstate_unused,
- zeroable::Zeroable,
+ prelude::*, vmstate::VMStateDescription, vmstate_clock, vmstate_fields, vmstate_of,
+ vmstate_struct, vmstate_subsections, vmstate_unused, zeroable::Zeroable,
};
use crate::device::{PL011Registers, PL011State};
@@ -82,22 +79,3 @@ extern "C" fn pl011_post_load(opaque: *mut c_void, version_id: c_int) -> c_int {
},
..Zeroable::ZERO
};
-
-qemu_api::declare_properties! {
- PL011_PROPERTIES,
- qemu_api::define_property!(
- c"chardev",
- PL011State,
- char_backend,
- unsafe { &qdev_prop_chr },
- CharBackend
- ),
- qemu_api::define_property!(
- c"migrate-clk",
- PL011State,
- migrate_clock,
- unsafe { &qdev_prop_bool },
- bool,
- default = true
- ),
-}
diff --git a/rust/hw/timer/hpet/src/device.rs b/rust/hw/timer/hpet/src/device.rs
index acf7251029e912f18a5690b0d6cf04ea8151c5e1..35b8e57fa897f625a6b3e266f9a751a630c21a64 100644
--- a/rust/hw/timer/hpet/src/device.rs
+++ b/rust/hw/timer/hpet/src/device.rs
@@ -1031,11 +1031,13 @@ impl ObjectImpl for HPETState {
..Zeroable::ZERO
};
-impl DeviceImpl for HPETState {
+impl qemu_api::qdev::DevicePropertiesImpl for HPETState {
fn properties() -> &'static [Property] {
&HPET_PROPERTIES
}
+}
+impl DeviceImpl for HPETState {
fn vmsd() -> Option<&'static VMStateDescription> {
Some(&VMSTATE_HPET)
}
diff --git a/rust/qemu-api-macros/src/lib.rs b/rust/qemu-api-macros/src/lib.rs
index c18bb4e036f4e7737f9b95ac300b7d1e8742ef1f..1746da4d967b7ec733c0d97c26d3275fb1cd6645 100644
--- a/rust/qemu-api-macros/src/lib.rs
+++ b/rust/qemu-api-macros/src/lib.rs
@@ -3,10 +3,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
use proc_macro::TokenStream;
-use quote::quote;
+use quote::{quote, quote_spanned, ToTokens};
use syn::{
- parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, Data,
- DeriveInput, Field, Fields, FieldsUnnamed, Ident, Meta, Path, Token, Variant,
+ parse::Parse, parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned,
+ token::Comma, Data, DeriveInput, Field, Fields, FieldsUnnamed, Ident, Meta, Path, Token,
+ Variant,
};
mod utils;
@@ -146,6 +147,216 @@ pub const fn raw_get(slot: *mut Self) -> *mut <Self as crate::cell::Wrapper>::Wr
})
}
+#[derive(Debug)]
+enum DevicePropertyName {
+ CStr(syn::LitCStr),
+ Str(syn::LitStr),
+}
+
+#[derive(Debug)]
+struct DeviceProperty {
+ rename: Option<DevicePropertyName>,
+ qdev_prop: Option<syn::Path>,
+ bitnr: Option<syn::Expr>,
+ defval: Option<syn::Expr>,
+}
+
+impl Parse for DeviceProperty {
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+ let _: syn::Token![#] = input.parse()?;
+ let bracketed;
+ _ = syn::bracketed!(bracketed in input);
+ let _attribute = bracketed.parse::<syn::Ident>()?;
+ debug_assert_eq!(&_attribute.to_string(), "property");
+ let mut retval = Self {
+ rename: None,
+ qdev_prop: None,
+ bitnr: None,
+ defval: None,
+ };
+ let content;
+ _ = syn::parenthesized!(content in bracketed);
+ while !content.is_empty() {
+ let value: syn::Ident = content.parse()?;
+ if value == "rename" {
+ let _: syn::Token![=] = content.parse()?;
+ if retval.rename.is_some() {
+ return Err(syn::Error::new(
+ value.span(),
+ "`rename` can only be used at most once",
+ ));
+ }
+ if content.peek(syn::LitStr) {
+ retval.rename = Some(DevicePropertyName::Str(content.parse::<syn::LitStr>()?));
+ } else {
+ retval.rename =
+ Some(DevicePropertyName::CStr(content.parse::<syn::LitCStr>()?));
+ }
+ } else if value == "qdev_prop" {
+ let _: syn::Token![=] = content.parse()?;
+ if retval.qdev_prop.is_some() {
+ return Err(syn::Error::new(
+ value.span(),
+ "`qdev_prop` can only be used at most once",
+ ));
+ }
+ retval.qdev_prop = Some(content.parse()?);
+ } else if value == "bitnr" {
+ let _: syn::Token![=] = content.parse()?;
+ if retval.bitnr.is_some() {
+ return Err(syn::Error::new(
+ value.span(),
+ "`bitnr` can only be used at most once",
+ ));
+ }
+ retval.bitnr = Some(content.parse()?);
+ } else if value == "default" {
+ let _: syn::Token![=] = content.parse()?;
+ if retval.defval.is_some() {
+ return Err(syn::Error::new(
+ value.span(),
+ "`default` can only be used at most once",
+ ));
+ }
+ retval.defval = Some(content.parse()?);
+ } else {
+ return Err(syn::Error::new(
+ value.span(),
+ format!("unrecognized field `{value}`"),
+ ));
+ }
+
+ if !content.is_empty() {
+ let _: syn::Token![,] = content.parse()?;
+ }
+ }
+ Ok(retval)
+ }
+}
+
+#[proc_macro_derive(DeviceProperties, attributes(property))]
+pub fn derive_device_properties(input: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
+ let expanded = derive_device_properties_or_error(input).unwrap_or_else(Into::into);
+
+ TokenStream::from(expanded)
+}
+
+fn derive_device_properties_or_error(
+ input: DeriveInput,
+) -> Result<proc_macro2::TokenStream, MacroError> {
+ let span = proc_macro::Span::call_site();
+ let properties: Vec<(syn::Field, proc_macro2::Span, DeviceProperty)> =
+ get_fields(&input, "#[derive(DeviceProperties)]")?
+ .iter()
+ .flat_map(|f| {
+ f.attrs
+ .iter()
+ .filter(|a| a.path().is_ident("property"))
+ .map(|a| {
+ Ok((
+ f.clone(),
+ f.span(),
+ syn::parse(a.to_token_stream().into()).map_err(|err| {
+ MacroError::Message(
+ format!("Could not parse `property` attribute: {err}"),
+ f.span(),
+ )
+ })?,
+ ))
+ })
+ })
+ .collect::<Result<Vec<_>, MacroError>>()?;
+ let name = &input.ident;
+ let mut properties_expanded = vec![];
+ let zero = syn::Expr::Verbatim(quote! { 0 });
+
+ for (field, field_span, prop) in properties {
+ let DeviceProperty {
+ rename,
+ qdev_prop,
+ bitnr,
+ defval,
+ } = prop;
+ let field_name = field.ident.as_ref().unwrap();
+ let prop_name = rename
+ .as_ref()
+ .map(|lit| -> Result<proc_macro2::TokenStream, MacroError> {
+ match lit {
+ DevicePropertyName::CStr(lit) => {
+ let span = lit.span();
+ Ok(quote_spanned! {span=>
+ #lit
+ })
+ }
+ DevicePropertyName::Str(lit) => {
+ let span = lit.span();
+ let value = lit.value();
+ let lit = std::ffi::CString::new(value.as_str())
+ .map_err(|err| {
+ MacroError::Message(
+ format!("Property name `{value}` cannot be represented as a C string: {err}"),
+ span
+ )
+ })?;
+ let lit = syn::LitCStr::new(&lit, span);
+ Ok(quote_spanned! {span=>
+ #lit
+ })
+ }
+ }})
+ .unwrap_or_else(|| {
+ let span = field_name.span();
+ let field_name_value = field_name.to_string();
+ let lit = std::ffi::CString::new(field_name_value.as_str()).map_err(|err| {
+ MacroError::Message(
+ format!("Field `{field_name_value}` cannot be represented as a C string: {err}\nPlease set an explicit property name using the `rename=...` option in the field's `property` attribute."),
+ span
+ )
+ })?;
+ let lit = syn::LitCStr::new(&lit, span);
+ Ok(quote_spanned! {span=>
+ #lit
+ })
+ })?;
+ let field_ty = field.ty.clone();
+ let qdev_prop = qdev_prop
+ .as_ref()
+ .map(|path| {
+ quote_spanned! {field_span=>
+ unsafe { &#path }
+ }
+ })
+ .unwrap_or_else(
+ || quote_spanned! {field_span=> <#field_ty as ::qemu_api::qdev::QDevProp>::VALUE },
+ );
+ let set_default = defval.is_some();
+ let bitnr = bitnr.as_ref().unwrap_or(&zero);
+ let defval = defval.as_ref().unwrap_or(&zero);
+ properties_expanded.push(quote_spanned! {field_span=>
+ ::qemu_api::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: ::qemu_api::bindings::Property__bindgen_ty_1 { u: #defval as u64 },
+ ..::qemu_api::zeroable::Zeroable::ZERO
+ }
+ });
+ }
+
+ Ok(quote_spanned! {span.into()=>
+ impl ::qemu_api::qdev::DevicePropertiesImpl for #name {
+ fn properties() -> &'static [::qemu_api::bindings::Property] {
+ static PROPERTIES: &'static [::qemu_api::bindings::Property] = &[#(#properties_expanded),*];
+
+ PROPERTIES
+ }
+ }
+ })
+}
+
#[proc_macro_derive(Wrapper)]
pub fn derive_opaque(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
diff --git a/rust/qemu-api/src/qdev.rs b/rust/qemu-api/src/qdev.rs
index 36f02fb57dbffafb21a2e7cc96419ca42e865269..01f199f198c6a5f8a761beb143e567fc267028aa 100644
--- a/rust/qemu-api/src/qdev.rs
+++ b/rust/qemu-api/src/qdev.rs
@@ -101,8 +101,54 @@ pub trait ResettablePhasesImpl {
T::EXIT.unwrap()(unsafe { state.as_ref() }, typ);
}
+/// Helper trait to return pointer to a [`bindings::PropertyInfo`] for a type.
+///
+/// This trait is used by [`qemu_api_macros::DeviceProperty`] derive macro.
+///
+/// # Safety
+///
+/// This trait is marked as `unsafe` because currently having a `const` refer to an `extern static`
+/// results in this compiler error:
+///
+/// ```text
+/// constructing invalid value: encountered reference to `extern` static in `const`
+/// ```
+///
+/// It is the implementer's responsibility to provide a valid [`bindings::PropertyInfo`] pointer
+/// for the trait implementation to be safe.
+pub unsafe trait QDevProp {
+ const VALUE: *const bindings::PropertyInfo;
+}
+
+/// Use [`bindings::qdev_prop_bool`] for `bool`.
+unsafe impl QDevProp for bool {
+ const VALUE: *const bindings::PropertyInfo = unsafe { &bindings::qdev_prop_bool };
+}
+
+/// Use [`bindings::qdev_prop_uint64`] for `u64`.
+unsafe impl QDevProp for u64 {
+ const VALUE: *const bindings::PropertyInfo = unsafe { &bindings::qdev_prop_uint64 };
+}
+
+/// Use [`bindings::qdev_prop_chr`] for [`crate::chardev::CharBackend`].
+unsafe impl QDevProp for crate::chardev::CharBackend {
+ const VALUE: *const bindings::PropertyInfo = unsafe { &bindings::qdev_prop_chr };
+}
+
+/// Trait to define device properties.
+pub trait DevicePropertiesImpl {
+ /// An array providing the properties that the user can set on the
+ /// device. Not a `const` because referencing statics in constants
+ /// is unstable until Rust 1.83.0.
+ fn properties() -> &'static [Property] {
+ &[]
+ }
+}
+
/// Trait providing the contents of [`DeviceClass`].
-pub trait DeviceImpl: ObjectImpl + ResettablePhasesImpl + IsA<DeviceState> {
+pub trait DeviceImpl:
+ ObjectImpl + ResettablePhasesImpl + DevicePropertiesImpl + IsA<DeviceState>
+{
/// _Realization_ is the second stage of device creation. It contains
/// all operations that depend on device properties and can fail (note:
/// this is not yet supported for Rust devices).
@@ -111,13 +157,6 @@ pub trait DeviceImpl: ObjectImpl + ResettablePhasesImpl + IsA<DeviceState> {
/// with the function pointed to by `REALIZE`.
const REALIZE: Option<fn(&Self) -> Result<()>> = None;
- /// An array providing the properties that the user can set on the
- /// device. Not a `const` because referencing statics in constants
- /// is unstable until Rust 1.83.0.
- fn properties() -> &'static [Property] {
- &[]
- }
-
/// A `VMStateDescription` providing the migration format for the device
/// Not a `const` because referencing statics in constants is unstable
/// until Rust 1.83.0.
@@ -175,7 +214,7 @@ pub fn class_init<T: DeviceImpl>(&mut self) {
if let Some(vmsd) = <T as DeviceImpl>::vmsd() {
self.vmsd = vmsd;
}
- let prop = <T as DeviceImpl>::properties();
+ let prop = <T as DevicePropertiesImpl>::properties();
if !prop.is_empty() {
unsafe {
bindings::device_class_set_props_n(self, prop.as_ptr(), prop.len());
diff --git a/rust/qemu-api/tests/tests.rs b/rust/qemu-api/tests/tests.rs
index a658a49fcfdda8fa4b9d139c10afb6ff3243790b..e8eadfd6e9add385ffc97de015b84aae825c18ee 100644
--- a/rust/qemu-api/tests/tests.rs
+++ b/rust/qemu-api/tests/tests.rs
@@ -9,7 +9,7 @@
cell::{self, BqlCell},
declare_properties, define_property,
prelude::*,
- qdev::{DeviceImpl, DeviceState, Property, ResettablePhasesImpl},
+ qdev::{DeviceImpl, DevicePropertiesImpl, DeviceState, Property, ResettablePhasesImpl},
qom::{ObjectImpl, ParentField},
sysbus::SysBusDevice,
vmstate::VMStateDescription,
@@ -68,10 +68,13 @@ impl ObjectImpl for DummyState {
impl ResettablePhasesImpl for DummyState {}
-impl DeviceImpl for DummyState {
+impl DevicePropertiesImpl for DummyState {
fn properties() -> &'static [Property] {
&DUMMY_PROPERTIES
}
+}
+
+impl DeviceImpl for DummyState {
fn vmsd() -> Option<&'static VMStateDescription> {
Some(&VMSTATE)
}
@@ -85,6 +88,8 @@ pub struct DummyChildState {
qom_isa!(DummyChildState: Object, DeviceState, DummyState);
+impl DevicePropertiesImpl for DummyChildState {}
+
pub struct DummyChildClass {
parent_class: <DummyState as ObjectType>::Class,
}
---
base-commit: c77283dd5d79149f4e7e9edd00f65416c648ee59
change-id: 20250522-rust-qdev-properties-728e8f6a468e
prerequisite-change-id: 20250703-rust_bindings_allow_unnecessary_transmutes-d614db4517a4:v1
prerequisite-patch-id: 570fede8eee168ade58c7c7599bdc8b94c8c1a22
--
γαῖα πυρί μιχθήτω
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH RFC v2] rust: add qdev DeviceProperties derive macro
2025-07-03 14:37 [PATCH RFC v2] rust: add qdev DeviceProperties derive macro Manos Pitsidianakis
@ 2025-07-08 9:47 ` Paolo Bonzini
2025-07-10 9:40 ` Manos Pitsidianakis
0 siblings, 1 reply; 6+ messages in thread
From: Paolo Bonzini @ 2025-07-08 9:47 UTC (permalink / raw)
To: Manos Pitsidianakis, qemu-devel; +Cc: qemu-rust, Alex Bennée, Zhao Liu
On 7/3/25 16:37, Manos Pitsidianakis wrote:
> Add derive macro for declaring qdev properties directly above the field
> definitions. To do this, we split DeviceImpl::properties method on a
> separate trait so we can implement only that part in the derive macro
> expansion (we cannot partially implement the DeviceImpl trait).
>
> Adding a `property` attribute above the field declaration will generate
> a `qemu_api::bindings::Property` array member in the device's property
> list.
>
> Signed-off-by: Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
Very nice. I have relatively many comments but they are all very
simple. The main ones are about unused functionality that could be left
for later, and some code duplication.
Aside from that, I actually liked using Device for the macro name in
your earlier versions. Yes, it's just for properties in practice, but
it's nice and small to just say Device; and it mimics Object. It's your
choice anyway.
> - Update hpet code to use the derive macro
Don't worry about that for the first inclusion. More below.
> - Change MacroError use to syn::Error use if changed in upstream too
True. In the review below I'll just use syn::Error instead of MacroError.
> +impl DevicePropertiesImpl for PL011Luminary {}
Does it make sense to use #[derive()] anyway, and skip the import?
Especially because...
> +/// Trait to define device properties.
> +pub trait DevicePropertiesImpl {
> + /// An array providing the properties that the user can set on the
> + /// device. Not a `const` because referencing statics in constants
> + /// is unstable until Rust 1.83.0.
> + fn properties() -> &'static [Property] {
> + &[]
> + }
> +}
> +
... the trait should be declared unsafe(*), and it's nice to hide the
implementation of unsafe traits behind macros that do guarantee the safety.
(*) One can also declare properties() unsafe, but with 1.83.0
properties() becomes an associated const, and there are no
unsafe const declarations... might as well plan ahead.
And then, what about calling the trait "DeviceImplUnsafe"? It is a
clearer split: device code uses #[derive()] for anything that cannot be
declared safely (think of zerocopy), and impl for what *is* safe.
> + } else if value == "qdev_prop" {
> + let _: syn::Token![=] = content.parse()?;
> + if retval.qdev_prop.is_some() {
> + return Err(syn::Error::new(
> + value.span(),
> + "`qdev_prop` can only be used at most once",
> + ));
> + }
> + retval.qdev_prop = Some(content.parse()?);
qdev_prop is only needed together with bitnr, right? If so:
1) Thoughts for later: maybe if bitnr is used the macro should use a
different trait than QDevProp (e.g. QDevBitProp). Would this be enough
for qdev_prop to go away? (I think/hope so)
2) Thoughts for now: maybe leave out bitnr and qdev_prop? And revisit
together with the HPET conversion, which needs bitnr.
> + let prop_name = rename
> + .as_ref()
I think ".as_ref()" is not needed? I may be wrong though.
> + DevicePropertyName::Str(lit) => {> + let span = lit.span();
> + let value = lit.value();
> + let lit = std::ffi::CString::new(value.as_str())
> + .map_err(|err| {
> + MacroError::Message(
> + format!("Property name `{value}` cannot be represented as a C string: {err}"),
> + span
> + )
> + })?;
> + let lit = syn::LitCStr::new(&lit, span);
> + Ok(quote_spanned! {span=>
> + #lit
> + })
quote_spanned! is not needed here, because all the tokens that you're
producing are interpolated:
Any interpolated tokens preserve the Span information provided by
their ToTokens implementation. Tokens that originate within the
quote_spanned! invocation are spanned with the given span argument
(https://docs.rs/quote/1.0.40/quote/macro.quote_spanned.html)
Also please extract this into a separate function. That is, make
everything just
make_c_str(lit.value(), lit.span())
(make_c_str returns a Result<syn::LitCStr, syn::Error>).
> + .unwrap_or_else(|| {
> + let span = field_name.span();
> + let field_name_value = field_name.to_string();
> + let lit = std::ffi::CString::new(field_name_value.as_str()).map_err(|err| {
> + MacroError::Message(
> + format!("Field `{field_name_value}` cannot be represented as a C string: {err}\nPlease set an explicit property name using the `rename=...` option in the field's `property` attribute."),
I don't think this error can happen, because the field name cannot
contain a NUL character and that's the only way CString::new fails. So
just using the same function above is fine, because the more detailed
error isn't necessary.
Putting everything together and using .map_or_else() gives you something
like this:
let prop_name = rename
.map_or_else(
|| make_c_str(field_name.to_string(), field_name.span())
|lit| {
match lit {
DevicePropertyName::CStr(lit) => {
Ok(lit)
}
DevicePropertyName::Str(lit) => {
make_c_str(lit.value(), lit.span())
}
}
})?;
You could even go ahead and only accept syn::LitStr, dropping
DevicePropertyName altogether. But since you've written the code and
c"" support is only 10-15 lines of code overall, do as you wish.
> + span
> + )
> + })?;
> + let lit = syn::LitCStr::new(&lit, span);
> + Ok(quote_spanned! {span=>
> + #lit
> + })
> + })?;
> + let field_ty = field.ty.clone();
> + let qdev_prop = qdev_prop
> + .as_ref()
Again, .as_ref() might not be needed here either.
> + .map(|path| {
> + quote_spanned! {field_span=>
> + unsafe { &#path }
> + }
> + })
> + .unwrap_or_else(
> + || quote_spanned! {field_span=> <#field_ty as ::qemu_api::qdev::QDevProp>::VALUE },
> + );
If you decide to keep qdev_prop, .map_or_else() can be used here too.
> + let set_default = defval.is_some();
> + let bitnr = bitnr.as_ref().unwrap_or(&zero);
> + let defval = defval.as_ref().unwrap_or(&zero);
> + properties_expanded.push(quote_spanned! {field_span=>
> + ::qemu_api::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: ::qemu_api::bindings::Property__bindgen_ty_1 { u: #defval as u64 },
Maybe add a TODO that not all types should have a default (e.g.
pointers). No need to fix it now, but having a note in the code would
be nice.
> diff --git a/rust/qemu-api/src/qdev.rs b/rust/qemu-api/src/qdev.rs
> index 36f02fb57dbffafb21a2e7cc96419ca42e865269..01f199f198c6a5f8a761beb143e567fc267028aa 100644
> --- a/rust/qemu-api/src/qdev.rs
> +++ b/rust/qemu-api/src/qdev.rs
> @@ -101,8 +101,54 @@ pub trait ResettablePhasesImpl {
> T::EXIT.unwrap()(unsafe { state.as_ref() }, typ);
> }
>
> +/// Helper trait to return pointer to a [`bindings::PropertyInfo`] for a type.
> +///
> +/// This trait is used by [`qemu_api_macros::DeviceProperty`] derive macro.
> +///
> +/// # Safety
> +///
> +/// This trait is marked as `unsafe` because currently having a `const` refer to an `extern static`
> +/// results in this compiler error:
> +///
> +/// ```text
> +/// constructing invalid value: encountered reference to `extern` static in `const`
> +/// ```
> +///
> +/// It is the implementer's responsibility to provide a valid [`bindings::PropertyInfo`] pointer
> +/// for the trait implementation to be safe.
> +pub unsafe trait QDevProp {
> + const VALUE: *const bindings::PropertyInfo;
"*const" or "&"?
> @@ -68,10 +68,13 @@ impl ObjectImpl for DummyState {
>
> impl ResettablePhasesImpl for DummyState {}
>
> -impl DeviceImpl for DummyState {
> +impl DevicePropertiesImpl for DummyState {
> fn properties() -> &'static [Property] {
> &DUMMY_PROPERTIES
> }
> +}
You can easily use #[derive()] here too, since the DummyState code is
mostly copied from pl011.
> +impl DevicePropertiesImpl for DummyChildState {}
... and here too.
Thanks,
Paolo
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH RFC v2] rust: add qdev DeviceProperties derive macro
2025-07-08 9:47 ` Paolo Bonzini
@ 2025-07-10 9:40 ` Manos Pitsidianakis
2025-07-10 14:25 ` Paolo Bonzini
0 siblings, 1 reply; 6+ messages in thread
From: Manos Pitsidianakis @ 2025-07-10 9:40 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, qemu-rust, Alex Bennée, Zhao Liu
Thanks for the comments, I am preparing a new version with all
problems/suggestions fixed.
On Tue, Jul 8, 2025 at 12:48 PM Paolo Bonzini <pbonzini@redhat.com> wrote:
>
> On 7/3/25 16:37, Manos Pitsidianakis wrote:
> > Add derive macro for declaring qdev properties directly above the field
> > definitions. To do this, we split DeviceImpl::properties method on a
> > separate trait so we can implement only that part in the derive macro
> > expansion (we cannot partially implement the DeviceImpl trait).
> >
> > Adding a `property` attribute above the field declaration will generate
> > a `qemu_api::bindings::Property` array member in the device's property
> > list.
> >
> > Signed-off-by: Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
>
> Very nice. I have relatively many comments but they are all very
> simple. The main ones are about unused functionality that could be left
> for later, and some code duplication.
>
> Aside from that, I actually liked using Device for the macro name in
> your earlier versions. Yes, it's just for properties in practice, but
> it's nice and small to just say Device; and it mimics Object. It's your
> choice anyway.
I was thinking of making a `Device` derive macro that lets you also
define `DeviceImpl::REALIZE` and `DeviceImpl::vmsd` as macro
attributes on the struct definition, then merge DeviceProperties into
that. WDYT?
>
> > - Update hpet code to use the derive macro
>
> Don't worry about that for the first inclusion. More below.
>
> > - Change MacroError use to syn::Error use if changed in upstream too
>
> True. In the review below I'll just use syn::Error instead of MacroError.
>
> > +impl DevicePropertiesImpl for PL011Luminary {}
>
> Does it make sense to use #[derive()] anyway, and skip the import?
> Especially because...
Oops, yes.
>
> > +/// Trait to define device properties.
> > +pub trait DevicePropertiesImpl {
> > + /// An array providing the properties that the user can set on the
> > + /// device. Not a `const` because referencing statics in constants
> > + /// is unstable until Rust 1.83.0.
> > + fn properties() -> &'static [Property] {
> > + &[]
> > + }
> > +}
> > +
>
> ... the trait should be declared unsafe(*), and it's nice to hide the
> implementation of unsafe traits behind macros that do guarantee the safety.
Good idea, will do,
>
> (*) One can also declare properties() unsafe, but with 1.83.0
> properties() becomes an associated const, and there are no
> unsafe const declarations... might as well plan ahead.
>
> And then, what about calling the trait "DeviceImplUnsafe"? It is a
> clearer split: device code uses #[derive()] for anything that cannot be
> declared safely (think of zerocopy), and impl for what *is* safe.
Hm, isn't it redundant if the trait is marked as `unsafe`? Or maybe I
misunderstood your point.
>
> > + } else if value == "qdev_prop" {
> > + let _: syn::Token![=] = content.parse()?;
> > + if retval.qdev_prop.is_some() {
> > + return Err(syn::Error::new(
> > + value.span(),
> > + "`qdev_prop` can only be used at most once",
> > + ));
> > + }
> > + retval.qdev_prop = Some(content.parse()?);
>
> qdev_prop is only needed together with bitnr, right? If so:
>
> 1) Thoughts for later: maybe if bitnr is used the macro should use a
> different trait than QDevProp (e.g. QDevBitProp). Would this be enough
> for qdev_prop to go away? (I think/hope so)
>
> 2) Thoughts for now: maybe leave out bitnr and qdev_prop? And revisit
> together with the HPET conversion, which needs bitnr.
Agreed, that makes more sense
>
> > + let prop_name = rename
> > + .as_ref()
>
> I think ".as_ref()" is not needed? I may be wrong though.
>
> > + DevicePropertyName::Str(lit) => {> + let span = lit.span();
> > + let value = lit.value();
> > + let lit = std::ffi::CString::new(value.as_str())
> > + .map_err(|err| {
> > + MacroError::Message(
> > + format!("Property name `{value}` cannot be represented as a C string: {err}"),
> > + span
> > + )
> > + })?;
> > + let lit = syn::LitCStr::new(&lit, span);
> > + Ok(quote_spanned! {span=>
> > + #lit
> > + })
>
> quote_spanned! is not needed here, because all the tokens that you're
> producing are interpolated:
>
> Any interpolated tokens preserve the Span information provided by
> their ToTokens implementation. Tokens that originate within the
> quote_spanned! invocation are spanned with the given span argument
> (https://docs.rs/quote/1.0.40/quote/macro.quote_spanned.html)
>
> Also please extract this into a separate function. That is, make
> everything just
>
> make_c_str(lit.value(), lit.span())
>
> (make_c_str returns a Result<syn::LitCStr, syn::Error>).
>
> > + .unwrap_or_else(|| {
> > + let span = field_name.span();
> > + let field_name_value = field_name.to_string();
> > + let lit = std::ffi::CString::new(field_name_value.as_str()).map_err(|err| {
> > + MacroError::Message(
> > + format!("Field `{field_name_value}` cannot be represented as a C string: {err}\nPlease set an explicit property name using the `rename=...` option in the field's `property` attribute."),
>
> I don't think this error can happen, because the field name cannot
> contain a NUL character and that's the only way CString::new fails. So
> just using the same function above is fine, because the more detailed
> error isn't necessary.
>
> Putting everything together and using .map_or_else() gives you something
> like this:
>
> let prop_name = rename
> .map_or_else(
> || make_c_str(field_name.to_string(), field_name.span())
> |lit| {
> match lit {
> DevicePropertyName::CStr(lit) => {
> Ok(lit)
> }
> DevicePropertyName::Str(lit) => {
> make_c_str(lit.value(), lit.span())
> }
> }
> })?;
>
> You could even go ahead and only accept syn::LitStr, dropping
> DevicePropertyName altogether. But since you've written the code and
> c"" support is only 10-15 lines of code overall, do as you wish.
>
> > + span
> > + )
> > + })?;
> > + let lit = syn::LitCStr::new(&lit, span);
> > + Ok(quote_spanned! {span=>
> > + #lit
> > + })
> > + })?;
> > + let field_ty = field.ty.clone();
> > + let qdev_prop = qdev_prop
> > + .as_ref()
>
> Again, .as_ref() might not be needed here either.
>
> > + .map(|path| {
> > + quote_spanned! {field_span=>
> > + unsafe { &#path }
> > + }
> > + })
> > + .unwrap_or_else(
> > + || quote_spanned! {field_span=> <#field_ty as ::qemu_api::qdev::QDevProp>::VALUE },
> > + );
>
> If you decide to keep qdev_prop, .map_or_else() can be used here too.
>
> > + let set_default = defval.is_some();
> > + let bitnr = bitnr.as_ref().unwrap_or(&zero);
> > + let defval = defval.as_ref().unwrap_or(&zero);
> > + properties_expanded.push(quote_spanned! {field_span=>
> > + ::qemu_api::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: ::qemu_api::bindings::Property__bindgen_ty_1 { u: #defval as u64 },
>
> Maybe add a TODO that not all types should have a default (e.g.
> pointers). No need to fix it now, but having a note in the code would
> be nice.
>
> > diff --git a/rust/qemu-api/src/qdev.rs b/rust/qemu-api/src/qdev.rs
> > index 36f02fb57dbffafb21a2e7cc96419ca42e865269..01f199f198c6a5f8a761beb143e567fc267028aa 100644
> > --- a/rust/qemu-api/src/qdev.rs
> > +++ b/rust/qemu-api/src/qdev.rs
> > @@ -101,8 +101,54 @@ pub trait ResettablePhasesImpl {
> > T::EXIT.unwrap()(unsafe { state.as_ref() }, typ);
> > }
> >
> > +/// Helper trait to return pointer to a [`bindings::PropertyInfo`] for a type.
> > +///
> > +/// This trait is used by [`qemu_api_macros::DeviceProperty`] derive macro.
> > +///
> > +/// # Safety
> > +///
> > +/// This trait is marked as `unsafe` because currently having a `const` refer to an `extern static`
> > +/// results in this compiler error:
> > +///
> > +/// ```text
> > +/// constructing invalid value: encountered reference to `extern` static in `const`
> > +/// ```
> > +///
> > +/// It is the implementer's responsibility to provide a valid [`bindings::PropertyInfo`] pointer
> > +/// for the trait implementation to be safe.
> > +pub unsafe trait QDevProp {
> > + const VALUE: *const bindings::PropertyInfo;
>
> "*const" or "&"?
This is the thing I mentioned to you over IRC: Unfortunately even with
const refs for statics we get this because the static is extern:
error[E0080]: it is undefined behavior to use this value
--> build/rust/qemu-api/libqemu_api.rlib.p/structured/qdev.rs:135:5
|
135 | const VALUE: &'static bindings::PropertyInfo = unsafe {
&bindings::qdev_prop_chr };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ constructing
invalid value: encountered reference to `extern` static in `const`
|
= note: The rules on what exactly is undefined behavior aren't
clear, so this check might be overzealous. Please open an issue on the
rustc repository if you believe it should not be considered undefined
behavior.
= note: the raw bytes of the constant (size: 8, align: 8) {
╾────alloc93<imm>─────╼ │ ╾──────╼
}
IIUC the error, it's because const refs can get dereferenced in const
context by the compiler in general, but PropertyInfo does not get
dereferenced anywhere so we are good.
>
> > @@ -68,10 +68,13 @@ impl ObjectImpl for DummyState {
> >
> > impl ResettablePhasesImpl for DummyState {}
> >
> > -impl DeviceImpl for DummyState {
> > +impl DevicePropertiesImpl for DummyState {
> > fn properties() -> &'static [Property] {
> > &DUMMY_PROPERTIES
> > }
> > +}
>
> You can easily use #[derive()] here too, since the DummyState code is
> mostly copied from pl011.
>
> > +impl DevicePropertiesImpl for DummyChildState {}
>
> ... and here too.
>
> Thanks,
>
> Paolo
>
--
Manos Pitsidianakis
Emulation and Virtualization Engineer at Linaro Ltd
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH RFC v2] rust: add qdev DeviceProperties derive macro
2025-07-10 9:40 ` Manos Pitsidianakis
@ 2025-07-10 14:25 ` Paolo Bonzini
2025-07-10 14:47 ` Manos Pitsidianakis
0 siblings, 1 reply; 6+ messages in thread
From: Paolo Bonzini @ 2025-07-10 14:25 UTC (permalink / raw)
To: Manos Pitsidianakis; +Cc: qemu-devel, qemu-rust, Alex Bennée, Zhao Liu
On Thu, Jul 10, 2025 at 11:41 AM Manos Pitsidianakis
<manos.pitsidianakis@linaro.org> wrote:
> > Aside from that, I actually liked using Device for the macro name in
> > your earlier versions. Yes, it's just for properties in practice, but
> > it's nice and small to just say Device; and it mimics Object. It's your
> > choice anyway.
>
> I was thinking of making a `Device` derive macro that lets you also
> define `DeviceImpl::REALIZE` and `DeviceImpl::vmsd` as macro
> attributes on the struct definition, then merge DeviceProperties into
> that. WDYT?
Like #[derive(Device(realize = PL011State::realize))]? I kind of like
having traits for classes (the "const" does look a bit ugly/foreign,
but Linux has some other ideas using a #[vtable] procedural macro).
For vmsd yes, you could pass it as a VMStateDescription const's name
in the same style, like #[derive(Device(vmsd =
device_class::VMSTATE_PL011))].
WRT naming, I was thinking the other way round: call it Device
already, and then add functionality without having to change the name.
Your choice; naming suggestions are risky :) but I wasn't afraid of
making this suggestion because you had that name before.
> Hm, isn't it redundant if the trait is marked as `unsafe`? Or maybe I
> misunderstood your point.
Yes, you're right - just wanted something not too tied to properties
in case the derive macro is extended later. Maybe DeviceDerive, or
DeviceImplDerive? But any name is fine, just keep it consistent
between macro and trait.
> > > +/// It is the implementer's responsibility to provide a valid [`bindings::PropertyInfo`] pointer
> > > +/// for the trait implementation to be safe.
> > > +pub unsafe trait QDevProp {
> > > + const VALUE: *const bindings::PropertyInfo;
> >
> > "*const" or "&"?
>
> This is the thing I mentioned to you over IRC: Unfortunately even with
> const refs for statics we get this because the static is extern:
Ah okay - I just wasn't sure, which is why I wrote it as a question.
It worked in vmstate because there the const is a VMStateField which
has a *const inside. So yes, definitely a *const.
Paolo
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH RFC v2] rust: add qdev DeviceProperties derive macro
2025-07-10 14:25 ` Paolo Bonzini
@ 2025-07-10 14:47 ` Manos Pitsidianakis
2025-07-10 16:29 ` Paolo Bonzini
0 siblings, 1 reply; 6+ messages in thread
From: Manos Pitsidianakis @ 2025-07-10 14:47 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, qemu-rust, Alex Bennée, Zhao Liu
On Thu, Jul 10, 2025 at 5:26 PM Paolo Bonzini <pbonzini@redhat.com> wrote:
>
> On Thu, Jul 10, 2025 at 11:41 AM Manos Pitsidianakis
> <manos.pitsidianakis@linaro.org> wrote:
> > > Aside from that, I actually liked using Device for the macro name in
> > > your earlier versions. Yes, it's just for properties in practice, but
> > > it's nice and small to just say Device; and it mimics Object. It's your
> > > choice anyway.
> >
> > I was thinking of making a `Device` derive macro that lets you also
> > define `DeviceImpl::REALIZE` and `DeviceImpl::vmsd` as macro
> > attributes on the struct definition, then merge DeviceProperties into
> > that. WDYT?
>
> Like #[derive(Device(realize = PL011State::realize))]? I kind of like
> having traits for classes (the "const" does look a bit ugly/foreign,
> but Linux has some other ideas using a #[vtable] procedural macro).
I was thinking:
#[repr(C)]
#[derive(Device)]
#[device(realize = PL011State::realize, vmsd = VMSTATE_PL011)]
pub struct PL011State {
..
}
I agree about traits for class methods, it's definitely cleaner. The
lines blur here because we have REALIZE as a constant in order to make
it nullable from the C side 🤔
> For vmsd yes, you could pass it as a VMStateDescription const's name
> in the same style, like #[derive(Device(vmsd =
> device_class::VMSTATE_PL011))].
>
> WRT naming, I was thinking the other way round: call it Device
> already, and then add functionality without having to change the name.
> Your choice; naming suggestions are risky :) but I wasn't afraid of
> making this suggestion because you had that name before.
>
> > Hm, isn't it redundant if the trait is marked as `unsafe`? Or maybe I
> > misunderstood your point.
>
> Yes, you're right - just wanted something not too tied to properties
> in case the derive macro is extended later. Maybe DeviceDerive, or
> DeviceImplDerive? But any name is fine, just keep it consistent
> between macro and trait.
>
> > > > +/// It is the implementer's responsibility to provide a valid [`bindings::PropertyInfo`] pointer
> > > > +/// for the trait implementation to be safe.
> > > > +pub unsafe trait QDevProp {
> > > > + const VALUE: *const bindings::PropertyInfo;
> > >
> > > "*const" or "&"?
> >
> > This is the thing I mentioned to you over IRC: Unfortunately even with
> > const refs for statics we get this because the static is extern:
>
> Ah okay - I just wasn't sure, which is why I wrote it as a question.
> It worked in vmstate because there the const is a VMStateField which
> has a *const inside. So yes, definitely a *const.
>
> Paolo
>
Thanks!
--
Manos Pitsidianakis
Emulation and Virtualization Engineer at Linaro Ltd
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH RFC v2] rust: add qdev DeviceProperties derive macro
2025-07-10 14:47 ` Manos Pitsidianakis
@ 2025-07-10 16:29 ` Paolo Bonzini
0 siblings, 0 replies; 6+ messages in thread
From: Paolo Bonzini @ 2025-07-10 16:29 UTC (permalink / raw)
To: Manos Pitsidianakis; +Cc: qemu-devel, qemu-rust, Alex Bennée, Zhao Liu
On Thu, Jul 10, 2025 at 4:48 PM Manos Pitsidianakis
<manos.pitsidianakis@linaro.org> wrote:
>
> On Thu, Jul 10, 2025 at 5:26 PM Paolo Bonzini <pbonzini@redhat.com> wrote:
> >
> > On Thu, Jul 10, 2025 at 11:41 AM Manos Pitsidianakis
> > <manos.pitsidianakis@linaro.org> wrote:
> > > > Aside from that, I actually liked using Device for the macro name in
> > > > your earlier versions. Yes, it's just for properties in practice, but
> > > > it's nice and small to just say Device; and it mimics Object. It's your
> > > > choice anyway.
> > >
> > > I was thinking of making a `Device` derive macro that lets you also
> > > define `DeviceImpl::REALIZE` and `DeviceImpl::vmsd` as macro
> > > attributes on the struct definition, then merge DeviceProperties into
> > > that. WDYT?
> >
> > Like #[derive(Device(realize = PL011State::realize))]? I kind of like
> > having traits for classes (the "const" does look a bit ugly/foreign,
> > but Linux has some other ideas using a #[vtable] procedural macro).
>
> I was thinking:
>
> #[repr(C)]
> #[derive(Device)]
> #[device(realize = PL011State::realize, vmsd = VMSTATE_PL011)]
> pub struct PL011State {
> ..
> }
>
> I agree about traits for class methods, it's definitely cleaner. The
> lines blur here because we have REALIZE as a constant in order to make
> it nullable from the C side 🤔
Yes, I agree. Another factor to consider is the quality of error messages.
For what it's worth, this is the solution they use in Linux:
https://rust-for-linux.github.io/docs/macros/attr.vtable.html. In
short, they generate a HAS_REALIZE boolean const, and compute the
function pointer with something like
if Self::HAS_REALIZE { Some(Self::realize) } else { None }
Forgetting the #[vtable] attribute on the "impl" produces a decent
error message, too.
And here is the source:
https://rust-for-linux.github.io/docs/src/macros/vtable.rs.html. Linux
doesn't use proc_macro2 or quote so probably it would have to be
rewritten for QEMU, but it's small and easy to understand.
Even if it's *also* not the nicest, it may be worth adopting this
convention just for consistency among mixed C/Rust programs.
Paolo
Paolo
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2025-07-10 16:50 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-07-03 14:37 [PATCH RFC v2] rust: add qdev DeviceProperties derive macro Manos Pitsidianakis
2025-07-08 9:47 ` Paolo Bonzini
2025-07-10 9:40 ` Manos Pitsidianakis
2025-07-10 14:25 ` Paolo Bonzini
2025-07-10 14:47 ` Manos Pitsidianakis
2025-07-10 16:29 ` Paolo Bonzini
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).