* [PATCH v2 00/16] rust: QObject and QAPI bindings
@ 2026-01-08 13:10 Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 01/16] rust/qobject: add basic bindings Paolo Bonzini
` (19 more replies)
0 siblings, 20 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust
This adds two related parts of the Rust bindings:
- QAPI code generator that creates Rust structs from the JSON
description. The structs are *not* ABI compatible with the
C ones, instead they use native Rust data types.
- QObject bindings and (de)serialization support, which can be used to
convert QObjects to and from QAPI structs.
Unfortunately Rust code is not able to use visitors, other than by
creating an intermediate QObject. This is because of the different
architecture of serde vs. QAPI visitors, and because visitor's
dual-purpose functions, where the same function is used by both input and
output visitors, rely heavily on the structs using the same representation
as the visitor arguments (for example NUL-terminated strings).
The serde format implementation was co-authored by me and Marc-André.
Marc-André did all the bug fixing and integration testing.
As an example of how this would be used, the marshaling functions for
QMP commands would look like this:
fn qmp_marshal_query_stats(args: *mut QDict,
retp: *mut *mut QObject, errp: *mut *mut Error)
{
let qobj = unsafe { QObject::cloned_from_raw(args.cast()) };
let result = from_qobject::<StatsFilter>(qobj)
.map_err(anyhow::Error::from)
.and_then(qmp_query_stats)
.and_then(|ret| to_qobject::<Vec<StatsResult>>(ret).map_err(anyhow::Error::from));
match qmp_marshal_query_stats_rs(qobj) {
Ok(ret) => unsafe { *retp = ret.into_raw(); },
Err(e) => unsafe { crate::Error::from(e).propagate(errp) },
}
}
Despite the long v1->v2, the changes are mostly cosmetic; there are a few
small differences in the detection of types to be wrapped with Box<>,
because Vec<> is now considered to break cycles.
Available at branch rust-next of https://gitlab.com/bonzini/qemu (which
is now almost empty other than this series, yay).
Paolo
v1->v2, Rust:
- adjust comments in C code (include/qobject/qobject.h)
- improve or fix documentation in rust/util/src/qobject/mod.rs
- introduce a qobject! macro that is equivalent to QOBJECT()
- remark when functions are converted from C inlines
- rename from_int/from_uint/from_double macros
- fix incorrect "native: true" on serde and serde_core overrides
- rebase
qapi-gen:
- rewrite QAPISchemaIfCond.rsgen() to avoid messy typing
- new patches 10-11 to extend existing qapigen code
- rewritten rs_type() to avoid going through a C type first
- removed renaming of QAPICBackend
- reused C functions for case conversions
- clean up rs_name, move it to common.py (needed for rs_type() changes)
- Vec<> can break cycles and avoid boxing of structs
- restrict squashing of consecutive newlines to Rust code
- add copyright blurb
- annotate that using a separate qapi-gen backend is temporary
- drop #[derive(common::TryInto)] from enums
Marc-André Lureau (7):
rust/qobject: add Display/Debug
scripts/qapi: add QAPISchemaIfCond.rsgen()
scripts/qapi: generate high-level Rust bindings
scripts/rustc_args: add --no-strict-cfg
rust/util: build QAPI types
scripts/qapi: add serde attributes
rust/tests: QAPI integration tests
Paolo Bonzini (9):
rust/qobject: add basic bindings
subprojects: add serde
rust/qobject: add Serialize implementation
rust/qobject: add Serializer (to_qobject) implementation
rust/qobject: add Deserialize implementation
rust/qobject: add Deserializer (from_qobject) implementation
rust/qobject: add from/to JSON bindings for QObject
scripts/qapi: add QAPISchemaType.is_predefined
scripts/qapi: pull c_name from camel_to_upper to caller
docs/devel/rust.rst | 1 +
meson.build | 4 +-
include/qobject/qobject.h | 5 +-
rust/util/wrapper.h | 8 +
qapi/meson.build | 9 +
rust/Cargo.lock | 2 +
rust/Cargo.toml | 2 +
rust/meson.build | 4 +
rust/tests/meson.build | 21 +-
rust/tests/tests/integration.rs | 2 +
rust/tests/tests/qapi.rs | 444 +++++++++++++
rust/util/Cargo.toml | 2 +
rust/util/meson.build | 30 +-
rust/util/src/lib.rs | 4 +
rust/util/src/qobject/deserialize.rs | 134 ++++
rust/util/src/qobject/deserializer.rs | 371 +++++++++++
rust/util/src/qobject/error.rs | 58 ++
rust/util/src/qobject/mod.rs | 383 ++++++++++++
rust/util/src/qobject/serialize.rs | 59 ++
rust/util/src/qobject/serializer.rs | 585 ++++++++++++++++++
scripts/archive-source.sh | 3 +
scripts/make-release | 2 +-
scripts/qapi/backend.py | 25 +
scripts/qapi/common.py | 66 +-
scripts/qapi/rs.py | 61 ++
scripts/qapi/rs_types.py | 394 ++++++++++++
scripts/qapi/schema.py | 74 ++-
scripts/rust/rustc_args.py | 16 +-
subprojects/.gitignore | 3 +
.../packagefiles/serde-1-rs/meson.build | 36 ++
.../packagefiles/serde-1.0.226-include.patch | 16 +
.../packagefiles/serde_core-1-rs/meson.build | 25 +
.../serde_core-1.0.226-include.patch | 15 +
.../serde_derive-1-rs/meson.build | 35 ++
.../serde_derive-1.0.226-include.patch | 11 +
subprojects/serde-1-rs.wrap | 11 +
subprojects/serde_core-1-rs.wrap | 11 +
subprojects/serde_derive-1-rs.wrap | 11 +
38 files changed, 2910 insertions(+), 33 deletions(-)
create mode 100644 rust/tests/tests/integration.rs
create mode 100644 rust/tests/tests/qapi.rs
create mode 100644 rust/util/src/qobject/deserialize.rs
create mode 100644 rust/util/src/qobject/deserializer.rs
create mode 100644 rust/util/src/qobject/error.rs
create mode 100644 rust/util/src/qobject/mod.rs
create mode 100644 rust/util/src/qobject/serialize.rs
create mode 100644 rust/util/src/qobject/serializer.rs
create mode 100644 scripts/qapi/rs.py
create mode 100644 scripts/qapi/rs_types.py
create mode 100644 subprojects/packagefiles/serde-1-rs/meson.build
create mode 100644 subprojects/packagefiles/serde-1.0.226-include.patch
create mode 100644 subprojects/packagefiles/serde_core-1-rs/meson.build
create mode 100644 subprojects/packagefiles/serde_core-1.0.226-include.patch
create mode 100644 subprojects/packagefiles/serde_derive-1-rs/meson.build
create mode 100644 subprojects/packagefiles/serde_derive-1.0.226-include.patch
create mode 100644 subprojects/serde-1-rs.wrap
create mode 100644 subprojects/serde_core-1-rs.wrap
create mode 100644 subprojects/serde_derive-1-rs.wrap
--
2.52.0
^ permalink raw reply [flat|nested] 55+ messages in thread
* [PATCH v2 01/16] rust/qobject: add basic bindings
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-02-24 10:03 ` Markus Armbruster
2026-01-08 13:10 ` [PATCH v2 02/16] subprojects: add serde Paolo Bonzini
` (18 subsequent siblings)
19 siblings, 1 reply; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust, Zhao Liu
This is only a basic API, intended to be used by the serde traits.
Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
include/qobject/qobject.h | 5 +-
rust/util/wrapper.h | 7 +
rust/util/meson.build | 6 +-
rust/util/src/lib.rs | 4 +
rust/util/src/qobject/mod.rs | 331 +++++++++++++++++++++++++++++++++++
5 files changed, 350 insertions(+), 3 deletions(-)
create mode 100644 rust/util/src/qobject/mod.rs
diff --git a/include/qobject/qobject.h b/include/qobject/qobject.h
index 02f4c6a6eb2..567da7b6c51 100644
--- a/include/qobject/qobject.h
+++ b/include/qobject/qobject.h
@@ -35,7 +35,10 @@
#include "qemu/atomic.h"
#include "qapi/qapi-builtin-types.h"
-/* Not for use outside include/qobject/ */
+/*
+ * Not for use outside include/qobject/ (and Rust bindings, when they
+ * have to redo inline functions here).
+ */
struct QObjectBase_ {
QType type;
size_t refcnt;
diff --git a/rust/util/wrapper.h b/rust/util/wrapper.h
index b9ed68a01d8..0907dd59142 100644
--- a/rust/util/wrapper.h
+++ b/rust/util/wrapper.h
@@ -30,3 +30,10 @@ typedef enum memory_order {
#include "qemu/log.h"
#include "qemu/module.h"
#include "qemu/timer.h"
+#include "qobject/qnull.h"
+#include "qobject/qbool.h"
+#include "qobject/qnum.h"
+#include "qobject/qstring.h"
+#include "qobject/qobject.h"
+#include "qobject/qlist.h"
+#include "qobject/qdict.h"
diff --git a/rust/util/meson.build b/rust/util/meson.build
index 98629394afb..28593286134 100644
--- a/rust/util/meson.build
+++ b/rust/util/meson.build
@@ -37,8 +37,10 @@ _util_rs = static_library(
'src/prelude.rs',
'src/timer.rs',
],
- {'.': _util_bindings_inc_rs}
- ),
+ {'.': _util_bindings_inc_rs,
+ 'qobject': [
+ 'src/qobject/mod.rs',
+ ]}),
dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs],
)
diff --git a/rust/util/src/lib.rs b/rust/util/src/lib.rs
index 7d2de3ed811..96334466895 100644
--- a/rust/util/src/lib.rs
+++ b/rust/util/src/lib.rs
@@ -9,6 +9,10 @@
// for prelude-like modules
#[rustfmt::skip]
pub mod prelude;
+
+#[macro_use]
+pub mod qobject;
+
pub mod timer;
pub use error::{Error, Result, ResultExt};
diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
new file mode 100644
index 00000000000..5744870ad1c
--- /dev/null
+++ b/rust/util/src/qobject/mod.rs
@@ -0,0 +1,331 @@
+//! `QObject` bindings
+//!
+//! This module implements bindings for QEMU's `QObject` data structure.
+//! The bindings integrate with `serde`, which take the role of visitors
+//! in Rust code.
+
+#![deny(clippy::unwrap_used)]
+
+use std::{
+ cell::UnsafeCell,
+ ffi::{c_char, CString},
+ mem::ManuallyDrop,
+ ptr::{addr_of, addr_of_mut},
+ sync::atomic::{AtomicUsize, Ordering},
+};
+
+use common::assert_field_type;
+
+use crate::bindings;
+
+/// A wrapper for a C `QObject`.
+///
+/// Because `QObject` is not thread-safe, the safety of these bindings
+/// right now hinges on treating them as immutable. It is part of the
+/// contract with the `QObject` constructors that the Rust struct is
+/// only built after the contents are stable.
+///
+/// Only a bare bones API is public; production and consumption of `QObject`
+/// generally goes through `serde`.
+pub struct QObject(&'static UnsafeCell<bindings::QObject>);
+
+// SAFETY: the QObject API are not thread-safe other than reference counting;
+// but the Rust struct is only created once the contents are stable, and
+// therefore it obeys the aliased XOR mutable invariant.
+unsafe impl Send for QObject {}
+unsafe impl Sync for QObject {}
+
+// Since a QObject can be a floating-point value, and potentially a NaN,
+// do not implement Eq
+impl PartialEq for QObject {
+ fn eq(&self, other: &Self) -> bool {
+ unsafe { bindings::qobject_is_equal(self.0.get(), other.0.get()) }
+ }
+}
+
+impl QObject {
+ /// Construct a [`QObject`] from a C `QObjectBase` pointer.
+ /// The caller cedes its reference to the returned struct.
+ ///
+ /// # Safety
+ ///
+ /// The `QObjectBase` must not be changed from C code while
+ /// the Rust `QObject` lives
+ const unsafe fn from_base(p: *const bindings::QObjectBase_) -> Self {
+ QObject(unsafe { &*p.cast() })
+ }
+
+ /// Construct a [`QObject`] from a C `QObject` pointer.
+ /// The caller cedes its reference to the returned struct.
+ ///
+ /// # Safety
+ ///
+ /// The `QObject` must not be changed from C code while
+ /// the Rust `QObject` lives
+ pub const unsafe fn from_raw(p: *const bindings::QObject) -> Self {
+ QObject(unsafe { &*p.cast() })
+ }
+
+ /// Obtain a raw C pointer from a reference. `self` is consumed
+ /// without decreasing the reference count; therefore, the reference
+ /// is transferred to the `*mut bindings::QObject`.
+ pub fn into_raw(self) -> *mut bindings::QObject {
+ let src = ManuallyDrop::new(self);
+ src.0.get()
+ }
+
+ /// Construct a [`QObject`] from a C `QObjectBase` pointer.
+ /// The caller *does not* cede its reference to the returned struct.
+ ///
+ /// # Safety
+ ///
+ /// The `QObjectBase` must not be changed from C code while
+ /// the Rust `QObject` lives
+ unsafe fn cloned_from_base(p: *const bindings::QObjectBase_) -> Self {
+ let orig = unsafe { ManuallyDrop::new(QObject::from_base(p)) };
+ (*orig).clone()
+ }
+
+ /// Construct a [`QObject`] from a C `QObject` pointer.
+ /// The caller *does not* cede its reference to the returned struct.
+ ///
+ /// # Safety
+ ///
+ /// The `QObject` must not be changed from C code while
+ /// the Rust `QObject` lives
+ pub unsafe fn cloned_from_raw(p: *const bindings::QObject) -> Self {
+ let orig = unsafe { ManuallyDrop::new(QObject::from_raw(p)) };
+ (*orig).clone()
+ }
+
+ fn refcnt(&self) -> &AtomicUsize {
+ assert_field_type!(bindings::QObjectBase_, refcnt, usize);
+ let qobj = self.0.get();
+ unsafe { AtomicUsize::from_ptr(addr_of_mut!((*qobj).base.refcnt)) }
+ }
+}
+
+/// Rust equivalent of the C `QOBJECT` macro; for internal use only, because
+/// all access should go through `From` (which already returns [`QObject`]
+/// or serde.
+macro_rules! qobject {
+ ($qobj:expr) => {{
+ let qobj: &bindings::QObjectBase_ = &$qobj.base;
+ // SAFETY: this `let` guarantees that either $qobj is a reference
+ // (not a raw pointer), or we're in an outer unsafe block
+ unsafe { QObject::from_base(qobj) }
+ }};
+}
+
+impl From<()> for QObject {
+ fn from(_null: ()) -> Self {
+ // Conversion of the C inline `qnull` function
+ unsafe { QObject::cloned_from_base(addr_of!(bindings::qnull_.base)) }
+ }
+}
+
+impl<T> From<Option<T>> for QObject
+where
+ QObject: From<T>,
+{
+ fn from(o: Option<T>) -> Self {
+ o.map_or_else(|| ().into(), Into::into)
+ }
+}
+
+impl From<bool> for QObject {
+ fn from(b: bool) -> Self {
+ let qobj = unsafe { &*bindings::qbool_from_bool(b) };
+ qobject!(qobj)
+ }
+}
+
+macro_rules! impl_from_return_qnum_int {
+ ($t:ty) => {
+ impl From<$t> for QObject {
+ fn from(n: $t) -> Self {
+ let qobj = unsafe { &*bindings::qnum_from_int(n.into()) };
+ qobject!(qobj)
+ }
+ }
+ };
+}
+
+impl_from_return_qnum_int!(i8);
+impl_from_return_qnum_int!(i16);
+impl_from_return_qnum_int!(i32);
+impl_from_return_qnum_int!(i64);
+
+macro_rules! impl_from_return_qnum_uint {
+ ($t:ty) => {
+ impl From<$t> for QObject {
+ fn from(n: $t) -> Self {
+ let qobj = unsafe { &*bindings::qnum_from_uint(n.into()) };
+ qobject!(qobj)
+ }
+ }
+ };
+}
+
+impl_from_return_qnum_uint!(u8);
+impl_from_return_qnum_uint!(u16);
+impl_from_return_qnum_uint!(u32);
+impl_from_return_qnum_uint!(u64);
+
+macro_rules! impl_from_return_qnum_double {
+ ($t:ty) => {
+ impl From<$t> for QObject {
+ fn from(n: $t) -> Self {
+ let qobj = unsafe { &*bindings::qnum_from_double(n.into()) };
+ qobject!(qobj)
+ }
+ }
+ };
+}
+
+impl_from_return_qnum_double!(f32);
+impl_from_return_qnum_double!(f64);
+
+impl From<CString> for QObject {
+ fn from(s: CString) -> Self {
+ let qobj = unsafe { &*bindings::qstring_from_str(s.as_ptr()) };
+ qobject!(qobj)
+ }
+}
+
+impl<A> FromIterator<A> for QObject
+where
+ Self: From<A>,
+{
+ fn from_iter<I: IntoIterator<Item = A>>(it: I) -> Self {
+ let qlist = unsafe { &mut *bindings::qlist_new() };
+ for elem in it {
+ let elem: QObject = elem.into();
+ let elem = elem.into_raw();
+ unsafe {
+ bindings::qlist_append_obj(qlist, elem);
+ }
+ }
+ qobject!(qlist)
+ }
+}
+
+impl<A> FromIterator<(CString, A)> for QObject
+where
+ Self: From<A>,
+{
+ fn from_iter<I: IntoIterator<Item = (CString, A)>>(it: I) -> Self {
+ let qdict = unsafe { &mut *bindings::qdict_new() };
+ for (key, val) in it {
+ let val: QObject = val.into();
+ let val = val.into_raw();
+ unsafe {
+ bindings::qdict_put_obj(qdict, key.as_ptr().cast::<c_char>(), val);
+ }
+ }
+ qobject!(qdict)
+ }
+}
+
+impl Clone for QObject {
+ fn clone(&self) -> Self {
+ self.refcnt().fetch_add(1, Ordering::Acquire);
+ QObject(self.0)
+ }
+}
+
+impl Drop for QObject {
+ fn drop(&mut self) {
+ if self.refcnt().fetch_sub(1, Ordering::Release) == 1 {
+ unsafe {
+ bindings::qobject_destroy(self.0.get());
+ }
+ }
+ }
+}
+
+#[allow(unused)]
+macro_rules! match_qobject {
+ (@internal ($qobj:expr) =>
+ $(() => $unit:expr,)?
+ $(bool($boolvar:tt) => $bool:expr,)?
+ $(i64($i64var:tt) => $i64:expr,)?
+ $(u64($u64var:tt) => $u64:expr,)?
+ $(f64($f64var:tt) => $f64:expr,)?
+ $(CStr($cstrvar:tt) => $cstr:expr,)?
+ $(QList($qlistvar:tt) => $qlist:expr,)?
+ $(QDict($qdictvar:tt) => $qdict:expr,)?
+ $(_ => $other:expr,)?
+ ) => {
+ loop {
+ let qobj_ = $qobj.0.get();
+ match unsafe { &* qobj_ }.base.type_ {
+ $($crate::bindings::QTYPE_QNULL => break $unit,)?
+ $($crate::bindings::QTYPE_QBOOL => break {
+ let qbool__: *mut $crate::bindings::QBool = qobj_.cast();
+ let $boolvar = unsafe { (&*qbool__).value };
+ $bool
+ },)?
+ $crate::bindings::QTYPE_QNUM => {
+ let qnum__: *mut $crate::bindings::QNum = qobj_.cast();
+ let qnum__ = unsafe { &*qnum__ };
+ match qnum__.kind {
+ $crate::bindings::QNUM_I64 |
+ $crate::bindings::QNUM_U64 |
+ $crate::bindings::QNUM_DOUBLE => {}
+ _ => {
+ panic!("unreachable");
+ }
+ }
+
+ match qnum__.kind {
+ $($crate::bindings::QNUM_I64 => break {
+ let $i64var = unsafe { qnum__.u.i64_ };
+ $i64
+ },)?
+ $($crate::bindings::QNUM_U64 => break {
+ let $u64var = unsafe { qnum__.u.u64_ };
+ $u64
+ },)?
+ $($crate::bindings::QNUM_DOUBLE => break {
+ let $f64var = unsafe { qnum__.u.dbl };
+ $f64
+ },)?
+ _ => {} // evaluate $other
+ }
+ },
+ $($crate::bindings::QTYPE_QSTRING => break {
+ let qstring__: *mut $crate::bindings::QString = qobj_.cast();
+ let $cstrvar = unsafe { ::core::ffi::CStr::from_ptr((&*qstring__).string) };
+ $cstr
+ },)?
+ $($crate::bindings::QTYPE_QLIST => break {
+ let qlist__: *mut $crate::bindings::QList = qobj_.cast();
+ let $qlistvar = unsafe { &*qlist__ };
+ $qlist
+ },)?
+ $($crate::bindings::QTYPE_QDICT => break {
+ let qdict__: *mut $crate::bindings::QDict = qobj_.cast();
+ let $qdictvar = unsafe { &*qdict__ };
+ $qdict
+ },)?
+ _ => ()
+ };
+ $(break $other;)?
+ #[allow(unreachable_code)]
+ {
+ panic!("unreachable");
+ }
+ }
+ };
+
+ // first cleanup the syntax a bit, checking that there's at least
+ // one pattern and always adding a trailing comma
+ (($qobj:expr) =>
+ $($type:tt$(($val:tt))? => $code:expr ),+ $(,)?) => {
+ match_qobject!(@internal ($qobj) =>
+ $($type $(($val))? => $code,)+)
+ };
+}
+#[allow(unused_imports)]
+use match_qobject;
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH v2 02/16] subprojects: add serde
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 01/16] rust/qobject: add basic bindings Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 03/16] rust/qobject: add Serialize implementation Paolo Bonzini
` (17 subsequent siblings)
19 siblings, 0 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust, Zhao Liu
Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/Cargo.toml | 2 ++
rust/meson.build | 4 +++
scripts/archive-source.sh | 3 ++
scripts/make-release | 2 +-
subprojects/.gitignore | 3 ++
.../packagefiles/serde-1-rs/meson.build | 36 +++++++++++++++++++
.../packagefiles/serde-1.0.226-include.patch | 16 +++++++++
.../packagefiles/serde_core-1-rs/meson.build | 25 +++++++++++++
.../serde_core-1.0.226-include.patch | 15 ++++++++
.../serde_derive-1-rs/meson.build | 35 ++++++++++++++++++
.../serde_derive-1.0.226-include.patch | 11 ++++++
subprojects/serde-1-rs.wrap | 11 ++++++
subprojects/serde_core-1-rs.wrap | 11 ++++++
subprojects/serde_derive-1-rs.wrap | 11 ++++++
14 files changed, 184 insertions(+), 1 deletion(-)
create mode 100644 subprojects/packagefiles/serde-1-rs/meson.build
create mode 100644 subprojects/packagefiles/serde-1.0.226-include.patch
create mode 100644 subprojects/packagefiles/serde_core-1-rs/meson.build
create mode 100644 subprojects/packagefiles/serde_core-1.0.226-include.patch
create mode 100644 subprojects/packagefiles/serde_derive-1-rs/meson.build
create mode 100644 subprojects/packagefiles/serde_derive-1.0.226-include.patch
create mode 100644 subprojects/serde-1-rs.wrap
create mode 100644 subprojects/serde_core-1-rs.wrap
create mode 100644 subprojects/serde_derive-1-rs.wrap
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index ace0baf9bd7..45ebfa693a4 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -20,6 +20,8 @@ anyhow = "~1.0"
foreign = "~0.3.1"
libc = "0.2.162"
glib-sys = { version = "0.21.2", features = ["v2_66"] }
+serde = "1.0.226"
+serde_derive = "1.0.226"
[workspace.lints.rust]
unexpected_cfgs = { level = "deny", check-cfg = ['cfg(MESON)'] }
diff --git a/rust/meson.build b/rust/meson.build
index bacb7879102..bd995e2b955 100644
--- a/rust/meson.build
+++ b/rust/meson.build
@@ -11,6 +11,8 @@ subproject('foreign-0.3-rs', required: true)
subproject('glib-sys-0.21-rs', required: true)
subproject('libc-0.2-rs', required: true)
subproject('probe-0.5-rs', required: true)
+subproject('serde-1-rs', required: true)
+subproject('serde_derive-1-rs', required: true)
anyhow_rs = dependency('anyhow-1-rs')
bilge_rs = dependency('bilge-0.2-rs')
@@ -19,6 +21,8 @@ foreign_rs = dependency('foreign-0.3-rs')
glib_sys_rs = dependency('glib-sys-0.21-rs')
libc_rs = dependency('libc-0.2-rs')
probe_rs = dependency('probe-0.5-rs')
+serde_rs = dependency('serde-1-rs')
+serde_derive_rs = dependency('serde_derive-1-rs', native: true)
subproject('proc-macro2-1-rs', required: true)
subproject('quote-1-rs', required: true)
diff --git a/scripts/archive-source.sh b/scripts/archive-source.sh
index a37acab524e..ae768cf99f6 100755
--- a/scripts/archive-source.sh
+++ b/scripts/archive-source.sh
@@ -46,6 +46,9 @@ subprojects=(
proc-macro-error-attr-1-rs
proc-macro2-1-rs
quote-1-rs
+ serde-1-rs
+ serde_core-1-rs
+ serde_derive-1-rs
syn-2-rs
unicode-ident-1-rs
)
diff --git a/scripts/make-release b/scripts/make-release
index 5f54b0e7939..23fef08bcf3 100755
--- a/scripts/make-release
+++ b/scripts/make-release
@@ -44,7 +44,7 @@ SUBPROJECTS="libvfio-user keycodemapdb berkeley-softfloat-3
bilge-impl-0.2-rs either-1-rs foreign-0.3-rs itertools-0.11-rs
libc-0.2-rs probe-0.5-rs proc-macro2-1-rs
proc-macro-error-1-rs proc-macro-error-attr-1-rs quote-1-rs
- syn-2-rs unicode-ident-1-rs"
+ serde-1-rs serde_core-1-rs serde_derive-1-rs syn-2-rs unicode-ident-1-rs"
src="$1"
version="$2"
diff --git a/subprojects/.gitignore b/subprojects/.gitignore
index 011ce4dc3b7..2cdb0a6a910 100644
--- a/subprojects/.gitignore
+++ b/subprojects/.gitignore
@@ -21,6 +21,9 @@
/proc-macro-error-attr-*
/proc-macro*
/quote-*
+/serde-*
+/serde_core-*
+/serde_derive-*
/syn-*
/unicode-ident-*
diff --git a/subprojects/packagefiles/serde-1-rs/meson.build b/subprojects/packagefiles/serde-1-rs/meson.build
new file mode 100644
index 00000000000..775e0120f24
--- /dev/null
+++ b/subprojects/packagefiles/serde-1-rs/meson.build
@@ -0,0 +1,36 @@
+project('serde-1-rs', 'rust',
+ meson_version: '>=1.5.0',
+ version: '1.0.226',
+ license: 'MIT OR Apache-2.0',
+ default_options: [])
+
+subproject('serde_core-1-rs', required: true)
+subproject('serde_derive-1-rs', required: true)
+
+serde_core_dep = dependency('serde_core-1-rs')
+serde_derive_dep = dependency('serde_derive-1-rs')
+
+_serde_rs = static_library(
+ 'serde',
+ files('src/lib.rs'),
+ gnu_symbol_visibility: 'hidden',
+ override_options: ['rust_std=2021', 'build.rust_std=2021'],
+ rust_abi: 'rust',
+ rust_args: [
+ '--cap-lints', 'allow',
+ '--cfg', 'feature="alloc"',
+ '--cfg', 'feature="std"',
+ '--cfg', 'feature="derive"',
+ ],
+ dependencies: [
+ serde_core_dep,
+ serde_derive_dep,
+ ]
+)
+
+serde_dep = declare_dependency(
+ link_with: _serde_rs,
+ dependencies: serde_derive_dep,
+)
+
+meson.override_dependency('serde-1-rs', serde_dep)
diff --git a/subprojects/packagefiles/serde-1.0.226-include.patch b/subprojects/packagefiles/serde-1.0.226-include.patch
new file mode 100644
index 00000000000..92878136878
--- /dev/null
+++ b/subprojects/packagefiles/serde-1.0.226-include.patch
@@ -0,0 +1,16 @@
+--- a/src/lib.rs 2025-09-23 13:41:09.327582205 +0200
++++ b/src/lib.rs 2025-09-23 13:41:23.043271856 +0200
+@@ -241,7 +241,12 @@
+ #[doc(hidden)]
+ mod private;
+
+-include!(concat!(env!("OUT_DIR"), "/private.rs"));
++#[doc(hidden)]
++pub mod __private_MESON {
++ #[doc(hidden)]
++ pub use crate::private::*;
++}
++use serde_core::__private_MESON as serde_core_private;
+
+ // Re-export #[derive(Serialize, Deserialize)].
+ //
diff --git a/subprojects/packagefiles/serde_core-1-rs/meson.build b/subprojects/packagefiles/serde_core-1-rs/meson.build
new file mode 100644
index 00000000000..79c36f6b70e
--- /dev/null
+++ b/subprojects/packagefiles/serde_core-1-rs/meson.build
@@ -0,0 +1,25 @@
+project('serde_core-1-rs', 'rust',
+ meson_version: '>=1.5.0',
+ version: '1.0.226',
+ license: 'MIT OR Apache-2.0',
+ default_options: [])
+
+_serde_core_rs = static_library(
+ 'serde_core',
+ files('src/lib.rs'),
+ gnu_symbol_visibility: 'hidden',
+ override_options: ['rust_std=2021', 'build.rust_std=2021'],
+ rust_abi: 'rust',
+ rust_args: [
+ '--cap-lints', 'allow',
+ '--cfg', 'feature="alloc"',
+ '--cfg', 'feature="result"',
+ '--cfg', 'feature="std"',
+ ],
+)
+
+serde_core_dep = declare_dependency(
+ link_with: _serde_core_rs,
+)
+
+meson.override_dependency('serde_core-1-rs', serde_core_dep)
diff --git a/subprojects/packagefiles/serde_core-1.0.226-include.patch b/subprojects/packagefiles/serde_core-1.0.226-include.patch
new file mode 100644
index 00000000000..d1321dfe272
--- /dev/null
+++ b/subprojects/packagefiles/serde_core-1.0.226-include.patch
@@ -0,0 +1,15 @@
+--- a/src/lib.rs 2025-09-23 13:32:40.872421170 +0200
++++ b/src/lib.rs 2025-09-23 13:32:52.181098856 +0200
+@@ -263,7 +263,11 @@
+ pub use core::result::Result;
+ }
+
+-include!(concat!(env!("OUT_DIR"), "/private.rs"));
++#[doc(hidden)]
++pub mod __private_MESON {
++ #[doc(hidden)]
++ pub use crate::private::*;
++}
+
+ #[cfg(all(not(feature = "std"), no_core_error))]
+ mod std_error;
diff --git a/subprojects/packagefiles/serde_derive-1-rs/meson.build b/subprojects/packagefiles/serde_derive-1-rs/meson.build
new file mode 100644
index 00000000000..6c1001a844a
--- /dev/null
+++ b/subprojects/packagefiles/serde_derive-1-rs/meson.build
@@ -0,0 +1,35 @@
+project('serde_derive-1-rs', 'rust',
+ meson_version: '>=1.5.0',
+ version: '1.0.226',
+ license: 'MIT OR Apache-2.0',
+ default_options: [])
+
+subproject('quote-1-rs', required: true)
+subproject('syn-2-rs', required: true)
+subproject('proc-macro2-1-rs', required: true)
+
+quote_dep = dependency('quote-1-rs', native: true)
+syn_dep = dependency('syn-2-rs', native: true)
+proc_macro2_dep = dependency('proc-macro2-1-rs', native: true)
+
+rust = import('rust')
+
+_serde_derive_rs = rust.proc_macro(
+ 'serde_derive',
+ files('src/lib.rs'),
+ override_options: ['rust_std=2021', 'build.rust_std=2021'],
+ rust_args: [
+ '--cap-lints', 'allow',
+ ],
+ dependencies: [
+ quote_dep,
+ syn_dep,
+ proc_macro2_dep,
+ ],
+)
+
+serde_derive_dep = declare_dependency(
+ link_with: _serde_derive_rs,
+)
+
+meson.override_dependency('serde_derive-1-rs', serde_derive_dep)
diff --git a/subprojects/packagefiles/serde_derive-1.0.226-include.patch b/subprojects/packagefiles/serde_derive-1.0.226-include.patch
new file mode 100644
index 00000000000..81d65564d29
--- /dev/null
+++ b/subprojects/packagefiles/serde_derive-1.0.226-include.patch
@@ -0,0 +1,11 @@
+--- a/src/lib.rs 2025-09-23 13:51:51.540191923 +0200
++++ b/src/lib.rs 2025-09-23 13:52:07.690060195 +0200
+@@ -98,7 +98,7 @@
+ impl private {
+ fn ident(&self) -> Ident {
+ Ident::new(
+- concat!("__private", env!("CARGO_PKG_VERSION_PATCH")),
++ "__private_MESON",
+ Span::call_site(),
+ )
+ }
diff --git a/subprojects/serde-1-rs.wrap b/subprojects/serde-1-rs.wrap
new file mode 100644
index 00000000000..56746dd0f43
--- /dev/null
+++ b/subprojects/serde-1-rs.wrap
@@ -0,0 +1,11 @@
+[wrap-file]
+directory = serde-1.0.226
+source_url = https://crates.io/api/v1/crates/serde/1.0.226/download
+source_filename = serde-1.0.226.0.tar.gz
+source_hash = 0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd
+#method = cargo
+diff_files = serde-1.0.226-include.patch
+patch_directory = serde-1-rs
+
+# bump this version number on every change to meson.build or the patches:
+# v1
diff --git a/subprojects/serde_core-1-rs.wrap b/subprojects/serde_core-1-rs.wrap
new file mode 100644
index 00000000000..3692e754935
--- /dev/null
+++ b/subprojects/serde_core-1-rs.wrap
@@ -0,0 +1,11 @@
+[wrap-file]
+directory = serde_core-1.0.226
+source_url = https://crates.io/api/v1/crates/serde_core/1.0.226/download
+source_filename = serde_core-1.0.226.0.tar.gz
+source_hash = ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4
+#method = cargo
+diff_files = serde_core-1.0.226-include.patch
+patch_directory = serde_core-1-rs
+
+# bump this version number on every change to meson.build or the patches:
+# v1
diff --git a/subprojects/serde_derive-1-rs.wrap b/subprojects/serde_derive-1-rs.wrap
new file mode 100644
index 00000000000..00c92dc79cf
--- /dev/null
+++ b/subprojects/serde_derive-1-rs.wrap
@@ -0,0 +1,11 @@
+[wrap-file]
+directory = serde_derive-1.0.226
+source_url = https://crates.io/api/v1/crates/serde_derive/1.0.226/download
+source_filename = serde_derive-1.0.226.0.tar.gz
+source_hash = 8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33
+#method = cargo
+diff_files = serde_derive-1.0.226-include.patch
+patch_directory = serde_derive-1-rs
+
+# bump this version number on every change to meson.build or the patches:
+# v1
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH v2 03/16] rust/qobject: add Serialize implementation
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 01/16] rust/qobject: add basic bindings Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 02/16] subprojects: add serde Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-02-24 10:29 ` Markus Armbruster
2026-01-08 13:10 ` [PATCH v2 04/16] rust/qobject: add Serializer (to_qobject) implementation Paolo Bonzini
` (16 subsequent siblings)
19 siblings, 1 reply; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust, Zhao Liu
This allows QObject to be converted to other formats, for example
JSON via serde_json.
This is not too useful, since QObjects are consumed by
C code or deserialized into structs, but it can be used for testing
and it is part of the full implementation of a serde format.
Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/Cargo.lock | 1 +
rust/util/Cargo.toml | 1 +
rust/util/meson.build | 3 +-
rust/util/src/qobject/mod.rs | 4 +-
rust/util/src/qobject/serialize.rs | 59 ++++++++++++++++++++++++++++++
5 files changed, 65 insertions(+), 3 deletions(-)
create mode 100644 rust/util/src/qobject/serialize.rs
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 42ae857fe53..b6c9f934140 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -453,6 +453,7 @@ dependencies = [
"foreign",
"glib-sys",
"libc",
+ "serde",
]
[[package]]
diff --git a/rust/util/Cargo.toml b/rust/util/Cargo.toml
index 85f91436545..554004816eb 100644
--- a/rust/util/Cargo.toml
+++ b/rust/util/Cargo.toml
@@ -17,6 +17,7 @@ anyhow = { workspace = true }
foreign = { workspace = true }
glib-sys = { workspace = true }
libc = { workspace = true }
+serde = { workspace = true }
common = { path = "../common" }
[lints]
diff --git a/rust/util/meson.build b/rust/util/meson.build
index 28593286134..166a792bce7 100644
--- a/rust/util/meson.build
+++ b/rust/util/meson.build
@@ -40,8 +40,9 @@ _util_rs = static_library(
{'.': _util_bindings_inc_rs,
'qobject': [
'src/qobject/mod.rs',
+ 'src/qobject/serialize.rs',
]}),
- dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs],
+ dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs, serde_rs],
)
util_rs = declare_dependency(link_with: [_util_rs], dependencies: [qemuutil, qom])
diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
index 5744870ad1c..7b9f0516fe1 100644
--- a/rust/util/src/qobject/mod.rs
+++ b/rust/util/src/qobject/mod.rs
@@ -6,6 +6,8 @@
#![deny(clippy::unwrap_used)]
+mod serialize;
+
use std::{
cell::UnsafeCell,
ffi::{c_char, CString},
@@ -244,7 +246,6 @@ fn drop(&mut self) {
}
}
-#[allow(unused)]
macro_rules! match_qobject {
(@internal ($qobj:expr) =>
$(() => $unit:expr,)?
@@ -327,5 +328,4 @@ macro_rules! match_qobject {
$($type $(($val))? => $code,)+)
};
}
-#[allow(unused_imports)]
use match_qobject;
diff --git a/rust/util/src/qobject/serialize.rs b/rust/util/src/qobject/serialize.rs
new file mode 100644
index 00000000000..34ec3847c1d
--- /dev/null
+++ b/rust/util/src/qobject/serialize.rs
@@ -0,0 +1,59 @@
+//! `QObject` serialization
+//!
+//! This module implements the [`Serialize`] trait for `QObject`,
+//! allowing it to be converted to other formats, for example
+//! JSON.
+
+use std::{ffi::CStr, mem::ManuallyDrop, ptr::addr_of};
+
+use serde::ser::{self, Serialize, SerializeMap, SerializeSeq};
+
+use super::{match_qobject, QObject};
+use crate::bindings;
+
+impl Serialize for QObject {
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: ::serde::Serializer,
+ {
+ match_qobject! { (self) =>
+ () => serializer.serialize_unit(),
+ bool(b) => serializer.serialize_bool(b),
+ i64(i) => serializer.serialize_i64(i),
+ u64(u) => serializer.serialize_u64(u),
+ f64(f) => serializer.serialize_f64(f),
+ CStr(cstr) => cstr.to_str().map_or_else(
+ |_| Err(ser::Error::custom("invalid UTF-8 in QString")),
+ |s| serializer.serialize_str(s),
+ ),
+ QList(l) => {
+ let mut node_ptr = unsafe { l.head.tqh_first };
+ let mut state = serializer.serialize_seq(None)?;
+ while !node_ptr.is_null() {
+ let node = unsafe { &*node_ptr };
+ let elem = unsafe { ManuallyDrop::new(QObject::from_raw(addr_of!(*node.value))) };
+ state.serialize_element(&*elem)?;
+ node_ptr = unsafe { node.next.tqe_next };
+ }
+ state.end()
+ },
+ QDict(d) => {
+ let mut state = serializer.serialize_map(Some(d.size))?;
+ let mut e_ptr = unsafe { bindings::qdict_first(d) };
+ while !e_ptr.is_null() {
+ let e = unsafe { &*e_ptr };
+ let key = unsafe { CStr::from_ptr(e.key) };
+ key.to_str().map_or_else(
+ |_| Err(ser::Error::custom("invalid UTF-8 in key")),
+ |k| state.serialize_key(k),
+ )?;
+ let value = unsafe { ManuallyDrop::new(QObject::from_raw(addr_of!(*e.value))) };
+ state.serialize_value(&*value)?;
+ e_ptr = unsafe { bindings::qdict_next(d, e) };
+ }
+ state.end()
+ }
+ }
+ }
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH v2 04/16] rust/qobject: add Serializer (to_qobject) implementation
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (2 preceding siblings ...)
2026-01-08 13:10 ` [PATCH v2 03/16] rust/qobject: add Serialize implementation Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 05/16] rust/qobject: add Deserialize implementation Paolo Bonzini
` (15 subsequent siblings)
19 siblings, 0 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust, Zhao Liu
This allows creating QObject from any serializable data structure.
Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/util/meson.build | 2 +
rust/util/src/qobject/error.rs | 52 +++
rust/util/src/qobject/mod.rs | 4 +
rust/util/src/qobject/serializer.rs | 585 ++++++++++++++++++++++++++++
4 files changed, 643 insertions(+)
create mode 100644 rust/util/src/qobject/error.rs
create mode 100644 rust/util/src/qobject/serializer.rs
diff --git a/rust/util/meson.build b/rust/util/meson.build
index 166a792bce7..7829364c09a 100644
--- a/rust/util/meson.build
+++ b/rust/util/meson.build
@@ -40,6 +40,8 @@ _util_rs = static_library(
{'.': _util_bindings_inc_rs,
'qobject': [
'src/qobject/mod.rs',
+ 'src/qobject/error.rs',
+ 'src/qobject/serializer.rs',
'src/qobject/serialize.rs',
]}),
dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs, serde_rs],
diff --git a/rust/util/src/qobject/error.rs b/rust/util/src/qobject/error.rs
new file mode 100644
index 00000000000..5212e65c4f7
--- /dev/null
+++ b/rust/util/src/qobject/error.rs
@@ -0,0 +1,52 @@
+//! Error data type for `QObject`'s `serde` integration
+
+use std::{
+ ffi::NulError,
+ fmt::{self, Display},
+ str::Utf8Error,
+};
+
+use serde::ser;
+
+#[derive(Debug)]
+pub enum Error {
+ Custom(String),
+ KeyMustBeAString,
+ InvalidUtf8,
+ NulEncountered,
+ NumberOutOfRange,
+}
+
+impl ser::Error for Error {
+ fn custom<T: Display>(msg: T) -> Self {
+ Error::Custom(msg.to_string())
+ }
+}
+
+impl From<NulError> for Error {
+ fn from(_: NulError) -> Self {
+ Error::NulEncountered
+ }
+}
+
+impl From<Utf8Error> for Error {
+ fn from(_: Utf8Error) -> Self {
+ Error::InvalidUtf8
+ }
+}
+
+impl Display for Error {
+ fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Error::Custom(msg) => formatter.write_str(msg),
+ Error::KeyMustBeAString => formatter.write_str("key must be a string"),
+ Error::InvalidUtf8 => formatter.write_str("invalid UTF-8 in string"),
+ Error::NulEncountered => formatter.write_str("NUL character in string"),
+ Error::NumberOutOfRange => formatter.write_str("number out of range"),
+ }
+ }
+}
+
+impl std::error::Error for Error {}
+
+pub type Result<T> = std::result::Result<T, Error>;
diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
index 7b9f0516fe1..bfa3e9e34bb 100644
--- a/rust/util/src/qobject/mod.rs
+++ b/rust/util/src/qobject/mod.rs
@@ -6,7 +6,9 @@
#![deny(clippy::unwrap_used)]
+mod error;
mod serialize;
+mod serializer;
use std::{
cell::UnsafeCell,
@@ -17,6 +19,8 @@
};
use common::assert_field_type;
+pub use error::{Error, Result};
+pub use serializer::to_qobject;
use crate::bindings;
diff --git a/rust/util/src/qobject/serializer.rs b/rust/util/src/qobject/serializer.rs
new file mode 100644
index 00000000000..08730855731
--- /dev/null
+++ b/rust/util/src/qobject/serializer.rs
@@ -0,0 +1,585 @@
+//! `QObject` serializer
+//!
+//! This module implements a [`Serializer`](serde::ser::Serializer) that
+//! produces `QObject`s, allowing them to be created from serializable data
+//! structures (such as primitive data types, or structs that implement
+//! `Serialize`).
+
+use std::ffi::CString;
+
+use serde::ser::{Impossible, Serialize};
+
+use super::{
+ error::{Error, Result},
+ QObject,
+};
+
+pub struct SerializeTupleVariant {
+ name: CString,
+ vec: Vec<QObject>,
+}
+
+impl serde::ser::SerializeTupleVariant for SerializeTupleVariant {
+ type Ok = QObject;
+ type Error = Error;
+
+ fn serialize_field<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ self.vec.push(to_qobject(value)?);
+ Ok(())
+ }
+
+ fn end(self) -> Result<QObject> {
+ let SerializeTupleVariant { name, vec, .. } = self;
+
+ // TODO: insert elements one at a time
+ let list = QObject::from_iter(vec);
+
+ // serde by default represents enums as a single-entry object, with
+ // the variant stored in the key ("external tagging"). Internal tagging
+ // is implemented by the struct that requests it, not by the serializer.
+ let map = [(name, list)];
+ Ok(QObject::from_iter(map))
+ }
+}
+
+pub struct SerializeStructVariant {
+ name: CString,
+ vec: Vec<(CString, QObject)>,
+}
+
+impl serde::ser::SerializeStructVariant for SerializeStructVariant {
+ type Ok = QObject;
+ type Error = Error;
+
+ fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ self.vec.push((CString::new(key)?, to_qobject(value)?));
+ Ok(())
+ }
+
+ fn end(self) -> Result<QObject> {
+ // TODO: insert keys one at a time
+ let SerializeStructVariant { name, vec, .. } = self;
+ let list = QObject::from_iter(vec);
+
+ // serde by default represents enums as a single-entry object, with
+ // the variant stored in the key ("external tagging"). Internal tagging
+ // is implemented by the struct that requests it, not by the serializer.
+ let map = [(name, list)];
+ Ok(QObject::from_iter(map))
+ }
+}
+
+pub struct SerializeVec {
+ vec: Vec<QObject>,
+}
+
+impl serde::ser::SerializeSeq for SerializeVec {
+ type Ok = QObject;
+ type Error = Error;
+
+ fn serialize_element<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ self.vec.push(to_qobject(value)?);
+ Ok(())
+ }
+
+ fn end(self) -> Result<QObject> {
+ // TODO: insert elements one at a time
+ let SerializeVec { vec, .. } = self;
+ let list = QObject::from_iter(vec);
+ Ok(list)
+ }
+}
+
+impl serde::ser::SerializeTuple for SerializeVec {
+ type Ok = QObject;
+ type Error = Error;
+
+ fn serialize_element<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ serde::ser::SerializeSeq::serialize_element(self, value)
+ }
+
+ fn end(self) -> Result<QObject> {
+ serde::ser::SerializeSeq::end(self)
+ }
+}
+
+impl serde::ser::SerializeTupleStruct for SerializeVec {
+ type Ok = QObject;
+ type Error = Error;
+
+ fn serialize_field<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ serde::ser::SerializeSeq::serialize_element(self, value)
+ }
+
+ fn end(self) -> Result<QObject> {
+ serde::ser::SerializeSeq::end(self)
+ }
+}
+
+struct MapKeySerializer;
+
+impl serde::Serializer for MapKeySerializer {
+ type Ok = CString;
+ type Error = Error;
+
+ type SerializeSeq = Impossible<CString, Error>;
+ type SerializeTuple = Impossible<CString, Error>;
+ type SerializeTupleStruct = Impossible<CString, Error>;
+ type SerializeTupleVariant = Impossible<CString, Error>;
+ type SerializeMap = Impossible<CString, Error>;
+ type SerializeStruct = Impossible<CString, Error>;
+ type SerializeStructVariant = Impossible<CString, Error>;
+
+ #[inline]
+ fn serialize_unit_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ ) -> Result<CString> {
+ Ok(CString::new(variant)?)
+ }
+
+ #[inline]
+ fn serialize_newtype_struct<T>(self, _name: &'static str, value: &T) -> Result<CString>
+ where
+ T: ?Sized + Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_bool(self, _value: bool) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_i8(self, _value: i8) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_i16(self, _value: i16) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_i32(self, _value: i32) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_i64(self, _value: i64) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_i128(self, _value: i128) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_u8(self, _value: u8) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_u16(self, _value: u16) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_u32(self, _value: u32) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_u64(self, _value: u64) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_u128(self, _value: u128) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_f32(self, _value: f32) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_f64(self, _value: f64) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ #[inline]
+ fn serialize_char(self, _value: char) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ #[inline]
+ fn serialize_str(self, value: &str) -> Result<CString> {
+ Ok(CString::new(value)?)
+ }
+
+ fn serialize_bytes(self, value: &[u8]) -> Result<CString> {
+ Ok(CString::new(value)?)
+ }
+
+ fn serialize_unit(self) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_unit_struct(self, _name: &'static str) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_newtype_variant<T>(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _value: &T,
+ ) -> Result<CString>
+ where
+ T: ?Sized + Serialize,
+ {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_none(self) -> Result<CString> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_some<T>(self, _value: &T) -> Result<CString>
+ where
+ T: ?Sized + Serialize,
+ {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_tuple_struct(
+ self,
+ _name: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeTupleStruct> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_tuple_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeTupleVariant> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
+ Err(Error::KeyMustBeAString)
+ }
+
+ fn serialize_struct_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeStructVariant> {
+ Err(Error::KeyMustBeAString)
+ }
+}
+
+pub struct SerializeMap {
+ vec: Vec<(CString, QObject)>,
+ next_key: Option<CString>,
+}
+
+impl serde::ser::SerializeMap for SerializeMap {
+ type Ok = QObject;
+ type Error = Error;
+
+ fn serialize_key<T>(&mut self, key: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ self.next_key = Some(key.serialize(MapKeySerializer)?);
+ Ok(())
+ }
+
+ fn serialize_value<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ let key = self.next_key.take();
+ // Panic because this indicates a bug in the program rather than an
+ // expected failure.
+ let key = key.expect("serialize_value called before serialize_key");
+ self.vec.push((key, to_qobject(value)?));
+ Ok(())
+ }
+
+ fn end(self) -> Result<QObject> {
+ // TODO: insert keys one at a time
+ let SerializeMap { vec, .. } = self;
+ Ok(QObject::from_iter(vec))
+ }
+}
+
+impl serde::ser::SerializeStruct for SerializeMap {
+ type Ok = QObject;
+ type Error = Error;
+
+ fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ serde::ser::SerializeMap::serialize_entry(self, key, value)
+ }
+
+ fn end(self) -> Result<QObject> {
+ serde::ser::SerializeMap::end(self)
+ }
+}
+
+/// Serializer whose output is a `QObject`.
+///
+/// This is the serializer that backs [`to_qobject`].
+pub struct Serializer;
+
+impl serde::Serializer for Serializer {
+ type Ok = QObject;
+ type Error = Error;
+ type SerializeSeq = SerializeVec;
+ type SerializeTuple = SerializeVec;
+ type SerializeTupleStruct = SerializeVec;
+ type SerializeTupleVariant = SerializeTupleVariant;
+ type SerializeMap = SerializeMap;
+ type SerializeStruct = SerializeMap;
+ type SerializeStructVariant = SerializeStructVariant;
+
+ #[inline]
+ fn serialize_bool(self, value: bool) -> Result<QObject> {
+ Ok(value.into())
+ }
+
+ #[inline]
+ fn serialize_i8(self, value: i8) -> Result<QObject> {
+ Ok(value.into())
+ }
+
+ #[inline]
+ fn serialize_i16(self, value: i16) -> Result<QObject> {
+ Ok(value.into())
+ }
+
+ #[inline]
+ fn serialize_i32(self, value: i32) -> Result<QObject> {
+ Ok(value.into())
+ }
+
+ fn serialize_i64(self, value: i64) -> Result<QObject> {
+ Ok(value.into())
+ }
+
+ fn serialize_i128(self, value: i128) -> Result<QObject> {
+ if let Ok(value) = u64::try_from(value) {
+ Ok(value.into())
+ } else if let Ok(value) = i64::try_from(value) {
+ Ok(value.into())
+ } else {
+ Err(Error::NumberOutOfRange)
+ }
+ }
+
+ #[inline]
+ fn serialize_u8(self, value: u8) -> Result<QObject> {
+ Ok(value.into())
+ }
+
+ #[inline]
+ fn serialize_u16(self, value: u16) -> Result<QObject> {
+ Ok(value.into())
+ }
+
+ #[inline]
+ fn serialize_u32(self, value: u32) -> Result<QObject> {
+ Ok(value.into())
+ }
+
+ #[inline]
+ fn serialize_u64(self, value: u64) -> Result<QObject> {
+ Ok(value.into())
+ }
+
+ fn serialize_u128(self, value: u128) -> Result<QObject> {
+ if let Ok(value) = u64::try_from(value) {
+ Ok(value.into())
+ } else {
+ Err(Error::NumberOutOfRange)
+ }
+ }
+
+ #[inline]
+ fn serialize_f32(self, float: f32) -> Result<QObject> {
+ Ok(float.into())
+ }
+
+ #[inline]
+ fn serialize_f64(self, float: f64) -> Result<QObject> {
+ Ok(float.into())
+ }
+
+ #[inline]
+ fn serialize_char(self, value: char) -> Result<QObject> {
+ let mut s = String::new();
+ s.push(value);
+ Ok(CString::new(s)?.into())
+ }
+
+ #[inline]
+ fn serialize_str(self, value: &str) -> Result<QObject> {
+ Ok(CString::new(value)?.into())
+ }
+
+ fn serialize_bytes(self, value: &[u8]) -> Result<QObject> {
+ // Serialize into a vector of numeric QObjects
+ let it = value.iter().copied();
+ Ok(QObject::from_iter(it))
+ }
+
+ #[inline]
+ fn serialize_unit(self) -> Result<QObject> {
+ Ok(().into())
+ }
+
+ #[inline]
+ fn serialize_unit_struct(self, _name: &'static str) -> Result<QObject> {
+ self.serialize_unit()
+ }
+
+ #[inline]
+ fn serialize_unit_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ ) -> Result<QObject> {
+ self.serialize_str(variant)
+ }
+
+ #[inline]
+ fn serialize_newtype_struct<T>(self, _name: &'static str, value: &T) -> Result<QObject>
+ where
+ T: ?Sized + Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_newtype_variant<T>(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ value: &T,
+ ) -> Result<QObject>
+ where
+ T: ?Sized + Serialize,
+ {
+ // serde by default represents enums as a single-entry object, with
+ // the variant stored in the key ("external tagging"). Internal tagging
+ // is implemented by the struct that requests it, not by the serializer.
+ let map = [(CString::new(variant)?, to_qobject(value)?)];
+ Ok(QObject::from_iter(map))
+ }
+
+ #[inline]
+ fn serialize_none(self) -> Result<QObject> {
+ self.serialize_unit()
+ }
+
+ #[inline]
+ fn serialize_some<T>(self, value: &T) -> Result<QObject>
+ where
+ T: ?Sized + Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq> {
+ Ok(SerializeVec {
+ vec: Vec::with_capacity(len.unwrap_or(0)),
+ })
+ }
+
+ fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple> {
+ self.serialize_seq(Some(len))
+ }
+
+ fn serialize_tuple_struct(
+ self,
+ _name: &'static str,
+ len: usize,
+ ) -> Result<Self::SerializeTupleStruct> {
+ self.serialize_seq(Some(len))
+ }
+
+ fn serialize_tuple_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ len: usize,
+ ) -> Result<Self::SerializeTupleVariant> {
+ Ok(SerializeTupleVariant {
+ name: CString::new(variant)?,
+ vec: Vec::with_capacity(len),
+ })
+ }
+
+ fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
+ Ok(SerializeMap {
+ vec: Vec::new(),
+ next_key: None,
+ })
+ }
+ fn serialize_struct(self, _name: &'static str, len: usize) -> Result<Self::SerializeStruct> {
+ self.serialize_map(Some(len))
+ }
+
+ fn serialize_struct_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeStructVariant> {
+ Ok(SerializeStructVariant {
+ name: CString::new(variant)?,
+ vec: Vec::new(),
+ })
+ }
+}
+
+pub fn to_qobject<T>(input: T) -> Result<QObject>
+where
+ T: Serialize,
+{
+ input.serialize(Serializer)
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH v2 05/16] rust/qobject: add Deserialize implementation
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (3 preceding siblings ...)
2026-01-08 13:10 ` [PATCH v2 04/16] rust/qobject: add Serializer (to_qobject) implementation Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 06/16] rust/qobject: add Deserializer (from_qobject) implementation Paolo Bonzini
` (14 subsequent siblings)
19 siblings, 0 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust, Zhao Liu
This allows QObject to be created from any serializable format, for
example JSON via serde_json.
This is not too useful, since QObjects are produced by
C code or by serializing structs, but it can be used for testing
and it is part of the full implementation of a serde format.
It is relatively simple and similar to the Serializer, just with
the extra indirection of a Visitor.
Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/util/meson.build | 1 +
rust/util/src/qobject/deserialize.rs | 134 +++++++++++++++++++++++++++
rust/util/src/qobject/mod.rs | 1 +
3 files changed, 136 insertions(+)
create mode 100644 rust/util/src/qobject/deserialize.rs
diff --git a/rust/util/meson.build b/rust/util/meson.build
index 7829364c09a..4621932fdcb 100644
--- a/rust/util/meson.build
+++ b/rust/util/meson.build
@@ -40,6 +40,7 @@ _util_rs = static_library(
{'.': _util_bindings_inc_rs,
'qobject': [
'src/qobject/mod.rs',
+ 'src/qobject/deserialize.rs',
'src/qobject/error.rs',
'src/qobject/serializer.rs',
'src/qobject/serialize.rs',
diff --git a/rust/util/src/qobject/deserialize.rs b/rust/util/src/qobject/deserialize.rs
new file mode 100644
index 00000000000..280a577b6be
--- /dev/null
+++ b/rust/util/src/qobject/deserialize.rs
@@ -0,0 +1,134 @@
+//! `QObject` deserialization
+//!
+//! This module implements the [`Deserialize`] trait for `QObject`,
+//! allowing it to be created from any serializable format, for
+//! example JSON.
+
+use core::fmt;
+use std::ffi::CString;
+
+use serde::de::{self, Deserialize, MapAccess, SeqAccess, Visitor};
+
+use super::{to_qobject, QObject};
+
+impl<'de> Deserialize<'de> for QObject {
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<QObject, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ struct ValueVisitor;
+
+ impl<'de> Visitor<'de> for ValueVisitor {
+ type Value = QObject;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("any valid JSON value")
+ }
+
+ #[inline]
+ fn visit_bool<E>(self, value: bool) -> Result<QObject, E> {
+ Ok(value.into())
+ }
+
+ #[inline]
+ fn visit_i64<E>(self, value: i64) -> Result<QObject, E> {
+ Ok(value.into())
+ }
+
+ fn visit_i128<E>(self, value: i128) -> Result<QObject, E>
+ where
+ E: serde::de::Error,
+ {
+ to_qobject(value).map_err(|_| de::Error::custom("number out of range"))
+ }
+
+ #[inline]
+ fn visit_u64<E>(self, value: u64) -> Result<QObject, E> {
+ Ok(value.into())
+ }
+
+ fn visit_u128<E>(self, value: u128) -> Result<QObject, E>
+ where
+ E: serde::de::Error,
+ {
+ to_qobject(value).map_err(|_| de::Error::custom("number out of range"))
+ }
+
+ #[inline]
+ fn visit_f64<E>(self, value: f64) -> Result<QObject, E> {
+ Ok(value.into())
+ }
+
+ #[inline]
+ fn visit_str<E>(self, value: &str) -> Result<QObject, E>
+ where
+ E: serde::de::Error,
+ {
+ CString::new(value)
+ .map_err(|_| de::Error::custom("NUL character in string"))
+ .map(QObject::from)
+ }
+
+ #[inline]
+ fn visit_string<E>(self, value: String) -> Result<QObject, E>
+ where
+ E: serde::de::Error,
+ {
+ CString::new(value)
+ .map_err(|_| de::Error::custom("NUL character in string"))
+ .map(QObject::from)
+ }
+
+ #[inline]
+ fn visit_none<E>(self) -> Result<QObject, E> {
+ Ok(().into())
+ }
+
+ #[inline]
+ fn visit_some<D>(self, deserializer: D) -> Result<QObject, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ Deserialize::deserialize(deserializer)
+ }
+
+ #[inline]
+ fn visit_unit<E>(self) -> Result<QObject, E> {
+ Ok(().into())
+ }
+
+ #[inline]
+ fn visit_seq<V>(self, mut visitor: V) -> Result<QObject, V::Error>
+ where
+ V: SeqAccess<'de>,
+ {
+ // TODO: insert elements one at a time
+ let mut vec = Vec::<QObject>::new();
+
+ while let Some(elem) = visitor.next_element()? {
+ vec.push(elem);
+ }
+ Ok(QObject::from_iter(vec))
+ }
+
+ fn visit_map<V>(self, mut visitor: V) -> Result<QObject, V::Error>
+ where
+ V: MapAccess<'de>,
+ {
+ // TODO: insert elements one at a time
+ let mut vec = Vec::<(CString, QObject)>::new();
+
+ if let Some(first_key) = visitor.next_key()? {
+ vec.push((first_key, visitor.next_value()?));
+ while let Some((key, value)) = visitor.next_entry()? {
+ vec.push((key, value));
+ }
+ }
+ Ok(QObject::from_iter(vec))
+ }
+ }
+
+ deserializer.deserialize_any(ValueVisitor)
+ }
+}
diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
index bfa3e9e34bb..c0c16f28049 100644
--- a/rust/util/src/qobject/mod.rs
+++ b/rust/util/src/qobject/mod.rs
@@ -6,6 +6,7 @@
#![deny(clippy::unwrap_used)]
+mod deserialize;
mod error;
mod serialize;
mod serializer;
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH v2 06/16] rust/qobject: add Deserializer (from_qobject) implementation
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (4 preceding siblings ...)
2026-01-08 13:10 ` [PATCH v2 05/16] rust/qobject: add Deserialize implementation Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 07/16] rust/qobject: add from/to JSON bindings for QObject Paolo Bonzini
` (13 subsequent siblings)
19 siblings, 0 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust, Zhao Liu
This allows creating any serializable data structure from QObject.
The purpose of all the code is to typecheck each variant in the
serde data model and check that it's one of the corresponding
QObject data types.
Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
docs/devel/rust.rst | 1 +
rust/util/meson.build | 1 +
rust/util/src/qobject/deserializer.rs | 371 ++++++++++++++++++++++++++
rust/util/src/qobject/error.rs | 8 +-
rust/util/src/qobject/mod.rs | 2 +
5 files changed, 382 insertions(+), 1 deletion(-)
create mode 100644 rust/util/src/qobject/deserializer.rs
diff --git a/docs/devel/rust.rst b/docs/devel/rust.rst
index 79c26d9d165..c7f3a496a25 100644
--- a/docs/devel/rust.rst
+++ b/docs/devel/rust.rst
@@ -162,6 +162,7 @@ module status
``util::error`` stable
``util::log`` proof of concept
``util::module`` complete
+``util::qobject`` stable
``util::timer`` stable
========================== ======================
diff --git a/rust/util/meson.build b/rust/util/meson.build
index 4621932fdcb..92d27957eda 100644
--- a/rust/util/meson.build
+++ b/rust/util/meson.build
@@ -40,6 +40,7 @@ _util_rs = static_library(
{'.': _util_bindings_inc_rs,
'qobject': [
'src/qobject/mod.rs',
+ 'src/qobject/deserializer.rs',
'src/qobject/deserialize.rs',
'src/qobject/error.rs',
'src/qobject/serializer.rs',
diff --git a/rust/util/src/qobject/deserializer.rs b/rust/util/src/qobject/deserializer.rs
new file mode 100644
index 00000000000..b0f8aa23773
--- /dev/null
+++ b/rust/util/src/qobject/deserializer.rs
@@ -0,0 +1,371 @@
+//! `QObject` deserializer
+//!
+//! This module implements a [`Deserializer`](serde::de::Deserializer) that
+//! consumes `QObject`s, allowing them to be turned into deserializable data
+//! structures (such as primitive data types, or structs that implement
+//! `Deserialize`).
+
+use std::ffi::CStr;
+
+use serde::de::{
+ self, value::StrDeserializer, DeserializeSeed, Expected, IntoDeserializer, MapAccess,
+ SeqAccess, Unexpected, Visitor,
+};
+
+use super::{
+ error::{Error, Result},
+ match_qobject, QObject,
+};
+use crate::bindings;
+
+impl QObject {
+ #[cold]
+ fn invalid_type<E>(&self, exp: &dyn Expected) -> E
+ where
+ E: serde::de::Error,
+ {
+ serde::de::Error::invalid_type(self.unexpected(), exp)
+ }
+
+ #[cold]
+ fn unexpected(&self) -> Unexpected<'_> {
+ match_qobject! { (self) =>
+ () => Unexpected::Unit,
+ bool(b) => Unexpected::Bool(b),
+ i64(n) => Unexpected::Signed(n),
+ u64(n) => Unexpected::Unsigned(n),
+ f64(n) => Unexpected::Float(n),
+ CStr(s) => s.to_str().map_or_else(
+ |_| Unexpected::Other("string with invalid UTF-8"),
+ Unexpected::Str),
+ QList(_) => Unexpected::Seq,
+ QDict(_) => Unexpected::Map,
+ }
+ }
+}
+
+fn visit_qlist_ref<'de, V>(qlist: &'de bindings::QList, visitor: V) -> Result<V::Value>
+where
+ V: Visitor<'de>,
+{
+ struct QListDeserializer(*mut bindings::QListEntry, usize);
+
+ impl<'de> SeqAccess<'de> for QListDeserializer {
+ type Error = Error;
+
+ fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>>
+ where
+ T: DeserializeSeed<'de>,
+ {
+ if self.0.is_null() {
+ return Ok(None);
+ }
+
+ let e = unsafe { &*self.0 };
+ // increment the reference count because deserialize consumes `value`.
+ let value = unsafe { QObject::cloned_from_raw(e.value.cast_const()) };
+ let result = seed.deserialize(value);
+ self.0 = unsafe { e.next.tqe_next };
+ self.1 += 1;
+ result.map(Some)
+ }
+ }
+
+ let mut deserializer = QListDeserializer(unsafe { qlist.head.tqh_first }, 0);
+ let seq = visitor.visit_seq(&mut deserializer)?;
+ if deserializer.0.is_null() {
+ Ok(seq)
+ } else {
+ Err(serde::de::Error::invalid_length(
+ deserializer.1,
+ &"fewer elements in array",
+ ))
+ }
+}
+
+fn visit_qdict_ref<'de, V>(qdict: &'de bindings::QDict, visitor: V) -> Result<V::Value>
+where
+ V: Visitor<'de>,
+{
+ struct QDictDeserializer(*mut bindings::QDict, *const bindings::QDictEntry);
+
+ impl<'de> MapAccess<'de> for QDictDeserializer {
+ type Error = Error;
+
+ fn next_key_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>>
+ where
+ T: DeserializeSeed<'de>,
+ {
+ if self.1.is_null() {
+ return Ok(None);
+ }
+
+ let e = unsafe { &*self.1 };
+ let key = unsafe { CStr::from_ptr(e.key) };
+ let key_de = StrDeserializer::new(key.to_str()?);
+ seed.deserialize(key_de).map(Some)
+ }
+
+ fn next_value_seed<T>(&mut self, seed: T) -> Result<T::Value>
+ where
+ T: DeserializeSeed<'de>,
+ {
+ if self.1.is_null() {
+ panic!("next_key must have returned None");
+ }
+
+ let e = unsafe { &*self.1 };
+ // increment the reference count because deserialize consumes `value`.
+ let value = unsafe { QObject::cloned_from_raw(e.value) };
+ let result = seed.deserialize(value);
+ self.1 = unsafe { bindings::qdict_next(self.0, self.1) };
+ result
+ }
+ }
+
+ let qdict = (qdict as *const bindings::QDict).cast_mut();
+ let e = unsafe { bindings::qdict_first(qdict) };
+ let mut deserializer = QDictDeserializer(qdict, e);
+ let map = visitor.visit_map(&mut deserializer)?;
+ if deserializer.1.is_null() {
+ Ok(map)
+ } else {
+ Err(serde::de::Error::invalid_length(
+ unsafe { bindings::qdict_size(qdict) },
+ &"fewer elements in map",
+ ))
+ }
+}
+
+fn visit_qnum_ref<'de, V>(qnum: QObject, visitor: V) -> Result<V::Value>
+where
+ V: Visitor<'de>,
+{
+ match_qobject! { (qnum) =>
+ i64(n) => visitor.visit_i64(n),
+ u64(n) => visitor.visit_u64(n),
+ f64(n) => visitor.visit_f64(n),
+ _ => Err(qnum.invalid_type(&"number")),
+ }
+}
+
+macro_rules! deserialize_number {
+ ($method:ident) => {
+ fn $method<V>(self, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ visit_qnum_ref(self, visitor)
+ }
+ };
+}
+
+impl<'de> serde::Deserializer<'de> for QObject {
+ type Error = Error;
+
+ fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ match_qobject! { (self) =>
+ () => visitor.visit_unit(),
+ bool(v) => visitor.visit_bool(v),
+ i64(n) => visitor.visit_i64(n),
+ u64(n) => visitor.visit_u64(n),
+ f64(n) => visitor.visit_f64(n),
+ CStr(cstr) => visitor.visit_str(cstr.to_str()?),
+ QList(qlist) => visit_qlist_ref(qlist, visitor),
+ QDict(qdict) => visit_qdict_ref(qdict, visitor),
+ }
+ }
+
+ deserialize_number!(deserialize_i8);
+ deserialize_number!(deserialize_i16);
+ deserialize_number!(deserialize_i32);
+ deserialize_number!(deserialize_i64);
+ deserialize_number!(deserialize_i128);
+ deserialize_number!(deserialize_u8);
+ deserialize_number!(deserialize_u16);
+ deserialize_number!(deserialize_u32);
+ deserialize_number!(deserialize_u64);
+ deserialize_number!(deserialize_u128);
+ deserialize_number!(deserialize_f32);
+ deserialize_number!(deserialize_f64);
+
+ fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ match_qobject! { (self) =>
+ () => visitor.visit_none(),
+ _ => visitor.visit_some(self),
+ }
+ }
+
+ fn deserialize_enum<V>(
+ self,
+ _name: &'static str,
+ _variants: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ match_qobject! { (self) =>
+ CStr(cstr) => visitor.visit_enum(cstr.to_str()?.into_deserializer()),
+ _ => Err(self.invalid_type(&"string")),
+ }
+ }
+
+ fn deserialize_newtype_struct<V>(self, _name: &'static str, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ visitor.visit_newtype_struct(self)
+ }
+
+ fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ match_qobject! { (self) =>
+ bool(v) => visitor.visit_bool(v),
+ _ => Err(self.invalid_type(&visitor)),
+ }
+ }
+
+ fn deserialize_char<V>(self, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ self.deserialize_str(visitor)
+ }
+
+ fn deserialize_str<V>(self, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ match_qobject! { (self) =>
+ CStr(cstr) => visitor.visit_str(cstr.to_str()?),
+ _ => Err(self.invalid_type(&visitor)),
+ }
+ }
+
+ fn deserialize_string<V>(self, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ self.deserialize_str(visitor)
+ }
+
+ fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ match_qobject! { (self) =>
+ CStr(cstr) => visitor.visit_str(cstr.to_str()?),
+ QList(qlist) => visit_qlist_ref(qlist, visitor),
+ _ => Err(self.invalid_type(&visitor)),
+ }
+ }
+
+ fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ self.deserialize_bytes(visitor)
+ }
+
+ fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ match_qobject! { (self) =>
+ () => visitor.visit_unit(),
+ _ => Err(self.invalid_type(&visitor)),
+ }
+ }
+
+ fn deserialize_unit_struct<V>(self, _name: &'static str, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ self.deserialize_unit(visitor)
+ }
+
+ fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ match_qobject! { (self) =>
+ QList(qlist) => visit_qlist_ref(qlist, visitor),
+ _ => Err(self.invalid_type(&visitor)),
+ }
+ }
+
+ fn deserialize_tuple<V>(self, _len: usize, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ self.deserialize_seq(visitor)
+ }
+
+ fn deserialize_tuple_struct<V>(
+ self,
+ _name: &'static str,
+ _len: usize,
+ visitor: V,
+ ) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ self.deserialize_seq(visitor)
+ }
+
+ fn deserialize_map<V>(self, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ match_qobject! { (self) =>
+ QDict(qdict) => visit_qdict_ref(qdict, visitor),
+ _ => Err(self.invalid_type(&visitor)),
+ }
+ }
+
+ fn deserialize_struct<V>(
+ self,
+ _name: &'static str,
+ _fields: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ match_qobject! { (self) =>
+ QList(qlist) => visit_qlist_ref(qlist, visitor),
+ QDict(qdict) => visit_qdict_ref(qdict, visitor),
+ _ => Err(self.invalid_type(&visitor)),
+ }
+ }
+
+ fn deserialize_identifier<V>(self, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ self.deserialize_str(visitor)
+ }
+
+ fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value>
+ where
+ V: Visitor<'de>,
+ {
+ visitor.visit_unit()
+ }
+}
+
+pub fn from_qobject<T>(value: QObject) -> Result<T>
+where
+ T: de::DeserializeOwned,
+{
+ T::deserialize(value)
+}
diff --git a/rust/util/src/qobject/error.rs b/rust/util/src/qobject/error.rs
index 5212e65c4f7..2d7c180187a 100644
--- a/rust/util/src/qobject/error.rs
+++ b/rust/util/src/qobject/error.rs
@@ -6,7 +6,7 @@
str::Utf8Error,
};
-use serde::ser;
+use serde::{de, ser};
#[derive(Debug)]
pub enum Error {
@@ -23,6 +23,12 @@ fn custom<T: Display>(msg: T) -> Self {
}
}
+impl de::Error for Error {
+ fn custom<T: Display>(msg: T) -> Self {
+ Error::Custom(msg.to_string())
+ }
+}
+
impl From<NulError> for Error {
fn from(_: NulError) -> Self {
Error::NulEncountered
diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
index c0c16f28049..b95dddddedc 100644
--- a/rust/util/src/qobject/mod.rs
+++ b/rust/util/src/qobject/mod.rs
@@ -7,6 +7,7 @@
#![deny(clippy::unwrap_used)]
mod deserialize;
+mod deserializer;
mod error;
mod serialize;
mod serializer;
@@ -20,6 +21,7 @@
};
use common::assert_field_type;
+pub use deserializer::from_qobject;
pub use error::{Error, Result};
pub use serializer::to_qobject;
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH v2 07/16] rust/qobject: add from/to JSON bindings for QObject
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (5 preceding siblings ...)
2026-01-08 13:10 ` [PATCH v2 06/16] rust/qobject: add Deserializer (from_qobject) implementation Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-01-15 13:17 ` Zhao Liu
2026-01-08 13:10 ` [PATCH v2 08/16] rust/qobject: add Display/Debug Paolo Bonzini
` (12 subsequent siblings)
19 siblings, 1 reply; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust
These are used by tests. However it could even be an idea to use
serde_json + transcoding and get rid of the C version...
Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/util/wrapper.h | 1 +
rust/util/src/qobject/mod.rs | 17 +++++++++++++++++
2 files changed, 18 insertions(+)
diff --git a/rust/util/wrapper.h b/rust/util/wrapper.h
index 0907dd59142..c88820a5e5b 100644
--- a/rust/util/wrapper.h
+++ b/rust/util/wrapper.h
@@ -37,3 +37,4 @@ typedef enum memory_order {
#include "qobject/qobject.h"
#include "qobject/qlist.h"
#include "qobject/qdict.h"
+#include "qobject/qjson.h"
diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
index b95dddddedc..95d41cec4f1 100644
--- a/rust/util/src/qobject/mod.rs
+++ b/rust/util/src/qobject/mod.rs
@@ -23,6 +23,7 @@
use common::assert_field_type;
pub use deserializer::from_qobject;
pub use error::{Error, Result};
+use foreign::prelude::*;
pub use serializer::to_qobject;
use crate::bindings;
@@ -112,6 +113,22 @@ fn refcnt(&self) -> &AtomicUsize {
let qobj = self.0.get();
unsafe { AtomicUsize::from_ptr(addr_of_mut!((*qobj).base.refcnt)) }
}
+
+ pub fn to_json(&self) -> String {
+ let qobj = self.0.get();
+ unsafe {
+ let json = bindings::qobject_to_json(qobj);
+ glib_sys::g_string_free(json, glib_sys::GFALSE).into_native()
+ }
+ }
+
+ pub fn from_json(json: &str) -> std::result::Result<Self, crate::Error> {
+ let c_json = std::ffi::CString::new(json)?;
+ unsafe {
+ crate::Error::with_errp(|errp| bindings::qobject_from_json(c_json.as_ptr(), errp))
+ .map(|qobj| QObject::from_raw(qobj))
+ }
+ }
}
/// Rust equivalent of the C `QOBJECT` macro; for internal use only, because
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH v2 08/16] rust/qobject: add Display/Debug
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (6 preceding siblings ...)
2026-01-08 13:10 ` [PATCH v2 07/16] rust/qobject: add from/to JSON bindings for QObject Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-01-15 13:19 ` Zhao Liu
2026-01-08 13:10 ` [PATCH v2 09/16] scripts/qapi: add QAPISchemaIfCond.rsgen() Paolo Bonzini
` (11 subsequent siblings)
19 siblings, 1 reply; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/util/src/qobject/mod.rs | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
index 95d41cec4f1..8033b5fafa0 100644
--- a/rust/util/src/qobject/mod.rs
+++ b/rust/util/src/qobject/mod.rs
@@ -12,6 +12,7 @@
mod serialize;
mod serializer;
+use core::fmt::{self, Debug, Display};
use std::{
cell::UnsafeCell,
ffi::{c_char, CString},
@@ -270,6 +271,33 @@ fn drop(&mut self) {
}
}
+impl Display for QObject {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ // replace with a plain serializer?
+ match_qobject! { (self) =>
+ () => write!(f, "QNull"),
+ bool(b) => write!(f, "QBool({})", if b { "true" } else { "false" }),
+ i64(n) => write!(f, "QNumI64({})", n),
+ u64(n) => write!(f, "QNumU64({})", n),
+ f64(n) => write!(f, "QNumDouble({})", n),
+ CStr(s) => write!(f, "QString({})", s.to_str().unwrap_or("bad CStr")),
+ QList(_) => write!(f, "QList"),
+ QDict(_) => write!(f, "QDict"),
+ }
+ }
+}
+
+impl Debug for QObject {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let val = self.to_string();
+ f.debug_struct("QObject")
+ .field("ptr", &self.0.get())
+ .field("refcnt()", &self.refcnt())
+ .field("to_string()", &val)
+ .finish()
+ }
+}
+
macro_rules! match_qobject {
(@internal ($qobj:expr) =>
$(() => $unit:expr,)?
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH v2 09/16] scripts/qapi: add QAPISchemaIfCond.rsgen()
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (7 preceding siblings ...)
2026-01-08 13:10 ` [PATCH v2 08/16] rust/qobject: add Display/Debug Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-01-19 6:58 ` Zhao Liu
2026-02-25 6:48 ` Markus Armbruster
2026-01-08 13:10 ` [PATCH v2 10/16] scripts/qapi: add QAPISchemaType.is_predefined Paolo Bonzini
` (10 subsequent siblings)
19 siblings, 2 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Generate Rust #[cfg(...)] guards from QAPI 'if' conditions; it
turns out that they are very similar, with both of them using
not/any/all, so just walk the tree.
The next commit will put it to use.
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Link: https://lore.kernel.org/r/20210907121943.3498701-15-marcandre.lureau@redhat.com
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
scripts/qapi/common.py | 19 +++++++++++++++++++
scripts/qapi/schema.py | 4 ++++
2 files changed, 23 insertions(+)
diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
index d7c8aa3365c..14d5dd259c4 100644
--- a/scripts/qapi/common.py
+++ b/scripts/qapi/common.py
@@ -199,6 +199,25 @@ def guardend(name: str) -> str:
name=c_fname(name).upper())
+def rsgen_ifcond(ifcond: Optional[Union[str, Dict[str, Any]]]) -> str:
+
+ def cfg(ifcond: Union[str, Dict[str, Any]]) -> str:
+ if isinstance(ifcond, str):
+ return ifcond
+ assert isinstance(ifcond, dict) and len(ifcond) == 1
+ if 'not' in ifcond:
+ oper = 'not'
+ arg = cfg(ifcond['not'])
+ else:
+ oper, operands = next(iter(ifcond.items()))
+ arg = ', '.join([cfg(c) for c in operands])
+ return f'{oper}({arg})'
+
+ if not ifcond:
+ return ''
+ return '#[cfg(%s)]' % cfg(ifcond)
+
+
def gen_ifcond(ifcond: Optional[Union[str, Dict[str, Any]]],
cond_fmt: str, not_fmt: str,
all_operator: str, any_operator: str) -> str:
diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 8d88b40de2e..848a7401251 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -37,6 +37,7 @@
docgen_ifcond,
gen_endif,
gen_if,
+ rsgen_ifcond,
)
from .error import QAPIError, QAPISemError, QAPISourceError
from .expr import check_exprs
@@ -63,6 +64,9 @@ def gen_endif(self) -> str:
def docgen(self) -> str:
return docgen_ifcond(self.ifcond)
+ def rsgen(self) -> str:
+ return rsgen_ifcond(self.ifcond)
+
def is_present(self) -> bool:
return bool(self.ifcond)
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH v2 10/16] scripts/qapi: add QAPISchemaType.is_predefined
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (8 preceding siblings ...)
2026-01-08 13:10 ` [PATCH v2 09/16] scripts/qapi: add QAPISchemaIfCond.rsgen() Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-02-25 7:33 ` Markus Armbruster
2026-01-08 13:10 ` [PATCH v2 11/16] scripts/qapi: pull c_name from camel_to_upper to caller Paolo Bonzini
` (9 subsequent siblings)
19 siblings, 1 reply; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust
It is impossible to call is_implicit on an enum type from the visitor, because
the QAPISchemaEnumType has already been exploded into its costituent fields.
The Rust backend is also not modular (yet?) so it is not possible to filter
out the builtin module; add a way to query for implicit type names without
having the object itself.
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
scripts/qapi/schema.py | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 848a7401251..15f5d97418f 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -1243,6 +1243,17 @@ def _def_builtin_type(
# schema.
self._make_array_type(name, None)
+ def is_predefined(self, name: str) -> bool:
+ # See QAPISchema._def_predefineds()
+ entity = self._entity_dict[name]
+ if isinstance(entity, QAPISchemaBuiltinType):
+ return True
+ if entity is self.the_empty_object_type:
+ return True
+ if name == 'QType':
+ return True
+ return False
+
def _def_predefineds(self) -> None:
for t in [('str', 'string', 'char' + POINTER_SUFFIX),
('number', 'number', 'double'),
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH v2 11/16] scripts/qapi: pull c_name from camel_to_upper to caller
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (9 preceding siblings ...)
2026-01-08 13:10 ` [PATCH v2 10/16] scripts/qapi: add QAPISchemaType.is_predefined Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-01-19 7:05 ` Zhao Liu
2026-02-25 8:32 ` Markus Armbruster
2026-01-08 13:10 ` [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings Paolo Bonzini
` (8 subsequent siblings)
19 siblings, 2 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust
Allow using it for other languages too.
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
scripts/qapi/common.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
index 14d5dd259c4..c75396a01b5 100644
--- a/scripts/qapi/common.py
+++ b/scripts/qapi/common.py
@@ -61,7 +61,7 @@ def camel_to_upper(value: str) -> str:
ret += ch
upc = ch.isupper()
- return c_name(ret.upper()).lstrip('_')
+ return ret.upper()
def c_enum_const(type_name: str,
@@ -75,7 +75,7 @@ def c_enum_const(type_name: str,
:param prefix: Optional, prefix that overrides the type_name.
"""
if prefix is None:
- prefix = camel_to_upper(type_name)
+ prefix = c_name(camel_to_upper(type_name)).lstrip('_')
return prefix + '_' + c_name(const_name, False).upper()
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (10 preceding siblings ...)
2026-01-08 13:10 ` [PATCH v2 11/16] scripts/qapi: pull c_name from camel_to_upper to caller Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-02-23 12:36 ` Markus Armbruster
` (2 more replies)
2026-01-08 13:10 ` [PATCH v2 13/16] scripts/rustc_args: add --no-strict-cfg Paolo Bonzini
` (7 subsequent siblings)
19 siblings, 3 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Generate high-level native Rust declarations for the QAPI types.
- char* is mapped to String, scalars to there corresponding Rust types
- enums use #[repr(u32)] and can be transmuted to their C counterparts
- has_foo/foo members are mapped to Option<T>
- lists are represented as Vec<T>
- structures map fields 1:1 to Rust
- alternate are represented as Rust enum, each variant being a 1-element
tuple
- unions are represented in a similar way as in C: a struct S with a "u"
member (since S may have extra 'base' fields). The discriminant
isn't a member of S, since Rust enum already include it, but it can be
recovered with "mystruct.u.into()"
Anything that includes a recursive struct puts it in a Box. Lists are
not considered recursive, because Vec breaks the recursion (it's possible
to construct an object containing an empty Vec of its own type).
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Link: https://lore.kernel.org/r/20210907121943.3498701-21-marcandre.lureau@redhat.com
[Paolo: rewrite conversion of schema types to Rust types]
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
meson.build | 4 +-
scripts/qapi/backend.py | 25 +++
scripts/qapi/common.py | 43 +++++
scripts/qapi/rs.py | 61 +++++++
scripts/qapi/rs_types.py | 373 +++++++++++++++++++++++++++++++++++++++
scripts/qapi/schema.py | 59 +++++--
6 files changed, 546 insertions(+), 19 deletions(-)
create mode 100644 scripts/qapi/rs.py
create mode 100644 scripts/qapi/rs_types.py
diff --git a/meson.build b/meson.build
index db87358d62d..4228792f0f6 100644
--- a/meson.build
+++ b/meson.build
@@ -3540,11 +3540,13 @@ qapi_gen_depends = [ meson.current_source_dir() / 'scripts/qapi/__init__.py',
meson.current_source_dir() / 'scripts/qapi/introspect.py',
meson.current_source_dir() / 'scripts/qapi/main.py',
meson.current_source_dir() / 'scripts/qapi/parser.py',
+ meson.current_source_dir() / 'scripts/qapi/rs_types.py',
meson.current_source_dir() / 'scripts/qapi/schema.py',
meson.current_source_dir() / 'scripts/qapi/source.py',
meson.current_source_dir() / 'scripts/qapi/types.py',
meson.current_source_dir() / 'scripts/qapi/visit.py',
- meson.current_source_dir() / 'scripts/qapi-gen.py'
+ meson.current_source_dir() / 'scripts/qapi-gen.py',
+ meson.current_source_dir() / 'scripts/qapi/rs.py',
]
tracetool = [
diff --git a/scripts/qapi/backend.py b/scripts/qapi/backend.py
index 49ae6ecdd33..8023acce0d6 100644
--- a/scripts/qapi/backend.py
+++ b/scripts/qapi/backend.py
@@ -7,6 +7,7 @@
from .events import gen_events
from .features import gen_features
from .introspect import gen_introspect
+from .rs_types import gen_rs_types
from .schema import QAPISchema
from .types import gen_types
from .visit import gen_visit
@@ -63,3 +64,27 @@ def generate(self,
gen_commands(schema, output_dir, prefix, gen_tracing)
gen_events(schema, output_dir, prefix)
gen_introspect(schema, output_dir, prefix, unmask)
+
+
+class QAPIRsBackend(QAPIBackend):
+ # pylint: disable=too-few-public-methods
+
+ def generate(self,
+ schema: QAPISchema,
+ output_dir: str,
+ prefix: str,
+ unmask: bool,
+ builtins: bool,
+ gen_tracing: bool) -> None:
+ """
+ Generate Rust code for the given schema into the target directory.
+
+ :param schema_file: The primary QAPI schema file.
+ :param output_dir: The output directory to store generated code.
+ :param prefix: Optional C-code prefix for symbol names.
+ :param unmask: Expose non-ABI names through introspection?
+ :param builtins: Generate code for built-in types?
+
+ :raise QAPIError: On failures.
+ """
+ gen_rs_types(schema, output_dir, prefix)
diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
index c75396a01b5..e9261a3411e 100644
--- a/scripts/qapi/common.py
+++ b/scripts/qapi/common.py
@@ -64,6 +64,13 @@ def camel_to_upper(value: str) -> str:
return ret.upper()
+def camel_to_lower(value: str) -> str:
+ """
+ Converts CamelCase to camel_case.
+ """
+ return camel_to_upper(value).lower()
+
+
def c_enum_const(type_name: str,
const_name: str,
prefix: Optional[str] = None) -> str:
@@ -129,6 +136,42 @@ def c_name(name: str, protect: bool = True) -> str:
return name
+def rs_name(name: str) -> str:
+ """
+ Map @name to a valid, possibly raw Rust identifier.
+ """
+ name = re.sub(r'[^A-Za-z0-9_]', '_', name)
+ if name[0].isnumeric():
+ name = '_' + name
+ # based from the list:
+ # https://doc.rust-lang.org/reference/keywords.html
+ if name in ('Self', 'abstract', 'as', 'async',
+ 'await', 'become', 'box', 'break',
+ 'const', 'continue', 'crate', 'do',
+ 'dyn', 'else', 'enum', 'extern',
+ 'false', 'final', 'fn', 'for',
+ 'if', 'impl', 'in', 'let',
+ 'loop', 'macro', 'match', 'mod',
+ 'move', 'mut', 'override', 'priv',
+ 'pub', 'ref', 'return', 'self',
+ 'static', 'struct', 'super', 'trait',
+ 'true', 'try', 'type', 'typeof',
+ 'union', 'unsafe', 'unsized', 'use',
+ 'virtual', 'where', 'while', 'yield'):
+ name = 'r#' + name
+ # avoid some clashes with the standard library
+ if name in ('String',):
+ name = 'Qapi' + name
+
+ return name
+
+
+def to_camel_case(value: str) -> str:
+ return ''.join('_' + word if word[0].isdigit()
+ else word[:1].upper() + word[1:]
+ for word in filter(None, re.split("[-_]+", value)))
+
+
class Indentation:
"""
Indentation level management.
diff --git a/scripts/qapi/rs.py b/scripts/qapi/rs.py
new file mode 100644
index 00000000000..2cf0c0e07f1
--- /dev/null
+++ b/scripts/qapi/rs.py
@@ -0,0 +1,61 @@
+# This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+"""
+QAPI Rust generator
+"""
+
+import os
+import re
+import subprocess
+import sys
+
+from .common import mcgen as mcgen_common
+from .gen import QAPIGen
+from .schema import QAPISchemaVisitor
+
+
+def mcgen(s: str, **kwds: object) -> str:
+ s = mcgen_common(s, **kwds)
+ return re.sub(r'(?: *\n)+', '\n', s)
+
+
+class QAPIGenRs(QAPIGen):
+ def __init__(self, fname: str, blurb: str, pydoc: str):
+ super().__init__(fname)
+ self._blurb = blurb
+ self._copyright = '\n//! '.join(re.findall(r'^Copyright .*', pydoc,
+ re.MULTILINE))
+
+ def _top(self) -> str:
+ return mcgen('''
+// @generated by qapi-gen, DO NOT EDIT
+
+//!
+//! Schema-defined QAPI types
+//!
+//! %(copyright)s
+//!
+//! This work is licensed under the terms of the GNU LGPL, version 2.1 or
+//! later. See the COPYING.LIB file in the top-level directory.
+
+''',
+ tool=os.path.basename(sys.argv[0]),
+ blurb=self._blurb, copyright=self._copyright)
+
+
+class QAPISchemaRsVisitor(QAPISchemaVisitor):
+
+ def __init__(self, prefix: str, what: str,
+ blurb: str, pydoc: str):
+ super().__init__()
+ self._prefix = prefix
+ self._what = what
+ self._gen = QAPIGenRs(self._prefix + self._what + '.rs', blurb, pydoc)
+
+ def write(self, output_dir: str) -> None:
+ self._gen.write(output_dir)
+
+ try:
+ subprocess.check_call(['rustfmt', self._gen.fname], cwd=output_dir)
+ except FileNotFoundError:
+ pass
diff --git a/scripts/qapi/rs_types.py b/scripts/qapi/rs_types.py
new file mode 100644
index 00000000000..64702eb54ae
--- /dev/null
+++ b/scripts/qapi/rs_types.py
@@ -0,0 +1,373 @@
+# This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+"""
+QAPI Rust types generator
+
+Copyright (c) 2025 Red Hat, Inc.
+
+This work is licensed under the terms of the GNU GPL, version 2.
+See the COPYING file in the top-level directory.
+"""
+
+from typing import List, Optional, Set
+
+from .common import (
+ camel_to_lower,
+ camel_to_upper,
+ rs_name,
+ to_camel_case,
+)
+from .rs import QAPISchemaRsVisitor, mcgen
+from .schema import (
+ QAPISchema,
+ QAPISchemaAlternateType,
+ QAPISchemaEnumMember,
+ QAPISchemaFeature,
+ QAPISchemaIfCond,
+ QAPISchemaObjectType,
+ QAPISchemaObjectTypeMember,
+ QAPISchemaType,
+ QAPISchemaVariants,
+)
+from .source import QAPISourceInfo
+
+
+objects_seen = set()
+
+
+def gen_rs_variants_to_tag(name: str,
+ ifcond: QAPISchemaIfCond,
+ variants: QAPISchemaVariants) -> str:
+ ret = mcgen('''
+
+%(cfg)s
+impl From<&%(rs_name)sVariant> for %(tag)s {
+ fn from(e: &%(rs_name)sVariant) -> Self {
+ match e {
+''',
+ cfg=ifcond.rsgen(),
+ rs_name=rs_name(name),
+ tag=variants.tag_member.type.rs_type())
+
+ for var in variants.variants:
+ type_name = var.type.name
+ tag_name = var.name
+ patt = '(_)'
+ if type_name == 'q_empty':
+ patt = ''
+ ret += mcgen('''
+ %(cfg)s
+ %(rs_name)sVariant::%(var_name)s%(patt)s => Self::%(tag_name)s,
+''',
+ cfg=var.ifcond.rsgen(),
+ rs_name=rs_name(name),
+ tag_name=rs_name(camel_to_upper(tag_name)),
+ var_name=rs_name(to_camel_case(tag_name)),
+ patt=patt)
+
+ ret += mcgen('''
+ }
+ }
+}
+''')
+ return ret
+
+
+def gen_rs_variants(name: str,
+ ifcond: QAPISchemaIfCond,
+ variants: QAPISchemaVariants) -> str:
+ ret = mcgen('''
+
+%(cfg)s
+#[derive(Clone, Debug, PartialEq)]
+pub enum %(rs_name)sVariant {
+''',
+ cfg=ifcond.rsgen(),
+ rs_name=rs_name(name))
+
+ for var in variants.variants:
+ type_name = var.type.name
+ var_name = rs_name(to_camel_case(var.name))
+ if type_name == 'q_empty':
+ ret += mcgen('''
+ %(cfg)s
+ %(var_name)s,
+''',
+ cfg=var.ifcond.rsgen(),
+ var_name=var_name)
+ else:
+ ret += mcgen('''
+ %(cfg)s
+ %(var_name)s(%(rs_type)s),
+''',
+ cfg=var.ifcond.rsgen(),
+ var_name=var_name,
+ rs_type=var.type.rs_type())
+
+ ret += mcgen('''
+}
+''')
+
+ ret += gen_rs_variants_to_tag(name, ifcond, variants)
+
+ return ret
+
+
+def gen_rs_members(members: List[QAPISchemaObjectTypeMember],
+ exclude: Optional[List[str]] = None) -> List[str]:
+ exclude = exclude or []
+ return [f"{m.ifcond.rsgen()} {rs_name(camel_to_lower(m.name))}"
+ for m in members if m.name not in exclude]
+
+
+def has_recursive_type(memb: QAPISchemaType,
+ name: str,
+ visited: Set[str]) -> bool:
+ # pylint: disable=too-many-return-statements
+ if name == memb.name:
+ return True
+ if memb.name in visited:
+ return False
+ visited.add(memb.name)
+ if isinstance(memb, QAPISchemaObjectType):
+ if memb.base and has_recursive_type(memb.base, name, visited):
+ return True
+ if memb.branches and \
+ any(has_recursive_type(m.type, name, visited)
+ for m in memb.branches.variants):
+ return True
+ if any(has_recursive_type(m.type, name, visited)
+ for m in memb.members):
+ return True
+ return any(has_recursive_type(m.type, name, visited)
+ for m in memb.local_members)
+ if isinstance(memb, QAPISchemaAlternateType):
+ return any(has_recursive_type(m.type, name, visited)
+ for m in memb.alternatives.variants)
+ return False
+
+
+def gen_struct_members(members: List[QAPISchemaObjectTypeMember],
+ name: str) -> str:
+ ret = ''
+ for memb in members:
+ typ = memb.type.rs_type()
+ if has_recursive_type(memb.type, name, set()):
+ typ = 'Box<%s>' % typ
+ if memb.optional:
+ typ = 'Option<%s>' % typ
+ ret += mcgen('''
+ %(cfg)s
+ pub %(rs_name)s: %(rs_type)s,
+''',
+ cfg=memb.ifcond.rsgen(),
+ rs_type=typ,
+ rs_name=rs_name(camel_to_lower(memb.name)))
+ return ret
+
+
+def gen_rs_object(name: str,
+ ifcond: QAPISchemaIfCond,
+ base: Optional[QAPISchemaObjectType],
+ members: List[QAPISchemaObjectTypeMember],
+ variants: Optional[QAPISchemaVariants]) -> str:
+ if name in objects_seen:
+ return ''
+
+ if variants:
+ members = [m for m in members
+ if m.name != variants.tag_member.name]
+
+ ret = ''
+ objects_seen.add(name)
+
+ if variants:
+ ret += gen_rs_variants(name, ifcond, variants)
+
+ ret += mcgen('''
+
+%(cfg)s
+#[derive(Clone, Debug, PartialEq)]
+pub struct %(rs_name)s {
+''',
+ cfg=ifcond.rsgen(),
+ rs_name=rs_name(name))
+
+ if base:
+ if not base.is_implicit():
+ ret += mcgen('''
+ // Members inherited:
+''',
+ c_name=base.c_name())
+ base_members = base.members
+ if variants:
+ base_members = [m for m in base.members
+ if m.name != variants.tag_member.name]
+ ret += gen_struct_members(base_members, name)
+ if not base.is_implicit():
+ ret += mcgen('''
+ // Own members:
+''')
+
+ ret += gen_struct_members(members, name)
+
+ if variants:
+ ret += mcgen('''
+ pub u: %(rs_type)sVariant,
+''', rs_type=rs_name(name))
+ ret += mcgen('''
+}
+''')
+ return ret
+
+
+def gen_rs_enum(name: str,
+ ifcond: QAPISchemaIfCond,
+ members: List[QAPISchemaEnumMember]) -> str:
+ ret = mcgen('''
+
+%(cfg)s
+#[derive(Copy, Clone, Debug, PartialEq)]
+''',
+ cfg=ifcond.rsgen())
+
+ if members:
+ ret += '''#[repr(u32)]
+#[derive(common::TryInto)]
+'''
+ ret += mcgen('''
+pub enum %(rs_name)s {
+''',
+ rs_name=rs_name(name))
+
+ for member in members:
+ ret += mcgen('''
+ %(cfg)s
+ %(c_enum)s,
+''',
+ cfg=member.ifcond.rsgen(),
+ c_enum=rs_name(camel_to_upper(member.name)))
+ ret += '''}
+
+'''
+
+ # pick the first, since that's what malloc0 does
+ if members:
+ default = rs_name(camel_to_upper(members[0].name))
+ ret += mcgen('''
+%(cfg)s
+impl Default for %(rs_name)s {
+ #[inline]
+ fn default() -> %(rs_name)s {
+ Self::%(default)s
+ }
+}
+''',
+ cfg=ifcond.rsgen(),
+ rs_name=rs_name(name),
+ default=default)
+ return ret
+
+
+def gen_rs_alternate(name: str,
+ ifcond: QAPISchemaIfCond,
+ variants: QAPISchemaVariants) -> str:
+ if name in objects_seen:
+ return ''
+
+ ret = ''
+ objects_seen.add(name)
+
+ ret += mcgen('''
+%(cfg)s
+#[derive(Clone, Debug, PartialEq)]
+pub enum %(rs_name)s {
+''',
+ cfg=ifcond.rsgen(),
+ rs_name=rs_name(name))
+
+ for var in variants.variants:
+ if var.type.name == 'q_empty':
+ continue
+ typ = var.type.rs_type()
+ if has_recursive_type(var.type, name, set()):
+ typ = 'Box<%s>' % typ
+ ret += mcgen('''
+ %(cfg)s
+ %(mem_name)s(%(rs_type)s),
+''',
+ cfg=var.ifcond.rsgen(),
+ rs_type=typ,
+ mem_name=rs_name(to_camel_case(var.name)))
+
+ ret += mcgen('''
+}
+''')
+ return ret
+
+
+class QAPISchemaGenRsTypeVisitor(QAPISchemaRsVisitor):
+ _schema: Optional[QAPISchema]
+
+ def __init__(self, prefix: str) -> None:
+ super().__init__(prefix, 'qapi-types',
+ 'Schema-defined QAPI types', __doc__)
+
+ def visit_begin(self, schema: QAPISchema) -> None:
+ self._schema = schema
+ objects_seen.add(schema.the_empty_object_type.name)
+
+ self._gen.preamble_add(
+ mcgen('''
+#![allow(unexpected_cfgs)]
+#![allow(non_camel_case_types)]
+#![allow(clippy::empty_structs_with_brackets)]
+#![allow(clippy::large_enum_variant)]
+#![allow(clippy::pub_underscore_fields)]
+
+// Because QAPI structs can contain float, for simplicity we never
+// derive Eq. Clippy however would complain for those structs
+// that *could* be Eq too.
+#![allow(clippy::derive_partial_eq_without_eq)]
+
+use util::qobject::QObject;
+'''))
+
+ def visit_object_type(self,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ base: Optional[QAPISchemaObjectType],
+ members: List[QAPISchemaObjectTypeMember],
+ branches: Optional[QAPISchemaVariants]) -> None:
+ assert self._schema is not None
+ if self._schema.is_predefined(name) or name.startswith('q_'):
+ return
+ self._gen.add(gen_rs_object(name, ifcond, base, members, branches))
+
+ def visit_enum_type(self,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ members: List[QAPISchemaEnumMember],
+ prefix: Optional[str]) -> None:
+ assert self._schema is not None
+ if self._schema.is_predefined(name):
+ return
+ self._gen.add(gen_rs_enum(name, ifcond, members))
+
+ def visit_alternate_type(self,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ alternatives: QAPISchemaVariants) -> None:
+ self._gen.add(gen_rs_alternate(name, ifcond, alternatives))
+
+
+def gen_rs_types(schema: QAPISchema, output_dir: str, prefix: str) -> None:
+ vis = QAPISchemaGenRsTypeVisitor(prefix)
+ schema.visit(vis)
+ vis.write(output_dir)
diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 15f5d97418f..a65b25141fa 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -37,6 +37,7 @@
docgen_ifcond,
gen_endif,
gen_if,
+ rs_name,
rsgen_ifcond,
)
from .error import QAPIError, QAPISemError, QAPISourceError
@@ -341,6 +342,11 @@ def c_param_type(self) -> str:
def c_unboxed_type(self) -> str:
return self.c_type()
+ # Return the Rust type for common use
+ @abstractmethod
+ def rs_type(self) -> str:
+ pass
+
@abstractmethod
def json_type(self) -> str:
pass
@@ -382,11 +388,12 @@ def describe(self) -> str:
class QAPISchemaBuiltinType(QAPISchemaType):
meta = 'built-in'
- def __init__(self, name: str, json_type: str, c_type: str):
+ def __init__(self, name: str, json_type: str, rs_type: str, c_type: str):
super().__init__(name, None, None)
assert json_type in ('string', 'number', 'int', 'boolean', 'null',
'value')
self._json_type_name = json_type
+ self._rs_type_name = rs_type
self._c_type_name = c_type
def c_name(self) -> str:
@@ -406,6 +413,9 @@ def json_type(self) -> str:
def doc_type(self) -> str:
return self.json_type()
+ def rs_type(self) -> str:
+ return self._rs_type_name
+
def visit(self, visitor: QAPISchemaVisitor) -> None:
super().visit(visitor)
visitor.visit_builtin_type(self.name, self.info, self.json_type())
@@ -449,6 +459,9 @@ def is_implicit(self) -> bool:
def c_type(self) -> str:
return c_name(self.name)
+ def rs_type(self) -> str:
+ return rs_name(self.name)
+
def member_names(self) -> List[str]:
return [m.name for m in self.members]
@@ -498,6 +511,9 @@ def is_implicit(self) -> bool:
def c_type(self) -> str:
return c_name(self.name) + POINTER_SUFFIX
+ def rs_type(self) -> str:
+ return 'Vec<%s>' % self.element_type.rs_type()
+
def json_type(self) -> str:
return 'array'
@@ -630,6 +646,9 @@ def c_type(self) -> str:
def c_unboxed_type(self) -> str:
return c_name(self.name)
+ def rs_type(self) -> str:
+ return rs_name(self.name)
+
def json_type(self) -> str:
return 'object'
@@ -711,6 +730,9 @@ def c_type(self) -> str:
def json_type(self) -> str:
return 'value'
+ def rs_type(self) -> str:
+ return rs_name(self.name)
+
def visit(self, visitor: QAPISchemaVisitor) -> None:
super().visit(visitor)
visitor.visit_alternate_type(
@@ -1234,9 +1256,10 @@ def _def_include(self, expr: QAPIExpression) -> None:
QAPISchemaInclude(self._make_module(include), expr.info))
def _def_builtin_type(
- self, name: str, json_type: str, c_type: str
+ self, name: str, json_type: str, rs_type: str, c_type: str
) -> None:
- self._def_definition(QAPISchemaBuiltinType(name, json_type, c_type))
+ builtin = QAPISchemaBuiltinType(name, json_type, rs_type, c_type)
+ self._def_definition(builtin)
# Instantiating only the arrays that are actually used would
# be nice, but we can't as long as their generated code
# (qapi-builtin-types.[ch]) may be shared by some other
@@ -1255,21 +1278,21 @@ def is_predefined(self, name: str) -> bool:
return False
def _def_predefineds(self) -> None:
- for t in [('str', 'string', 'char' + POINTER_SUFFIX),
- ('number', 'number', 'double'),
- ('int', 'int', 'int64_t'),
- ('int8', 'int', 'int8_t'),
- ('int16', 'int', 'int16_t'),
- ('int32', 'int', 'int32_t'),
- ('int64', 'int', 'int64_t'),
- ('uint8', 'int', 'uint8_t'),
- ('uint16', 'int', 'uint16_t'),
- ('uint32', 'int', 'uint32_t'),
- ('uint64', 'int', 'uint64_t'),
- ('size', 'int', 'uint64_t'),
- ('bool', 'boolean', 'bool'),
- ('any', 'value', 'QObject' + POINTER_SUFFIX),
- ('null', 'null', 'QNull' + POINTER_SUFFIX)]:
+ for t in [('str', 'string', 'String', 'char' + POINTER_SUFFIX),
+ ('number', 'number', 'f64', 'double'),
+ ('int', 'int', 'i64', 'int64_t'),
+ ('int8', 'int', 'i8', 'int8_t'),
+ ('int16', 'int', 'i16', 'int16_t'),
+ ('int32', 'int', 'i32', 'int32_t'),
+ ('int64', 'int', 'i64', 'int64_t'),
+ ('uint8', 'int', 'u8', 'uint8_t'),
+ ('uint16', 'int', 'u16', 'uint16_t'),
+ ('uint32', 'int', 'u32', 'uint32_t'),
+ ('uint64', 'int', 'u64', 'uint64_t'),
+ ('size', 'int', 'u64', 'uint64_t'),
+ ('bool', 'boolean', 'bool', 'bool'),
+ ('any', 'value', 'QObject', 'QObject' + POINTER_SUFFIX),
+ ('null', 'null', '()', 'QNull' + POINTER_SUFFIX)]:
self._def_builtin_type(*t)
self.the_empty_object_type = QAPISchemaObjectType(
'q_empty', None, None, None, None, None, [], None)
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH v2 13/16] scripts/rustc_args: add --no-strict-cfg
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (11 preceding siblings ...)
2026-01-08 13:10 ` [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 14/16] rust/util: build QAPI types Paolo Bonzini
` (6 subsequent siblings)
19 siblings, 0 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Allow to generate all --cfg flags, regardless of Cargo.toml content.
We can't easily list and include all the features used by QAPI types.
Access via #[cfg()] then requires #![allow(unexpected_cfgs)].
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
scripts/rust/rustc_args.py | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/scripts/rust/rustc_args.py b/scripts/rust/rustc_args.py
index 8098053720a..6a156b9608e 100644
--- a/scripts/rust/rustc_args.py
+++ b/scripts/rust/rustc_args.py
@@ -108,7 +108,7 @@ def generate_lint_flags(cargo_toml: CargoTOML) -> Iterable[str]:
yield from lint.flags
-def generate_cfg_flags(header: str, cargo_toml: CargoTOML) -> Iterable[str]:
+def generate_cfg_flags(header: str, cargo_toml: Optional[CargoTOML]) -> Iterable[str]:
"""Converts defines from config[..].h headers to rustc --cfg flags."""
with open(header, encoding="utf-8") as cfg:
@@ -117,8 +117,9 @@ def generate_cfg_flags(header: str, cargo_toml: CargoTOML) -> Iterable[str]:
cfg_list = []
for cfg in config:
name = cfg[0]
- if f'cfg({name})' not in cargo_toml.check_cfg:
- continue
+ if cargo_toml:
+ if f'cfg({name})' not in cargo_toml.check_cfg:
+ continue
if len(cfg) >= 2 and cfg[1] != "1":
continue
cfg_list.append("--cfg")
@@ -179,6 +180,13 @@ def main() -> None:
required=False,
default="1.0.0",
)
+ parser.add_argument(
+ "--no-strict-cfg",
+ help="only generate expected cfg",
+ action="store_false",
+ dest="strict_cfg",
+ default=True,
+ )
args = parser.parse_args()
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
@@ -209,7 +217,7 @@ def main() -> None:
print(f'cfg(feature,values("{feature}"))')
for header in args.config_headers:
- for tok in generate_cfg_flags(header, cargo_toml):
+ for tok in generate_cfg_flags(header, cargo_toml if args.strict_cfg else None):
print(tok)
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH v2 14/16] rust/util: build QAPI types
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (12 preceding siblings ...)
2026-01-08 13:10 ` [PATCH v2 13/16] scripts/rustc_args: add --no-strict-cfg Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 15/16] scripts/qapi: add serde attributes Paolo Bonzini
` (5 subsequent siblings)
19 siblings, 0 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
qapi/meson.build | 9 +++++++++
rust/Cargo.lock | 1 +
rust/Cargo.toml | 2 +-
rust/util/Cargo.toml | 1 +
rust/util/meson.build | 17 +++++++++++++++++
5 files changed, 29 insertions(+), 1 deletion(-)
diff --git a/qapi/meson.build b/qapi/meson.build
index a46269b5a0c..a019ec19db1 100644
--- a/qapi/meson.build
+++ b/qapi/meson.build
@@ -130,3 +130,12 @@ foreach output : qapi_outputs
util_ss.add(qapi_files[i])
i = i + 1
endforeach
+
+# TODO: build together with the other files, perhaps when Rust is not
+# optional and/or the Rust backend is complete (currently lacking
+# commands, events, modules)
+qapi_rs_files = custom_target('QAPI Rust',
+ output: 'qapi-types.rs',
+ input: [ files('qapi-schema.json') ],
+ command: [ qapi_gen, '-o', 'qapi', '-b', '@INPUT0@', '-B', 'qapi.backend.QAPIRsBackend' ],
+ depend_files: [ qapi_inputs, qapi_gen_depends ])
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index b6c9f934140..7bca9b189db 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -454,6 +454,7 @@ dependencies = [
"glib-sys",
"libc",
"serde",
+ "serde_derive",
]
[[package]]
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 45ebfa693a4..caddb93722e 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -20,7 +20,7 @@ anyhow = "~1.0"
foreign = "~0.3.1"
libc = "0.2.162"
glib-sys = { version = "0.21.2", features = ["v2_66"] }
-serde = "1.0.226"
+serde = { version = "1.0.226", features = ["derive"] }
serde_derive = "1.0.226"
[workspace.lints.rust]
diff --git a/rust/util/Cargo.toml b/rust/util/Cargo.toml
index 554004816eb..9f6c52c5acd 100644
--- a/rust/util/Cargo.toml
+++ b/rust/util/Cargo.toml
@@ -18,6 +18,7 @@ foreign = { workspace = true }
glib-sys = { workspace = true }
libc = { workspace = true }
serde = { workspace = true }
+serde_derive = { workspace = true }
common = { path = "../common" }
[lints]
diff --git a/rust/util/meson.build b/rust/util/meson.build
index 92d27957eda..7987056962c 100644
--- a/rust/util/meson.build
+++ b/rust/util/meson.build
@@ -63,3 +63,20 @@ rust.doctest('rust-util-rs-doctests',
dependencies: util_rs,
suite: ['doc', 'rust']
)
+
+_qapi_cfg = run_command(rustc_args,
+ '--no-strict-cfg',
+ '--config-headers', config_host_h,
+ capture: true, check: true).stdout().strip().splitlines()
+
+_qapi_rs = static_library(
+ 'qapi',
+ qapi_rs_files,
+ rust_args: _qapi_cfg,
+ override_options: ['rust_std=2021', 'build.rust_std=2021'],
+ rust_abi: 'rust',
+ link_with: [_util_rs],
+ dependencies: [common_rs, serde_rs],
+)
+
+qapi_rs = declare_dependency(link_with: [_qapi_rs])
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH v2 15/16] scripts/qapi: add serde attributes
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (13 preceding siblings ...)
2026-01-08 13:10 ` [PATCH v2 14/16] rust/util: build QAPI types Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 16/16] rust/tests: QAPI integration tests Paolo Bonzini
` (4 subsequent siblings)
19 siblings, 0 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Generate serde attributes to match the serialization format to QAPI's:
- for enums, map Rust enum variants to original QAPI names
- for structs, rejects JSON with extra fields and omit optional fields
(as opposed to serializing them as null)
- for union variants:
- use tagged union format matching QAPI's discriminator,
- map variant names to original QAPI names
- flatten union data into parent struct
- for alternates, use type-based discrimination
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
scripts/qapi/rs_types.py | 39 ++++++++++++++++++++++++++++++---------
1 file changed, 30 insertions(+), 9 deletions(-)
diff --git a/scripts/qapi/rs_types.py b/scripts/qapi/rs_types.py
index 64702eb54ae..fb3362ad601 100644
--- a/scripts/qapi/rs_types.py
+++ b/scripts/qapi/rs_types.py
@@ -33,6 +33,7 @@
objects_seen = set()
+SERDE_SKIP_NONE = '#[serde(skip_serializing_if = "Option::is_none")]'
def gen_rs_variants_to_tag(name: str,
@@ -79,11 +80,13 @@ def gen_rs_variants(name: str,
ret = mcgen('''
%(cfg)s
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(tag = "%(tag)s")]
pub enum %(rs_name)sVariant {
''',
cfg=ifcond.rsgen(),
- rs_name=rs_name(name))
+ rs_name=rs_name(name),
+ tag=variants.tag_member.name)
for var in variants.variants:
type_name = var.type.name
@@ -91,18 +94,22 @@ def gen_rs_variants(name: str,
if type_name == 'q_empty':
ret += mcgen('''
%(cfg)s
+ #[serde(rename = "%(rename)s")]
%(var_name)s,
''',
cfg=var.ifcond.rsgen(),
- var_name=var_name)
+ var_name=var_name,
+ rename=var.name)
else:
ret += mcgen('''
%(cfg)s
+ #[serde(rename = "%(rename)s")]
%(var_name)s(%(rs_type)s),
''',
cfg=var.ifcond.rsgen(),
var_name=var_name,
- rs_type=var.type.rs_type())
+ rs_type=var.type.rs_type(),
+ rename=var.name)
ret += mcgen('''
}
@@ -158,9 +165,11 @@ def gen_struct_members(members: List[QAPISchemaObjectTypeMember],
typ = 'Option<%s>' % typ
ret += mcgen('''
%(cfg)s
+ %(serde_skip_if)s
pub %(rs_name)s: %(rs_type)s,
''',
cfg=memb.ifcond.rsgen(),
+ serde_skip_if=SERDE_SKIP_NONE if memb.optional else '',
rs_type=typ,
rs_name=rs_name(camel_to_lower(memb.name)))
return ret
@@ -181,17 +190,23 @@ def gen_rs_object(name: str,
ret = ''
objects_seen.add(name)
+ serde_deny_unknown_fields = "#[serde(deny_unknown_fields)]"
if variants:
ret += gen_rs_variants(name, ifcond, variants)
+ # we can't use because of the flatten unions
+ # serde FlatMapAccess should consume the fields?
+ serde_deny_unknown_fields = ""
ret += mcgen('''
%(cfg)s
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+%(serde_deny_unknown_fields)s
pub struct %(rs_name)s {
''',
cfg=ifcond.rsgen(),
- rs_name=rs_name(name))
+ rs_name=rs_name(name),
+ serde_deny_unknown_fields=serde_deny_unknown_fields)
if base:
if not base.is_implicit():
@@ -213,6 +228,7 @@ def gen_rs_object(name: str,
if variants:
ret += mcgen('''
+ #[serde(flatten)]
pub u: %(rs_type)sVariant,
''', rs_type=rs_name(name))
ret += mcgen('''
@@ -227,7 +243,7 @@ def gen_rs_enum(name: str,
ret = mcgen('''
%(cfg)s
-#[derive(Copy, Clone, Debug, PartialEq)]
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
''',
cfg=ifcond.rsgen())
@@ -243,10 +259,12 @@ def gen_rs_enum(name: str,
for member in members:
ret += mcgen('''
%(cfg)s
+ #[serde(rename = "%(member_name)s")]
%(c_enum)s,
''',
cfg=member.ifcond.rsgen(),
- c_enum=rs_name(camel_to_upper(member.name)))
+ c_enum=rs_name(camel_to_upper(member.name)),
+ member_name=member.name)
ret += '''}
'''
@@ -280,7 +298,8 @@ def gen_rs_alternate(name: str,
ret += mcgen('''
%(cfg)s
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(untagged)]
pub enum %(rs_name)s {
''',
cfg=ifcond.rsgen(),
@@ -330,6 +349,8 @@ def visit_begin(self, schema: QAPISchema) -> None:
// that *could* be Eq too.
#![allow(clippy::derive_partial_eq_without_eq)]
+use serde_derive::{Serialize, Deserialize};
+
use util::qobject::QObject;
'''))
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH v2 16/16] rust/tests: QAPI integration tests
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (14 preceding siblings ...)
2026-01-08 13:10 ` [PATCH v2 15/16] scripts/qapi: add serde attributes Paolo Bonzini
@ 2026-01-08 13:10 ` Paolo Bonzini
2026-02-17 8:10 ` [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (3 subsequent siblings)
19 siblings, 0 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-01-08 13:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, Marc-André Lureau, qemu-rust
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/tests/meson.build | 21 +-
rust/tests/tests/integration.rs | 2 +
rust/tests/tests/qapi.rs | 444 ++++++++++++++++++++++++++++++++
3 files changed, 464 insertions(+), 3 deletions(-)
create mode 100644 rust/tests/tests/integration.rs
create mode 100644 rust/tests/tests/qapi.rs
diff --git a/rust/tests/meson.build b/rust/tests/meson.build
index 3c5020490b0..d781b52bd78 100644
--- a/rust/tests/meson.build
+++ b/rust/tests/meson.build
@@ -1,10 +1,25 @@
+test_qapi_rs_files = custom_target('QAPI Rust',
+ output: 'test-qapi-types.rs',
+ input: [ files(meson.project_source_root() + '/tests/qapi-schema/qapi-schema-test.json') ],
+ command: [ qapi_gen, '-o', meson.current_build_dir(), '-b', '@INPUT0@', '-B', 'qapi.backend.QAPIRsBackend', '-p', 'test-' ],
+ depend_files: [ qapi_inputs, qapi_gen_depends ])
+
+_test_qapi_rs = static_library(
+ 'test_qapi',
+ test_qapi_rs_files,
+ override_options: ['rust_std=2021', 'build.rust_std=2021'],
+ rust_abi: 'rust',
+ dependencies: [common_rs, util_rs, serde_rs, serde_derive_rs])
+
+test_qapi_rs = declare_dependency(link_with: [_test_qapi_rs])
+
test('rust-integration',
executable(
'rust-integration',
- files('tests/vmstate_tests.rs'),
- rust_args: ['--test'],
+ files('tests/integration.rs'),
+ rust_args: ['--test'] + _qapi_cfg,
install: false,
- dependencies: [bql_rs, common_rs, util_rs, migration_rs, qom_rs]),
+ dependencies: [bql_rs, common_rs, util_rs, migration_rs, qom_rs, qapi_rs, test_qapi_rs]),
args: [
'--test', '--test-threads', '1',
'--format', 'pretty',
diff --git a/rust/tests/tests/integration.rs b/rust/tests/tests/integration.rs
new file mode 100644
index 00000000000..ebc17cb5550
--- /dev/null
+++ b/rust/tests/tests/integration.rs
@@ -0,0 +1,2 @@
+mod qapi;
+mod vmstate_tests;
diff --git a/rust/tests/tests/qapi.rs b/rust/tests/tests/qapi.rs
new file mode 100644
index 00000000000..f8a585e5802
--- /dev/null
+++ b/rust/tests/tests/qapi.rs
@@ -0,0 +1,444 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#![allow(unexpected_cfgs)]
+#![allow(clippy::shadow_unrelated)]
+
+use util::qobject::{from_qobject, to_qobject, QObject};
+
+#[test]
+fn test_char() {
+ let json = "\"v\"";
+ let qo = QObject::from_json(json).unwrap();
+ let c: char = from_qobject(qo).unwrap();
+ assert_eq!(c, 'v');
+ assert_eq!(to_qobject(c).unwrap().to_json(), json);
+
+ let json = "'va'";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<char>(qo).unwrap_err();
+}
+
+#[test]
+fn test_enum() {
+ let json = "\"value1\"";
+ let qo = QObject::from_json(json).unwrap();
+ let e: test_qapi::EnumOne = from_qobject(qo).unwrap();
+ assert_eq!(e, test_qapi::EnumOne::VALUE1);
+ assert_eq!(to_qobject(e).unwrap().to_json(), json);
+}
+
+#[test]
+fn test_struct() {
+ let expected = test_qapi::TestStruct {
+ integer: -42,
+ boolean: true,
+ string: "foo".into(),
+ };
+ let json = "{\"integer\": -42, \"boolean\": true, \"string\": \"foo\"}";
+ let qo = QObject::from_json(json).unwrap();
+ let ts: test_qapi::TestStruct = from_qobject(qo).unwrap();
+ assert_eq!(ts, expected);
+ assert_eq!(to_qobject(ts).unwrap().to_json(), json);
+}
+
+#[test]
+fn test_struct_nested() {
+ let expected = test_qapi::UserDefTwo {
+ string0: "string0".into(),
+ dict1: test_qapi::UserDefTwoDict {
+ string1: "string1".into(),
+ dict2: test_qapi::UserDefTwoDictDict {
+ userdef: test_qapi::UserDefOne {
+ integer: 42,
+ string: "string".into(),
+ enum1: None,
+ },
+ string: "string2".into(),
+ },
+ dict3: None,
+ },
+ };
+ let json = "{\"string0\": \"string0\", \"dict1\": {\"dict2\": {\"string\": \"string2\", \
+ \"userdef\": {\"integer\": 42, \"string\": \"string\"}}, \"string1\": \
+ \"string1\"}}";
+ let qo = QObject::from_json(json).unwrap();
+ let udt: test_qapi::UserDefTwo = from_qobject(qo).unwrap();
+ assert_eq!(udt, expected);
+ assert_eq!(to_qobject(udt).unwrap().to_json(), json);
+}
+
+#[test]
+fn test_list() {
+ let expected = [
+ test_qapi::UserDefOne {
+ integer: 42,
+ string: "string0".into(),
+ enum1: None,
+ },
+ test_qapi::UserDefOne {
+ integer: 43,
+ string: "string1".into(),
+ enum1: None,
+ },
+ test_qapi::UserDefOne {
+ integer: 44,
+ string: "string2".into(),
+ enum1: None,
+ },
+ ];
+ let json = "[{\"integer\": 42, \"string\": \"string0\"}, {\"integer\": 43, \"string\": \
+ \"string1\"}, {\"integer\": 44, \"string\": \"string2\"}]";
+ let qo = QObject::from_json(json).unwrap();
+ let ud_list: Vec<test_qapi::UserDefOne> = from_qobject(qo).unwrap();
+ assert_eq!(ud_list, expected);
+ assert_eq!(to_qobject(ud_list).unwrap().to_json(), json);
+}
+
+#[test]
+fn test_flat_union() {
+ let expected = test_qapi::UserDefFlatUnion {
+ integer: 41,
+ string: "str".into(),
+ u: test_qapi::UserDefFlatUnionVariant::Value1(test_qapi::UserDefA {
+ boolean: true,
+ a_b: None,
+ }),
+ };
+ let json = "{\"integer\": 41, \"boolean\": true, \"enum1\": \"value1\", \"string\": \"str\"}";
+ let qo = QObject::from_json(json).unwrap();
+ let ud_fu: test_qapi::UserDefFlatUnion = from_qobject(qo).unwrap();
+ assert_eq!(ud_fu, expected);
+ assert_eq!(to_qobject(ud_fu).unwrap().to_json(), json);
+}
+
+#[test]
+fn test_union_in_union() {
+ let expected = test_qapi::TestUnionInUnion {
+ u: test_qapi::TestUnionInUnionVariant::ValueA(test_qapi::TestUnionTypeA {
+ u: test_qapi::TestUnionTypeAVariant::ValueA1(test_qapi::TestUnionTypeA1 {
+ integer: 2,
+ name: "fish".into(),
+ }),
+ }),
+ };
+ let json =
+ "{\"name\": \"fish\", \"integer\": 2, \"type-a\": \"value-a1\", \"type\": \"value-a\"}";
+ let qo = QObject::from_json(json).unwrap();
+ let uu: test_qapi::TestUnionInUnion = from_qobject(qo).unwrap();
+ assert_eq!(uu, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+
+ let expected = test_qapi::TestUnionInUnion {
+ u: test_qapi::TestUnionInUnionVariant::ValueA(test_qapi::TestUnionTypeA {
+ u: test_qapi::TestUnionTypeAVariant::ValueA2(test_qapi::TestUnionTypeA2 {
+ integer: 1729,
+ size: 87539319,
+ }),
+ }),
+ };
+ let json =
+ "{\"integer\": 1729, \"type-a\": \"value-a2\", \"size\": 87539319, \"type\": \"value-a\"}";
+ let qo = QObject::from_json(json).unwrap();
+ let uu: test_qapi::TestUnionInUnion = from_qobject(qo).unwrap();
+ assert_eq!(uu, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+
+ let expected = test_qapi::TestUnionInUnion {
+ u: test_qapi::TestUnionInUnionVariant::ValueB(test_qapi::TestUnionTypeB {
+ integer: 1729,
+ onoff: true,
+ }),
+ };
+ let json = "{\"integer\": 1729, \"onoff\": true, \"type\": \"value-b\"}";
+ let qo = QObject::from_json(json).unwrap();
+ let uu: test_qapi::TestUnionInUnion = from_qobject(qo).unwrap();
+ assert_eq!(uu, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+}
+
+#[test]
+fn test_alternate() {
+ let expected = test_qapi::UserDefAlternate::I(42);
+ let json = "42";
+ let qo = QObject::from_json(json).unwrap();
+ let uda: test_qapi::UserDefAlternate = from_qobject(qo).unwrap();
+ assert_eq!(uda, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+
+ let expected = test_qapi::UserDefAlternate::E(test_qapi::EnumOne::VALUE1);
+ let json = "\"value1\"";
+ let qo = QObject::from_json(json).unwrap();
+ let uda: test_qapi::UserDefAlternate = from_qobject(qo).unwrap();
+ assert_eq!(uda, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+
+ let expected = test_qapi::UserDefAlternate::N(());
+ let json = "null";
+ let qo = QObject::from_json(json).unwrap();
+ let uda: test_qapi::UserDefAlternate = from_qobject(qo).unwrap();
+ assert_eq!(uda, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+
+ let expected = test_qapi::UserDefAlternate::Udfu(test_qapi::UserDefFlatUnion {
+ integer: 42,
+ string: "str".to_string(),
+ u: test_qapi::UserDefFlatUnionVariant::Value1(test_qapi::UserDefA {
+ boolean: true,
+ a_b: None,
+ }),
+ });
+ let json = "{\"integer\": 42, \"boolean\": true, \"enum1\": \"value1\", \"string\": \"str\"}";
+ let qo = QObject::from_json(json).unwrap();
+ let uda: test_qapi::UserDefAlternate = from_qobject(qo).unwrap();
+ assert_eq!(uda, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+
+ let expected = test_qapi::WrapAlternate {
+ alt: test_qapi::UserDefAlternate::I(42),
+ };
+ let json = "{\"alt\": 42}";
+ let qo = QObject::from_json(json).unwrap();
+ let uda: test_qapi::WrapAlternate = from_qobject(qo).unwrap();
+ assert_eq!(uda, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+
+ let expected = test_qapi::WrapAlternate {
+ alt: test_qapi::UserDefAlternate::E(test_qapi::EnumOne::VALUE1),
+ };
+ let json = "{\"alt\": \"value1\"}";
+ let qo = QObject::from_json(json).unwrap();
+ let uda: test_qapi::WrapAlternate = from_qobject(qo).unwrap();
+ assert_eq!(uda, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+
+ let expected = test_qapi::WrapAlternate {
+ alt: test_qapi::UserDefAlternate::Udfu(test_qapi::UserDefFlatUnion {
+ integer: 1,
+ string: "str".to_string(),
+ u: test_qapi::UserDefFlatUnionVariant::Value1(test_qapi::UserDefA {
+ boolean: true,
+ a_b: None,
+ }),
+ }),
+ };
+ let json = "{\"alt\": {\"integer\": 1, \"boolean\": true, \"enum1\": \"value1\", \"string\": \
+ \"str\"}}";
+ let qo = QObject::from_json(json).unwrap();
+ let uda: test_qapi::WrapAlternate = from_qobject(qo).unwrap();
+ assert_eq!(uda, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+}
+
+#[test]
+fn test_alternate_number() {
+ let expected = test_qapi::AltEnumNum::N(42.0);
+ let json = "42";
+ let qo = QObject::from_json(json).unwrap();
+ let uda: test_qapi::AltEnumNum = from_qobject(qo).unwrap();
+ assert_eq!(uda, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+
+ let expected = test_qapi::AltNumEnum::N(42.0);
+ let json = "42";
+ let qo = QObject::from_json(json).unwrap();
+ let uda: test_qapi::AltNumEnum = from_qobject(qo).unwrap();
+ assert_eq!(uda, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+
+ let expected = test_qapi::AltEnumInt::I(42);
+ let json = "42";
+ let qo = QObject::from_json(json).unwrap();
+ let uda: test_qapi::AltEnumInt = from_qobject(qo).unwrap();
+ assert_eq!(uda, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+
+ let expected = test_qapi::AltListInt::I(42);
+ let json = "42";
+ let qo = QObject::from_json(json).unwrap();
+ let uda: test_qapi::AltListInt = from_qobject(qo).unwrap();
+ assert_eq!(&uda, &expected);
+ assert_eq!(to_qobject(&expected).unwrap().to_json(), json);
+
+ // double
+ let json = "42.5";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<test_qapi::AltEnumBool>(qo).unwrap_err();
+
+ let expected = test_qapi::AltEnumNum::N(42.5);
+ let json = "42.5";
+ let qo = QObject::from_json(json).unwrap();
+ let uda: test_qapi::AltEnumNum = from_qobject(qo).unwrap();
+ assert_eq!(uda, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+
+ let expected = test_qapi::AltNumEnum::N(42.5);
+ let json = "42.5";
+ let qo = QObject::from_json(json).unwrap();
+ let uda: test_qapi::AltNumEnum = from_qobject(qo).unwrap();
+ assert_eq!(uda, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+
+ let json = "42.5";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<test_qapi::AltEnumInt>(qo).unwrap_err();
+}
+
+#[test]
+fn test_alternate_list() {
+ let expected = test_qapi::AltListInt::L(vec![42, 43, 44]);
+ let json = "[42, 43, 44]";
+ let qo = QObject::from_json(json).unwrap();
+ let uda: test_qapi::AltListInt = from_qobject(qo).unwrap();
+ assert_eq!(uda, expected);
+ assert_eq!(to_qobject(expected).unwrap().to_json(), json);
+}
+
+#[test]
+fn test_errors() {
+ let json = "{ 'integer': false, 'boolean': 'foo', 'string': -42 }";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<test_qapi::TestStruct>(qo).unwrap_err();
+
+ let json = "[ '1', '2', false, '3' ]";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<Vec<String>>(qo).unwrap_err();
+
+ let json = "{ 'str': 'hi' }";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<test_qapi::UserDefTwo>(qo).unwrap_err();
+
+ let json = "{}";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<test_qapi::WrapAlternate>(qo).unwrap_err();
+}
+
+#[test]
+fn test_wrong_type() {
+ let json = "[]";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<test_qapi::TestStruct>(qo).unwrap_err();
+
+ let json = "{}";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<Vec<String>>(qo).unwrap_err();
+
+ let json = "1";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<test_qapi::TestStruct>(qo).unwrap_err();
+
+ let json = "{}";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<i64>(qo).unwrap_err();
+
+ let json = "1";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<Vec<String>>(qo).unwrap_err();
+
+ let json = "[]";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<i64>(qo).unwrap_err();
+}
+
+#[test]
+fn test_fail_struct() {
+ let json = "{ 'integer': -42, 'boolean': true, 'string': 'foo', 'extra': 42 }";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<test_qapi::TestStruct>(qo).unwrap_err();
+}
+
+#[test]
+fn test_fail_struct_nested() {
+ let json = "{ 'string0': 'string0', 'dict1': { 'string1': 'string1', 'dict2': { 'userdef1': { \
+ 'integer': 42, 'string': 'string', 'extra': [42, 23, {'foo':'bar'}] }, 'string2': \
+ 'string2'}}}";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<test_qapi::UserDefTwo>(qo).unwrap_err();
+}
+
+#[test]
+fn test_fail_struct_in_list() {
+ let json = "[ { 'string': 'string0', 'integer': 42 }, { 'string': 'string1', 'integer': 43 }, \
+ { 'string': 'string2', 'integer': 44, 'extra': 'ggg' } ]";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<Vec<test_qapi::UserDefOne>>(qo).unwrap_err();
+}
+
+#[test]
+fn test_fail_union_flat() {
+ let json = "{ 'enum1': 'value2', 'string': 'c', 'integer': 41, 'boolean': true }";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<Vec<test_qapi::UserDefFlatUnion>>(qo).unwrap_err();
+}
+
+#[test]
+fn test_fail_union_flat_no_discrim() {
+ // test situation where discriminator field ('enum1' here) is missing
+ let json = "{ 'integer': 42, 'string': 'c', 'string1': 'd', 'string2': 'e' }";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<Vec<test_qapi::UserDefFlatUnion2>>(qo).unwrap_err();
+}
+
+#[test]
+fn test_fail_alternate() {
+ let json = "3.14";
+ let qo = QObject::from_json(json).unwrap();
+ from_qobject::<Vec<test_qapi::UserDefAlternate>>(qo).unwrap_err();
+}
+
+#[test]
+fn test_qapi() {
+ let expected = qapi::InetSocketAddress {
+ host: "host-val".to_string(),
+ port: "port-val".to_string(),
+ numeric: None,
+ to: None,
+ ipv4: None,
+ ipv6: None,
+ keep_alive: None,
+ #[cfg(HAVE_TCP_KEEPCNT)]
+ keep_alive_count: None,
+ #[cfg(HAVE_TCP_KEEPIDLE)]
+ keep_alive_idle: Some(42),
+ #[cfg(HAVE_TCP_KEEPINTVL)]
+ keep_alive_interval: None,
+ #[cfg(HAVE_IPPROTO_MPTCP)]
+ mptcp: None,
+ };
+
+ let qsa = to_qobject(&expected).unwrap();
+ let json = qsa.to_json();
+ assert_eq!(
+ json,
+ "{\"port\": \"port-val\", \"keep_alive_idle\": 42, \"host\": \"host-val\"}"
+ );
+ let sa: qapi::InetSocketAddress = from_qobject(qsa).unwrap();
+ assert_eq!(sa, expected);
+
+ let expected = qapi::SocketAddressVariant::Inet(expected);
+ let qsav = to_qobject(&expected).unwrap();
+ let json = qsav.to_json();
+ assert_eq!(
+ json,
+ "{\"port\": \"port-val\", \"keep_alive_idle\": 42, \"host\": \"host-val\", \"type\": \
+ \"inet\"}"
+ );
+ let sav: qapi::SocketAddressVariant = from_qobject(qsav).unwrap();
+ assert_eq!(sav, expected);
+
+ let expected = qapi::Qcow2BitmapInfo {
+ name: "name-val".to_string(),
+ granularity: 4096,
+ flags: vec![
+ qapi::Qcow2BitmapInfoFlags::IN_USE,
+ qapi::Qcow2BitmapInfoFlags::AUTO,
+ ],
+ };
+ let qbi = to_qobject(&expected).unwrap();
+ let json = qbi.to_json();
+ assert_eq!(
+ json,
+ "{\"flags\": [\"in-use\", \"auto\"], \"name\": \"name-val\", \"granularity\": 4096}"
+ );
+ let bi: qapi::Qcow2BitmapInfo = from_qobject(qbi).unwrap();
+ assert_eq!(bi, expected);
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 55+ messages in thread
* Re: [PATCH v2 07/16] rust/qobject: add from/to JSON bindings for QObject
2026-01-08 13:10 ` [PATCH v2 07/16] rust/qobject: add from/to JSON bindings for QObject Paolo Bonzini
@ 2026-01-15 13:17 ` Zhao Liu
0 siblings, 0 replies; 55+ messages in thread
From: Zhao Liu @ 2026-01-15 13:17 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, armbru, Marc-André Lureau, qemu-rust
On Thu, Jan 08, 2026 at 02:10:34PM +0100, Paolo Bonzini wrote:
> Date: Thu, 8 Jan 2026 14:10:34 +0100
> From: Paolo Bonzini <pbonzini@redhat.com>
> Subject: [PATCH v2 07/16] rust/qobject: add from/to JSON bindings for
> QObject
> X-Mailer: git-send-email 2.52.0
>
> These are used by tests. However it could even be an idea to use
> serde_json + transcoding and get rid of the C version...
>
> Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
> rust/util/wrapper.h | 1 +
> rust/util/src/qobject/mod.rs | 17 +++++++++++++++++
> 2 files changed, 18 insertions(+)
Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 08/16] rust/qobject: add Display/Debug
2026-01-08 13:10 ` [PATCH v2 08/16] rust/qobject: add Display/Debug Paolo Bonzini
@ 2026-01-15 13:19 ` Zhao Liu
0 siblings, 0 replies; 55+ messages in thread
From: Zhao Liu @ 2026-01-15 13:19 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, armbru, Marc-André Lureau, qemu-rust
On Thu, Jan 08, 2026 at 02:10:35PM +0100, Paolo Bonzini wrote:
> Date: Thu, 8 Jan 2026 14:10:35 +0100
> From: Paolo Bonzini <pbonzini@redhat.com>
> Subject: [PATCH v2 08/16] rust/qobject: add Display/Debug
> X-Mailer: git-send-email 2.52.0
>
> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
> rust/util/src/qobject/mod.rs | 28 ++++++++++++++++++++++++++++
> 1 file changed, 28 insertions(+)
Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 09/16] scripts/qapi: add QAPISchemaIfCond.rsgen()
2026-01-08 13:10 ` [PATCH v2 09/16] scripts/qapi: add QAPISchemaIfCond.rsgen() Paolo Bonzini
@ 2026-01-19 6:58 ` Zhao Liu
2026-02-25 6:48 ` Markus Armbruster
1 sibling, 0 replies; 55+ messages in thread
From: Zhao Liu @ 2026-01-19 6:58 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, armbru, Marc-André Lureau, qemu-rust
On Thu, Jan 08, 2026 at 02:10:36PM +0100, Paolo Bonzini wrote:
> Date: Thu, 8 Jan 2026 14:10:36 +0100
> From: Paolo Bonzini <pbonzini@redhat.com>
> Subject: [PATCH v2 09/16] scripts/qapi: add QAPISchemaIfCond.rsgen()
> X-Mailer: git-send-email 2.52.0
>
> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>
> Generate Rust #[cfg(...)] guards from QAPI 'if' conditions; it
> turns out that they are very similar, with both of them using
> not/any/all, so just walk the tree.
>
> The next commit will put it to use.
>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Link: https://lore.kernel.org/r/20210907121943.3498701-15-marcandre.lureau@redhat.com
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
> scripts/qapi/common.py | 19 +++++++++++++++++++
> scripts/qapi/schema.py | 4 ++++
> 2 files changed, 23 insertions(+)
>
> diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
> index d7c8aa3365c..14d5dd259c4 100644
> --- a/scripts/qapi/common.py
> +++ b/scripts/qapi/common.py
> @@ -199,6 +199,25 @@ def guardend(name: str) -> str:
> name=c_fname(name).upper())
>
LGTM,
Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 11/16] scripts/qapi: pull c_name from camel_to_upper to caller
2026-01-08 13:10 ` [PATCH v2 11/16] scripts/qapi: pull c_name from camel_to_upper to caller Paolo Bonzini
@ 2026-01-19 7:05 ` Zhao Liu
2026-02-25 8:32 ` Markus Armbruster
1 sibling, 0 replies; 55+ messages in thread
From: Zhao Liu @ 2026-01-19 7:05 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, armbru, Marc-André Lureau, qemu-rust
On Thu, Jan 08, 2026 at 02:10:38PM +0100, Paolo Bonzini wrote:
> Date: Thu, 8 Jan 2026 14:10:38 +0100
> From: Paolo Bonzini <pbonzini@redhat.com>
> Subject: [PATCH v2 11/16] scripts/qapi: pull c_name from camel_to_upper to
> caller
> X-Mailer: git-send-email 2.52.0
>
> Allow using it for other languages too.
>
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
> scripts/qapi/common.py | 4 ++--
> 1 file changed, 2 insertions(+), 2 deletions(-)
Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 00/16] rust: QObject and QAPI bindings
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (15 preceding siblings ...)
2026-01-08 13:10 ` [PATCH v2 16/16] rust/tests: QAPI integration tests Paolo Bonzini
@ 2026-02-17 8:10 ` Paolo Bonzini
2026-02-19 13:39 ` Markus Armbruster
` (2 subsequent siblings)
19 siblings, 0 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-02-17 8:10 UTC (permalink / raw)
To: qemu-devel
Cc: Armbruster, Markus, Marc-André Lureau,
open list:Rust-related patc...
[-- Attachment #1: Type: text/plain, Size: 7566 bytes --]
It's been more than a month... Looking for reviews of the qapi-gen changes,
especially the non-Rust-specific parts.
Paolo
Il gio 8 gen 2026, 14:10 Paolo Bonzini <pbonzini@redhat.com> ha scritto:
> This adds two related parts of the Rust bindings:
>
> - QAPI code generator that creates Rust structs from the JSON
> description. The structs are *not* ABI compatible with the
> C ones, instead they use native Rust data types.
>
> - QObject bindings and (de)serialization support, which can be used to
> convert QObjects to and from QAPI structs.
>
> Unfortunately Rust code is not able to use visitors, other than by
> creating an intermediate QObject. This is because of the different
> architecture of serde vs. QAPI visitors, and because visitor's
> dual-purpose functions, where the same function is used by both input and
> output visitors, rely heavily on the structs using the same representation
> as the visitor arguments (for example NUL-terminated strings).
>
> The serde format implementation was co-authored by me and Marc-André.
> Marc-André did all the bug fixing and integration testing.
>
> As an example of how this would be used, the marshaling functions for
> QMP commands would look like this:
>
> fn qmp_marshal_query_stats(args: *mut QDict,
> retp: *mut *mut QObject, errp: *mut *mut Error)
> {
> let qobj = unsafe { QObject::cloned_from_raw(args.cast()) };
>
> let result = from_qobject::<StatsFilter>(qobj)
> .map_err(anyhow::Error::from)
> .and_then(qmp_query_stats)
> .and_then(|ret|
> to_qobject::<Vec<StatsResult>>(ret).map_err(anyhow::Error::from));
>
> match qmp_marshal_query_stats_rs(qobj) {
> Ok(ret) => unsafe { *retp = ret.into_raw(); },
> Err(e) => unsafe { crate::Error::from(e).propagate(errp) },
> }
> }
>
> Despite the long v1->v2, the changes are mostly cosmetic; there are a few
> small differences in the detection of types to be wrapped with Box<>,
> because Vec<> is now considered to break cycles.
>
> Available at branch rust-next of https://gitlab.com/bonzini/qemu (which
> is now almost empty other than this series, yay).
>
> Paolo
>
> v1->v2, Rust:
> - adjust comments in C code (include/qobject/qobject.h)
> - improve or fix documentation in rust/util/src/qobject/mod.rs
> - introduce a qobject! macro that is equivalent to QOBJECT()
> - remark when functions are converted from C inlines
> - rename from_int/from_uint/from_double macros
> - fix incorrect "native: true" on serde and serde_core overrides
> - rebase
>
> qapi-gen:
> - rewrite QAPISchemaIfCond.rsgen() to avoid messy typing
> - new patches 10-11 to extend existing qapigen code
> - rewritten rs_type() to avoid going through a C type first
> - removed renaming of QAPICBackend
> - reused C functions for case conversions
> - clean up rs_name, move it to common.py (needed for rs_type() changes)
> - Vec<> can break cycles and avoid boxing of structs
> - restrict squashing of consecutive newlines to Rust code
> - add copyright blurb
> - annotate that using a separate qapi-gen backend is temporary
> - drop #[derive(common::TryInto)] from enums
>
> Marc-André Lureau (7):
> rust/qobject: add Display/Debug
> scripts/qapi: add QAPISchemaIfCond.rsgen()
> scripts/qapi: generate high-level Rust bindings
> scripts/rustc_args: add --no-strict-cfg
> rust/util: build QAPI types
> scripts/qapi: add serde attributes
> rust/tests: QAPI integration tests
>
> Paolo Bonzini (9):
> rust/qobject: add basic bindings
> subprojects: add serde
> rust/qobject: add Serialize implementation
> rust/qobject: add Serializer (to_qobject) implementation
> rust/qobject: add Deserialize implementation
> rust/qobject: add Deserializer (from_qobject) implementation
> rust/qobject: add from/to JSON bindings for QObject
> scripts/qapi: add QAPISchemaType.is_predefined
> scripts/qapi: pull c_name from camel_to_upper to caller
>
> docs/devel/rust.rst | 1 +
> meson.build | 4 +-
> include/qobject/qobject.h | 5 +-
> rust/util/wrapper.h | 8 +
> qapi/meson.build | 9 +
> rust/Cargo.lock | 2 +
> rust/Cargo.toml | 2 +
> rust/meson.build | 4 +
> rust/tests/meson.build | 21 +-
> rust/tests/tests/integration.rs | 2 +
> rust/tests/tests/qapi.rs | 444 +++++++++++++
> rust/util/Cargo.toml | 2 +
> rust/util/meson.build | 30 +-
> rust/util/src/lib.rs | 4 +
> rust/util/src/qobject/deserialize.rs | 134 ++++
> rust/util/src/qobject/deserializer.rs | 371 +++++++++++
> rust/util/src/qobject/error.rs | 58 ++
> rust/util/src/qobject/mod.rs | 383 ++++++++++++
> rust/util/src/qobject/serialize.rs | 59 ++
> rust/util/src/qobject/serializer.rs | 585 ++++++++++++++++++
> scripts/archive-source.sh | 3 +
> scripts/make-release | 2 +-
> scripts/qapi/backend.py | 25 +
> scripts/qapi/common.py | 66 +-
> scripts/qapi/rs.py | 61 ++
> scripts/qapi/rs_types.py | 394 ++++++++++++
> scripts/qapi/schema.py | 74 ++-
> scripts/rust/rustc_args.py | 16 +-
> subprojects/.gitignore | 3 +
> .../packagefiles/serde-1-rs/meson.build | 36 ++
> .../packagefiles/serde-1.0.226-include.patch | 16 +
> .../packagefiles/serde_core-1-rs/meson.build | 25 +
> .../serde_core-1.0.226-include.patch | 15 +
> .../serde_derive-1-rs/meson.build | 35 ++
> .../serde_derive-1.0.226-include.patch | 11 +
> subprojects/serde-1-rs.wrap | 11 +
> subprojects/serde_core-1-rs.wrap | 11 +
> subprojects/serde_derive-1-rs.wrap | 11 +
> 38 files changed, 2910 insertions(+), 33 deletions(-)
> create mode 100644 rust/tests/tests/integration.rs
> create mode 100644 rust/tests/tests/qapi.rs
> create mode 100644 rust/util/src/qobject/deserialize.rs
> create mode 100644 rust/util/src/qobject/deserializer.rs
> create mode 100644 rust/util/src/qobject/error.rs
> create mode 100644 rust/util/src/qobject/mod.rs
> create mode 100644 rust/util/src/qobject/serialize.rs
> create mode 100644 rust/util/src/qobject/serializer.rs
> create mode 100644 scripts/qapi/rs.py
> create mode 100644 scripts/qapi/rs_types.py
> create mode 100644 subprojects/packagefiles/serde-1-rs/meson.build
> create mode 100644 subprojects/packagefiles/serde-1.0.226-include.patch
> create mode 100644 subprojects/packagefiles/serde_core-1-rs/meson.build
> create mode 100644
> subprojects/packagefiles/serde_core-1.0.226-include.patch
> create mode 100644 subprojects/packagefiles/serde_derive-1-rs/meson.build
> create mode 100644
> subprojects/packagefiles/serde_derive-1.0.226-include.patch
> create mode 100644 subprojects/serde-1-rs.wrap
> create mode 100644 subprojects/serde_core-1-rs.wrap
> create mode 100644 subprojects/serde_derive-1-rs.wrap
>
> --
> 2.52.0
>
[-- Attachment #2: Type: text/html, Size: 8907 bytes --]
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 00/16] rust: QObject and QAPI bindings
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (16 preceding siblings ...)
2026-02-17 8:10 ` [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
@ 2026-02-19 13:39 ` Markus Armbruster
2026-02-19 16:28 ` Paolo Bonzini
2026-02-23 9:53 ` Daniel P. Berrangé
2026-02-24 14:06 ` Markus Armbruster
19 siblings, 1 reply; 55+ messages in thread
From: Markus Armbruster @ 2026-02-19 13:39 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, armbru, Marc-André Lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> This adds two related parts of the Rust bindings:
>
> - QAPI code generator that creates Rust structs from the JSON
> description. The structs are *not* ABI compatible with the
> C ones, instead they use native Rust data types.
>
> - QObject bindings and (de)serialization support, which can be used to
> convert QObjects to and from QAPI structs.
To and from generated Rust structs, I presume.
> Unfortunately Rust code is not able to use visitors, other than by
> creating an intermediate QObject. This is because of the different
> architecture of serde vs. QAPI visitors, and because visitor's
> dual-purpose functions, where the same function is used by both input and
> output visitors, rely heavily on the structs using the same representation
> as the visitor arguments (for example NUL-terminated strings).
Prior discussion of some aspects:
Subject: Re: [PATCH preview 0/3] reviving minimal QAPI generation from 2021
Date: Mon, 23 Jun 2025 14:52:46 +0200
Message-ID: <877c1262n5.fsf@pond.sub.org>
https://lore.kernel.org/qemu-devel/877c1262n5.fsf@pond.sub.org/
> The serde format implementation was co-authored by me and Marc-André.
> Marc-André did all the bug fixing and integration testing.
>
> As an example of how this would be used, the marshaling functions for
> QMP commands would look like this:
>
> fn qmp_marshal_query_stats(args: *mut QDict,
> retp: *mut *mut QObject, errp: *mut *mut Error)
> {
> let qobj = unsafe { QObject::cloned_from_raw(args.cast()) };
>
> let result = from_qobject::<StatsFilter>(qobj)
> .map_err(anyhow::Error::from)
> .and_then(qmp_query_stats)
> .and_then(|ret| to_qobject::<Vec<StatsResult>>(ret).map_err(anyhow::Error::from));
>
> match qmp_marshal_query_stats_rs(qobj) {
> Ok(ret) => unsafe { *retp = ret.into_raw(); },
> Err(e) => unsafe { crate::Error::from(e).propagate(errp) },
> }
> }
Scoring 0.23 UOUPNBL (uses of unsafe per non-blank line). SCNR!
> Despite the long v1->v2, the changes are mostly cosmetic; there are a few
> small differences in the detection of types to be wrapped with Box<>,
> because Vec<> is now considered to break cycles.
>
> Available at branch rust-next of https://gitlab.com/bonzini/qemu (which
> is now almost empty other than this series, yay).
>
> Paolo
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 00/16] rust: QObject and QAPI bindings
2026-02-19 13:39 ` Markus Armbruster
@ 2026-02-19 16:28 ` Paolo Bonzini
0 siblings, 0 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-02-19 16:28 UTC (permalink / raw)
To: Markus Armbruster
Cc: qemu-devel, Marc-André Lureau,
open list:Rust-related patc...
[-- Attachment #1: Type: text/plain, Size: 2061 bytes --]
Il gio 19 feb 2026, 14:39 Markus Armbruster <armbru@redhat.com> ha scritto:
> Paolo Bonzini <pbonzini@redhat.com> writes:
>
> > This adds two related parts of the Rust bindings:
> >
> > - QAPI code generator that creates Rust structs from the JSON
> > description. The structs are *not* ABI compatible with the
> > C ones, instead they use native Rust data types.
> >
> > - QObject bindings and (de)serialization support, which can be used to
> > convert QObjects to and from QAPI structs.
>
> To and from generated Rust structs, I presume.
>
Yes, or enums—and actually any (de)serializable Rust type, of which QAPI
structs should be the most common case.
> As an example of how this would be used, the marshaling functions for
> > QMP commands would look like this:
> >
> > fn qmp_marshal_query_stats(args: *mut QDict,
> > retp: *mut *mut QObject, errp: *mut *mut Error)
> > {
> > let qobj = unsafe { QObject::cloned_from_raw(args.cast()) };
> >
> > let result = from_qobject::<StatsFilter>(qobj)
> > .map_err(anyhow::Error::from)
> > .and_then(qmp_query_stats)
> > .and_then(|ret|
> to_qobject::<Vec<StatsResult>>(ret).map_err(anyhow::Error::from));
> >
> > match qmp_marshal_query_stats_rs(qobj) {
> > Ok(ret) => unsafe { *retp = ret.into_raw(); },
> > Err(e) => unsafe { crate::Error::from(e).propagate(errp) },
> > }
> > }
>
> Scoring 0.23 UOUPNBL (uses of unsafe per non-blank line). SCNR!
>
Well, that's why you automatically generate this! You could also use only
one or two unsafe blocks but I preferred to mark explicitly the code that
requires it.
Paolo
> > Despite the long v1->v2, the changes are mostly cosmetic; there are a few
> > small differences in the detection of types to be wrapped with Box<>,
> > because Vec<> is now considered to break cycles.
> >
> > Available at branch rust-next of https://gitlab.com/bonzini/qemu (which
> > is now almost empty other than this series, yay).
> >
> > Paolo
>
>
[-- Attachment #2: Type: text/html, Size: 3412 bytes --]
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 00/16] rust: QObject and QAPI bindings
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (17 preceding siblings ...)
2026-02-19 13:39 ` Markus Armbruster
@ 2026-02-23 9:53 ` Daniel P. Berrangé
2026-02-23 15:54 ` Paolo Bonzini
2026-02-24 14:06 ` Markus Armbruster
19 siblings, 1 reply; 55+ messages in thread
From: Daniel P. Berrangé @ 2026-02-23 9:53 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, armbru, Marc-André Lureau, qemu-rust
On Thu, Jan 08, 2026 at 02:10:27PM +0100, Paolo Bonzini wrote:
> This adds two related parts of the Rust bindings:
>
> - QAPI code generator that creates Rust structs from the JSON
> description. The structs are *not* ABI compatible with the
> C ones, instead they use native Rust data types.
>
> - QObject bindings and (de)serialization support, which can be used to
> convert QObjects to and from QAPI structs.
>
> Unfortunately Rust code is not able to use visitors, other than by
> creating an intermediate QObject. This is because of the different
> architecture of serde vs. QAPI visitors, and because visitor's
> dual-purpose functions, where the same function is used by both input and
> output visitors, rely heavily on the structs using the same representation
> as the visitor arguments (for example NUL-terminated strings).
>
> The serde format implementation was co-authored by me and Marc-André.
> Marc-André did all the bug fixing and integration testing.
>
> As an example of how this would be used, the marshaling functions for
> QMP commands would look like this:
Can you give more of the big picture about what follows this ? From
this example you're showing, are you suggesting that you'll be soon
moving QMP command impls from C to Rust ?
>
> fn qmp_marshal_query_stats(args: *mut QDict,
> retp: *mut *mut QObject, errp: *mut *mut Error)
> {
> let qobj = unsafe { QObject::cloned_from_raw(args.cast()) };
>
> let result = from_qobject::<StatsFilter>(qobj)
> .map_err(anyhow::Error::from)
> .and_then(qmp_query_stats)
> .and_then(|ret| to_qobject::<Vec<StatsResult>>(ret).map_err(anyhow::Error::from));
>
> match qmp_marshal_query_stats_rs(qobj) {
> Ok(ret) => unsafe { *retp = ret.into_raw(); },
> Err(e) => unsafe { crate::Error::from(e).propagate(errp) },
> }
> }
>
With regards,
Daniel
--
|: https://berrange.com ~~ https://hachyderm.io/@berrange :|
|: https://libvirt.org ~~ https://entangle-photo.org :|
|: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :|
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings
2026-01-08 13:10 ` [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings Paolo Bonzini
@ 2026-02-23 12:36 ` Markus Armbruster
2026-02-23 16:11 ` Paolo Bonzini
2026-02-25 14:39 ` Markus Armbruster
2026-03-03 9:19 ` Markus Armbruster
2 siblings, 1 reply; 55+ messages in thread
From: Markus Armbruster @ 2026-02-23 12:36 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>
> Generate high-level native Rust declarations for the QAPI types.
>
> - char* is mapped to String, scalars to there corresponding Rust types
>
> - enums use #[repr(u32)] and can be transmuted to their C counterparts
>
> - has_foo/foo members are mapped to Option<T>
>
> - lists are represented as Vec<T>
>
> - structures map fields 1:1 to Rust
>
> - alternate are represented as Rust enum, each variant being a 1-element
> tuple
>
> - unions are represented in a similar way as in C: a struct S with a "u"
> member (since S may have extra 'base' fields). The discriminant
> isn't a member of S, since Rust enum already include it, but it can be
> recovered with "mystruct.u.into()"
>
> Anything that includes a recursive struct puts it in a Box. Lists are
> not considered recursive, because Vec breaks the recursion (it's possible
> to construct an object containing an empty Vec of its own type).
>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Link: https://lore.kernel.org/r/20210907121943.3498701-21-marcandre.lureau@redhat.com
> [Paolo: rewrite conversion of schema types to Rust types]
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
Please mention how to actually generate Rust, because it's anything but
obvious: pass -B qapi.backend.QAPIRsBackend to qapi-gen.py. Recommend
to mention it again in the cover letter.
Additionally, state that this is a hack we'll want to replace both in
the commit message and a TODO comment. See review of v1's cover letter
for why.
[...]
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 00/16] rust: QObject and QAPI bindings
2026-02-23 9:53 ` Daniel P. Berrangé
@ 2026-02-23 15:54 ` Paolo Bonzini
2026-02-23 16:24 ` Daniel P. Berrangé
0 siblings, 1 reply; 55+ messages in thread
From: Paolo Bonzini @ 2026-02-23 15:54 UTC (permalink / raw)
To: Daniel P. Berrangé
Cc: qemu-devel, armbru, Marc-André Lureau, qemu-rust
On 2/23/26 10:53, Daniel P. Berrangé wrote:
> On Thu, Jan 08, 2026 at 02:10:27PM +0100, Paolo Bonzini wrote:
>> This adds two related parts of the Rust bindings:
>>
>> - QAPI code generator that creates Rust structs from the JSON
>> description. The structs are *not* ABI compatible with the
>> C ones, instead they use native Rust data types.
>>
>> - QObject bindings and (de)serialization support, which can be used to
>> convert QObjects to and from QAPI structs.
>>
>> Unfortunately Rust code is not able to use visitors, other than by
>> creating an intermediate QObject. This is because of the different
>> architecture of serde vs. QAPI visitors, and because visitor's
>> dual-purpose functions, where the same function is used by both input and
>> output visitors, rely heavily on the structs using the same representation
>> as the visitor arguments (for example NUL-terminated strings).
>>
>> The serde format implementation was co-authored by me and Marc-André.
>> Marc-André did all the bug fixing and integration testing.
>>
>> As an example of how this would be used, the marshaling functions for
>> QMP commands would look like this:
>
> Can you give more of the big picture about what follows this ? From
> this example you're showing, are you suggesting that you'll be soon
> moving QMP command impls from C to Rust ?
The reasons for Marc-André and I to do this was just as an exploration.
The patches show a different way to do what he had already tackled in
2022, and allow a direct comparison the relative benefits between a more
Rust-focused approach and a more direct translation of QEMU's C code.
Marc-André indeed had command implementations in Rust and that would be
an obvious next step to continue on the QAPI side. On the other hand,
the QObject part (without QAPI structs) also works as a step towards
more QOM integration, especially with respect to properties and
backends. Having a full-blown QObject implementation to move around
bools is perhaps over-engineered, but is also an easy way to make a
generic implementation that works for all QAPI types.
The block layer also needs QAPI for option parsing, so Kevin's work on
block layer bindings would also benefit from having this integration.
In other words, there is no real big picture from me other than "someone
needs to do the hard part for others": QOM, QAPI and the build system
are the three central components from which all other bindings branch
out. Zhao and Marc-André have helped a lot with this work, and they
also showed how to use these hard parts (see Zhao's vm-memory work and
Marc-André's GStreamer backend), now it's also time for others to try
and to take inspiration from Rust for improving the C side of things.
Paolo
>
>>
>> fn qmp_marshal_query_stats(args: *mut QDict,
>> retp: *mut *mut QObject, errp: *mut *mut Error)
>> {
>> let qobj = unsafe { QObject::cloned_from_raw(args.cast()) };
>>
>> let result = from_qobject::<StatsFilter>(qobj)
>> .map_err(anyhow::Error::from)
>> .and_then(qmp_query_stats)
>> .and_then(|ret| to_qobject::<Vec<StatsResult>>(ret).map_err(anyhow::Error::from));
>>
>> match qmp_marshal_query_stats_rs(qobj) {
>> Ok(ret) => unsafe { *retp = ret.into_raw(); },
>> Err(e) => unsafe { crate::Error::from(e).propagate(errp) },
>> }
>> }
>>
>
> With regards,
> Daniel
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings
2026-02-23 12:36 ` Markus Armbruster
@ 2026-02-23 16:11 ` Paolo Bonzini
2026-02-24 13:46 ` Markus Armbruster
0 siblings, 1 reply; 55+ messages in thread
From: Paolo Bonzini @ 2026-02-23 16:11 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
On 2/23/26 13:36, Markus Armbruster wrote:
> Paolo Bonzini <pbonzini@redhat.com> writes:
>
>> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>>
>> Generate high-level native Rust declarations for the QAPI types.
>>
>> - char* is mapped to String, scalars to there corresponding Rust types
>>
>> - enums use #[repr(u32)] and can be transmuted to their C counterparts
>>
>> - has_foo/foo members are mapped to Option<T>
>>
>> - lists are represented as Vec<T>
>>
>> - structures map fields 1:1 to Rust
>>
>> - alternate are represented as Rust enum, each variant being a 1-element
>> tuple
>>
>> - unions are represented in a similar way as in C: a struct S with a "u"
>> member (since S may have extra 'base' fields). The discriminant
>> isn't a member of S, since Rust enum already include it, but it can be
>> recovered with "mystruct.u.into()"
>>
>> Anything that includes a recursive struct puts it in a Box. Lists are
>> not considered recursive, because Vec breaks the recursion (it's possible
>> to construct an object containing an empty Vec of its own type).
>>
>> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
>> Link: https://lore.kernel.org/r/20210907121943.3498701-21-marcandre.lureau@redhat.com
>> [Paolo: rewrite conversion of schema types to Rust types]
>> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
>
> Please mention how to actually generate Rust, because it's anything but
> obvious: pass -B qapi.backend.QAPIRsBackend to qapi-gen.py. Recommend
> to mention it again in the cover letter.
>
> Additionally, state that this is a hack we'll want to replace both in
> the commit message and a TODO comment. See review of v1's cover letter
> for why.
Yes, the TODO comment is in patch 14 where the thing actually gets used.
I can make it more prominent.
Paolo
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 00/16] rust: QObject and QAPI bindings
2026-02-23 15:54 ` Paolo Bonzini
@ 2026-02-23 16:24 ` Daniel P. Berrangé
2026-02-23 19:03 ` Paolo Bonzini
0 siblings, 1 reply; 55+ messages in thread
From: Daniel P. Berrangé @ 2026-02-23 16:24 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, armbru, Marc-André Lureau, qemu-rust
On Mon, Feb 23, 2026 at 04:54:07PM +0100, Paolo Bonzini wrote:
> On 2/23/26 10:53, Daniel P. Berrangé wrote:
> > On Thu, Jan 08, 2026 at 02:10:27PM +0100, Paolo Bonzini wrote:
> > > This adds two related parts of the Rust bindings:
> > >
> > > - QAPI code generator that creates Rust structs from the JSON
> > > description. The structs are *not* ABI compatible with the
> > > C ones, instead they use native Rust data types.
> > >
> > > - QObject bindings and (de)serialization support, which can be used to
> > > convert QObjects to and from QAPI structs.
> > >
> > > Unfortunately Rust code is not able to use visitors, other than by
> > > creating an intermediate QObject. This is because of the different
> > > architecture of serde vs. QAPI visitors, and because visitor's
> > > dual-purpose functions, where the same function is used by both input and
> > > output visitors, rely heavily on the structs using the same representation
> > > as the visitor arguments (for example NUL-terminated strings).
> > >
> > > The serde format implementation was co-authored by me and Marc-André.
> > > Marc-André did all the bug fixing and integration testing.
> > >
> > > As an example of how this would be used, the marshaling functions for
> > > QMP commands would look like this:
> >
> > Can you give more of the big picture about what follows this ? From
> > this example you're showing, are you suggesting that you'll be soon
> > moving QMP command impls from C to Rust ?
>
> The reasons for Marc-André and I to do this was just as an exploration. The
> patches show a different way to do what he had already tackled in 2022, and
> allow a direct comparison the relative benefits between a more Rust-focused
> approach and a more direct translation of QEMU's C code.
>
> Marc-André indeed had command implementations in Rust and that would be an
> obvious next step to continue on the QAPI side. On the other hand, the
> QObject part (without QAPI structs) also works as a step towards more QOM
> integration, especially with respect to properties and backends. Having a
> full-blown QObject implementation to move around bools is perhaps
> over-engineered, but is also an easy way to make a generic implementation
> that works for all QAPI types.
>
> The block layer also needs QAPI for option parsing, so Kevin's work on block
> layer bindings would also benefit from having this integration.
>
> In other words, there is no real big picture from me other than "someone
> needs to do the hard part for others": QOM, QAPI and the build system are
> the three central components from which all other bindings branch out. Zhao
> and Marc-André have helped a lot with this work, and they also showed how to
> use these hard parts (see Zhao's vm-memory work and Marc-André's GStreamer
> backend), now it's also time for others to try and to take inspiration from
> Rust for improving the C side of things.
I guess the thing I'm wondering about when I ask about the big picture
is to understand tne interoperability implications of this impl approach,
given the starting point that the Rust & C structs are *not* ABI compatible.
If we're anticipating that the Rust usage flows up from a Rust QMP command
handler, then the data is in the Rust struct right from the entry point
and perhaps the interoperability needs are reduced in scope.
If we're anticipating a calls back & forth between C & Rust code, that
directly handle the deserialized structs, then we have an open interop
question. Is the implication that we round-trip via JSON every time we
need to swap between a Rust struct and C struct for a given type ? Or
will we have a direct struct<->struct conversion mechanism ? Or is there
something else entirely ?
I feel a bit lost in how to evaluate this series without seeing some
real world usage, in a way that exposes the possible implications of
the lack of C/Rust ABI compat in the structs.
With regards,
Daniel
--
|: https://berrange.com ~~ https://hachyderm.io/@berrange :|
|: https://libvirt.org ~~ https://entangle-photo.org :|
|: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :|
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 00/16] rust: QObject and QAPI bindings
2026-02-23 16:24 ` Daniel P. Berrangé
@ 2026-02-23 19:03 ` Paolo Bonzini
0 siblings, 0 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-02-23 19:03 UTC (permalink / raw)
To: Daniel P. Berrangé
Cc: qemu-devel, Armbruster, Markus, Marc-André Lureau,
open list:Rust-related patc...
[-- Attachment #1: Type: text/plain, Size: 1871 bytes --]
Il lun 23 feb 2026, 17:25 Daniel P. Berrangé <berrange@redhat.com> ha
scritto:
> I guess the thing I'm wondering about when I ask about the big picture
> is to understand tne interoperability implications of this impl approach,
> given the starting point that the Rust & C structs are *not* ABI
> compatible.
>
> If we're anticipating that the Rust usage flows up from a Rust QMP command
> handler, then the data is in the Rust struct right from the entry point
> and perhaps the interoperability needs are reduced in scope.
>
> If we're anticipating a calls back & forth between C & Rust code, that
> directly handle the deserialized structs, then we have an open interop
> question. Is the implication that we round-trip via JSON every time we
> need to swap between a Rust struct and C struct for a given type ?
Right now yes.
Or
> will we have a direct struct<->struct conversion mechanism ? Or is there
> something else entirely ?
>
Marc-André had such a conversion mechanism in his patches. However I would
not include it unless there's a need. Generally speaking I don't expect
anything involving QAPI to be performance critical.
I feel a bit lost in how to evaluate this series without seeing some
> real world usage, in a way that exposes the possible implications of
> the lack of C/Rust ABI compat in the structs.
>
Yes, I understand and it's a recurrent problem with all the Rust
infrastructure, it's hard to evaluate it beyond test cases. Right now my
best hope for a first user would be either QMP commands, or the block layer.
Paolo
> With regards,
> Daniel
> --
> |: https://berrange.com ~~ https://hachyderm.io/@berrange :|
> |: https://libvirt.org ~~ https://entangle-photo.org :|
> |: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :|
>
>
[-- Attachment #2: Type: text/html, Size: 3676 bytes --]
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 01/16] rust/qobject: add basic bindings
2026-01-08 13:10 ` [PATCH v2 01/16] rust/qobject: add basic bindings Paolo Bonzini
@ 2026-02-24 10:03 ` Markus Armbruster
2026-02-24 10:35 ` Paolo Bonzini
0 siblings, 1 reply; 55+ messages in thread
From: Markus Armbruster @ 2026-02-24 10:03 UTC (permalink / raw)
To: Paolo Bonzini
Cc: qemu-devel, armbru, Marc-André Lureau, qemu-rust, Zhao Liu
Paolo Bonzini <pbonzini@redhat.com> writes:
> This is only a basic API, intended to be used by the serde traits.
>
> Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
> include/qobject/qobject.h | 5 +-
> rust/util/wrapper.h | 7 +
> rust/util/meson.build | 6 +-
> rust/util/src/lib.rs | 4 +
> rust/util/src/qobject/mod.rs | 331 +++++++++++++++++++++++++++++++++++
> 5 files changed, 350 insertions(+), 3 deletions(-)
> create mode 100644 rust/util/src/qobject/mod.rs
>
> diff --git a/include/qobject/qobject.h b/include/qobject/qobject.h
> index 02f4c6a6eb2..567da7b6c51 100644
> --- a/include/qobject/qobject.h
> +++ b/include/qobject/qobject.h
> @@ -35,7 +35,10 @@
> #include "qemu/atomic.h"
> #include "qapi/qapi-builtin-types.h"
>
> -/* Not for use outside include/qobject/ */
> +/*
> + * Not for use outside include/qobject/ (and Rust bindings, when they
> + * have to redo inline functions here).
Suggest "defined here".
> + */
> struct QObjectBase_ {
> QType type;
> size_t refcnt;
> diff --git a/rust/util/wrapper.h b/rust/util/wrapper.h
> index b9ed68a01d8..0907dd59142 100644
> --- a/rust/util/wrapper.h
> +++ b/rust/util/wrapper.h
> @@ -30,3 +30,10 @@ typedef enum memory_order {
> #include "qemu/log.h"
> #include "qemu/module.h"
> #include "qemu/timer.h"
> +#include "qobject/qnull.h"
> +#include "qobject/qbool.h"
> +#include "qobject/qnum.h"
> +#include "qobject/qstring.h"
> +#include "qobject/qobject.h"
> +#include "qobject/qlist.h"
> +#include "qobject/qdict.h"
Is this file just for bindgen, and never actually fed to the C compiler?
> diff --git a/rust/util/meson.build b/rust/util/meson.build
> index 98629394afb..28593286134 100644
> --- a/rust/util/meson.build
> +++ b/rust/util/meson.build
> @@ -37,8 +37,10 @@ _util_rs = static_library(
> 'src/prelude.rs',
> 'src/timer.rs',
> ],
> - {'.': _util_bindings_inc_rs}
> - ),
> + {'.': _util_bindings_inc_rs,
> + 'qobject': [
> + 'src/qobject/mod.rs',
> + ]}),
> dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs],
> )
>
> diff --git a/rust/util/src/lib.rs b/rust/util/src/lib.rs
> index 7d2de3ed811..96334466895 100644
> --- a/rust/util/src/lib.rs
> +++ b/rust/util/src/lib.rs
> @@ -9,6 +9,10 @@
> // for prelude-like modules
> #[rustfmt::skip]
> pub mod prelude;
> +
> +#[macro_use]
> +pub mod qobject;
> +
> pub mod timer;
>
> pub use error::{Error, Result, ResultExt};
> diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
> new file mode 100644
> index 00000000000..5744870ad1c
> --- /dev/null
> +++ b/rust/util/src/qobject/mod.rs
> @@ -0,0 +1,331 @@
> +//! `QObject` bindings
> +//!
> +//! This module implements bindings for QEMU's `QObject` data structure.
> +//! The bindings integrate with `serde`, which take the role of visitors
> +//! in Rust code.
> +
> +#![deny(clippy::unwrap_used)]
> +
> +use std::{
> + cell::UnsafeCell,
> + ffi::{c_char, CString},
> + mem::ManuallyDrop,
> + ptr::{addr_of, addr_of_mut},
> + sync::atomic::{AtomicUsize, Ordering},
> +};
> +
> +use common::assert_field_type;
> +
> +use crate::bindings;
> +
> +/// A wrapper for a C `QObject`.
> +///
> +/// Because `QObject` is not thread-safe, the safety of these bindings
> +/// right now hinges on treating them as immutable. It is part of the
> +/// contract with the `QObject` constructors that the Rust struct is
> +/// only built after the contents are stable.
> +///
> +/// Only a bare bones API is public; production and consumption of `QObject`
> +/// generally goes through `serde`.
> +pub struct QObject(&'static UnsafeCell<bindings::QObject>);
> +
> +// SAFETY: the QObject API are not thread-safe other than reference counting;
> +// but the Rust struct is only created once the contents are stable, and
> +// therefore it obeys the aliased XOR mutable invariant.
> +unsafe impl Send for QObject {}
> +unsafe impl Sync for QObject {}
> +
> +// Since a QObject can be a floating-point value, and potentially a NaN,
> +// do not implement Eq
> +impl PartialEq for QObject {
> + fn eq(&self, other: &Self) -> bool {
> + unsafe { bindings::qobject_is_equal(self.0.get(), other.0.get()) }
> + }
> +}
> +
> +impl QObject {
> + /// Construct a [`QObject`] from a C `QObjectBase` pointer.
It's spelled QObjectBase_. More of the same below, not flagging again.
> + /// The caller cedes its reference to the returned struct.
> + ///
> + /// # Safety
> + ///
> + /// The `QObjectBase` must not be changed from C code while
> + /// the Rust `QObject` lives
> + const unsafe fn from_base(p: *const bindings::QObjectBase_) -> Self {
> + QObject(unsafe { &*p.cast() })
> + }
> +
> + /// Construct a [`QObject`] from a C `QObject` pointer.
> + /// The caller cedes its reference to the returned struct.
> + ///
> + /// # Safety
> + ///
> + /// The `QObject` must not be changed from C code while
> + /// the Rust `QObject` lives
> + pub const unsafe fn from_raw(p: *const bindings::QObject) -> Self {
> + QObject(unsafe { &*p.cast() })
> + }
> +
> + /// Obtain a raw C pointer from a reference. `self` is consumed
> + /// without decreasing the reference count; therefore, the reference
> + /// is transferred to the `*mut bindings::QObject`.
> + pub fn into_raw(self) -> *mut bindings::QObject {
> + let src = ManuallyDrop::new(self);
> + src.0.get()
> + }
> +
> + /// Construct a [`QObject`] from a C `QObjectBase` pointer.
> + /// The caller *does not* cede its reference to the returned struct.
> + ///
> + /// # Safety
> + ///
> + /// The `QObjectBase` must not be changed from C code while
> + /// the Rust `QObject` lives
> + unsafe fn cloned_from_base(p: *const bindings::QObjectBase_) -> Self {
> + let orig = unsafe { ManuallyDrop::new(QObject::from_base(p)) };
> + (*orig).clone()
> + }
> +
> + /// Construct a [`QObject`] from a C `QObject` pointer.
> + /// The caller *does not* cede its reference to the returned struct.
> + ///
> + /// # Safety
> + ///
> + /// The `QObject` must not be changed from C code while
> + /// the Rust `QObject` lives
> + pub unsafe fn cloned_from_raw(p: *const bindings::QObject) -> Self {
> + let orig = unsafe { ManuallyDrop::new(QObject::from_raw(p)) };
> + (*orig).clone()
> + }
> +
> + fn refcnt(&self) -> &AtomicUsize {
> + assert_field_type!(bindings::QObjectBase_, refcnt, usize);
> + let qobj = self.0.get();
> + unsafe { AtomicUsize::from_ptr(addr_of_mut!((*qobj).base.refcnt)) }
> + }
> +}
> +
> +/// Rust equivalent of the C `QOBJECT` macro; for internal use only, because
> +/// all access should go through `From` (which already returns [`QObject`]
> +/// or serde.
(which already returns [`QObject`]),
> +macro_rules! qobject {
> + ($qobj:expr) => {{
> + let qobj: &bindings::QObjectBase_ = &$qobj.base;
> + // SAFETY: this `let` guarantees that either $qobj is a reference
> + // (not a raw pointer), or we're in an outer unsafe block
> + unsafe { QObject::from_base(qobj) }
> + }};
> +}
> +
> +impl From<()> for QObject {
> + fn from(_null: ()) -> Self {
> + // Conversion of the C inline `qnull` function
> + unsafe { QObject::cloned_from_base(addr_of!(bindings::qnull_.base)) }
> + }
> +}
Could we call C qnull() instead of using qnull_.base?
> +
> +impl<T> From<Option<T>> for QObject
> +where
> + QObject: From<T>,
> +{
> + fn from(o: Option<T>) -> Self {
> + o.map_or_else(|| ().into(), Into::into)
> + }
> +}
> +
> +impl From<bool> for QObject {
> + fn from(b: bool) -> Self {
> + let qobj = unsafe { &*bindings::qbool_from_bool(b) };
> + qobject!(qobj)
> + }
> +}
> +
> +macro_rules! impl_from_return_qnum_int {
> + ($t:ty) => {
> + impl From<$t> for QObject {
> + fn from(n: $t) -> Self {
> + let qobj = unsafe { &*bindings::qnum_from_int(n.into()) };
> + qobject!(qobj)
> + }
> + }
> + };
> +}
> +
> +impl_from_return_qnum_int!(i8);
> +impl_from_return_qnum_int!(i16);
> +impl_from_return_qnum_int!(i32);
> +impl_from_return_qnum_int!(i64);
> +
> +macro_rules! impl_from_return_qnum_uint {
> + ($t:ty) => {
> + impl From<$t> for QObject {
> + fn from(n: $t) -> Self {
> + let qobj = unsafe { &*bindings::qnum_from_uint(n.into()) };
> + qobject!(qobj)
> + }
> + }
> + };
> +}
> +
> +impl_from_return_qnum_uint!(u8);
> +impl_from_return_qnum_uint!(u16);
> +impl_from_return_qnum_uint!(u32);
> +impl_from_return_qnum_uint!(u64);
> +
> +macro_rules! impl_from_return_qnum_double {
> + ($t:ty) => {
> + impl From<$t> for QObject {
> + fn from(n: $t) -> Self {
> + let qobj = unsafe { &*bindings::qnum_from_double(n.into()) };
> + qobject!(qobj)
> + }
> + }
> + };
> +}
> +
> +impl_from_return_qnum_double!(f32);
> +impl_from_return_qnum_double!(f64);
> +
> +impl From<CString> for QObject {
> + fn from(s: CString) -> Self {
> + let qobj = unsafe { &*bindings::qstring_from_str(s.as_ptr()) };
> + qobject!(qobj)
> + }
> +}
> +
> +impl<A> FromIterator<A> for QObject
> +where
> + Self: From<A>,
> +{
> + fn from_iter<I: IntoIterator<Item = A>>(it: I) -> Self {
> + let qlist = unsafe { &mut *bindings::qlist_new() };
> + for elem in it {
> + let elem: QObject = elem.into();
> + let elem = elem.into_raw();
> + unsafe {
> + bindings::qlist_append_obj(qlist, elem);
> + }
> + }
> + qobject!(qlist)
> + }
> +}
> +
> +impl<A> FromIterator<(CString, A)> for QObject
> +where
> + Self: From<A>,
> +{
> + fn from_iter<I: IntoIterator<Item = (CString, A)>>(it: I) -> Self {
> + let qdict = unsafe { &mut *bindings::qdict_new() };
> + for (key, val) in it {
> + let val: QObject = val.into();
> + let val = val.into_raw();
> + unsafe {
> + bindings::qdict_put_obj(qdict, key.as_ptr().cast::<c_char>(), val);
> + }
> + }
> + qobject!(qdict)
> + }
> +}
> +
> +impl Clone for QObject {
> + fn clone(&self) -> Self {
> + self.refcnt().fetch_add(1, Ordering::Acquire);
> + QObject(self.0)
> + }
> +}
> +
> +impl Drop for QObject {
> + fn drop(&mut self) {
> + if self.refcnt().fetch_sub(1, Ordering::Release) == 1 {
> + unsafe {
> + bindings::qobject_destroy(self.0.get());
> + }
> + }
> + }
> +}
> +
> +#[allow(unused)]
> +macro_rules! match_qobject {
> + (@internal ($qobj:expr) =>
> + $(() => $unit:expr,)?
> + $(bool($boolvar:tt) => $bool:expr,)?
> + $(i64($i64var:tt) => $i64:expr,)?
> + $(u64($u64var:tt) => $u64:expr,)?
> + $(f64($f64var:tt) => $f64:expr,)?
> + $(CStr($cstrvar:tt) => $cstr:expr,)?
> + $(QList($qlistvar:tt) => $qlist:expr,)?
> + $(QDict($qdictvar:tt) => $qdict:expr,)?
> + $(_ => $other:expr,)?
> + ) => {
> + loop {
> + let qobj_ = $qobj.0.get();
> + match unsafe { &* qobj_ }.base.type_ {
> + $($crate::bindings::QTYPE_QNULL => break $unit,)?
> + $($crate::bindings::QTYPE_QBOOL => break {
> + let qbool__: *mut $crate::bindings::QBool = qobj_.cast();
> + let $boolvar = unsafe { (&*qbool__).value };
> + $bool
> + },)?
> + $crate::bindings::QTYPE_QNUM => {
> + let qnum__: *mut $crate::bindings::QNum = qobj_.cast();
> + let qnum__ = unsafe { &*qnum__ };
> + match qnum__.kind {
> + $crate::bindings::QNUM_I64 |
> + $crate::bindings::QNUM_U64 |
> + $crate::bindings::QNUM_DOUBLE => {}
> + _ => {
> + panic!("unreachable");
> + }
> + }
> +
> + match qnum__.kind {
> + $($crate::bindings::QNUM_I64 => break {
> + let $i64var = unsafe { qnum__.u.i64_ };
> + $i64
> + },)?
> + $($crate::bindings::QNUM_U64 => break {
> + let $u64var = unsafe { qnum__.u.u64_ };
> + $u64
> + },)?
> + $($crate::bindings::QNUM_DOUBLE => break {
> + let $f64var = unsafe { qnum__.u.dbl };
> + $f64
> + },)?
> + _ => {} // evaluate $other
> + }
> + },
> + $($crate::bindings::QTYPE_QSTRING => break {
> + let qstring__: *mut $crate::bindings::QString = qobj_.cast();
> + let $cstrvar = unsafe { ::core::ffi::CStr::from_ptr((&*qstring__).string) };
> + $cstr
> + },)?
> + $($crate::bindings::QTYPE_QLIST => break {
> + let qlist__: *mut $crate::bindings::QList = qobj_.cast();
> + let $qlistvar = unsafe { &*qlist__ };
> + $qlist
> + },)?
> + $($crate::bindings::QTYPE_QDICT => break {
> + let qdict__: *mut $crate::bindings::QDict = qobj_.cast();
> + let $qdictvar = unsafe { &*qdict__ };
> + $qdict
> + },)?
> + _ => ()
> + };
> + $(break $other;)?
> + #[allow(unreachable_code)]
> + {
> + panic!("unreachable");
> + }
> + }
> + };
> +
> + // first cleanup the syntax a bit, checking that there's at least
> + // one pattern and always adding a trailing comma
> + (($qobj:expr) =>
> + $($type:tt$(($val:tt))? => $code:expr ),+ $(,)?) => {
> + match_qobject!(@internal ($qobj) =>
> + $($type $(($val))? => $code,)+)
> + };
> +}
> +#[allow(unused_imports)]
> +use match_qobject;
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 03/16] rust/qobject: add Serialize implementation
2026-01-08 13:10 ` [PATCH v2 03/16] rust/qobject: add Serialize implementation Paolo Bonzini
@ 2026-02-24 10:29 ` Markus Armbruster
2026-02-24 10:48 ` Paolo Bonzini
0 siblings, 1 reply; 55+ messages in thread
From: Markus Armbruster @ 2026-02-24 10:29 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, Marc-André Lureau, qemu-rust, Zhao Liu
Paolo Bonzini <pbonzini@redhat.com> writes:
> This allows QObject to be converted to other formats, for example
> JSON via serde_json.
>
> This is not too useful, since QObjects are consumed by
> C code or deserialized into structs, but it can be used for testing
> and it is part of the full implementation of a serde format.
>
> Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
[...]
> diff --git a/rust/util/src/qobject/serialize.rs b/rust/util/src/qobject/serialize.rs
> new file mode 100644
> index 00000000000..34ec3847c1d
> --- /dev/null
> +++ b/rust/util/src/qobject/serialize.rs
> @@ -0,0 +1,59 @@
> +//! `QObject` serialization
> +//!
> +//! This module implements the [`Serialize`] trait for `QObject`,
> +//! allowing it to be converted to other formats, for example
> +//! JSON.
> +
> +use std::{ffi::CStr, mem::ManuallyDrop, ptr::addr_of};
> +
> +use serde::ser::{self, Serialize, SerializeMap, SerializeSeq};
> +
> +use super::{match_qobject, QObject};
> +use crate::bindings;
> +
> +impl Serialize for QObject {
> + #[inline]
> + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
> + where
> + S: ::serde::Serializer,
> + {
> + match_qobject! { (self) =>
> + () => serializer.serialize_unit(),
> + bool(b) => serializer.serialize_bool(b),
> + i64(i) => serializer.serialize_i64(i),
> + u64(u) => serializer.serialize_u64(u),
> + f64(f) => serializer.serialize_f64(f),
> + CStr(cstr) => cstr.to_str().map_or_else(
> + |_| Err(ser::Error::custom("invalid UTF-8 in QString")),
.to_str() fails when its argument is invalid UTF-8. It returns "an
error with details of where UTF-8 validation failed."[1]
Why are we replacing this error with a custom one? I guess we add the
clue "in QString". We also lose the details of where. Feels like a
questionable trade.
Moreover, I believe this is a programming error: QString is not meant to
wrap arbitrary byte sequences, it's meant to wrap UTF-8 strings.
Programming errors should panic.
Can we use .expect()? It panics "if the value is an Err, with a panic
message including the passed message, and the content of the Err."[2]
If we decide this isn't a programming error (because QString may contain
arbitrary zero-terminated byte sequences[3]): can we combine all the
readily available information like .expect() does?
In review of v2, you wrote
I'd rather not special case this into the only abort (see the
"#![deny(clippy::unwrap_used)]" in patch 4).
Is this an argument for pretending this isn't a programming error? If
yes, I need a bit more help to understand it.
> + |s| serializer.serialize_str(s),
> + ),
> + QList(l) => {
> + let mut node_ptr = unsafe { l.head.tqh_first };
> + let mut state = serializer.serialize_seq(None)?;
> + while !node_ptr.is_null() {
> + let node = unsafe { &*node_ptr };
> + let elem = unsafe { ManuallyDrop::new(QObject::from_raw(addr_of!(*node.value))) };
> + state.serialize_element(&*elem)?;
> + node_ptr = unsafe { node.next.tqe_next };
> + }
> + state.end()
> + },
> + QDict(d) => {
> + let mut state = serializer.serialize_map(Some(d.size))?;
> + let mut e_ptr = unsafe { bindings::qdict_first(d) };
> + while !e_ptr.is_null() {
> + let e = unsafe { &*e_ptr };
> + let key = unsafe { CStr::from_ptr(e.key) };
> + key.to_str().map_or_else(
> + |_| Err(ser::Error::custom("invalid UTF-8 in key")),
> + |k| state.serialize_key(k),
> + )?;
> + let value = unsafe { ManuallyDrop::new(QObject::from_raw(addr_of!(*e.value))) };
> + state.serialize_value(&*value)?;
> + e_ptr = unsafe { bindings::qdict_next(d, e) };
> + }
> + state.end()
> + }
> + }
> + }
> +}
[1] https://doc.rust-lang.org/std/ffi/struct.CStr.html#method.to_str
[2] https://doc.rust-lang.org/std/result/enum.Result.html#method.expect
[3] Feels like a problematic idea to me.
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 01/16] rust/qobject: add basic bindings
2026-02-24 10:03 ` Markus Armbruster
@ 2026-02-24 10:35 ` Paolo Bonzini
2026-02-24 13:33 ` Markus Armbruster
0 siblings, 1 reply; 55+ messages in thread
From: Paolo Bonzini @ 2026-02-24 10:35 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, Marc-André Lureau, qemu-rust, Zhao Liu
On 2/24/26 11:03, Markus Armbruster wrote:
>> diff --git a/rust/util/wrapper.h b/rust/util/wrapper.h
>> index b9ed68a01d8..0907dd59142 100644
>> --- a/rust/util/wrapper.h
>> +++ b/rust/util/wrapper.h
>> @@ -30,3 +30,10 @@ typedef enum memory_order {
>> #include "qemu/log.h"
>> #include "qemu/module.h"
>> #include "qemu/timer.h"
>> +#include "qobject/qnull.h"
>> +#include "qobject/qbool.h"
>> +#include "qobject/qnum.h"
>> +#include "qobject/qstring.h"
>> +#include "qobject/qobject.h"
>> +#include "qobject/qlist.h"
>> +#include "qobject/qdict.h"
>
> Is this file just for bindgen, and never actually fed to the C compiler?
Yes.
>> +impl From<()> for QObject {
>> + fn from(_null: ()) -> Self {
>> + // Conversion of the C inline `qnull` function
>> + unsafe { QObject::cloned_from_base(addr_of!(bindings::qnull_.base)) }
>> + }
>> +}
>
> Could we call C qnull() instead of using qnull_.base?
See v1 review - no, because qnull() is inline. I can make it not
inline, of course.
Paolo
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 03/16] rust/qobject: add Serialize implementation
2026-02-24 10:29 ` Markus Armbruster
@ 2026-02-24 10:48 ` Paolo Bonzini
2026-02-24 13:41 ` Markus Armbruster
0 siblings, 1 reply; 55+ messages in thread
From: Paolo Bonzini @ 2026-02-24 10:48 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, Marc-André Lureau, qemu-rust, Zhao Liu
On 2/24/26 11:29, Markus Armbruster wrote:
> Paolo Bonzini <pbonzini@redhat.com> writes:
>
>> This allows QObject to be converted to other formats, for example
>> JSON via serde_json.
>>
>> This is not too useful, since QObjects are consumed by
>> C code or deserialized into structs, but it can be used for testing
>> and it is part of the full implementation of a serde format.
>>
>> Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
>> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
>> Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
>> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
>
> [...]
>
>> diff --git a/rust/util/src/qobject/serialize.rs b/rust/util/src/qobject/serialize.rs
>> new file mode 100644
>> index 00000000000..34ec3847c1d
>> --- /dev/null
>> +++ b/rust/util/src/qobject/serialize.rs
>> @@ -0,0 +1,59 @@
>> +//! `QObject` serialization
>> +//!
>> +//! This module implements the [`Serialize`] trait for `QObject`,
>> +//! allowing it to be converted to other formats, for example
>> +//! JSON.
>> +
>> +use std::{ffi::CStr, mem::ManuallyDrop, ptr::addr_of};
>> +
>> +use serde::ser::{self, Serialize, SerializeMap, SerializeSeq};
>> +
>> +use super::{match_qobject, QObject};
>> +use crate::bindings;
>> +
>> +impl Serialize for QObject {
>> + #[inline]
>> + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
>> + where
>> + S: ::serde::Serializer,
>> + {
>> + match_qobject! { (self) =>
>> + () => serializer.serialize_unit(),
>> + bool(b) => serializer.serialize_bool(b),
>> + i64(i) => serializer.serialize_i64(i),
>> + u64(u) => serializer.serialize_u64(u),
>> + f64(f) => serializer.serialize_f64(f),
>> + CStr(cstr) => cstr.to_str().map_or_else(
>> + |_| Err(ser::Error::custom("invalid UTF-8 in QString")),
>
> .to_str() fails when its argument is invalid UTF-8. It returns "an
> error with details of where UTF-8 validation failed."[1]
>
> Why are we replacing this error with a custom one? I guess we add the
> clue "in QString". We also lose the details of where. Feels like a
> questionable trade.
Changing the error is required in order to return an S::Error (that is,
the serializer asks for a type of error that it understands). I can
also preserve the message with "|e| Err(ser::Error::custom(e))" or
something like that.
While it is undoubtedly a programming error, I didn't like that the
error (and thus the panic) happens in a different place than where the
QString is constructed. I like panicking for *my* programming errors,
but not for someone else's :) especially if I can recover easily.
Paolo
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 01/16] rust/qobject: add basic bindings
2026-02-24 10:35 ` Paolo Bonzini
@ 2026-02-24 13:33 ` Markus Armbruster
2026-02-25 8:05 ` Paolo Bonzini
0 siblings, 1 reply; 55+ messages in thread
From: Markus Armbruster @ 2026-02-24 13:33 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, Marc-André Lureau, qemu-rust, Zhao Liu
Paolo Bonzini <pbonzini@redhat.com> writes:
> On 2/24/26 11:03, Markus Armbruster wrote:
>>> diff --git a/rust/util/wrapper.h b/rust/util/wrapper.h
>>> index b9ed68a01d8..0907dd59142 100644
>>> --- a/rust/util/wrapper.h
>>> +++ b/rust/util/wrapper.h
>>> @@ -30,3 +30,10 @@ typedef enum memory_order {
>>> #include "qemu/log.h"
>>> #include "qemu/module.h"
>>> #include "qemu/timer.h"
>>> +#include "qobject/qnull.h"
>>> +#include "qobject/qbool.h"
>>> +#include "qobject/qnum.h"
>>> +#include "qobject/qstring.h"
>>> +#include "qobject/qobject.h"
>>> +#include "qobject/qlist.h"
>>> +#include "qobject/qdict.h"
>>
>> Is this file just for bindgen, and never actually fed to the C compiler?
>
> Yes.
Thanks!
>>> +impl From<()> for QObject {
>>> + fn from(_null: ()) -> Self {
>>> + // Conversion of the C inline `qnull` function
>>> + unsafe { QObject::cloned_from_base(addr_of!(bindings::qnull_.base)) }
>>> + }
>>> +}
>>
>> Could we call C qnull() instead of using qnull_.base?
>
> See v1 review - no, because qnull() is inline. I can make it not
> inline, of course.
I feel making it inline was a bit silly. Perhaps I had a non-silly
reason back then, but I can't see it anymore.
Up to you.
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 03/16] rust/qobject: add Serialize implementation
2026-02-24 10:48 ` Paolo Bonzini
@ 2026-02-24 13:41 ` Markus Armbruster
0 siblings, 0 replies; 55+ messages in thread
From: Markus Armbruster @ 2026-02-24 13:41 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, Marc-André Lureau, qemu-rust, Zhao Liu
Paolo Bonzini <pbonzini@redhat.com> writes:
> On 2/24/26 11:29, Markus Armbruster wrote:
>> Paolo Bonzini <pbonzini@redhat.com> writes:
>>
>>> This allows QObject to be converted to other formats, for example
>>> JSON via serde_json.
>>>
>>> This is not too useful, since QObjects are consumed by
>>> C code or deserialized into structs, but it can be used for testing
>>> and it is part of the full implementation of a serde format.
>>>
>>> Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
>>> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
>>> Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
>>> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
>>
>> [...]
>>
>>> diff --git a/rust/util/src/qobject/serialize.rs b/rust/util/src/qobject/serialize.rs
>>> new file mode 100644
>>> index 00000000000..34ec3847c1d
>>> --- /dev/null
>>> +++ b/rust/util/src/qobject/serialize.rs
>>> @@ -0,0 +1,59 @@
>>> +//! `QObject` serialization
>>> +//!
>>> +//! This module implements the [`Serialize`] trait for `QObject`,
>>> +//! allowing it to be converted to other formats, for example
>>> +//! JSON.
>>> +
>>> +use std::{ffi::CStr, mem::ManuallyDrop, ptr::addr_of};
>>> +
>>> +use serde::ser::{self, Serialize, SerializeMap, SerializeSeq};
>>> +
>>> +use super::{match_qobject, QObject};
>>> +use crate::bindings;
>>> +
>>> +impl Serialize for QObject {
>>> + #[inline]
>>> + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
>>> + where
>>> + S: ::serde::Serializer,
>>> + {
>>> + match_qobject! { (self) =>
>>> + () => serializer.serialize_unit(),
>>> + bool(b) => serializer.serialize_bool(b),
>>> + i64(i) => serializer.serialize_i64(i),
>>> + u64(u) => serializer.serialize_u64(u),
>>> + f64(f) => serializer.serialize_f64(f),
>>> + CStr(cstr) => cstr.to_str().map_or_else(
>>> + |_| Err(ser::Error::custom("invalid UTF-8 in QString")),
>>
>> .to_str() fails when its argument is invalid UTF-8. It returns "an
>> error with details of where UTF-8 validation failed."[1]
>>
>> Why are we replacing this error with a custom one? I guess we add the
>> clue "in QString". We also lose the details of where. Feels like a
>> questionable trade.
>
> Changing the error is required in order to return an S::Error (that is,
> the serializer asks for a type of error that it understands). I can
> also preserve the message with "|e| Err(ser::Error::custom(e))" or
> something like that.
If it's not too much trouble...
> While it is undoubtedly a programming error, I didn't like that the
> error (and thus the panic) happens in a different place than where the
> QString is constructed. I like panicking for *my* programming errors,
> but not for someone else's :) especially if I can recover easily.
I ruthlessly panic when I find invariants violated, preconditions like
this one very much included.
If you'd rather not panic here, please make it locally obvious that the
error is not supposed to happen. Suggest to put it into the error
message, so increase the likelihood of the programming error actually
getting fixed if it ever triggers.
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings
2026-02-23 16:11 ` Paolo Bonzini
@ 2026-02-24 13:46 ` Markus Armbruster
0 siblings, 0 replies; 55+ messages in thread
From: Markus Armbruster @ 2026-02-24 13:46 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> On 2/23/26 13:36, Markus Armbruster wrote:
>> Paolo Bonzini <pbonzini@redhat.com> writes:
>>
>>> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>>>
>>> Generate high-level native Rust declarations for the QAPI types.
>>>
>>> - char* is mapped to String, scalars to there corresponding Rust types
>>>
>>> - enums use #[repr(u32)] and can be transmuted to their C counterparts
>>>
>>> - has_foo/foo members are mapped to Option<T>
>>>
>>> - lists are represented as Vec<T>
>>>
>>> - structures map fields 1:1 to Rust
>>>
>>> - alternate are represented as Rust enum, each variant being a 1-element
>>> tuple
>>>
>>> - unions are represented in a similar way as in C: a struct S with a "u"
>>> member (since S may have extra 'base' fields). The discriminant
>>> isn't a member of S, since Rust enum already include it, but it can be
>>> recovered with "mystruct.u.into()"
>>>
>>> Anything that includes a recursive struct puts it in a Box. Lists are
>>> not considered recursive, because Vec breaks the recursion (it's possible
>>> to construct an object containing an empty Vec of its own type).
>>>
>>> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
>>> Link: https://lore.kernel.org/r/20210907121943.3498701-21-marcandre.lureau@redhat.com
>>> [Paolo: rewrite conversion of schema types to Rust types]
>>> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
>>
>> Please mention how to actually generate Rust, because it's anything but
>> obvious: pass -B qapi.backend.QAPIRsBackend to qapi-gen.py. Recommend
>> to mention it again in the cover letter.
>>
>> Additionally, state that this is a hack we'll want to replace both in
>> the commit message and a TODO comment. See review of v1's cover letter
>> for why.
>
> Yes, the TODO comment is in patch 14 where the thing actually gets used.
> I can make it more prominent.
I missed the comment, because I looked for it here, and not in PATCH 14.
The comment is fine as is. However, we could have another comment near
QAPIRsBackend in this patch.
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 00/16] rust: QObject and QAPI bindings
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
` (18 preceding siblings ...)
2026-02-23 9:53 ` Daniel P. Berrangé
@ 2026-02-24 14:06 ` Markus Armbruster
2026-02-24 17:28 ` Paolo Bonzini
19 siblings, 1 reply; 55+ messages in thread
From: Markus Armbruster @ 2026-02-24 14:06 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> This adds two related parts of the Rust bindings:
[...]
> docs/devel/rust.rst | 1 +
> meson.build | 4 +-
> include/qobject/qobject.h | 5 +-
> rust/util/wrapper.h | 8 +
> qapi/meson.build | 9 +
> rust/Cargo.lock | 2 +
> rust/Cargo.toml | 2 +
> rust/meson.build | 4 +
> rust/tests/meson.build | 21 +-
> rust/tests/tests/integration.rs | 2 +
> rust/tests/tests/qapi.rs | 444 +++++++++++++
> rust/util/Cargo.toml | 2 +
> rust/util/meson.build | 30 +-
> rust/util/src/lib.rs | 4 +
> rust/util/src/qobject/deserialize.rs | 134 ++++
> rust/util/src/qobject/deserializer.rs | 371 +++++++++++
Are you sure having two source filenames differ in just one character is
a good idea?
> rust/util/src/qobject/error.rs | 58 ++
> rust/util/src/qobject/mod.rs | 383 ++++++++++++
> rust/util/src/qobject/serialize.rs | 59 ++
> rust/util/src/qobject/serializer.rs | 585 ++++++++++++++++++
Likewise.
These caused a bit of friction in review already...
[...]
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 00/16] rust: QObject and QAPI bindings
2026-02-24 14:06 ` Markus Armbruster
@ 2026-02-24 17:28 ` Paolo Bonzini
2026-02-26 12:42 ` Markus Armbruster
0 siblings, 1 reply; 55+ messages in thread
From: Paolo Bonzini @ 2026-02-24 17:28 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
On 2/24/26 15:06, Markus Armbruster wrote:
> Paolo Bonzini <pbonzini@redhat.com> writes:
>
>> This adds two related parts of the Rust bindings:
>
> [...]
>
>> docs/devel/rust.rst | 1 +
>> meson.build | 4 +-
>> include/qobject/qobject.h | 5 +-
>> rust/util/wrapper.h | 8 +
>> qapi/meson.build | 9 +
>> rust/Cargo.lock | 2 +
>> rust/Cargo.toml | 2 +
>> rust/meson.build | 4 +
>> rust/tests/meson.build | 21 +-
>> rust/tests/tests/integration.rs | 2 +
>> rust/tests/tests/qapi.rs | 444 +++++++++++++
>> rust/util/Cargo.toml | 2 +
>> rust/util/meson.build | 30 +-
>> rust/util/src/lib.rs | 4 +
>> rust/util/src/qobject/deserialize.rs | 134 ++++
>> rust/util/src/qobject/deserializer.rs | 371 +++++++++++
>
> Are you sure having two source filenames differ in just one character is
> a good idea?
>
>> rust/util/src/qobject/error.rs | 58 ++
>> rust/util/src/qobject/mod.rs | 383 ++++++++++++
>> rust/util/src/qobject/serialize.rs | 59 ++
>> rust/util/src/qobject/serializer.rs | 585 ++++++++++++++++++
>
> Likewise.
I feel and share your pain... That's how serde calls the things ("-er"
for the struct and verb for the trait) and I cannot really thing of a
different naming for files.
I thought of renaming serializer.rs to to_qobject.rs, and
deserializer.rs to from_qobject.rs. However, that doesn't work out
because I want "qobject::to_object" to be the function, not the module
containing to_qobject() and Serializer.
serde_json places the struct and trait in the same file (e.g.
https://github.com/serde-rs/json/blob/master/src/value/ser.rs). I can
do that if you prefer it that way.
Paolo
> These caused a bit of friction in review already...
>
> [...]
>
>
>
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 09/16] scripts/qapi: add QAPISchemaIfCond.rsgen()
2026-01-08 13:10 ` [PATCH v2 09/16] scripts/qapi: add QAPISchemaIfCond.rsgen() Paolo Bonzini
2026-01-19 6:58 ` Zhao Liu
@ 2026-02-25 6:48 ` Markus Armbruster
2026-02-25 7:53 ` Paolo Bonzini
1 sibling, 1 reply; 55+ messages in thread
From: Markus Armbruster @ 2026-02-25 6:48 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>
> Generate Rust #[cfg(...)] guards from QAPI 'if' conditions; it
> turns out that they are very similar, with both of them using
> not/any/all, so just walk the tree.
>
> The next commit will put it to use.
>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Link: https://lore.kernel.org/r/20210907121943.3498701-15-marcandre.lureau@redhat.com
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
> scripts/qapi/common.py | 19 +++++++++++++++++++
> scripts/qapi/schema.py | 4 ++++
> 2 files changed, 23 insertions(+)
>
> diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
> index d7c8aa3365c..14d5dd259c4 100644
> --- a/scripts/qapi/common.py
> +++ b/scripts/qapi/common.py
> @@ -199,6 +199,25 @@ def guardend(name: str) -> str:
> name=c_fname(name).upper())
>
>
> +def rsgen_ifcond(ifcond: Optional[Union[str, Dict[str, Any]]]) -> str:
> +
> + def cfg(ifcond: Union[str, Dict[str, Any]]) -> str:
> + if isinstance(ifcond, str):
> + return ifcond
> + assert isinstance(ifcond, dict) and len(ifcond) == 1
> + if 'not' in ifcond:
> + oper = 'not'
> + arg = cfg(ifcond['not'])
> + else:
> + oper, operands = next(iter(ifcond.items()))
> + arg = ', '.join([cfg(c) for c in operands])
> + return f'{oper}({arg})'
> +
> + if not ifcond:
> + return ''
> + return '#[cfg(%s)]' % cfg(ifcond)
> +
> +
Why not reuse the existing tree walker? See appended diff.
> def gen_ifcond(ifcond: Optional[Union[str, Dict[str, Any]]],
> cond_fmt: str, not_fmt: str,
> all_operator: str, any_operator: str) -> str:
> diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
> index 8d88b40de2e..848a7401251 100644
> --- a/scripts/qapi/schema.py
> +++ b/scripts/qapi/schema.py
> @@ -37,6 +37,7 @@
> docgen_ifcond,
> gen_endif,
> gen_if,
> + rsgen_ifcond,
> )
> from .error import QAPIError, QAPISemError, QAPISourceError
> from .expr import check_exprs
> @@ -63,6 +64,9 @@ def gen_endif(self) -> str:
> def docgen(self) -> str:
> return docgen_ifcond(self.ifcond)
>
> + def rsgen(self) -> str:
> + return rsgen_ifcond(self.ifcond)
> +
> def is_present(self) -> bool:
> return bool(self.ifcond)
diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
index d7c8aa3365..d8accae835 100644
--- a/scripts/qapi/common.py
+++ b/scripts/qapi/common.py
@@ -201,7 +201,8 @@ def guardend(name: str) -> str:
def gen_ifcond(ifcond: Optional[Union[str, Dict[str, Any]]],
cond_fmt: str, not_fmt: str,
- all_operator: str, any_operator: str) -> str:
+ all_fmt: str, all_sep: str,
+ any_fmt: str, any_sep: str) -> str:
def do_gen(ifcond: Union[str, Dict[str, Any]],
need_parens: bool) -> str:
@@ -211,15 +212,15 @@ def do_gen(ifcond: Union[str, Dict[str, Any]],
if 'not' in ifcond:
return not_fmt % do_gen(ifcond['not'], True)
if 'all' in ifcond:
- gen = gen_infix(all_operator, ifcond['all'])
+ gen = gen_infix(all_fmt, all_sep, ifcond['all'])
else:
- gen = gen_infix(any_operator, ifcond['any'])
+ gen = gen_infix(any_fmt, any_sep, ifcond['any'])
if need_parens:
gen = '(' + gen + ')'
return gen
- def gen_infix(operator: str, operands: Sequence[Any]) -> str:
- return operator.join([do_gen(o, True) for o in operands])
+ def gen_infix(fmt: str, sep: str, operands: Sequence[Any]) -> str:
+ return fmt % sep.join([do_gen(o, True) for o in operands])
if not ifcond:
return ''
@@ -227,12 +228,18 @@ def gen_infix(operator: str, operands: Sequence[Any]) -> str:
def cgen_ifcond(ifcond: Optional[Union[str, Dict[str, Any]]]) -> str:
- return gen_ifcond(ifcond, 'defined(%s)', '!%s', ' && ', ' || ')
+ return gen_ifcond(ifcond, 'defined(%s)', '!%s',
+ '%s', ' && ', '%s', ' || ')
+
+
+def rsgen_ifcond(ifcond: Optional[Union[str, Dict[str, Any]]]) -> str:
+ return '#[cfg(%s)]' % gen_ifcond(ifcond, '%s', 'not(%s)',
+ 'all(%s)', ', ', 'any(%s)', ', ')
def docgen_ifcond(ifcond: Optional[Union[str, Dict[str, Any]]]) -> str:
# TODO Doc generated for conditions needs polish
- return gen_ifcond(ifcond, '%s', 'not %s', ' and ', ' or ')
+ return gen_ifcond(ifcond, '%s', 'not %s', '%s', ' and ', '%s', ' or ')
def gen_if(cond: str) -> str:
^ permalink raw reply related [flat|nested] 55+ messages in thread
* Re: [PATCH v2 10/16] scripts/qapi: add QAPISchemaType.is_predefined
2026-01-08 13:10 ` [PATCH v2 10/16] scripts/qapi: add QAPISchemaType.is_predefined Paolo Bonzini
@ 2026-02-25 7:33 ` Markus Armbruster
2026-02-25 8:01 ` Paolo Bonzini
0 siblings, 1 reply; 55+ messages in thread
From: Markus Armbruster @ 2026-02-25 7:33 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> It is impossible to call is_implicit on an enum type from the visitor, because
> the QAPISchemaEnumType has already been exploded into its costituent fields.
constituent
Passing selected attributes instead of the entire object to its visitor
method limits what the visitor can do. This is both good and bad.
We've run into "bad" a couple of times. It's never been bad enough to
change the interface, though.
Thoughts?
> The Rust backend is also not modular (yet?) so it is not possible to filter
> out the builtin module;
Really?
The visitors are all based on QAPISchemaVisitor. Protocol:
.visit_begin()
for all modules:
.visit_module()
for all entities:
if .visit_needed():
.visit_FOO()
.visit_end()
QAPISchemaModularCVisitor implements .visit_module() to generate code
per module. Its .write() skips builtin modules unless opt_builtins.
QAPISchemaMonolithicCVisitor is oblivious of modules: it doesn't
implement .visit_module(), and writes out everything. This is fine,
because we use it only to generate qapi-features.[ch] and
qapi-introspect.[ch]. Generating the former has no need for recognizing
the built-ins because there are no built-in features. Generating the
latter has no need because it treats built-in stuff exactly like
user-defined stuff.
QAPISchemaRsVisitor [PATCH 12] also doesn't implement .visit_module().
It uses QAPISchema.is_predefined(), defined in this patch, to skip
built-in. Could it rely on .visit_module() instead?
> add a way to query for implicit type names without
> having the object itself.
>
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
> scripts/qapi/schema.py | 11 +++++++++++
> 1 file changed, 11 insertions(+)
>
> diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
> index 848a7401251..15f5d97418f 100644
> --- a/scripts/qapi/schema.py
> +++ b/scripts/qapi/schema.py
> @@ -1243,6 +1243,17 @@ def _def_builtin_type(
> # schema.
> self._make_array_type(name, None)
>
> + def is_predefined(self, name: str) -> bool:
> + # See QAPISchema._def_predefineds()
> + entity = self._entity_dict[name]
> + if isinstance(entity, QAPISchemaBuiltinType):
> + return True
> + if entity is self.the_empty_object_type:
> + return True
> + if name == 'QType':
> + return True
> + return False
> +
> def _def_predefineds(self) -> None:
> for t in [('str', 'string', 'char' + POINTER_SUFFIX),
> ('number', 'number', 'double'),
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 09/16] scripts/qapi: add QAPISchemaIfCond.rsgen()
2026-02-25 6:48 ` Markus Armbruster
@ 2026-02-25 7:53 ` Paolo Bonzini
0 siblings, 0 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-02-25 7:53 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
On 2/25/26 07:48, Markus Armbruster wrote:
> Paolo Bonzini <pbonzini@redhat.com> writes:
>
>> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>>
>> Generate Rust #[cfg(...)] guards from QAPI 'if' conditions; it
>> turns out that they are very similar, with both of them using
>> not/any/all, so just walk the tree.
>>
>> The next commit will put it to use.
>>
>> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
>> Link: https://lore.kernel.org/r/20210907121943.3498701-15-marcandre.lureau@redhat.com
>> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
>> ---
>> scripts/qapi/common.py | 19 +++++++++++++++++++
>> scripts/qapi/schema.py | 4 ++++
>> 2 files changed, 23 insertions(+)
>>
>> diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
>> index d7c8aa3365c..14d5dd259c4 100644
>> --- a/scripts/qapi/common.py
>> +++ b/scripts/qapi/common.py
>> @@ -199,6 +199,25 @@ def guardend(name: str) -> str:
>> name=c_fname(name).upper())
>>
>>
>> +def rsgen_ifcond(ifcond: Optional[Union[str, Dict[str, Any]]]) -> str:
>> +
>> + def cfg(ifcond: Union[str, Dict[str, Any]]) -> str:
>> + if isinstance(ifcond, str):
>> + return ifcond
>> + assert isinstance(ifcond, dict) and len(ifcond) == 1
>> + if 'not' in ifcond:
>> + oper = 'not'
>> + arg = cfg(ifcond['not'])
>> + else:
>> + oper, operands = next(iter(ifcond.items()))
>> + arg = ', '.join([cfg(c) for c in operands])
>> + return f'{oper}({arg})'
>> +
>> + if not ifcond:
>> + return ''
>> + return '#[cfg(%s)]' % cfg(ifcond)
>> +
>> +
>
> Why not reuse the existing tree walker? See appended diff.
I think it won't work due to need_parens? gen_ifcond is really designed
for infix notation, I have no doubt that it *can* be made to work (for
example pass 'all' and 'any' and prefix them after processing
need_parens), but there is little duplication in the code other than
if isinstance(ifcond, str):
return cond_fmt % ifcond
assert isinstance(ifcond, dict) and len(ifcond) == 1
because unary and n-ary operators in prefix notation are quite similar,
unlike unary and binary operators in infix notation.
Paolo
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 10/16] scripts/qapi: add QAPISchemaType.is_predefined
2026-02-25 7:33 ` Markus Armbruster
@ 2026-02-25 8:01 ` Paolo Bonzini
2026-02-25 8:44 ` Markus Armbruster
0 siblings, 1 reply; 55+ messages in thread
From: Paolo Bonzini @ 2026-02-25 8:01 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
On 2/25/26 08:33, Markus Armbruster wrote:
> Paolo Bonzini <pbonzini@redhat.com> writes:
>
>> It is impossible to call is_implicit on an enum type from the visitor, because
>> the QAPISchemaEnumType has already been exploded into its costituent fields.
>
> constituent
>
> Passing selected attributes instead of the entire object to its visitor
> method limits what the visitor can do. This is both good and bad.
> We've run into "bad" a couple of times. It's never been bad enough to
> change the interface, though.
>
> Thoughts?
I think that applies here, is_predefined() is a reasonable addition.
>> The Rust backend is also not modular (yet?) so it is not possible to filter
>> out the builtin module;
>
> Really?
>
> The visitors are all based on QAPISchemaVisitor. Protocol:
>
> .visit_begin()
> for all modules:
> .visit_module()
> for all entities:
> if .visit_needed():
> .visit_FOO()
> .visit_end()
>
> QAPISchemaModularCVisitor implements .visit_module() to generate code
> per module. Its .write() skips builtin modules unless opt_builtins.
... because its .write() already builds multiple QAPIGen{C,H,Trace}, one
per module. Here instead there is just one QAPIGenRs.
Using multiple QAPIGenRs instances, one per module, would be hackish.
I'd rather just go modular instead of that. I can take a look, since
this series will be (early) 11.1 material anyway.
Paolo
> QAPISchemaMonolithicCVisitor is oblivious of modules: it doesn't
> implement .visit_module(), and writes out everything. This is fine,
> because we use it only to generate qapi-features.[ch] and
> qapi-introspect.[ch]. Generating the former has no need for recognizing
> the built-ins because there are no built-in features. Generating the
> latter has no need because it treats built-in stuff exactly like
> user-defined stuff.
>
> QAPISchemaRsVisitor [PATCH 12] also doesn't implement .visit_module().
> It uses QAPISchema.is_predefined(), defined in this patch, to skip
> built-in. Could it rely on .visit_module() instead?
>
>> add a way to query for implicit type names without
>> having the object itself.
>>
>> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
>> ---
>> scripts/qapi/schema.py | 11 +++++++++++
>> 1 file changed, 11 insertions(+)
>>
>> diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
>> index 848a7401251..15f5d97418f 100644
>> --- a/scripts/qapi/schema.py
>> +++ b/scripts/qapi/schema.py
>> @@ -1243,6 +1243,17 @@ def _def_builtin_type(
>> # schema.
>> self._make_array_type(name, None)
>>
>> + def is_predefined(self, name: str) -> bool:
>> + # See QAPISchema._def_predefineds()
>> + entity = self._entity_dict[name]
>> + if isinstance(entity, QAPISchemaBuiltinType):
>> + return True
>> + if entity is self.the_empty_object_type:
>> + return True
>> + if name == 'QType':
>> + return True
>> + return False
>> +
>> def _def_predefineds(self) -> None:
>> for t in [('str', 'string', 'char' + POINTER_SUFFIX),
>> ('number', 'number', 'double'),
>
>
>
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 01/16] rust/qobject: add basic bindings
2026-02-24 13:33 ` Markus Armbruster
@ 2026-02-25 8:05 ` Paolo Bonzini
0 siblings, 0 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-02-25 8:05 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, Marc-André Lureau, qemu-rust, Zhao Liu
On 2/24/26 14:33, Markus Armbruster wrote:
> Paolo Bonzini <pbonzini@redhat.com> writes:
>
>> On 2/24/26 11:03, Markus Armbruster wrote:
>>>> diff --git a/rust/util/wrapper.h b/rust/util/wrapper.h
>>>> index b9ed68a01d8..0907dd59142 100644
>>>> --- a/rust/util/wrapper.h
>>>> +++ b/rust/util/wrapper.h
>>>> @@ -30,3 +30,10 @@ typedef enum memory_order {
>>>> #include "qemu/log.h"
>>>> #include "qemu/module.h"
>>>> #include "qemu/timer.h"
>>>> +#include "qobject/qnull.h"
>>>> +#include "qobject/qbool.h"
>>>> +#include "qobject/qnum.h"
>>>> +#include "qobject/qstring.h"
>>>> +#include "qobject/qobject.h"
>>>> +#include "qobject/qlist.h"
>>>> +#include "qobject/qdict.h"
>>>
>>> Is this file just for bindgen, and never actually fed to the C compiler?
>>
>> Yes.
>
> Thanks!
>
>>>> +impl From<()> for QObject {
>>>> + fn from(_null: ()) -> Self {
>>>> + // Conversion of the C inline `qnull` function
>>>> + unsafe { QObject::cloned_from_base(addr_of!(bindings::qnull_.base)) }
>>>> + }
>>>> +}
>>>
>>> Could we call C qnull() instead of using qnull_.base?
>>
>> See v1 review - no, because qnull() is inline. I can make it not
>> inline, of course.
>
> I feel making it inline was a bit silly. Perhaps I had a non-silly
> reason back then, but I can't see it anymore.
Maybe it was, but qnull_ is also used by tests/unit/check-qnull.c; and
it would also be silly to make qnull() extern while keeping the global
qnull_. In doubt, I'd leave it as is.
Paolo
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 11/16] scripts/qapi: pull c_name from camel_to_upper to caller
2026-01-08 13:10 ` [PATCH v2 11/16] scripts/qapi: pull c_name from camel_to_upper to caller Paolo Bonzini
2026-01-19 7:05 ` Zhao Liu
@ 2026-02-25 8:32 ` Markus Armbruster
1 sibling, 0 replies; 55+ messages in thread
From: Markus Armbruster @ 2026-02-25 8:32 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> Allow using it for other languages too.
>
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
> scripts/qapi/common.py | 4 ++--
> 1 file changed, 2 insertions(+), 2 deletions(-)
>
> diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
> index 14d5dd259c4..c75396a01b5 100644
> --- a/scripts/qapi/common.py
> +++ b/scripts/qapi/common.py
> @@ -61,7 +61,7 @@ def camel_to_upper(value: str) -> str:
> ret += ch
> upc = ch.isupper()
>
> - return c_name(ret.upper()).lstrip('_')
> + return ret.upper()
>
>
> def c_enum_const(type_name: str,
> @@ -75,7 +75,7 @@ def c_enum_const(type_name: str,
> :param prefix: Optional, prefix that overrides the type_name.
> """
> if prefix is None:
> - prefix = camel_to_upper(type_name)
> + prefix = c_name(camel_to_upper(type_name)).lstrip('_')
> return prefix + '_' + c_name(const_name, False).upper()
No change in behavior, because this is the only use of camel_to_upper().
The move of c_name() out of camel_to_upper() is clearly wanted to make
it usable for other languages.
What about .lstrip('_')? I wonder why we even have it. Goes back all
the way to commit 6299659f544 (qapi script: code move for
generate_enum_name()) from 2014. It seems to affect just
tests/qapi-schema/qapi-schema-test.json's
{ 'enum': '__org.qemu_x-Enum', 'data': [ '__org.qemu_x-value' ] }
This tests a downstream extension prefix __RFQDN_.
As is, we generate
typedef enum __org_qemu_x_Enum {
ORG_QEMU_X_ENUM___ORG_QEMU_X_VALUE,
ORG_QEMU_X_ENUM__MAX,
} __org_qemu_x_Enum;
Without the .lstrip('_'), we'd generate
typedef enum __org_qemu_x_Enum {
__ORG_QEMU_X_ENUM___ORG_QEMU_X_VALUE,
__ORG_QEMU_X_ENUM__MAX,
} __org_qemu_x_Enum;
Meh.
Turns out this isn't on purpose. Back then, the loop to map camel to
upper worked differently, and produced unwanted leading '_'. For
instance, it mapped 'AbraCadabra' to '_ABRA_CADABRA'. The .lstrip()
made the function produce 'ABRA_CADABRA'. It also makes it eat
downstream extensions' leading '__'. Oopsie.
By moving it, we insulate Rust from this accident. Okay.
We could delete it instead. Might inconvenience downstreams. Not a
demand; you may wish to keep this series focused on Rust.
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 10/16] scripts/qapi: add QAPISchemaType.is_predefined
2026-02-25 8:01 ` Paolo Bonzini
@ 2026-02-25 8:44 ` Markus Armbruster
2026-02-26 14:12 ` Paolo Bonzini
0 siblings, 1 reply; 55+ messages in thread
From: Markus Armbruster @ 2026-02-25 8:44 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> On 2/25/26 08:33, Markus Armbruster wrote:
>> Paolo Bonzini <pbonzini@redhat.com> writes:
>>
>>> It is impossible to call is_implicit on an enum type from the visitor, because
>>> the QAPISchemaEnumType has already been exploded into its costituent fields.
>>
>> constituent
>>
>> Passing selected attributes instead of the entire object to its visitor
>> method limits what the visitor can do. This is both good and bad.
>> We've run into "bad" a couple of times. It's never been bad enough to
>> change the interface, though.
>>
>> Thoughts?
>
> I think that applies here, is_predefined() is a reasonable addition.
>
>>> The Rust backend is also not modular (yet?) so it is not possible to filter
>>> out the builtin module;
>>
>> Really?
>>
>> The visitors are all based on QAPISchemaVisitor. Protocol:
>>
>> .visit_begin()
>> for all modules:
>> .visit_module()
>> for all entities:
>> if .visit_needed():
>> .visit_FOO()
>> .visit_end()
>>
>> QAPISchemaModularCVisitor implements .visit_module() to generate code
>> per module. Its .write() skips builtin modules unless opt_builtins.
>
> ... because its .write() already builds multiple QAPIGen{C,H,Trace}, one
> per module. Here instead there is just one QAPIGenRs.
>
> Using multiple QAPIGenRs instances, one per module, would be hackish.
I was thinking of having .visit_module() save .is_builtin_module(name)
in self.in_builtin_module, then use that to recognize built-ins.
> I'd rather just go modular instead of that. I can take a look, since
> this series will be (early) 11.1 material anyway.
Modular C code generation reduces incremental build time massively.
Before, we suffered from "touch the schema, recompile the world". If
Rust would similarly profit, then I'm all for going modular.
[...]
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings
2026-01-08 13:10 ` [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings Paolo Bonzini
2026-02-23 12:36 ` Markus Armbruster
@ 2026-02-25 14:39 ` Markus Armbruster
2026-03-03 10:00 ` Paolo Bonzini
2026-03-03 9:19 ` Markus Armbruster
2 siblings, 1 reply; 55+ messages in thread
From: Markus Armbruster @ 2026-02-25 14:39 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, armbru, Marc-André Lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>
> Generate high-level native Rust declarations for the QAPI types.
>
> - char* is mapped to String, scalars to there corresponding Rust types
>
> - enums use #[repr(u32)] and can be transmuted to their C counterparts
>
> - has_foo/foo members are mapped to Option<T>
>
> - lists are represented as Vec<T>
>
> - structures map fields 1:1 to Rust
>
> - alternate are represented as Rust enum, each variant being a 1-element
> tuple
>
> - unions are represented in a similar way as in C: a struct S with a "u"
> member (since S may have extra 'base' fields). The discriminant
> isn't a member of S, since Rust enum already include it, but it can be
> recovered with "mystruct.u.into()"
>
> Anything that includes a recursive struct puts it in a Box. Lists are
> not considered recursive, because Vec breaks the recursion (it's possible
> to construct an object containing an empty Vec of its own type).
>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Link: https://lore.kernel.org/r/20210907121943.3498701-21-marcandre.lureau@redhat.com
> [Paolo: rewrite conversion of schema types to Rust types]
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
> meson.build | 4 +-
> scripts/qapi/backend.py | 25 +++
> scripts/qapi/common.py | 43 +++++
> scripts/qapi/rs.py | 61 +++++++
> scripts/qapi/rs_types.py | 373 +++++++++++++++++++++++++++++++++++++++
> scripts/qapi/schema.py | 59 +++++--
> 6 files changed, 546 insertions(+), 19 deletions(-)
> create mode 100644 scripts/qapi/rs.py
> create mode 100644 scripts/qapi/rs_types.py
>
> diff --git a/meson.build b/meson.build
> index db87358d62d..4228792f0f6 100644
> --- a/meson.build
> +++ b/meson.build
> @@ -3540,11 +3540,13 @@ qapi_gen_depends = [ meson.current_source_dir() / 'scripts/qapi/__init__.py',
> meson.current_source_dir() / 'scripts/qapi/introspect.py',
> meson.current_source_dir() / 'scripts/qapi/main.py',
> meson.current_source_dir() / 'scripts/qapi/parser.py',
> + meson.current_source_dir() / 'scripts/qapi/rs_types.py',
> meson.current_source_dir() / 'scripts/qapi/schema.py',
> meson.current_source_dir() / 'scripts/qapi/source.py',
> meson.current_source_dir() / 'scripts/qapi/types.py',
> meson.current_source_dir() / 'scripts/qapi/visit.py',
> - meson.current_source_dir() / 'scripts/qapi-gen.py'
> + meson.current_source_dir() / 'scripts/qapi-gen.py',
> + meson.current_source_dir() / 'scripts/qapi/rs.py',
> ]
>
> tracetool = [
> diff --git a/scripts/qapi/backend.py b/scripts/qapi/backend.py
> index 49ae6ecdd33..8023acce0d6 100644
> --- a/scripts/qapi/backend.py
> +++ b/scripts/qapi/backend.py
> @@ -7,6 +7,7 @@
> from .events import gen_events
> from .features import gen_features
> from .introspect import gen_introspect
> +from .rs_types import gen_rs_types
> from .schema import QAPISchema
> from .types import gen_types
> from .visit import gen_visit
> @@ -63,3 +64,27 @@ def generate(self,
> gen_commands(schema, output_dir, prefix, gen_tracing)
> gen_events(schema, output_dir, prefix)
> gen_introspect(schema, output_dir, prefix, unmask)
> +
> +
> +class QAPIRsBackend(QAPIBackend):
> + # pylint: disable=too-few-public-methods
> +
> + def generate(self,
> + schema: QAPISchema,
> + output_dir: str,
> + prefix: str,
> + unmask: bool,
> + builtins: bool,
> + gen_tracing: bool) -> None:
> + """
> + Generate Rust code for the given schema into the target directory.
> +
> + :param schema_file: The primary QAPI schema file.
> + :param output_dir: The output directory to store generated code.
> + :param prefix: Optional C-code prefix for symbol names.
> + :param unmask: Expose non-ABI names through introspection?
> + :param builtins: Generate code for built-in types?
> +
> + :raise QAPIError: On failures.
> + """
> + gen_rs_types(schema, output_dir, prefix)
> diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
> index c75396a01b5..e9261a3411e 100644
> --- a/scripts/qapi/common.py
> +++ b/scripts/qapi/common.py
> @@ -64,6 +64,13 @@ def camel_to_upper(value: str) -> str:
> return ret.upper()
>
>
> +def camel_to_lower(value: str) -> str:
> + """
> + Converts CamelCase to camel_case.
> + """
> + return camel_to_upper(value).lower()
> +
> +
> def c_enum_const(type_name: str,
> const_name: str,
> prefix: Optional[str] = None) -> str:
> @@ -129,6 +136,42 @@ def c_name(name: str, protect: bool = True) -> str:
> return name
>
>
> +def rs_name(name: str) -> str:
> + """
> + Map @name to a valid, possibly raw Rust identifier.
> + """
> + name = re.sub(r'[^A-Za-z0-9_]', '_', name)
> + if name[0].isnumeric():
.isdigit()? It's what c_name() uses...
> + name = '_' + name
In review of v1, I pointed to "The Rust Reference"
Identifiers starting with an underscore are typically used to
indicate an identifier that is intentionally unused, and will
silence the unused warning in rustc.
https://doc.rust-lang.org/reference/identifiers.html
You replied "In this case it doesn't really matter: public items (such
as QAPI enum entries, or struct fields) do not raise the unused warning
anyway."
What gives us confidence rs_name() will only be used where it doesn't
really matter?
> + # based from the list:
> + # https://doc.rust-lang.org/reference/keywords.html
> + if name in ('Self', 'abstract', 'as', 'async',
> + 'await', 'become', 'box', 'break',
> + 'const', 'continue', 'crate', 'do',
> + 'dyn', 'else', 'enum', 'extern',
> + 'false', 'final', 'fn', 'for',
> + 'if', 'impl', 'in', 'let',
> + 'loop', 'macro', 'match', 'mod',
> + 'move', 'mut', 'override', 'priv',
> + 'pub', 'ref', 'return', 'self',
> + 'static', 'struct', 'super', 'trait',
> + 'true', 'try', 'type', 'typeof',
> + 'union', 'unsafe', 'unsized', 'use',
> + 'virtual', 'where', 'while', 'yield'):
> + name = 'r#' + name
TIL...
> + # avoid some clashes with the standard library
> + if name in ('String',):
> + name = 'Qapi' + name
This hides the unwise use of 'String' in qapi/net.json from Rust. I'd
rather rename that one.
> +
> + return name
> +
> +
> +def to_camel_case(value: str) -> str:
> + return ''.join('_' + word if word[0].isdigit()
> + else word[:1].upper() + word[1:]
> + for word in filter(None, re.split("[-_]+", value)))
Please use r'...' for regular expressions always.
Why do you need filter()?
This maps 'foo-0123-bar' to 'Foo_0123Bar'. Intentional? I'd kind of
expect 'Foo0123Bar'.
> +
> +
> class Indentation:
> """
> Indentation level management.
> diff --git a/scripts/qapi/rs.py b/scripts/qapi/rs.py
> new file mode 100644
> index 00000000000..2cf0c0e07f1
> --- /dev/null
> +++ b/scripts/qapi/rs.py
> @@ -0,0 +1,61 @@
> +# This work is licensed under the terms of the GNU GPL, version 2.
> +# See the COPYING file in the top-level directory.
> +"""
> +QAPI Rust generator
> +"""
> +
> +import os
> +import re
> +import subprocess
> +import sys
> +
> +from .common import mcgen as mcgen_common
> +from .gen import QAPIGen
> +from .schema import QAPISchemaVisitor
> +
> +
> +def mcgen(s: str, **kwds: object) -> str:
> + s = mcgen_common(s, **kwds)
> + return re.sub(r'(?: *\n)+', '\n', s)
This eats trailing spaces and blank lines. The latter is a big hammer.
Without it, I see unwanted blank lines generated. With it, I see wanted
blank lines eaten. For instance:
// @generated by qapi-gen, DO NOT EDIT
//!
//! Schema-defined QAPI types
//!
//! Copyright (c) 2025 Red Hat, Inc.
//!
//! This work is licensed under the terms of the GNU LGPL, version 2.1 or
//! later. See the COPYING.LIB file in the top-level directory.
#![allow(unexpected_cfgs)]
#![allow(non_camel_case_types)]
#![allow(clippy::empty_structs_with_brackets)]
#![allow(clippy::large_enum_variant)]
#![allow(clippy::pub_underscore_fields)]
// Because QAPI structs can contain float, for simplicity we never
// derive Eq. Clippy however would complain for those structs
// that *could* be Eq too.
#![allow(clippy::derive_partial_eq_without_eq)]
use serde_derive::{Serialize, Deserialize};
use util::qobject::QObject;
becomes
// @generated by qapi-gen, DO NOT EDIT
//!
//! Schema-defined QAPI types
//!
//! Copyright (c) 2025 Red Hat, Inc.
//!
//! This work is licensed under the terms of the GNU LGPL, version 2.1 or
//! later. See the COPYING.LIB file in the top-level directory.
#![allow(unexpected_cfgs)]
#![allow(non_camel_case_types)]
#![allow(clippy::empty_structs_with_brackets)]
#![allow(clippy::large_enum_variant)]
#![allow(clippy::pub_underscore_fields)]
// Because QAPI structs can contain float, for simplicity we never
// derive Eq. Clippy however would complain for those structs
// that *could* be Eq too.
#![allow(clippy::derive_partial_eq_without_eq)]
use serde_derive::{Serialize, Deserialize};
use util::qobject::QObject;
This text is generated by QAPIGenRs._top() and
QAPISchemaGenRsTypeVisitor.visit_begin(). The blank lines are clearly
intentional there.
Hmm.
Possibly related: rustfmt below.
> +
> +
> +class QAPIGenRs(QAPIGen):
> + def __init__(self, fname: str, blurb: str, pydoc: str):
> + super().__init__(fname)
> + self._blurb = blurb
> + self._copyright = '\n//! '.join(re.findall(r'^Copyright .*', pydoc,
> + re.MULTILINE))
> +
> + def _top(self) -> str:
> + return mcgen('''
> +// @generated by qapi-gen, DO NOT EDIT
> +
> +//!
> +//! Schema-defined QAPI types
I think you want %(blurb) here.
> +//!
> +//! %(copyright)s
> +//!
> +//! This work is licensed under the terms of the GNU LGPL, version 2.1 or
> +//! later. See the COPYING.LIB file in the top-level directory.
> +
> +''',
> + tool=os.path.basename(sys.argv[0]),
> + blurb=self._blurb, copyright=self._copyright)
> +
> +
> +class QAPISchemaRsVisitor(QAPISchemaVisitor):
> +
> + def __init__(self, prefix: str, what: str,
> + blurb: str, pydoc: str):
> + super().__init__()
> + self._prefix = prefix
> + self._what = what
> + self._gen = QAPIGenRs(self._prefix + self._what + '.rs', blurb, pydoc)
Break the line before blurb, please.
> +
> + def write(self, output_dir: str) -> None:
> + self._gen.write(output_dir)
> +
> + try:
> + subprocess.check_call(['rustfmt', self._gen.fname], cwd=output_dir)
Break the line before cwd=, please.
> + except FileNotFoundError:
> + pass
This runs rustfmt to clean up the generated file. Silently does nothing
if we don't have rustfmt.
Should we make rustfmt a hard requirement? Please discuss this briefly
in the commit message.
> diff --git a/scripts/qapi/rs_types.py b/scripts/qapi/rs_types.py
> new file mode 100644
> index 00000000000..64702eb54ae
> --- /dev/null
> +++ b/scripts/qapi/rs_types.py
[Interesting part left for tomorrow...]
> diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
> index 15f5d97418f..a65b25141fa 100644
> --- a/scripts/qapi/schema.py
> +++ b/scripts/qapi/schema.py
> @@ -37,6 +37,7 @@
> docgen_ifcond,
> gen_endif,
> gen_if,
> + rs_name,
> rsgen_ifcond,
> )
> from .error import QAPIError, QAPISemError, QAPISourceError
> @@ -341,6 +342,11 @@ def c_param_type(self) -> str:
class QAPISchemaType(QAPISchemaDefinition, ABC):
# Return the C type for common use.
# For the types we commonly box, this is a pointer type.
@abstractmethod
def c_type(self) -> str:
pass
# Return the C type to be used in a parameter list.
def c_param_type(self) -> str:
return self.c_type()
# Return the C type to be used where we suppress boxing.
> def c_unboxed_type(self) -> str:
> return self.c_type()
>
> + # Return the Rust type for common use
Are the uncommon uses?
There are for C types, and that's why we have both .c_type(),
.c_param_type(), nad .c_unboxed_type().
> + @abstractmethod
> + def rs_type(self) -> str:
> + pass
> +
> @abstractmethod
> def json_type(self) -> str:
> pass
> @@ -382,11 +388,12 @@ def describe(self) -> str:
> class QAPISchemaBuiltinType(QAPISchemaType):
> meta = 'built-in'
>
> - def __init__(self, name: str, json_type: str, c_type: str):
> + def __init__(self, name: str, json_type: str, rs_type: str, c_type: str):
> super().__init__(name, None, None)
> assert json_type in ('string', 'number', 'int', 'boolean', 'null',
> 'value')
> self._json_type_name = json_type
> + self._rs_type_name = rs_type
> self._c_type_name = c_type
>
> def c_name(self) -> str:
> @@ -406,6 +413,9 @@ def json_type(self) -> str:
> def doc_type(self) -> str:
> return self.json_type()
>
> + def rs_type(self) -> str:
> + return self._rs_type_name
> +
> def visit(self, visitor: QAPISchemaVisitor) -> None:
> super().visit(visitor)
> visitor.visit_builtin_type(self.name, self.info, self.json_type())
> @@ -449,6 +459,9 @@ def is_implicit(self) -> bool:
> def c_type(self) -> str:
> return c_name(self.name)
>
> + def rs_type(self) -> str:
> + return rs_name(self.name)
> +
> def member_names(self) -> List[str]:
> return [m.name for m in self.members]
>
> @@ -498,6 +511,9 @@ def is_implicit(self) -> bool:
> def c_type(self) -> str:
> return c_name(self.name) + POINTER_SUFFIX
>
> + def rs_type(self) -> str:
> + return 'Vec<%s>' % self.element_type.rs_type()
This may be called only after .check(), because that's when
.element_type becomes valid. .ifcond() has the same precondition, and
states it explicitly with assert self._checked. Let's do the same here.
> +
> def json_type(self) -> str:
> return 'array'
>
> @@ -630,6 +646,9 @@ def c_type(self) -> str:
> def c_unboxed_type(self) -> str:
> return c_name(self.name)
>
> + def rs_type(self) -> str:
> + return rs_name(self.name)
> +
> def json_type(self) -> str:
> return 'object'
>
> @@ -711,6 +730,9 @@ def c_type(self) -> str:
> def json_type(self) -> str:
> return 'value'
>
> + def rs_type(self) -> str:
> + return rs_name(self.name)
> +
> def visit(self, visitor: QAPISchemaVisitor) -> None:
> super().visit(visitor)
> visitor.visit_alternate_type(
> @@ -1234,9 +1256,10 @@ def _def_include(self, expr: QAPIExpression) -> None:
> QAPISchemaInclude(self._make_module(include), expr.info))
>
> def _def_builtin_type(
> - self, name: str, json_type: str, c_type: str
> + self, name: str, json_type: str, rs_type: str, c_type: str
> ) -> None:
> - self._def_definition(QAPISchemaBuiltinType(name, json_type, c_type))
> + builtin = QAPISchemaBuiltinType(name, json_type, rs_type, c_type)
> + self._def_definition(builtin)
> # Instantiating only the arrays that are actually used would
> # be nice, but we can't as long as their generated code
> # (qapi-builtin-types.[ch]) may be shared by some other
> @@ -1255,21 +1278,21 @@ def is_predefined(self, name: str) -> bool:
> return False
>
> def _def_predefineds(self) -> None:
> - for t in [('str', 'string', 'char' + POINTER_SUFFIX),
> - ('number', 'number', 'double'),
> - ('int', 'int', 'int64_t'),
> - ('int8', 'int', 'int8_t'),
> - ('int16', 'int', 'int16_t'),
> - ('int32', 'int', 'int32_t'),
> - ('int64', 'int', 'int64_t'),
> - ('uint8', 'int', 'uint8_t'),
> - ('uint16', 'int', 'uint16_t'),
> - ('uint32', 'int', 'uint32_t'),
> - ('uint64', 'int', 'uint64_t'),
> - ('size', 'int', 'uint64_t'),
> - ('bool', 'boolean', 'bool'),
> - ('any', 'value', 'QObject' + POINTER_SUFFIX),
> - ('null', 'null', 'QNull' + POINTER_SUFFIX)]:
> + for t in [('str', 'string', 'String', 'char' + POINTER_SUFFIX),
> + ('number', 'number', 'f64', 'double'),
> + ('int', 'int', 'i64', 'int64_t'),
> + ('int8', 'int', 'i8', 'int8_t'),
> + ('int16', 'int', 'i16', 'int16_t'),
> + ('int32', 'int', 'i32', 'int32_t'),
> + ('int64', 'int', 'i64', 'int64_t'),
> + ('uint8', 'int', 'u8', 'uint8_t'),
> + ('uint16', 'int', 'u16', 'uint16_t'),
> + ('uint32', 'int', 'u32', 'uint32_t'),
> + ('uint64', 'int', 'u64', 'uint64_t'),
> + ('size', 'int', 'u64', 'uint64_t'),
> + ('bool', 'boolean', 'bool', 'bool'),
> + ('any', 'value', 'QObject', 'QObject' + POINTER_SUFFIX),
> + ('null', 'null', '()', 'QNull' + POINTER_SUFFIX)]:
> self._def_builtin_type(*t)
> self.the_empty_object_type = QAPISchemaObjectType(
> 'q_empty', None, None, None, None, None, [], None)
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 00/16] rust: QObject and QAPI bindings
2026-02-24 17:28 ` Paolo Bonzini
@ 2026-02-26 12:42 ` Markus Armbruster
0 siblings, 0 replies; 55+ messages in thread
From: Markus Armbruster @ 2026-02-26 12:42 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> On 2/24/26 15:06, Markus Armbruster wrote:
>> Paolo Bonzini <pbonzini@redhat.com> writes:
>>
>>> This adds two related parts of the Rust bindings:
>>
>> [...]
>>
>>> docs/devel/rust.rst | 1 +
>>> meson.build | 4 +-
>>> include/qobject/qobject.h | 5 +-
>>> rust/util/wrapper.h | 8 +
>>> qapi/meson.build | 9 +
>>> rust/Cargo.lock | 2 +
>>> rust/Cargo.toml | 2 +
>>> rust/meson.build | 4 +
>>> rust/tests/meson.build | 21 +-
>>> rust/tests/tests/integration.rs | 2 +
>>> rust/tests/tests/qapi.rs | 444 +++++++++++++
>>> rust/util/Cargo.toml | 2 +
>>> rust/util/meson.build | 30 +-
>>> rust/util/src/lib.rs | 4 +
>>> rust/util/src/qobject/deserialize.rs | 134 ++++
>>> rust/util/src/qobject/deserializer.rs | 371 +++++++++++
>>
>> Are you sure having two source filenames differ in just one character is
>> a good idea?
>>
>>> rust/util/src/qobject/error.rs | 58 ++
>>> rust/util/src/qobject/mod.rs | 383 ++++++++++++
>>> rust/util/src/qobject/serialize.rs | 59 ++
>>> rust/util/src/qobject/serializer.rs | 585 ++++++++++++++++++
>>
>> Likewise.
>
> I feel and share your pain... That's how serde calls the things ("-er"
> for the struct and verb for the trait) and I cannot really thing of a
> different naming for files.
>
> I thought of renaming serializer.rs to to_qobject.rs, and
> deserializer.rs to from_qobject.rs. However, that doesn't work out
> because I want "qobject::to_object" to be the function, not the module
> containing to_qobject() and Serializer.
>
> serde_json places the struct and trait in the same file (e.g.
> https://github.com/serde-rs/json/blob/master/src/value/ser.rs). I can
> do that if you prefer it that way.
Any drawbacks?
You decide!
> Paolo
>
>> These caused a bit of friction in review already...
>>
>> [...]
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 10/16] scripts/qapi: add QAPISchemaType.is_predefined
2026-02-25 8:44 ` Markus Armbruster
@ 2026-02-26 14:12 ` Paolo Bonzini
0 siblings, 0 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-02-26 14:12 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
On 2/25/26 09:44, Markus Armbruster wrote:
> Paolo Bonzini <pbonzini@redhat.com> writes:
>
>> On 2/25/26 08:33, Markus Armbruster wrote:
>>> Paolo Bonzini <pbonzini@redhat.com> writes:
>>>
>>>> It is impossible to call is_implicit on an enum type from the visitor, because
>>>> the QAPISchemaEnumType has already been exploded into its costituent fields.
>>>
>>> constituent
>>>
>>> Passing selected attributes instead of the entire object to its visitor
>>> method limits what the visitor can do. This is both good and bad.
>>> We've run into "bad" a couple of times. It's never been bad enough to
>>> change the interface, though.
>>>
>>> Thoughts?
>>
>> I think that applies here, is_predefined() is a reasonable addition.
>>
>>>> The Rust backend is also not modular (yet?) so it is not possible to filter
>>>> out the builtin module;
>>>
>>> Really?
>>>
>>> The visitors are all based on QAPISchemaVisitor. Protocol:
>>>
>>> .visit_begin()
>>> for all modules:
>>> .visit_module()
>>> for all entities:
>>> if .visit_needed():
>>> .visit_FOO()
>>> .visit_end()
>>>
>>> QAPISchemaModularCVisitor implements .visit_module() to generate code
>>> per module. Its .write() skips builtin modules unless opt_builtins.
>>
>> ... because its .write() already builds multiple QAPIGen{C,H,Trace}, one
>> per module. Here instead there is just one QAPIGenRs.
>>
>> Using multiple QAPIGenRs instances, one per module, would be hackish.
>
> I was thinking of having .visit_module() save .is_builtin_module(name)
> in self.in_builtin_module, then use that to recognize built-ins.
>
>> I'd rather just go modular instead of that. I can take a look, since
>> this series will be (early) 11.1 material anyway.
>
> Modular C code generation reduces incremental build time massively.
> Before, we suffered from "touch the schema, recompile the world". If
> Rust would similarly profit, then I'm all for going modular.
Rust compiles by crate rather than file (crate == toplevel module), so
it would need some benchmarking but yeah I expect that to help. That
would mean something like
use qapi_block::*;
Paolo
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings
2026-01-08 13:10 ` [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings Paolo Bonzini
2026-02-23 12:36 ` Markus Armbruster
2026-02-25 14:39 ` Markus Armbruster
@ 2026-03-03 9:19 ` Markus Armbruster
2026-03-03 13:17 ` Paolo Bonzini
2 siblings, 1 reply; 55+ messages in thread
From: Markus Armbruster @ 2026-03-03 9:19 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, armbru, Marc-André Lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>
> Generate high-level native Rust declarations for the QAPI types.
>
> - char* is mapped to String, scalars to there corresponding Rust types
>
> - enums use #[repr(u32)] and can be transmuted to their C counterparts
>
> - has_foo/foo members are mapped to Option<T>
>
> - lists are represented as Vec<T>
>
> - structures map fields 1:1 to Rust
>
> - alternate are represented as Rust enum, each variant being a 1-element
> tuple
>
> - unions are represented in a similar way as in C: a struct S with a "u"
> member (since S may have extra 'base' fields). The discriminant
> isn't a member of S, since Rust enum already include it, but it can be
> recovered with "mystruct.u.into()"
>
> Anything that includes a recursive struct puts it in a Box. Lists are
> not considered recursive, because Vec breaks the recursion (it's possible
> to construct an object containing an empty Vec of its own type).
>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Link: https://lore.kernel.org/r/20210907121943.3498701-21-marcandre.lureau@redhat.com
> [Paolo: rewrite conversion of schema types to Rust types]
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
Let's look at the generated Rust. I'm going to ignore formatting issues
like blank lines. My test inputs are adapted from
tests/qapi-schema/qapi-schema-test.json.
= Boilerplate and built-in stuff =
The qapi-types.rs generated for an empty schema is basically empty:
// @generated by qapi-gen, DO NOT EDIT
//!
//! Schema-defined QAPI types
//!
//! Copyright (c) 2025 Red Hat, Inc.
//!
//! This work is licensed under the terms of the GNU LGPL, version 2.1 or
//! later. See the COPYING.LIB file in the top-level directory.
#![allow(unexpected_cfgs)]
#![allow(non_camel_case_types)]
#![allow(clippy::empty_structs_with_brackets)]
#![allow(clippy::large_enum_variant)]
#![allow(clippy::pub_underscore_fields)]
// Because QAPI structs can contain float, for simplicity we never
// derive Eq. Clippy however would complain for those structs
// that *could* be Eq too.
#![allow(clippy::derive_partial_eq_without_eq)]
use util::qobject::QObject;
Okay.
Note we generate nothing for the built-in types. For C, we generate
qapi-builtin-types.[ch], which provides lists of built-in types, and
enum QType. Rust gives us lists for free, and QType we don't need right
now. Good.
= Enum types =
Input:
{ 'enum': 'TestIfEnum',
'data': [ 'foo', { 'name' : 'bar', 'if': 'TEST_IF_ENUM_MEMBER' } ],
'if': 'TEST_IF_UNION' }
Output without the boilerplate:
#[cfg(TEST_IF_UNION)]
#[derive(Copy, Clone, Debug, PartialEq)]
Ignoring such lines for now.
#[repr(u32)]
As explained in the commit message.
#[derive(common::TryInto)]
Ignoring these, too.
pub enum TestIfEnum {
FOO,
#[cfg(TEST_IF_ENUM_MEMBER)]
BAR,
}
The obvious part :)
#[cfg(TEST_IF_UNION)]
impl Default for TestIfEnum {
#[inline]
fn default() -> TestIfEnum {
Self::FOO
}
}
This specifies the enum type's default value. Hmm... Modified input:
{ 'enum': 'TestIfEnum',
'data': [ { 'name' : 'bar', 'if': 'TEST_IF_ENUM_MEMBER' }, 'foo' ],
'if': 'TEST_IF_UNION' }
Output:
#[cfg(TEST_IF_UNION)]
#[derive(Copy, Clone, Debug, PartialEq)]
#[repr(u32)]
#[derive(common::TryInto)]
pub enum TestIfEnum {
#[cfg(TEST_IF_ENUM_MEMBER)]
BAR,
FOO,
}
#[cfg(TEST_IF_UNION)]
impl Default for TestIfEnum {
#[inline]
fn default() -> TestIfEnum {
Self::BAR
}
}
If TEST_IF_ENUM_MEMBER is off, member BAR does not exist. default()
uses it anyway. Bug?
Not tested: enum definition's optional 'prefix' member, because Rust
generation has no need for it and does not use it. Good.
= Object types =
Input:
{ 'struct': 'UserDefOne',
'base': 'UserDefZero',
'data': { 'string': 'str',
'*number': 'number' } }
{ 'struct': 'UserDefZero',
'data': { 'integer': 'int' } }
Output:
#[derive(Clone, Debug, PartialEq)]
Ignoring such lines for now.
pub struct UserDefOne {
// Members inherited:
pub integer: i64,
// Own members:
pub string: String,
pub number: Option<f64>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct UserDefZero {
pub integer: i64,
}
Obvious enough.
More input:
{ 'union': 'TestIfUnion',
'base': { 'type': 'TestIfEnum' },
'discriminator': 'type',
'data': { 'foo': 'UserDefOne',
'bar': { 'type': 'UserDefZero', 'if': 'TEST_IF_ENUM_MEMBER'} },
'if': { 'all': ['TEST_IF_UNION', 'TEST_IF_STRUCT'] } }
Output:
#[cfg(all(TEST_IF_UNION, TEST_IF_STRUCT))]
#[derive(Clone, Debug, PartialEq)]
pub enum TestIfUnionVariant {
Possible trouble if somebody defines a QAPI type name ending with
'Variant'.
Code generated for a QAPI schema name named 'Frob' typically defines a
'Frob'. Sometimes, we transform 'Frob' in (hopefully obvious) ways, say
to adhere to naming rules. For instance, QAPI enum member 'foo' of
'TestIfEnum' becomes FOO in generated Rust. This risks naming
collisions in generated code. Hasn't been much of a problem in
practice.
We also need names for helpers. These we commonly derive from QAPI
schema names, and we reserve names for the purpose:
* Names beginning with 'q_'
* Type names ending with 'List'
* Member name 'u' and names starting with 'has_'
Source: docs/devel/qapi-code-gen.rst section "Naming rules and reserved
names".
We could reserve names ending with 'Variant'. Or we use a 'q_' prefix
here.
Foo(UserDefOne),
#[cfg(TEST_IF_ENUM_MEMBER)]
Bar(UserDefZero),
}
#[cfg(all(TEST_IF_UNION, TEST_IF_STRUCT))]
impl From<&TestIfUnionVariant> for TestIfEnum {
fn from(e: &TestIfUnionVariant) -> Self {
match e {
TestIfUnionVariant::Foo(_) => Self::FOO,
#[cfg(TEST_IF_ENUM_MEMBER)]
TestIfUnionVariant::Bar(_) => Self::BAR,
}
}
}
#[cfg(all(TEST_IF_UNION, TEST_IF_STRUCT))]
#[derive(Clone, Debug, PartialEq)]
pub struct TestIfUnion {
pub u: TestIfUnionVariant,
}
Why do we need to wrap TestIfUnionVariant in a struct? Hmm, we need it
in case we have more common members. Modified input to demonstrate:
{ 'union': 'TestIfUnion',
'base': { 'type': 'TestIfEnum', 'common': 'int' },
'discriminator': 'type',
'data': { 'foo': 'UserDefOne',
'bar': { 'type': 'UserDefZero', 'if': 'TEST_IF_ENUM_MEMBER'} },
'if': { 'all': ['TEST_IF_UNION', 'TEST_IF_STRUCT'] } }
Output:
#[cfg(all(TEST_IF_UNION, TEST_IF_STRUCT))]
#[derive(Clone, Debug, PartialEq)]
pub enum TestIfUnionVariant {
Foo(UserDefOne),
#[cfg(TEST_IF_ENUM_MEMBER)]
Bar(UserDefZero),
}
#[cfg(all(TEST_IF_UNION, TEST_IF_STRUCT))]
impl From<&TestIfUnionVariant> for TestIfEnum {
fn from(e: &TestIfUnionVariant) -> Self {
match e {
TestIfUnionVariant::Foo(_) => Self::FOO,
#[cfg(TEST_IF_ENUM_MEMBER)]
TestIfUnionVariant::Bar(_) => Self::BAR,
}
}
}
#[cfg(all(TEST_IF_UNION, TEST_IF_STRUCT))]
#[derive(Clone, Debug, PartialEq)]
pub struct TestIfUnion {
pub common: i64,
pub u: TestIfUnionVariant,
}
Okay.
= Array types =
Input:
{ 'struct': 'ArrayStruct',
'data': { 'integer': ['int'],
'*any': ['any'] } }
Output:
#[derive(Clone, Debug, PartialEq)]
pub struct ArrayStruct {
pub integer: Vec<i64>,
pub any: Option<Vec<QObject>>,
}
Okay.
= Alternate types =
Input:
{ 'alternate': 'TestIfAlternate',
'data': { 'foo': 'int',
'bar': { 'type': 'UserDefZero', 'if': 'TEST_IF_ALT_MEMBER'} },
'if': { 'all': ['TEST_IF_ALT', 'TEST_IF_STRUCT'] } }
Output:
#[cfg(all(TEST_IF_ALT, TEST_IF_STRUCT))]
#[derive(Clone, Debug, PartialEq)]
pub enum TestIfAlternate {
Foo(i64),
#[cfg(TEST_IF_ALT_MEMBER)]
Bar(UserDefZero),
}
Doesn't use QType for the tag like C does. Okay.
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings
2026-02-25 14:39 ` Markus Armbruster
@ 2026-03-03 10:00 ` Paolo Bonzini
2026-03-03 12:31 ` Markus Armbruster
0 siblings, 1 reply; 55+ messages in thread
From: Paolo Bonzini @ 2026-03-03 10:00 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
On 2/25/26 15:39, Markus Armbruster wrote:
>> +def rs_name(name: str) -> str:
>> + """
>> + Map @name to a valid, possibly raw Rust identifier.
>> + """
>> + name = re.sub(r'[^A-Za-z0-9_]', '_', name)
>> + if name[0].isnumeric():
>
> .isdigit()? It's what c_name() uses...
>
>> + name = '_' + name
>
> In review of v1, I pointed to "The Rust Reference"
>
> Identifiers starting with an underscore are typically used to
> indicate an identifier that is intentionally unused, and will
> silence the unused warning in rustc.
>
> https://doc.rust-lang.org/reference/identifiers.html
>
> You replied "In this case it doesn't really matter: public items (such
> as QAPI enum entries, or struct fields) do not raise the unused warning
> anyway."
>
> What gives us confidence rs_name() will only be used where it doesn't
> really matter?
The fact that all QAPI type definitions are (more or less by design) public.
>> + # avoid some clashes with the standard library
>> + if name in ('String',):
>> + name = 'Qapi' + name
>
> This hides the unwise use of 'String' in qapi/net.json from Rust. I'd
> rather rename that one.
Ok, BoxedString?
>> +
>> + return name
>> +
>> +
>> +def to_camel_case(value: str) -> str:
>> + return ''.join('_' + word if word[0].isdigit()
>> + else word[:1].upper() + word[1:]
>> + for word in filter(None, re.split("[-_]+", value)))
>
> Please use r'...' for regular expressions always.
>
> Why do you need filter()?
To handle - or _ at the beginning or ending of a string, where an empty
string would cause an IndexError in word[0].isdigit().
> This maps 'foo-0123-bar' to 'Foo_0123Bar'. Intentional? I'd kind of
> expect 'Foo0123Bar'.
Will fix (it is meant for 0123-45). New version is:
def to_camel_case(value: str) -> str:
result = ''
for p in re.split(r'[-_]+', value):
if not p:
pass
elif p[0].isalpha() or (result and result[-1].isalpha()):
result += p[0].upper() + p[1:]
else:
result += '_' + p
return result
>> +def mcgen(s: str, **kwds: object) -> str:
>> + s = mcgen_common(s, **kwds)
>> + return re.sub(r'(?: *\n)+', '\n', s)
>
> This eats trailing spaces and blank lines. The latter is a big hammer.
> Without it, I see unwanted blank lines generated. With it, I see wanted
> blank lines eaten. For instance:
Ok, I can look into adding rstrip here and there.
>> + except FileNotFoundError:
>> + pass
>
> This runs rustfmt to clean up the generated file. Silently does nothing
> if we don't have rustfmt.
>
> Should we make rustfmt a hard requirement? Please discuss this briefly
> in the commit message.
It's unnecessary, but it does make the output look nicer. If I add
rstrip, I don't need it.
>> + # Return the Rust type for common use
>
> Are the uncommon uses?
>
> There are for C types, and that's why we have both .c_type(),
> .c_param_type(), nad .c_unboxed_type().
Yes, Box<> and Vec<>. They just don't deserve their own function unlike C.
Paolo
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings
2026-03-03 10:00 ` Paolo Bonzini
@ 2026-03-03 12:31 ` Markus Armbruster
2026-03-03 15:55 ` Paolo Bonzini
0 siblings, 1 reply; 55+ messages in thread
From: Markus Armbruster @ 2026-03-03 12:31 UTC (permalink / raw)
To: Paolo Bonzini
Cc: Markus Armbruster, qemu-devel, Marc-André Lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> On 2/25/26 15:39, Markus Armbruster wrote:
>>> +def rs_name(name: str) -> str:
>>> + """
>>> + Map @name to a valid, possibly raw Rust identifier.
>>> + """
>>> + name = re.sub(r'[^A-Za-z0-9_]', '_', name)
>>> + if name[0].isnumeric():
>>
>> .isdigit()? It's what c_name() uses...
>>
>>> + name = '_' + name
>>
>> In review of v1, I pointed to "The Rust Reference"
>>
>> Identifiers starting with an underscore are typically used to
>> indicate an identifier that is intentionally unused, and will
>> silence the unused warning in rustc.
>>
>> https://doc.rust-lang.org/reference/identifiers.html
>>
>> You replied "In this case it doesn't really matter: public items (such
>> as QAPI enum entries, or struct fields) do not raise the unused warning
>> anyway."
>>
>> What gives us confidence rs_name() will only be used where it doesn't
>> really matter?
>
> The fact that all QAPI type definitions are (more or less by design) public.
Any particular reason not to use the same 'q_' prefix as in C?
>>> + # avoid some clashes with the standard library
>>> + if name in ('String',):
>>> + name = 'Qapi' + name
>>
>> This hides the unwise use of 'String' in qapi/net.json from Rust. I'd
>> rather rename that one.
>
> Ok, BoxedString?
Works for me.
>>> +
>>> + return name
>>> +
>>> +
>>> +def to_camel_case(value: str) -> str:
>>> + return ''.join('_' + word if word[0].isdigit()
>>> + else word[:1].upper() + word[1:]
>>> + for word in filter(None, re.split("[-_]+", value)))
>>
>> Please use r'...' for regular expressions always.
>>
>> Why do you need filter()?
>
> To handle - or _ at the beginning or ending of a string, where an empty
> string would cause an IndexError in word[0].isdigit().
Got it, thanks.
>> This maps 'foo-0123-bar' to 'Foo_0123Bar'. Intentional? I'd kind of
>> expect 'Foo0123Bar'.
>
> Will fix (it is meant for 0123-45). New version is:
>
> def to_camel_case(value: str) -> str:
> result = ''
> for p in re.split(r'[-_]+', value):
> if not p:
> pass
> elif p[0].isalpha() or (result and result[-1].isalpha()):
> result += p[0].upper() + p[1:]
> else:
> result += '_' + p
> return result
Maps '0123-45' to '_0123_45'. Is the leading '_' intentional?
>>> +def mcgen(s: str, **kwds: object) -> str:
>>> + s = mcgen_common(s, **kwds)
>>> + return re.sub(r'(?: *\n)+', '\n', s)
>>
>> This eats trailing spaces and blank lines. The latter is a big hammer.
>> Without it, I see unwanted blank lines generated. With it, I see wanted
>> blank lines eaten. For instance:
>
> Ok, I can look into adding rstrip here and there.
>
>>> + except FileNotFoundError:
>>> + pass
>>
>> This runs rustfmt to clean up the generated file. Silently does nothing
>> if we don't have rustfmt.
>>
>> Should we make rustfmt a hard requirement? Please discuss this briefly
>> in the commit message.
I'm fine with not running rustfmt, and I'm fine with running it always
(makes it a hard requirement). Running it sometimes feels like more
trouble than it's worth.
> It's unnecessary, but it does make the output look nicer. If I add
> rstrip, I don't need it.
>
>>> + # Return the Rust type for common use
>>
>> Are the uncommon uses?
>>
>> There are for C types, and that's why we have both .c_type(),
>> .c_param_type(), nad .c_unboxed_type().
>
> Yes, Box<> and Vec<>. They just don't deserve their own function unlike C.
I see.
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings
2026-03-03 9:19 ` Markus Armbruster
@ 2026-03-03 13:17 ` Paolo Bonzini
0 siblings, 0 replies; 55+ messages in thread
From: Paolo Bonzini @ 2026-03-03 13:17 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
On 3/3/26 10:19, Markus Armbruster wrote:
> Paolo Bonzini <pbonzini@redhat.com> writes:
>
>> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>>
>> Generate high-level native Rust declarations for the QAPI types.
>>
>> - char* is mapped to String, scalars to there corresponding Rust types
>>
>> - enums use #[repr(u32)] and can be transmuted to their C counterparts
>>
>> - has_foo/foo members are mapped to Option<T>
>>
>> - lists are represented as Vec<T>
>>
>> - structures map fields 1:1 to Rust
>>
>> - alternate are represented as Rust enum, each variant being a 1-element
>> tuple
>>
>> - unions are represented in a similar way as in C: a struct S with a "u"
>> member (since S may have extra 'base' fields). The discriminant
>> isn't a member of S, since Rust enum already include it, but it can be
>> recovered with "mystruct.u.into()"
>>
>> Anything that includes a recursive struct puts it in a Box. Lists are
>> not considered recursive, because Vec breaks the recursion (it's possible
>> to construct an object containing an empty Vec of its own type).
>>
>> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
>> Link: https://lore.kernel.org/r/20210907121943.3498701-21-marcandre.lureau@redhat.com
>> [Paolo: rewrite conversion of schema types to Rust types]
>> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
>
> Let's look at the generated Rust. I'm going to ignore formatting issues
> like blank lines. My test inputs are adapted from
> tests/qapi-schema/qapi-schema-test.json.
>
> = Boilerplate and built-in stuff =
>
> The qapi-types.rs generated for an empty schema is basically empty:
>
> // @generated by qapi-gen, DO NOT EDIT
> //!
> //! Schema-defined QAPI types
> //!
> //! Copyright (c) 2025 Red Hat, Inc.
> //!
> //! This work is licensed under the terms of the GNU LGPL, version 2.1 or
> //! later. See the COPYING.LIB file in the top-level directory.
> #![allow(unexpected_cfgs)]
> #![allow(non_camel_case_types)]
> #![allow(clippy::empty_structs_with_brackets)]
> #![allow(clippy::large_enum_variant)]
> #![allow(clippy::pub_underscore_fields)]
> // Because QAPI structs can contain float, for simplicity we never
> // derive Eq. Clippy however would complain for those structs
> // that *could* be Eq too.
> #![allow(clippy::derive_partial_eq_without_eq)]
> use util::qobject::QObject;
>
> Okay.
>
> Note we generate nothing for the built-in types. For C, we generate
> qapi-builtin-types.[ch], which provides lists of built-in types, and
> enum QType. Rust gives us lists for free, and QType we don't need right
> now. Good.
>
> = Enum types =
>
> Input:
>
> { 'enum': 'TestIfEnum',
> 'data': [ 'foo', { 'name' : 'bar', 'if': 'TEST_IF_ENUM_MEMBER' } ],
> 'if': 'TEST_IF_UNION' }
>
> Output without the boilerplate:
>
> #[cfg(TEST_IF_UNION)]
> #[derive(Copy, Clone, Debug, PartialEq)]
>
> Ignoring such lines for now.
>
> #[repr(u32)]
>
> As explained in the commit message.
>
> #[derive(common::TryInto)]
>
> Ignoring these, too.
>
> pub enum TestIfEnum {
>
> FOO,
> #[cfg(TEST_IF_ENUM_MEMBER)]
> BAR,
> }
>
> The obvious part :)
>
> #[cfg(TEST_IF_UNION)]
> impl Default for TestIfEnum {
> #[inline]
> fn default() -> TestIfEnum {
> Self::FOO
> }
> }
>
> This specifies the enum type's default value. Hmm... Modified input:
>
> { 'enum': 'TestIfEnum',
> 'data': [ { 'name' : 'bar', 'if': 'TEST_IF_ENUM_MEMBER' }, 'foo' ],
> 'if': 'TEST_IF_UNION' }
>
> Output:
>
> #[cfg(TEST_IF_UNION)]
> #[derive(Copy, Clone, Debug, PartialEq)]
> #[repr(u32)]
> #[derive(common::TryInto)]
> pub enum TestIfEnum {
> #[cfg(TEST_IF_ENUM_MEMBER)]
> BAR,
>
> FOO,
> }
>
> #[cfg(TEST_IF_UNION)]
> impl Default for TestIfEnum {
> #[inline]
> fn default() -> TestIfEnum {
> Self::BAR
> }
> }
>
> If TEST_IF_ENUM_MEMBER is off, member BAR does not exist. default()
> uses it anyway. Bug?
Nice catch. I can make it produce the first element (as in C) with a
tiny bit of cheating
unsafe { ::std::mem::transmute_copy(&0u32) }
or some reliance on the optimizer
0.try_into().unwrap()
However, I'm inclined to handle this generically. For example, an enum
where the first element is configuration dependent could be usable only
if optional. And then the Rust code can skip the "impl Default". Let
me check how often it would trigger.
Paolo
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings
2026-03-03 12:31 ` Markus Armbruster
@ 2026-03-03 15:55 ` Paolo Bonzini
2026-03-04 8:09 ` Markus Armbruster
0 siblings, 1 reply; 55+ messages in thread
From: Paolo Bonzini @ 2026-03-03 15:55 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
On Tue, Mar 3, 2026 at 1:31 PM Markus Armbruster <armbru@redhat.com> wrote:
> >> You replied "In this case it doesn't really matter: public items (such
> >> as QAPI enum entries, or struct fields) do not raise the unused warning
> >> anyway."
> >>
> >> What gives us confidence rs_name() will only be used where it doesn't
> >> really matter?
> >
> > The fact that all QAPI type definitions are (more or less by design) public.
>
> Any particular reason not to use the same 'q_' prefix as in C?
The other Rust convention is that you usually have enums in CamelCase
(starting with an uppercase letter). "q_" or "Q_" would also trigger a
warning, and they would complicate to_camel_case a bit (because the
prefix would have to be kept including the underscore).
> >> This maps 'foo-0123-bar' to 'Foo_0123Bar'. Intentional? I'd kind of
> >> expect 'Foo0123Bar'.
> >
> > Will fix (it is meant for 0123-45). New version is:
> >
> > def to_camel_case(value: str) -> str:
> > result = ''
> > for p in re.split(r'[-_]+', value):
> > if not p:
> > pass
> > elif p[0].isalpha() or (result and result[-1].isalpha()):
> > result += p[0].upper() + p[1:]
> > else:
> > result += '_' + p
> > return result
>
> Maps '0123-45' to '_0123_45'. Is the leading '_' intentional?
Yes, because otherwise the output is not an identifier; but it doesn't
matter since the input is actually an identifier already and
to_camel_case('_0123_45') does give '_0123_45'. In neither case you
get the invalid identifier '0123_45'.
> I'm fine with not running rustfmt, and I'm fine with running it always
> (makes it a hard requirement). Running it sometimes feels like more
> trouble than it's worth.
Yeah, I will drop it. I changed mcgen to allow removing empty lines in
the middle of a declaration, like this:
def mcgen(*code: T.Sequence[str], **kwds: object) -> str:
'''
Generate ``code`` with ``kwds`` interpolated. Separate
positional arguments represent separate segments that could
expand to empty strings; empty segments are omitted and no
blank lines are introduced at their boundaries.
'''
last = len(code) - 1
result = []
for i, s in enumerate(code):
if s.startswith('\n'):
s = s[1:]
if i != last:
s = s.rstrip()
s = cgen(s, **kwds)
if s:
result.append(s)
return '\n'.join(result)
For the case of a single argument, the result is equivalent to the
existing implementation:
# already checked by current mcgen()
if s.startswith('\n'):
s = s[1:]
# skipped for a single argument
if i != last:
s = s.rstrip()
s = cgen(s, **kwds)
if s:
result.append(s)
# result has zero or a single element, no '\n' added
return '\n'.join(result)
Paolo
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings
2026-03-03 15:55 ` Paolo Bonzini
@ 2026-03-04 8:09 ` Markus Armbruster
0 siblings, 0 replies; 55+ messages in thread
From: Markus Armbruster @ 2026-03-04 8:09 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, Marc-André Lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> On Tue, Mar 3, 2026 at 1:31 PM Markus Armbruster <armbru@redhat.com> wrote:
>
>> >> You replied "In this case it doesn't really matter: public items (such
>> >> as QAPI enum entries, or struct fields) do not raise the unused warning
>> >> anyway."
>> >>
>> >> What gives us confidence rs_name() will only be used where it doesn't
>> >> really matter?
>> >
>> > The fact that all QAPI type definitions are (more or less by design) public.
>>
>> Any particular reason not to use the same 'q_' prefix as in C?
>
> The other Rust convention is that you usually have enums in CamelCase
> (starting with an uppercase letter). "q_" or "Q_" would also trigger a
> warning, and they would complicate to_camel_case a bit (because the
> prefix would have to be kept including the underscore).
Alright.
One of the QAPI generator's known shortcomings is weak protection
against name clashes in generated code.
The generator was built with cavalier disregard for name clashes. The
existing naming rules and their enforcement was retrofitted to limit the
damage.
There are two kinds:
* The user's names clashing with the generator's.
Naming rules help avoid such clashes.
Example: ['Frob'] makes the generator define a C type FrobList, which
could clash with a user-defined 'FrobList' if we didn't reject
user-defined names ending with 'List'. Test case reserved-type-list.
But there are gaps.
Example: a user-defined type QAPIEvent clashes with generated enum
QAPIEvent.
The naming rules should reflect the names the generator wants to use.
When we add new patterns of generated names, we should update the
rules. One pattern this series adds is names ending with 'Variant',
as I pointed out in the review of the generated code I sent yesterday.
* The user's names clashing with themselves.
Naming rules again help avoid such clashes.
Example: struct members 'a_b and 'a-b' would both map to C identifier
a_b if we didn't reject that. Test case struct-member-name-clash.
The rejection code checks for clashes among the values of c_name().
This guards against clashes in generated C. To also guard against
clashes in generated Rust, we'd need to check the values of rs_name().
I'd accept a TODO comment for now.
Again, there are gaps.
Example: value 'ni-cator' of enum 'Frob' and value 'cator' of enum
'FrobNi' both generate C enumeration constant FROB_NI_CATOR.
The more the generator massages names, the more we risk such clashes.
The C generator doesn't massage all that much, mostly funny characters
to '_', and enumeration constants. The Rust generator needs to
massage more, because Rust's naming rules differ from C's. I think we
should at least document the problem clearly. Anything else? Better
ideas?
See also docs/devel/qapi-code-gen.rst section "Naming rules and reserved
names".
>> >> This maps 'foo-0123-bar' to 'Foo_0123Bar'. Intentional? I'd kind of
>> >> expect 'Foo0123Bar'.
>> >
>> > Will fix (it is meant for 0123-45). New version is:
>> >
>> > def to_camel_case(value: str) -> str:
>> > result = ''
>> > for p in re.split(r'[-_]+', value):
>> > if not p:
>> > pass
>> > elif p[0].isalpha() or (result and result[-1].isalpha()):
>> > result += p[0].upper() + p[1:]
>> > else:
>> > result += '_' + p
>> > return result
>>
>> Maps '0123-45' to '_0123_45'. Is the leading '_' intentional?
>
> Yes, because otherwise the output is not an identifier; but it doesn't
> matter since the input is actually an identifier already and
> to_camel_case('_0123_45') does give '_0123_45'. In neither case you
> get the invalid identifier '0123_45'.
Shouldn't that be left to rs_name()?
>> I'm fine with not running rustfmt, and I'm fine with running it always
>> (makes it a hard requirement). Running it sometimes feels like more
>> trouble than it's worth.
>
> Yeah, I will drop it. I changed mcgen to allow removing empty lines in
> the middle of a declaration, like this:
>
> def mcgen(*code: T.Sequence[str], **kwds: object) -> str:
> '''
> Generate ``code`` with ``kwds`` interpolated. Separate
> positional arguments represent separate segments that could
> expand to empty strings; empty segments are omitted and no
> blank lines are introduced at their boundaries.
> '''
> last = len(code) - 1
> result = []
> for i, s in enumerate(code):
> if s.startswith('\n'):
> s = s[1:]
> if i != last:
> s = s.rstrip()
> s = cgen(s, **kwds)
> if s:
> result.append(s)
> return '\n'.join(result)
>
> For the case of a single argument, the result is equivalent to the
> existing implementation:
>
> # already checked by current mcgen()
> if s.startswith('\n'):
> s = s[1:]
> # skipped for a single argument
> if i != last:
> s = s.rstrip()
> s = cgen(s, **kwds)
> if s:
> result.append(s)
> # result has zero or a single element, no '\n' added
> return '\n'.join(result)
I wonder how the generated C would change if we used this there.
^ permalink raw reply [flat|nested] 55+ messages in thread
end of thread, other threads:[~2026-03-04 8:10 UTC | newest]
Thread overview: 55+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-01-08 13:10 [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 01/16] rust/qobject: add basic bindings Paolo Bonzini
2026-02-24 10:03 ` Markus Armbruster
2026-02-24 10:35 ` Paolo Bonzini
2026-02-24 13:33 ` Markus Armbruster
2026-02-25 8:05 ` Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 02/16] subprojects: add serde Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 03/16] rust/qobject: add Serialize implementation Paolo Bonzini
2026-02-24 10:29 ` Markus Armbruster
2026-02-24 10:48 ` Paolo Bonzini
2026-02-24 13:41 ` Markus Armbruster
2026-01-08 13:10 ` [PATCH v2 04/16] rust/qobject: add Serializer (to_qobject) implementation Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 05/16] rust/qobject: add Deserialize implementation Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 06/16] rust/qobject: add Deserializer (from_qobject) implementation Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 07/16] rust/qobject: add from/to JSON bindings for QObject Paolo Bonzini
2026-01-15 13:17 ` Zhao Liu
2026-01-08 13:10 ` [PATCH v2 08/16] rust/qobject: add Display/Debug Paolo Bonzini
2026-01-15 13:19 ` Zhao Liu
2026-01-08 13:10 ` [PATCH v2 09/16] scripts/qapi: add QAPISchemaIfCond.rsgen() Paolo Bonzini
2026-01-19 6:58 ` Zhao Liu
2026-02-25 6:48 ` Markus Armbruster
2026-02-25 7:53 ` Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 10/16] scripts/qapi: add QAPISchemaType.is_predefined Paolo Bonzini
2026-02-25 7:33 ` Markus Armbruster
2026-02-25 8:01 ` Paolo Bonzini
2026-02-25 8:44 ` Markus Armbruster
2026-02-26 14:12 ` Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 11/16] scripts/qapi: pull c_name from camel_to_upper to caller Paolo Bonzini
2026-01-19 7:05 ` Zhao Liu
2026-02-25 8:32 ` Markus Armbruster
2026-01-08 13:10 ` [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings Paolo Bonzini
2026-02-23 12:36 ` Markus Armbruster
2026-02-23 16:11 ` Paolo Bonzini
2026-02-24 13:46 ` Markus Armbruster
2026-02-25 14:39 ` Markus Armbruster
2026-03-03 10:00 ` Paolo Bonzini
2026-03-03 12:31 ` Markus Armbruster
2026-03-03 15:55 ` Paolo Bonzini
2026-03-04 8:09 ` Markus Armbruster
2026-03-03 9:19 ` Markus Armbruster
2026-03-03 13:17 ` Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 13/16] scripts/rustc_args: add --no-strict-cfg Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 14/16] rust/util: build QAPI types Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 15/16] scripts/qapi: add serde attributes Paolo Bonzini
2026-01-08 13:10 ` [PATCH v2 16/16] rust/tests: QAPI integration tests Paolo Bonzini
2026-02-17 8:10 ` [PATCH v2 00/16] rust: QObject and QAPI bindings Paolo Bonzini
2026-02-19 13:39 ` Markus Armbruster
2026-02-19 16:28 ` Paolo Bonzini
2026-02-23 9:53 ` Daniel P. Berrangé
2026-02-23 15:54 ` Paolo Bonzini
2026-02-23 16:24 ` Daniel P. Berrangé
2026-02-23 19:03 ` Paolo Bonzini
2026-02-24 14:06 ` Markus Armbruster
2026-02-24 17:28 ` Paolo Bonzini
2026-02-26 12:42 ` Markus Armbruster
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox