* [PATCH v3 01/19] rust/qobject: add basic bindings
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 02/19] subprojects: add serde Paolo Bonzini
` (17 subsequent siblings)
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau, 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/src/lib.rs | 4 +
rust/util/src/qobject/mod.rs | 333 +++++++++++++++++++++++++++++++++++
3 files changed, 341 insertions(+), 1 deletion(-)
create mode 100644 rust/util/src/qobject/mod.rs
diff --git a/include/qobject/qobject.h b/include/qobject/qobject.h
index 02f4c6a6eb2..f5bbcf243f7 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 defined here).
+ */
struct QObjectBase_ {
QType type;
size_t refcnt;
diff --git a/rust/util/src/lib.rs b/rust/util/src/lib.rs
index 436c67e139e..4793c23f4b0 100644
--- a/rust/util/src/lib.rs
+++ b/rust/util/src/lib.rs
@@ -10,6 +10,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..210078ea23e
--- /dev/null
+++ b/rust/util/src/qobject/mod.rs
@@ -0,0 +1,333 @@
+//! `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. The caller must own a reference
+ /// to the `QObject`; if not, [`QObject::cloned_from_raw`] should
+ /// be used instead.
+ 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.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 02/19] subprojects: add serde
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 01/19] rust/qobject: add basic bindings Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-06-01 14:47 ` Zhao Liu
2026-05-26 17:56 ` [PATCH v3 03/19] rust/qobject: add Serialize implementation Paolo Bonzini
` (16 subsequent siblings)
18 siblings, 1 reply; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau, 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 0d24eb84e1c..fe491f3aba6 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 b6711fe77dd..96e93f56e22 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.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* Re: [PATCH v3 02/19] subprojects: add serde
2026-05-26 17:56 ` [PATCH v3 02/19] subprojects: add serde Paolo Bonzini
@ 2026-06-01 14:47 ` Zhao Liu
0 siblings, 0 replies; 23+ messages in thread
From: Zhao Liu @ 2026-06-01 14:47 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, qemu-rust, armbru, marcandre.lureau, zhao1.liu
> 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)
^^^^^^^^^^^^
Here we register dependency on build system...
> +serde_derive_dep = declare_dependency(
> + link_with: _serde_derive_rs,
> +)
> +
> +meson.override_dependency('serde_derive-1-rs', serde_derive_dep)
... but here it seems we register serde_derive-1-rs to host system, so
this may trigger error for cross-compilation.
IIUC, we should have:
meson.override_dependency('serde_derive-1-rs', serde_derive_dep, native: true)
Regards,
Zhao
^ permalink raw reply [flat|nested] 23+ messages in thread
* [PATCH v3 03/19] rust/qobject: add Serialize implementation
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 01/19] rust/qobject: add basic bindings Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 02/19] subprojects: add serde Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 04/19] rust/qobject: add Serializer (to_qobject) implementation Paolo Bonzini
` (15 subsequent siblings)
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau, 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 | 2 +-
rust/util/src/qobject/mod.rs | 4 +--
rust/util/src/qobject/serialize.rs | 57 ++++++++++++++++++++++++++++++
5 files changed, 62 insertions(+), 3 deletions(-)
create mode 100644 rust/util/src/qobject/serialize.rs
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index cbb3ca15f77..b9e8636b8bc 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -513,6 +513,7 @@ dependencies = [
"foreign",
"glib-sys",
"libc",
+ "serde",
"util-sys",
]
diff --git a/rust/util/Cargo.toml b/rust/util/Cargo.toml
index 2ad5940daca..0a0400278f3 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" }
util-sys = { path = "../bindings/util-sys" }
diff --git a/rust/util/meson.build b/rust/util/meson.build
index 6d175ae0b0f..a8d9978e1a9 100644
--- a/rust/util/meson.build
+++ b/rust/util/meson.build
@@ -1,7 +1,7 @@
_util_rs = static_library(
'util',
'src/lib.rs',
- dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs, util_sys_rs],
+ dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs, serde_rs, util_sys_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 210078ea23e..c42b75f3199 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},
@@ -246,7 +248,6 @@ fn drop(&mut self) {
}
}
-#[allow(unused)]
macro_rules! match_qobject {
(@internal ($qobj:expr) =>
$(() => $unit:expr,)?
@@ -329,5 +330,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..97250602cc3
--- /dev/null
+++ b/rust/util/src/qobject/serialize.rs
@@ -0,0 +1,57 @@
+//! `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) => {
+ let s = cstr.to_str().map_err(ser::Error::custom)?;
+ 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) };
+ let key = key.to_str().map_err(ser::Error::custom)?;
+ state.serialize_key(key)?;
+ 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.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 04/19] rust/qobject: add Serializer (to_qobject) implementation
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (2 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 03/19] rust/qobject: add Serialize implementation Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 05/19] rust/qobject: add Deserialize implementation Paolo Bonzini
` (14 subsequent siblings)
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau, 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/src/qobject/error.rs | 52 +++
rust/util/src/qobject/mod.rs | 4 +
rust/util/src/qobject/serializer.rs | 585 ++++++++++++++++++++++++++++
3 files changed, 641 insertions(+)
create mode 100644 rust/util/src/qobject/error.rs
create mode 100644 rust/util/src/qobject/serializer.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 c42b75f3199..c94b2028c1d 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.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 05/19] rust/qobject: add Deserialize implementation
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (3 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 04/19] rust/qobject: add Serializer (to_qobject) implementation Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 06/19] rust/qobject: add Deserializer (from_qobject) implementation Paolo Bonzini
` (13 subsequent siblings)
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau, 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/src/qobject/deserialize.rs | 134 +++++++++++++++++++++++++++
rust/util/src/qobject/mod.rs | 1 +
2 files changed, 135 insertions(+)
create mode 100644 rust/util/src/qobject/deserialize.rs
diff --git a/rust/util/src/qobject/deserialize.rs b/rust/util/src/qobject/deserialize.rs
new file mode 100644
index 00000000000..3ac1cabbae6
--- /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)
+ .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)
+ .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 c94b2028c1d..6920b9d2777 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.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 06/19] rust/qobject: add Deserializer (from_qobject) implementation
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (4 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 05/19] rust/qobject: add Deserialize implementation Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 07/19] rust/qobject: add from/to JSON bindings for QObject Paolo Bonzini
` (12 subsequent siblings)
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau, 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/src/qobject/deserializer.rs | 371 ++++++++++++++++++++++++++
rust/util/src/qobject/error.rs | 8 +-
rust/util/src/qobject/mod.rs | 2 +
4 files changed, 381 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 67ea84539a2..ebfa6c21883 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/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 6920b9d2777..76b167104d1 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.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 07/19] rust/qobject: add from/to JSON bindings for QObject
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (5 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 06/19] rust/qobject: add Deserializer (from_qobject) implementation Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 08/19] rust/qobject: add Display/Debug Paolo Bonzini
` (11 subsequent siblings)
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau
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/src/qobject/mod.rs | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
index 76b167104d1..d2dde9ba0f1 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;
@@ -114,6 +115,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.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 08/19] rust/qobject: add Display/Debug
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (6 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 07/19] rust/qobject: add from/to JSON bindings for QObject Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 09/19] scripts/qapi: reject empty enums Paolo Bonzini
` (10 subsequent siblings)
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau
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 d2dde9ba0f1..3e22dc8bb3f 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},
@@ -272,6 +273,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.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 09/19] scripts/qapi: reject empty enums
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (7 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 08/19] rust/qobject: add Display/Debug Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 10/19] scripts/qapi: enum with conditional first item must be optional Paolo Bonzini
` (9 subsequent siblings)
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau
Raise an error if an enum has no members. Such enums cannot be populated with
a valid value. Do not extend the same limitation to enum whose members are
all compiled out; they can still be used as optional members.
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
scripts/qapi/schema.py | 4 ++++
tests/qapi-schema/enum-empty.err | 2 ++
tests/qapi-schema/enum-empty.json | 2 ++
tests/qapi-schema/enum-empty.out | 0
tests/qapi-schema/qapi-schema-test.json | 3 ---
tests/qapi-schema/qapi-schema-test.out | 1 -
tests/qapi-schema/union-empty.err | 4 ++--
7 files changed, 10 insertions(+), 6 deletions(-)
create mode 100644 tests/qapi-schema/enum-empty.err
create mode 100644 tests/qapi-schema/enum-empty.json
create mode 100644 tests/qapi-schema/enum-empty.out
diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 8d88b40de2e..78c2a25fc9a 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -428,6 +428,10 @@ def __init__(
def check(self, schema: QAPISchema) -> None:
super().check(schema)
+ if not self.members:
+ raise QAPISemError(
+ self.info,
+ "enum '%s' must have at least one value" % self.name)
seen: Dict[str, QAPISchemaMember] = {}
for m in self.members:
m.check_clash(self.info, seen)
diff --git a/tests/qapi-schema/enum-empty.err b/tests/qapi-schema/enum-empty.err
new file mode 100644
index 00000000000..6070bf62cbd
--- /dev/null
+++ b/tests/qapi-schema/enum-empty.err
@@ -0,0 +1,2 @@
+enum-empty.json: In enum 'TestEmpty':
+enum-empty.json:2: enum 'TestEmpty' must have at least one value
diff --git a/tests/qapi-schema/enum-empty.json b/tests/qapi-schema/enum-empty.json
new file mode 100644
index 00000000000..3b3dfb2e3d8
--- /dev/null
+++ b/tests/qapi-schema/enum-empty.json
@@ -0,0 +1,2 @@
+# An enum must have at least one value
+{ 'enum': 'TestEmpty', 'data': [] }
diff --git a/tests/qapi-schema/enum-empty.out b/tests/qapi-schema/enum-empty.out
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/qapi-schema/qapi-schema-test.json b/tests/qapi-schema/qapi-schema-test.json
index 8ca977c49d2..195f1c4847b 100644
--- a/tests/qapi-schema/qapi-schema-test.json
+++ b/tests/qapi-schema/qapi-schema-test.json
@@ -23,9 +23,6 @@
'data': { 'enum1': 'EnumOne', # Intentional forward reference
'*enum2': 'EnumOne', 'enum3': 'EnumOne', '*enum4': 'EnumOne' } }
-# An empty enum, although unusual, is currently acceptable
-{ 'enum': 'MyEnum', 'data': [ ] }
-
# Likewise for an empty struct, including an empty base
{ 'struct': 'Empty1', 'data': { } }
{ 'struct': 'Empty2', 'base': 'Empty1', 'data': { } }
diff --git a/tests/qapi-schema/qapi-schema-test.out b/tests/qapi-schema/qapi-schema-test.out
index 4617eb4e98a..ddd8bf80d66 100644
--- a/tests/qapi-schema/qapi-schema-test.out
+++ b/tests/qapi-schema/qapi-schema-test.out
@@ -18,7 +18,6 @@ object NestedEnumsOne
member enum2: EnumOne optional=True
member enum3: EnumOne optional=False
member enum4: EnumOne optional=True
-enum MyEnum
object Empty1
object Empty2
base Empty1
diff --git a/tests/qapi-schema/union-empty.err b/tests/qapi-schema/union-empty.err
index d4284399621..c07dcf32a5a 100644
--- a/tests/qapi-schema/union-empty.err
+++ b/tests/qapi-schema/union-empty.err
@@ -1,2 +1,2 @@
-union-empty.json: In union 'Union':
-union-empty.json:4: union has no branches
+union-empty.json: In enum 'Empty':
+union-empty.json:2: enum 'Empty' must have at least one value
--
2.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 10/19] scripts/qapi: enum with conditional first item must be optional
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (8 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 09/19] scripts/qapi: reject empty enums Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 11/19] scripts/qapi: add QAPISchemaIfCond.rsgen() Paolo Bonzini
` (8 subsequent siblings)
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau
Prevent an all-zero struct from having different meanings with
different configurations or builds of QEMU.
This needs some changes to doc-good.json, which used unwittingly
such an enum.
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
scripts/qapi/schema.py | 8 ++++++++
tests/qapi-schema/doc-good.json | 16 ++++++++--------
tests/qapi-schema/doc-good.out | 12 ++++++------
tests/qapi-schema/doc-good.txt | 8 ++++----
tests/qapi-schema/enum-if-first-required.err | 2 ++
tests/qapi-schema/enum-if-first-required.json | 6 ++++++
tests/qapi-schema/enum-if-first-required.out | 0
tests/qapi-schema/meson.build | 1 +
8 files changed, 35 insertions(+), 18 deletions(-)
create mode 100644 tests/qapi-schema/enum-if-first-required.err
create mode 100644 tests/qapi-schema/enum-if-first-required.json
create mode 100644 tests/qapi-schema/enum-if-first-required.out
diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 78c2a25fc9a..a5a11298817 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -967,6 +967,14 @@ def check(self, schema: QAPISchema) -> None:
assert self.defined_in
self.type = schema.resolve_type(self._type_name, self.info,
self.describe)
+ if (not self.optional
+ and isinstance(self.type, QAPISchemaEnumType)
+ and self.type.members[0].ifcond.is_present()):
+ raise QAPISemError(
+ self.info,
+ "enum type '%s' of %s has a conditional first value"
+ " and must be optional"
+ % (self.type.name, self.describe(self.info)))
seen: Dict[str, QAPISchemaMember] = {}
for f in self.features:
f.check_clash(self.info, seen)
diff --git a/tests/qapi-schema/doc-good.json b/tests/qapi-schema/doc-good.json
index fac13425b72..76521ffe9e6 100644
--- a/tests/qapi-schema/doc-good.json
+++ b/tests/qapi-schema/doc-good.json
@@ -73,12 +73,12 @@
# @enum-feat: Also _one_ {and only}
# @enum-member-feat: a member feature
#
-# @two is undocumented
+# @zero is undocumented
##
{ 'enum': 'Enum',
- 'data': [ { 'name': 'one', 'if': 'IFONE',
- 'features': [ 'enum-member-feat' ] },
- 'two' ],
+ 'data': [ 'zero',
+ { 'name': 'one', 'if': 'IFONE',
+ 'features': [ 'enum-member-feat' ] } ],
'features': [ 'enum-feat' ],
'if': 'IFCOND' }
@@ -112,10 +112,10 @@
'if': 'IFSTR' } } }
##
-# @Variant2:
+# @Variant0:
#
##
-{ 'struct': 'Variant2', 'data': {} }
+{ 'struct': 'Variant0', 'data': {} }
##
# @Object:
@@ -128,8 +128,8 @@
'base': 'Base',
'discriminator': 'base1',
'data': { 'one': 'Variant1',
- 'two': { 'type': 'Variant2',
- 'if': { 'any': ['IFONE', 'IFTWO'] } } } }
+ 'zero': { 'type': 'Variant0',
+ 'if': { 'any': ['IFONE', 'IFTWO'] } } } }
##
# @Alternate:
diff --git a/tests/qapi-schema/doc-good.out b/tests/qapi-schema/doc-good.out
index 04a55072646..bf589061cbc 100644
--- a/tests/qapi-schema/doc-good.out
+++ b/tests/qapi-schema/doc-good.out
@@ -10,10 +10,10 @@ enum QType
member qbool
module doc-good.json
enum Enum
+ member zero
member one
if IFONE
feature enum-member-feat
- member two
if IFCOND
feature enum-feat
object Base
@@ -24,12 +24,12 @@ object Variant1
if IFSTR
feature member-feat
feature variant1-feat
-object Variant2
+object Variant0
object Object
base Base
tag base1
case one: Variant1
- case two: Variant2
+ case zero: Variant0
if {'any': ['IFONE', 'IFTWO']}
feature union-feat1
alternate Alternate
@@ -110,14 +110,14 @@ doc symbol=Enum
arg=one
The _one_ {and only}, description on the same line
- arg=two
+ arg=zero
feature=enum-feat
Also _one_ {and only}
feature=enum-member-feat
a member feature
section=Plain
-@two is undocumented
+@zero is undocumented
doc symbol=Base
body=
@@ -137,7 +137,7 @@ Another paragraph
a feature
feature=member-feat
a member feature
-doc symbol=Variant2
+doc symbol=Variant0
body=
doc symbol=Object
diff --git a/tests/qapi-schema/doc-good.txt b/tests/qapi-schema/doc-good.txt
index 74b73681d32..922a61dcf23 100644
--- a/tests/qapi-schema/doc-good.txt
+++ b/tests/qapi-schema/doc-good.txt
@@ -43,14 +43,14 @@ Enum Enum
Values:
* **one** -- The _one_ {and only}, description on the same line
- * **two** -- Not documented
+ * **zero** -- Not documented
Features:
* **enum-feat** -- Also _one_ {and only}
* **enum-member-feat** -- a member feature
- "two" is undocumented
+ "zero" is undocumented
Object Base
*Availability*: "IFALL1 and IFALL2"
@@ -75,7 +75,7 @@ Object Variant1
* **member-feat** -- a member feature
-Object Variant2
+Object Variant0
Object Object
@@ -84,7 +84,7 @@ Object Object
* When "base1" is "one": The members of "Variant1".
- * When "base1" is "two": The members of "Variant2".
+ * When "base1" is "zero": The members of "Variant0".
Features:
* **union-feat1** -- a feature
diff --git a/tests/qapi-schema/enum-if-first-required.err b/tests/qapi-schema/enum-if-first-required.err
new file mode 100644
index 00000000000..6d8bdcf2507
--- /dev/null
+++ b/tests/qapi-schema/enum-if-first-required.err
@@ -0,0 +1,2 @@
+enum-if-first-required.json: In struct 'TestStruct':
+enum-if-first-required.json:5: enum type 'TestEnum' of member 'field' has a conditional first value and must be optional
diff --git a/tests/qapi-schema/enum-if-first-required.json b/tests/qapi-schema/enum-if-first-required.json
new file mode 100644
index 00000000000..1769b5fdef9
--- /dev/null
+++ b/tests/qapi-schema/enum-if-first-required.json
@@ -0,0 +1,6 @@
+# Enum with conditional first value cannot be used in required fields
+{ 'enum': 'TestEnum',
+ 'data': [ { 'name': 'member1', 'if': 'CONFIG_FOO' },
+ 'member2' ] }
+{ 'struct': 'TestStruct',
+ 'data': { 'field': 'TestEnum' } }
diff --git a/tests/qapi-schema/enum-if-first-required.out b/tests/qapi-schema/enum-if-first-required.out
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/qapi-schema/meson.build b/tests/qapi-schema/meson.build
index debff633ac1..3b0c16a8b67 100644
--- a/tests/qapi-schema/meson.build
+++ b/tests/qapi-schema/meson.build
@@ -97,6 +97,7 @@ schemas = [
'enum-bad-prefix.json',
'enum-clash-member.json',
'enum-dict-member-unknown.json',
+ 'enum-if-first-required.json',
'enum-if-invalid.json',
'enum-int-member.json',
'enum-member-case.json',
--
2.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 11/19] scripts/qapi: add QAPISchemaIfCond.rsgen()
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (9 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 10/19] scripts/qapi: enum with conditional first item must be optional Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 12/19] scripts/qapi: add QAPISchemaType.is_predefined Paolo Bonzini
` (7 subsequent siblings)
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau
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 ++++
tests/qapi-schema/meson.build | 1 +
3 files changed, 24 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 a5a11298817..7bb9fb712cf 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/tests/qapi-schema/meson.build b/tests/qapi-schema/meson.build
index 3b0c16a8b67..40429a22774 100644
--- a/tests/qapi-schema/meson.build
+++ b/tests/qapi-schema/meson.build
@@ -97,6 +97,7 @@ schemas = [
'enum-bad-prefix.json',
'enum-clash-member.json',
'enum-dict-member-unknown.json',
+ 'enum-empty.json',
'enum-if-first-required.json',
'enum-if-invalid.json',
'enum-int-member.json',
--
2.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 12/19] scripts/qapi: add QAPISchemaType.is_predefined
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (10 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 11/19] scripts/qapi: add QAPISchemaIfCond.rsgen() Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 13/19] scripts/qapi: pull c_name and lstrip from camel_to_upper to caller Paolo Bonzini
` (6 subsequent siblings)
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau
It is impossible to call is_implicit on an enum type from the visitor, because
the QAPISchemaEnumType has already been exploded into its constituent 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 7bb9fb712cf..3459b8038e5 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -1255,6 +1255,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.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 13/19] scripts/qapi: pull c_name and lstrip from camel_to_upper to caller
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (11 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 12/19] scripts/qapi: add QAPISchemaType.is_predefined Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 14/19] scripts/qapi: allow passing multiple segments to mcgen Paolo Bonzini
` (5 subsequent siblings)
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau
Allow using camel_to_upper for other languages too.
In particular, the lstrip() is needed to avoid reserved C identifiers,
for example:
typedef enum __org_qemu_x_Enum {
__ORG_QEMU_X_ENUM___ORG_QEMU_X_VALUE,
__ORG_QEMU_X_ENUM__MAX,
} __org_qemu_x_Enum;
Insulate Rust from this, since underscores have a different meaning
in Rust (though only for the sake of warnings).
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.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 14/19] scripts/qapi: allow passing multiple segments to mcgen
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (12 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 13/19] scripts/qapi: pull c_name and lstrip from camel_to_upper to caller Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 15/19] scripts/qapi: generate high-level Rust bindings Paolo Bonzini
` (4 subsequent siblings)
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau
Make mcgen more flexible to allow for easier removal of empty lines.
For example, in Rust you can have #[cfg(X)] lines that would often
be empty; let mcgen remove the whole segment or at least the empty
spaces and lines at the end of it.
Separate invocations of mcgen still preserve the empty lines
between them.
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
scripts/qapi/common.py | 22 ++++++++++++++++++----
1 file changed, 18 insertions(+), 4 deletions(-)
diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
index c75396a01b5..44229c2e366 100644
--- a/scripts/qapi/common.py
+++ b/scripts/qapi/common.py
@@ -172,10 +172,24 @@ def cgen(code: str, **kwds: object) -> str:
return re.sub(re.escape(EATSPACE) + r' *', '', raw)
-def mcgen(code: str, **kwds: object) -> str:
- if code[0] == '\n':
- code = code[1:]
- return cgen(code, **kwds)
+def mcgen(*code: 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)
def c_fname(filename: str) -> str:
--
2.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 15/19] scripts/qapi: generate high-level Rust bindings
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (13 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 14/19] scripts/qapi: allow passing multiple segments to mcgen Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 21:03 ` Marc-André Lureau
2026-06-04 10:11 ` Zhao Liu
2026-05-26 17:56 ` [PATCH v3 16/19] scripts/rustc_args: add --no-strict-cfg Paolo Bonzini
` (3 subsequent siblings)
18 siblings, 2 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau
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).
Given the experimental nature of Rust, and the incompleteness of the
backend (it lacks commands and events), QAPIRsBackend is not modular
and is not built together with the C and trace-event files. It can
be used by specifying "-B qapi.backend.QAPIRsBackend" on the qapi-gen
command line.
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 leaf types]
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
meson.build | 4 +-
scripts/qapi/backend.py | 25 +++
scripts/qapi/common.py | 49 ++++++
scripts/qapi/rs.py | 50 ++++++
scripts/qapi/rs_types.py | 372 +++++++++++++++++++++++++++++++++++++++
scripts/qapi/schema.py | 59 +++++--
6 files changed, 540 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 eb074918193..bda5180c436 100644
--- a/meson.build
+++ b/meson.build
@@ -3485,11 +3485,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 44229c2e366..67a4b11e4f3 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,48 @@ 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
+
+ return name
+
+
+def to_camel_case(value: str) -> str:
+ result = ''
+ for p in re.split(r'[-_]+', value):
+ if not p:
+ pass
+ elif p[0].isalpha():
+ result += p[0].upper() + p[1:]
+ elif result and result[-1].isalpha():
+ result += p
+ else:
+ # digit_digit, or digit at start of value
+ result += '_' + p
+ return result
+
+
class Indentation:
"""
Indentation level management.
diff --git a/scripts/qapi/rs.py b/scripts/qapi/rs.py
new file mode 100644
index 00000000000..ad32b527cd6
--- /dev/null
+++ b/scripts/qapi/rs.py
@@ -0,0 +1,50 @@
+# 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 sys
+
+from .common import mcgen
+from .gen import QAPIGen
+from .schema import QAPISchemaVisitor
+
+
+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
+
+//!
+//! %(blurb)s
+//!
+//! %(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)
diff --git a/scripts/qapi/rs_types.py b/scripts/qapi/rs_types.py
new file mode 100644
index 00000000000..874ebdbfa41
--- /dev/null
+++ b/scripts/qapi/rs_types.py
@@ -0,0 +1,372 @@
+# 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,
+ mcgen,
+ rs_name,
+ to_camel_case,
+)
+from .rs import QAPISchemaRsVisitor
+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.append(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 '\n'.join(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 not members[0].ifcond.is_present():
+ 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 3459b8038e5..3cfe9bbc21d 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())
@@ -453,6 +463,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]
@@ -502,6 +515,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'
@@ -634,6 +650,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'
@@ -715,6 +734,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(
@@ -1246,9 +1268,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
@@ -1267,21 +1290,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.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* Re: [PATCH v3 15/19] scripts/qapi: generate high-level Rust bindings
2026-05-26 17:56 ` [PATCH v3 15/19] scripts/qapi: generate high-level Rust bindings Paolo Bonzini
@ 2026-05-26 21:03 ` Marc-André Lureau
2026-06-04 10:11 ` Zhao Liu
1 sibling, 0 replies; 23+ messages in thread
From: Marc-André Lureau @ 2026-05-26 21:03 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, qemu-rust, armbru
Hi Paolo
On Tue, May 26, 2026 at 9:58 PM Paolo Bonzini <pbonzini@redhat.com> wrote:
> +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]
> +
> +
Actually dead code now.
^ permalink raw reply [flat|nested] 23+ messages in thread* Re: [PATCH v3 15/19] scripts/qapi: generate high-level Rust bindings
2026-05-26 17:56 ` [PATCH v3 15/19] scripts/qapi: generate high-level Rust bindings Paolo Bonzini
2026-05-26 21:03 ` Marc-André Lureau
@ 2026-06-04 10:11 ` Zhao Liu
1 sibling, 0 replies; 23+ messages in thread
From: Zhao Liu @ 2026-06-04 10:11 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, qemu-rust, armbru
On Tue, May 26, 2026 at 07:56:14PM +0200, Paolo Bonzini wrote:
> Date: Tue, 26 May 2026 19:56:14 +0200
> From: Paolo Bonzini <pbonzini@redhat.com>
> Subject: [PATCH v3 15/19] scripts/qapi: generate high-level Rust bindings
> X-Mailer: git-send-email 2.54.0
>
> 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).
>
> Given the experimental nature of Rust, and the incompleteness of the
> backend (it lacks commands and events), QAPIRsBackend is not modular
> and is not built together with the C and trace-event files. It can
> be used by specifying "-B qapi.backend.QAPIRsBackend" on the qapi-gen
> command line.
>
> 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 leaf types]
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
> meson.build | 4 +-
> scripts/qapi/backend.py | 25 +++
> scripts/qapi/common.py | 49 ++++++
> scripts/qapi/rs.py | 50 ++++++
> scripts/qapi/rs_types.py | 372 +++++++++++++++++++++++++++++++++++++++
> scripts/qapi/schema.py | 59 +++++--
> 6 files changed, 540 insertions(+), 19 deletions(-)
> create mode 100644 scripts/qapi/rs.py
> create mode 100644 scripts/qapi/rs_types.py
Revisiting the v2 discussion, I think maybe it's still possible to use
visit_module to identify predefined cases (in schema builtn module)
instead of is_predefined?
(I guess that's what Markus wanted in v2, hopefully... and apologies if
this is a bit too verbose. I'm learning QAPI code so I want to explain
my undersatnding in as much details as possible (to ensure "IIUC") :). )
> +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;
> +'''))
IIUC, though there's only a single QAPIGenRs, but we can just use
visit_module to cache a flag and don't need to skip something like C
did:
def visit_module(self, name: str) -> None:
self._in_builtin_module = QAPISchemaModule.is_builtin_module(name)
builtin module is created by schema and is maintained in _module_dict
(in QAPISchema.__init__()). When QAPISchema iterates through _module_dict,
it will always visit he builtin module and once it enters builtin module,
visit_module() will be called (QAPISchemaModule.visit()).
With _in_builtin_module flag, then we just need to "proof" it has the
same effect as is_predefined() method.
> + 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 we use self._in_builtin_module, this assertion can go away.)
> + if self._schema.is_predefined(name) or name.startswith('q_'):
This is called by QAPISchemaObjectType not QAPISchemaBuiltinType, and
QType is QPAISchemaEnumType.
Only q_empty could hit is_predefined(), and q_empty has been included
into builtin module (in QAPISchema._def_predefineds()).
And a good thing is _def_predefineds() doesn't add other object type
into builtin module! So "self._schema.is_predefined(name)" is same as
"self._in_builtin_module"!
> + 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 we use self._in_builtin_module, this assertion can go away.
> + if self._schema.is_predefined(name):
> + return
Similiarly, only QType can hit is_predefined(), and builtin module has
only one enum: QType, therefore it's safe to use self._in_builtin_module
here, too.
> + 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:
I think maybe we also should ship builtin for alternatives type.
although for now there's no predefined alternatives...
> + self._gen.add(gen_rs_alternate(name, ifcond, alternatives))
> +
> +
... Overall, it seems checking builtin module is more robust and
porvides forward compatibility, and we don't need to add special case to
check.
Regards,
Zhao
^ permalink raw reply [flat|nested] 23+ messages in thread
* [PATCH v3 16/19] scripts/rustc_args: add --no-strict-cfg
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (14 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 15/19] scripts/qapi: generate high-level Rust bindings Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 17/19] rust/util: build QAPI types Paolo Bonzini
` (2 subsequent siblings)
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau
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.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 17/19] rust/util: build QAPI types
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (15 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 16/19] scripts/rustc_args: add --no-strict-cfg Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 18/19] scripts/qapi: add serde attributes Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 19/19] rust/tests: QAPI integration tests Paolo Bonzini
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau
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 b9e8636b8bc..c73f037d689 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -514,6 +514,7 @@ dependencies = [
"glib-sys",
"libc",
"serde",
+ "serde_derive",
"util-sys",
]
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index fe491f3aba6..15296ac8636 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 0a0400278f3..069ab167e87 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" }
util-sys = { path = "../bindings/util-sys" }
diff --git a/rust/util/meson.build b/rust/util/meson.build
index a8d9978e1a9..3edd8245bfc 100644
--- a/rust/util/meson.build
+++ b/rust/util/meson.build
@@ -18,3 +18,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.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 18/19] scripts/qapi: add serde attributes
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (16 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 17/19] rust/util: build QAPI types Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
2026-05-26 17:56 ` [PATCH v3 19/19] rust/tests: QAPI integration tests Paolo Bonzini
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau
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 874ebdbfa41..41ed83c381d 100644
--- a/scripts/qapi/rs_types.py
+++ b/scripts/qapi/rs_types.py
@@ -34,6 +34,7 @@
objects_seen = set()
+SERDE_SKIP_NONE = '#[serde(skip_serializing_if = "Option::is_none")]'
def gen_rs_variants_to_tag(name: str,
@@ -80,10 +81,12 @@ 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.append(mcgen('''
%(cfg)s''', '''
+%(skip_if)s''', '''
pub %(rs_name)s: %(rs_type)s,
''',
cfg=memb.ifcond.rsgen(),
+ skip_if=SERDE_SKIP_NONE if memb.optional else '',
rs_type=typ,
rs_name=rs_name(camel_to_lower(memb.name))))
return '\n'.join(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())
@@ -242,10 +258,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 += '''}
'''
@@ -279,7 +297,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(),
@@ -329,6 +348,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.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread* [PATCH v3 19/19] rust/tests: QAPI integration tests
2026-05-26 17:55 [PATCH v3 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (17 preceding siblings ...)
2026-05-26 17:56 ` [PATCH v3 18/19] scripts/qapi: add serde attributes Paolo Bonzini
@ 2026-05-26 17:56 ` Paolo Bonzini
18 siblings, 0 replies; 23+ messages in thread
From: Paolo Bonzini @ 2026-05-26 17:56 UTC (permalink / raw)
To: qemu-devel; +Cc: qemu-rust, armbru, marcandre.lureau
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.54.0
^ permalink raw reply related [flat|nested] 23+ messages in thread