* [PATCH 00/19] rust: QObject and QAPI bindings
@ 2025-10-10 15:09 Paolo Bonzini
2025-10-10 15:09 ` [PATCH 01/19] util: add ensure macro Paolo Bonzini
` (20 more replies)
0 siblings, 21 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:09 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
This adds two related parts of the Rust bindings:
- QAPI code generator that creates Rust structs from the JSON
description. The structs are *not* ABI compatible with the
C ones, instead they use native Rust data types.
- QObject bindings and (de)serialization support, which can be used to
convert QObjects to and from QAPI structs.
Unfortunately Rust code is not able to use visitors, other than by
creating an intermediate QObject. This is because of the different
architecture of serde vs. QAPI visitors, and because visitor's
dual-purpose functions, where the same function is used by both input and
output visitors, rely heavily on the structs using the same representation
as the visitor arguments (for example NUL-terminated strings).
The serde format implementation was co-authored by me and Marc-André.
Marc-André did all the bug fixing and integration testing.
As an example of how this would be used, the marshaling functions for
QMP commands would look like this:
fn qmp_marshal_query_stats(args: *mut QDict,
retp: *mut *mut QObject, errp: *mut *mut Error)
{
let qobj = unsafe { QObject::cloned_from_raw(args.cast()) };
let result = from_qobject::<StatsFilter>(qobj)
.map_err(anyhow::Error::from)
.and_then(qmp_query_stats)
.and_then(|ret| to_qobject::<Vec<StatsResult>>(ret).map_err(anyhow::Error::from));
match qmp_marshal_query_stats_rs(qobj) {
Ok(ret) => unsafe { *retp = ret.into_raw(); },
Err(e) => unsafe { crate::Error::from(e).propagate(errp) },
}
}
As a small extra, patches 1 and 2 rework a bit the error implementation
so that it is possible to convert any Rust error into a QEMU one. This
is because we noticed that we had to add several From<> implementations
to convert e.g. NulError or serde errors into util::Error.
The price is that it's a bit harder to convert *strings* into errors;
therefore, the first patch adds a macro wrapper for
"if !cond { return Err(...) }", where the dots build an error from a
formatted string.
Paolo
Marc-André Lureau (8):
rust/qobject: add Display/Debug
scripts/qapi: add QAPISchemaIfCond.rsgen()
scripts/qapi: generate high-level Rust bindings
scripts/qapi: add serde attributes
scripts/qapi: strip trailing whitespaces
scripts/rustc_args: add --no-strict-cfg
rust/util: build QAPI types
rust/tests: QAPI integration tests
Paolo Bonzini (11):
util: add ensure macro
rust/util: use anyhow's native chaining capabilities
rust: do not add qemuutil to Rust crates
rust/qobject: add basic bindings
subprojects: add serde
rust/qobject: add Serialize implementation
rust/qobject: add Serializer (to_qobject) implementation
rust/qobject: add Deserialize implementation
rust/qobject: add Deserializer (from_qobject) implementation
rust/util: replace Error::err_or_unit/err_or_else with
Error::with_errp
rust/qobject: add from/to JSON bindings for QObject
docs/devel/rust.rst | 1 +
meson.build | 4 +-
rust/util/wrapper.h | 8 +
qapi/meson.build | 6 +
rust/Cargo.lock | 2 +
rust/Cargo.toml | 2 +
rust/chardev/meson.build | 2 +-
rust/hw/timer/hpet/src/device.rs | 21 +-
rust/hw/timer/hpet/src/fw_cfg.rs | 7 +-
rust/meson.build | 4 +
rust/tests/meson.build | 25 +-
rust/tests/tests/integration.rs | 2 +
rust/tests/tests/qapi.rs | 444 +++++++++++++
rust/util/Cargo.toml | 2 +
rust/util/meson.build | 31 +-
rust/util/src/error.rs | 222 +++----
rust/util/src/lib.rs | 2 +
rust/util/src/qobject/deserialize.rs | 134 ++++
rust/util/src/qobject/deserializer.rs | 371 +++++++++++
rust/util/src/qobject/error.rs | 58 ++
rust/util/src/qobject/mod.rs | 369 +++++++++++
rust/util/src/qobject/serialize.rs | 59 ++
rust/util/src/qobject/serializer.rs | 585 ++++++++++++++++++
scripts/archive-source.sh | 3 +
scripts/make-release | 2 +-
scripts/qapi/backend.py | 27 +-
scripts/qapi/common.py | 16 +
scripts/qapi/gen.py | 6 +-
scripts/qapi/main.py | 4 +-
scripts/qapi/rs.py | 181 ++++++
scripts/qapi/rs_types.py | 387 ++++++++++++
scripts/qapi/schema.py | 4 +
scripts/rust/rustc_args.py | 16 +-
subprojects/.gitignore | 3 +
.../packagefiles/serde-1-rs/meson.build | 36 ++
.../packagefiles/serde-1.0.226-include.patch | 16 +
.../packagefiles/serde_core-1-rs/meson.build | 25 +
.../serde_core-1.0.226-include.patch | 15 +
.../serde_derive-1-rs/meson.build | 35 ++
.../serde_derive-1.0.226-include.patch | 11 +
subprojects/serde-1-rs.wrap | 11 +
subprojects/serde_core-1-rs.wrap | 11 +
subprojects/serde_derive-1-rs.wrap | 11 +
43 files changed, 3042 insertions(+), 139 deletions(-)
create mode 100644 rust/tests/tests/integration.rs
create mode 100644 rust/tests/tests/qapi.rs
create mode 100644 rust/util/src/qobject/deserialize.rs
create mode 100644 rust/util/src/qobject/deserializer.rs
create mode 100644 rust/util/src/qobject/error.rs
create mode 100644 rust/util/src/qobject/mod.rs
create mode 100644 rust/util/src/qobject/serialize.rs
create mode 100644 rust/util/src/qobject/serializer.rs
create mode 100644 scripts/qapi/rs.py
create mode 100644 scripts/qapi/rs_types.py
create mode 100644 subprojects/packagefiles/serde-1-rs/meson.build
create mode 100644 subprojects/packagefiles/serde-1.0.226-include.patch
create mode 100644 subprojects/packagefiles/serde_core-1-rs/meson.build
create mode 100644 subprojects/packagefiles/serde_core-1.0.226-include.patch
create mode 100644 subprojects/packagefiles/serde_derive-1-rs/meson.build
create mode 100644 subprojects/packagefiles/serde_derive-1.0.226-include.patch
create mode 100644 subprojects/serde-1-rs.wrap
create mode 100644 subprojects/serde_core-1-rs.wrap
create mode 100644 subprojects/serde_derive-1-rs.wrap
--
2.51.0
^ permalink raw reply [flat|nested] 48+ messages in thread
* [PATCH 01/19] util: add ensure macro
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
@ 2025-10-10 15:09 ` Paolo Bonzini
2025-10-10 15:09 ` [PATCH 02/19] rust/util: use anyhow's native chaining capabilities Paolo Bonzini
` (19 subsequent siblings)
20 siblings, 0 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:09 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
The macro is similar to anyhow::ensure but uses QEMU's variation
on anyhow::Error. It can be used to easily check a condition
and format an error message.
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/hw/timer/hpet/src/device.rs | 21 ++++++-----
rust/hw/timer/hpet/src/fw_cfg.rs | 7 ++--
rust/util/src/error.rs | 65 ++++++++++++++++++++++++++++++++
3 files changed, 80 insertions(+), 13 deletions(-)
diff --git a/rust/hw/timer/hpet/src/device.rs b/rust/hw/timer/hpet/src/device.rs
index 86638c07666..4218372a2a1 100644
--- a/rust/hw/timer/hpet/src/device.rs
+++ b/rust/hw/timer/hpet/src/device.rs
@@ -25,7 +25,10 @@
bindings::{address_space_memory, address_space_stl_le, hwaddr},
MemoryRegion, MemoryRegionOps, MemoryRegionOpsBuilder, MEMTXATTRS_UNSPECIFIED,
};
-use util::timer::{Timer, CLOCK_VIRTUAL, NANOSECONDS_PER_SECOND};
+use util::{
+ ensure,
+ timer::{Timer, CLOCK_VIRTUAL, NANOSECONDS_PER_SECOND},
+};
use crate::fw_cfg::HPETFwConfig;
@@ -728,14 +731,14 @@ fn post_init(&self) {
}
fn realize(&self) -> util::Result<()> {
- if self.num_timers < HPET_MIN_TIMERS || self.num_timers > HPET_MAX_TIMERS {
- Err(format!(
- "hpet.num_timers must be between {HPET_MIN_TIMERS} and {HPET_MAX_TIMERS}"
- ))?;
- }
- if self.int_route_cap == 0 {
- Err("hpet.hpet-intcap property not initialized")?;
- }
+ ensure!(
+ (HPET_MIN_TIMERS..=HPET_MAX_TIMERS).contains(&self.num_timers),
+ "hpet.num_timers must be between {HPET_MIN_TIMERS} and {HPET_MAX_TIMERS}"
+ );
+ ensure!(
+ self.int_route_cap != 0,
+ "hpet.hpet-intcap property not initialized"
+ );
self.hpet_id.set(HPETFwConfig::assign_hpet_id()?);
diff --git a/rust/hw/timer/hpet/src/fw_cfg.rs b/rust/hw/timer/hpet/src/fw_cfg.rs
index e569b57b93b..27b3b2495a0 100644
--- a/rust/hw/timer/hpet/src/fw_cfg.rs
+++ b/rust/hw/timer/hpet/src/fw_cfg.rs
@@ -5,6 +5,7 @@
use std::ptr::addr_of_mut;
use common::Zeroable;
+use util::{self, ensure};
/// Each `HPETState` represents a Event Timer Block. The v1 spec supports
/// up to 8 blocks. QEMU only uses 1 block (in PC machine).
@@ -36,7 +37,7 @@ unsafe impl Zeroable for HPETFwConfig {}
};
impl HPETFwConfig {
- pub(crate) fn assign_hpet_id() -> Result<usize, &'static str> {
+ pub(crate) fn assign_hpet_id() -> util::Result<usize> {
assert!(bql::is_locked());
// SAFETY: all accesses go through these methods, which guarantee
// that the accesses are protected by the BQL.
@@ -47,9 +48,7 @@ pub(crate) fn assign_hpet_id() -> Result<usize, &'static str> {
fw_cfg.count = 0;
}
- if fw_cfg.count == 8 {
- Err("Only 8 instances of HPET are allowed")?;
- }
+ ensure!(fw_cfg.count != 8, "Only 8 instances of HPET are allowed");
let id: usize = fw_cfg.count.into();
fw_cfg.count += 1;
diff --git a/rust/util/src/error.rs b/rust/util/src/error.rs
index bfa5a8685bc..20b8e7d5af5 100644
--- a/rust/util/src/error.rs
+++ b/rust/util/src/error.rs
@@ -86,6 +86,19 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
}
}
+impl From<Cow<'static, str>> for Error {
+ #[track_caller]
+ fn from(msg: Cow<'static, str>) -> Self {
+ let location = panic::Location::caller();
+ Error {
+ msg: Some(msg),
+ cause: None,
+ file: location.file(),
+ line: location.line(),
+ }
+ }
+}
+
impl From<String> for Error {
#[track_caller]
fn from(msg: String) -> Self {
@@ -126,6 +139,17 @@ fn from(error: anyhow::Error) -> Self {
}
impl Error {
+ #[track_caller]
+ #[doc(hidden)]
+ pub fn format(args: fmt::Arguments) -> Self {
+ if let Some(msg) = args.as_str() {
+ Self::from(msg)
+ } else {
+ let msg = std::fmt::format(args);
+ Self::from(msg)
+ }
+ }
+
/// Create a new error, prepending `msg` to the
/// description of `cause`
#[track_caller]
@@ -311,6 +335,47 @@ unsafe fn cloned_from_foreign(c_error: *const bindings::Error) -> Self {
}
}
+/// Ensure that a condition is true, returning an error if it is false.
+///
+/// This macro is similar to [`anyhow::ensure`] but returns a QEMU [`Result`].
+/// If the condition evaluates to `false`, the macro returns early with an error
+/// constructed from the provided message.
+///
+/// # Examples
+///
+/// ```
+/// # use util::{ensure, Result};
+/// # fn check_positive(x: i32) -> Result<()> {
+/// ensure!(x > 0, "value must be positive");
+/// # Ok(())
+/// # }
+/// ```
+///
+/// ```
+/// # use util::{ensure, Result};
+/// # const MIN: i32 = 123;
+/// # const MAX: i32 = 456;
+/// # fn check_range(x: i32) -> Result<()> {
+/// ensure!(x >= MIN && x <= MAX, "{} not between {} and {}", x, MIN, MAX);
+/// # Ok(())
+/// # }
+/// ```
+#[macro_export]
+macro_rules! ensure {
+ ($cond:expr, $fmt:literal, $($arg:tt)*) => {
+ if !$cond {
+ let e = $crate::Error::format(format_args!($fmt, $($arg)*));
+ return $crate::Result::Err(e);
+ }
+ };
+ ($cond:expr, $err:expr $(,)?) => {
+ if !$cond {
+ let s = ::std::borrow::Cow::<'static, str>::from($err);
+ return $crate::Result::Err(s.into());
+ }
+ };
+}
+
#[cfg(test)]
mod tests {
use std::ffi::CStr;
--
2.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 02/19] rust/util: use anyhow's native chaining capabilities
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
2025-10-10 15:09 ` [PATCH 01/19] util: add ensure macro Paolo Bonzini
@ 2025-10-10 15:09 ` Paolo Bonzini
2025-10-10 15:09 ` [PATCH 03/19] rust: do not add qemuutil to Rust crates Paolo Bonzini
` (18 subsequent siblings)
20 siblings, 0 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:09 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
This simplifies conversions, making it possible to convert any error
into a QEMU util::Error with ".into()" (and therefore with "?").
The cost is having a separate constructor for when the error is a simple
string, but that is made easier by the ensure! macro. If necessary,
another macro similar to "anyhow!" can be returned, but for now there
is no need for that.
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/util/src/error.rs | 139 ++++++++++++++---------------------------
1 file changed, 46 insertions(+), 93 deletions(-)
diff --git a/rust/util/src/error.rs b/rust/util/src/error.rs
index 20b8e7d5af5..bdbf2634170 100644
--- a/rust/util/src/error.rs
+++ b/rust/util/src/error.rs
@@ -38,6 +38,7 @@
borrow::Cow,
ffi::{c_char, c_int, c_void, CStr},
fmt::{self, Display},
+ ops::Deref,
panic, ptr,
};
@@ -49,104 +50,65 @@
#[derive(Debug)]
pub struct Error {
- msg: Option<Cow<'static, str>>,
- /// Appends the print string of the error to the msg if not None
- cause: Option<anyhow::Error>,
+ cause: anyhow::Error,
file: &'static str,
line: u32,
}
-impl std::error::Error for Error {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- self.cause.as_ref().map(AsRef::as_ref)
- }
+impl Deref for Error {
+ type Target = anyhow::Error;
- #[allow(deprecated)]
- fn description(&self) -> &str {
- self.msg
- .as_deref()
- .or_else(|| self.cause.as_deref().map(std::error::Error::description))
- .expect("no message nor cause?")
+ fn deref(&self) -> &Self::Target {
+ &self.cause
}
}
impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- let mut prefix = "";
- if let Some(ref msg) = self.msg {
- write!(f, "{msg}")?;
- prefix = ": ";
- }
- if let Some(ref cause) = self.cause {
- write!(f, "{prefix}{cause}")?;
- } else if prefix.is_empty() {
- panic!("no message nor cause?");
- }
- Ok(())
+ Display::fmt(&format_args!("{:#}", self.cause), f)
}
}
-impl From<Cow<'static, str>> for Error {
+impl<E> From<E> for Error where anyhow::Error: From<E> {
#[track_caller]
- fn from(msg: Cow<'static, str>) -> Self {
- let location = panic::Location::caller();
- Error {
- msg: Some(msg),
- cause: None,
- file: location.file(),
- line: location.line(),
- }
- }
-}
-
-impl From<String> for Error {
- #[track_caller]
- fn from(msg: String) -> Self {
- let location = panic::Location::caller();
- Error {
- msg: Some(Cow::Owned(msg)),
- cause: None,
- file: location.file(),
- line: location.line(),
- }
- }
-}
-
-impl From<&'static str> for Error {
- #[track_caller]
- fn from(msg: &'static str) -> Self {
- let location = panic::Location::caller();
- Error {
- msg: Some(Cow::Borrowed(msg)),
- cause: None,
- file: location.file(),
- line: location.line(),
- }
- }
-}
-
-impl From<anyhow::Error> for Error {
- #[track_caller]
- fn from(error: anyhow::Error) -> Self {
- let location = panic::Location::caller();
- Error {
- msg: None,
- cause: Some(error),
- file: location.file(),
- line: location.line(),
- }
+ fn from(src: E) -> Self {
+ Self::new(anyhow::Error::from(src))
}
}
impl Error {
+ /// Create a new error from an [`anyhow::Error`].
+ ///
+ /// This wraps the error with QEMU's location tracking information.
+ /// Most code should use the `?` operator instead of calling this directly.
+ #[track_caller]
+ pub fn new(cause: anyhow::Error) -> Self {
+ let location = panic::Location::caller();
+ Error {
+ cause,
+ file: location.file(),
+ line: location.line(),
+ }
+ }
+
+ /// Create a new error from a string message.
+ ///
+ /// This is a convenience wrapper around [`Error::new`] for simple string errors.
+ /// Most code should use the [`ensure!`](crate::ensure) macro instead of calling
+ /// this directly.
+ #[track_caller]
+ pub fn msg(src: impl Into<Cow<'static, str>>) -> Self {
+ Self::new(anyhow::Error::msg(src.into()))
+ }
+
#[track_caller]
#[doc(hidden)]
pub fn format(args: fmt::Arguments) -> Self {
if let Some(msg) = args.as_str() {
- Self::from(msg)
+ Self::new(anyhow::Error::msg(msg))
} else {
let msg = std::fmt::format(args);
- Self::from(msg)
+ Self::new(anyhow::Error::msg(msg))
}
}
@@ -155,9 +117,10 @@ pub fn format(args: fmt::Arguments) -> Self {
#[track_caller]
pub fn with_error(msg: impl Into<Cow<'static, str>>, cause: impl Into<anyhow::Error>) -> Self {
let location = panic::Location::caller();
+ let msg: Cow<'static, str> = msg.into();
+ let cause: anyhow::Error = cause.into();
Error {
- msg: Some(msg.into()),
- cause: Some(cause.into()),
+ cause: cause.context(msg),
file: location.file(),
line: location.line(),
}
@@ -326,8 +289,7 @@ unsafe fn cloned_from_foreign(c_error: *const bindings::Error) -> Self {
};
Error {
- msg: FromForeign::cloned_from_foreign(error.msg),
- cause: None,
+ cause: anyhow::Error::msg(String::cloned_from_foreign(error.msg)),
file: file.unwrap(),
line: error.line as u32,
}
@@ -370,8 +332,8 @@ macro_rules! ensure {
};
($cond:expr, $err:expr $(,)?) => {
if !$cond {
- let s = ::std::borrow::Cow::<'static, str>::from($err);
- return $crate::Result::Err(s.into());
+ let e = $crate::Error::msg($err);
+ return $crate::Result::Err(e);
}
};
}
@@ -410,19 +372,10 @@ unsafe fn error_get_pretty<'a>(local_err: *mut bindings::Error) -> &'a CStr {
unsafe { CStr::from_ptr(bindings::error_get_pretty(local_err)) }
}
- #[test]
- #[allow(deprecated)]
- fn test_description() {
- use std::error::Error;
-
- assert_eq!(super::Error::from("msg").description(), "msg");
- assert_eq!(super::Error::from("msg".to_owned()).description(), "msg");
- }
-
#[test]
fn test_display() {
- assert_eq!(&*format!("{}", Error::from("msg")), "msg");
- assert_eq!(&*format!("{}", Error::from("msg".to_owned())), "msg");
+ assert_eq!(&*format!("{}", Error::msg("msg")), "msg");
+ assert_eq!(&*format!("{}", Error::msg("msg".to_owned())), "msg");
assert_eq!(&*format!("{}", Error::from(anyhow!("msg"))), "msg");
assert_eq!(
@@ -439,7 +392,7 @@ fn test_bool_or_propagate() {
assert!(Error::bool_or_propagate(Ok(()), &mut local_err));
assert_eq!(local_err, ptr::null_mut());
- let my_err = Error::from("msg");
+ let my_err = Error::msg("msg");
assert!(!Error::bool_or_propagate(Err(my_err), &mut local_err));
assert_ne!(local_err, ptr::null_mut());
assert_eq!(error_get_pretty(local_err), c"msg");
@@ -456,7 +409,7 @@ fn test_ptr_or_propagate() {
assert_eq!(String::from_foreign(ret), "abc");
assert_eq!(local_err, ptr::null_mut());
- let my_err = Error::from("msg");
+ let my_err = Error::msg("msg");
assert_eq!(
Error::ptr_or_propagate(Err::<String, _>(my_err), &mut local_err),
ptr::null_mut()
--
2.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 03/19] rust: do not add qemuutil to Rust crates
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
2025-10-10 15:09 ` [PATCH 01/19] util: add ensure macro Paolo Bonzini
2025-10-10 15:09 ` [PATCH 02/19] rust/util: use anyhow's native chaining capabilities Paolo Bonzini
@ 2025-10-10 15:09 ` Paolo Bonzini
2025-12-05 8:30 ` Markus Armbruster
2025-10-10 15:09 ` [PATCH 04/19] rust/qobject: add basic bindings Paolo Bonzini
` (17 subsequent siblings)
20 siblings, 1 reply; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:09 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
This fails due to https://github.com/mesonbuild/meson/pull/15076.
The config-host.h file from the qemuutil dependency ends up on the
rustc command line for targets that do not use structured sources.
It will be reverted once Meson 1.9.2 is released, or replaced with
an update of the minimum supported version of Meson if 1.9.2 is
released sooner.
Reported-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/chardev/meson.build | 2 +-
rust/util/meson.build | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/rust/chardev/meson.build b/rust/chardev/meson.build
index d365d8dd0f4..36ada7c4546 100644
--- a/rust/chardev/meson.build
+++ b/rust/chardev/meson.build
@@ -39,4 +39,4 @@ _chardev_rs = static_library(
dependencies: [glib_sys_rs, common_rs, qemu_macros],
)
-chardev_rs = declare_dependency(link_with: [_chardev_rs], dependencies: [chardev, qemuutil])
+chardev_rs = declare_dependency(link_with: [_chardev_rs], dependencies: [chardev])
diff --git a/rust/util/meson.build b/rust/util/meson.build
index b0b75e93ff6..8ad344dccbd 100644
--- a/rust/util/meson.build
+++ b/rust/util/meson.build
@@ -43,7 +43,7 @@ _util_rs = static_library(
dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs, qom, qemuutil],
)
-util_rs = declare_dependency(link_with: [_util_rs], dependencies: [qemuutil, qom])
+util_rs = declare_dependency(link_with: [_util_rs])
rust.test('rust-util-tests', _util_rs,
dependencies: [qemuutil, qom],
--
2.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 04/19] rust/qobject: add basic bindings
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (2 preceding siblings ...)
2025-10-10 15:09 ` [PATCH 03/19] rust: do not add qemuutil to Rust crates Paolo Bonzini
@ 2025-10-10 15:09 ` Paolo Bonzini
2025-12-05 9:35 ` Markus Armbruster
2025-10-10 15:09 ` [PATCH 05/19] subprojects: add serde Paolo Bonzini
` (16 subsequent siblings)
20 siblings, 1 reply; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:09 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
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>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/util/wrapper.h | 7 +
rust/util/meson.build | 6 +-
rust/util/src/lib.rs | 2 +
rust/util/src/qobject/mod.rs | 317 +++++++++++++++++++++++++++++++++++
4 files changed, 330 insertions(+), 2 deletions(-)
create mode 100644 rust/util/src/qobject/mod.rs
diff --git a/rust/util/wrapper.h b/rust/util/wrapper.h
index b9ed68a01d8..0907dd59142 100644
--- a/rust/util/wrapper.h
+++ b/rust/util/wrapper.h
@@ -30,3 +30,10 @@ typedef enum memory_order {
#include "qemu/log.h"
#include "qemu/module.h"
#include "qemu/timer.h"
+#include "qobject/qnull.h"
+#include "qobject/qbool.h"
+#include "qobject/qnum.h"
+#include "qobject/qstring.h"
+#include "qobject/qobject.h"
+#include "qobject/qlist.h"
+#include "qobject/qdict.h"
diff --git a/rust/util/meson.build b/rust/util/meson.build
index 8ad344dccbd..ce468ea5227 100644
--- a/rust/util/meson.build
+++ b/rust/util/meson.build
@@ -36,8 +36,10 @@ _util_rs = static_library(
'src/module.rs',
'src/timer.rs',
],
- {'.': _util_bindings_inc_rs}
- ),
+ {'.': _util_bindings_inc_rs,
+ 'qobject': [
+ 'src/qobject/mod.rs',
+ ]}),
override_options: ['rust_std=2021', 'build.rust_std=2021'],
rust_abi: 'rust',
dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs, qom, qemuutil],
diff --git a/rust/util/src/lib.rs b/rust/util/src/lib.rs
index 16c89b95174..fe0128103c8 100644
--- a/rust/util/src/lib.rs
+++ b/rust/util/src/lib.rs
@@ -4,6 +4,8 @@
pub mod error;
pub mod log;
pub mod module;
+#[macro_use]
+pub mod qobject;
pub mod timer;
pub use error::{Error, Result};
diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
new file mode 100644
index 00000000000..9c6f168d6e1
--- /dev/null
+++ b/rust/util/src/qobject/mod.rs
@@ -0,0 +1,317 @@
+//! `QObject` bindings
+//!
+//! This module implements bindings for QEMU's `QObject` data structure.
+//! The bindings integrate with `serde`, which take the role of visitors
+//! in Rust code.
+
+#![deny(clippy::unwrap_used)]
+
+use std::{
+ cell::UnsafeCell,
+ ffi::{c_char, CString},
+ mem::ManuallyDrop,
+ ptr::{addr_of, addr_of_mut},
+ sync::atomic::{AtomicUsize, Ordering},
+};
+
+use common::assert_field_type;
+
+use crate::bindings;
+
+/// A wrapper for a C `QObject`.
+///
+/// Because `QObject` is not thread-safe, the safety of these bindings
+/// right now hinges on treating them as immutable. It is part of the
+/// contract with the `QObject` constructors that the Rust struct is
+/// only built after the contents are stable.
+///
+/// Only a bare bones API is public; production and consumption of `QObject`
+/// generally goes through `serde`.
+pub struct QObject(&'static UnsafeCell<bindings::QObject>);
+
+// SAFETY: the QObject API are not thread-safe other than reference counting;
+// but the Rust struct is only created once the contents are stable, and
+// therefore it obeys the aliased XOR mutable invariant.
+unsafe impl Send for QObject {}
+unsafe impl Sync for QObject {}
+
+// Since a QObject can be a floating-point value, and potentially a NaN,
+// do not implement Eq
+impl PartialEq for QObject {
+ fn eq(&self, other: &Self) -> bool {
+ unsafe { bindings::qobject_is_equal(self.0.get(), other.0.get()) }
+ }
+}
+
+impl QObject {
+ /// Construct a [`QObject`] from a C `QObjectBase` pointer.
+ /// The caller cedes its reference to the returned struct.
+ ///
+ /// # Safety
+ ///
+ /// The `QObjectBase` must not be changed from C code while
+ /// the Rust `QObject` lives
+ const unsafe fn from_base(p: *const bindings::QObjectBase_) -> Self {
+ QObject(unsafe { &*p.cast() })
+ }
+
+ /// Construct a [`QObject`] from a C `QObject` pointer.
+ /// The caller cedes its reference to the returned struct.
+ ///
+ /// # Safety
+ ///
+ /// The `QObject` must not be changed from C code while
+ /// the Rust `QObject` lives
+ pub const unsafe fn from_raw(p: *const bindings::QObject) -> Self {
+ QObject(unsafe { &*p.cast() })
+ }
+
+ /// Obtain a raw C pointer from a reference. `self` is consumed
+ /// and the C `QObject` pointer is leaked.
+ pub fn into_raw(self) -> *mut bindings::QObject {
+ let src = ManuallyDrop::new(self);
+ src.0.get()
+ }
+
+ /// Construct a [`QObject`] from a C `QObject` 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)) }
+ }
+}
+
+impl From<()> for QObject {
+ fn from(_null: ()) -> Self {
+ 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) };
+ unsafe { QObject::from_base(addr_of!(qobj.base)) }
+ }
+}
+
+macro_rules! from_int {
+ ($t:ty) => {
+ impl From<$t> for QObject {
+ fn from(n: $t) -> Self {
+ let qobj = unsafe { &*bindings::qnum_from_int(n.into()) };
+ unsafe { QObject::from_base(addr_of!(qobj.base)) }
+ }
+ }
+ };
+}
+
+from_int!(i8);
+from_int!(i16);
+from_int!(i32);
+from_int!(i64);
+
+macro_rules! from_uint {
+ ($t:ty) => {
+ impl From<$t> for QObject {
+ fn from(n: $t) -> Self {
+ let qobj = unsafe { &*bindings::qnum_from_uint(n.into()) };
+ unsafe { QObject::from_base(addr_of!(qobj.base)) }
+ }
+ }
+ };
+}
+
+from_uint!(u8);
+from_uint!(u16);
+from_uint!(u32);
+from_uint!(u64);
+
+macro_rules! from_double {
+ ($t:ty) => {
+ impl From<$t> for QObject {
+ fn from(n: $t) -> Self {
+ let qobj = unsafe { &*bindings::qnum_from_double(n.into()) };
+ unsafe { QObject::from_base(addr_of!(qobj.base)) }
+ }
+ }
+ };
+}
+
+from_double!(f32);
+from_double!(f64);
+
+impl From<CString> for QObject {
+ fn from(s: CString) -> Self {
+ let qobj = unsafe { &*bindings::qstring_from_str(s.as_ptr()) };
+ unsafe { QObject::from_base(addr_of!(qobj.base)) }
+ }
+}
+
+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);
+ }
+ }
+ unsafe { QObject::from_base(addr_of!(qlist.base)) }
+ }
+}
+
+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);
+ }
+ }
+ unsafe { QObject::from_base(addr_of!(qdict.base)) }
+ }
+}
+
+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
+ },)?
+ _ => {}
+ }
+ },
+ $($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.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 05/19] subprojects: add serde
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (3 preceding siblings ...)
2025-10-10 15:09 ` [PATCH 04/19] rust/qobject: add basic bindings Paolo Bonzini
@ 2025-10-10 15:09 ` Paolo Bonzini
2025-10-10 15:09 ` [PATCH 06/19] rust/qobject: add Serialize implementation Paolo Bonzini
` (15 subsequent siblings)
20 siblings, 0 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:09 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
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 4f98b2c03d3..76fdd9b97bf 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -30,6 +30,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 6ba075c8c71..f65cc4018c2 100644
--- a/rust/meson.build
+++ b/rust/meson.build
@@ -4,6 +4,8 @@ subproject('bilge-impl-0.2-rs', required: true)
subproject('foreign-0.3-rs', required: true)
subproject('glib-sys-0.21-rs', required: true)
subproject('libc-0.2-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')
@@ -11,6 +13,8 @@ bilge_impl_rs = dependency('bilge-impl-0.2-rs')
foreign_rs = dependency('foreign-0.3-rs')
glib_sys_rs = dependency('glib-sys-0.21-rs')
libc_rs = dependency('libc-0.2-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 8f97b19a088..3ed0429d806 100755
--- a/scripts/archive-source.sh
+++ b/scripts/archive-source.sh
@@ -45,6 +45,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 bc1b43caa25..eb5808b83ec 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 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 c00c8478372..697d1ef3bdb 100644
--- a/subprojects/.gitignore
+++ b/subprojects/.gitignore
@@ -20,6 +20,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..6cb2b59a147
--- /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, native: true)
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..e917d9337f6
--- /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, native: true)
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.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 06/19] rust/qobject: add Serialize implementation
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (4 preceding siblings ...)
2025-10-10 15:09 ` [PATCH 05/19] subprojects: add serde Paolo Bonzini
@ 2025-10-10 15:09 ` Paolo Bonzini
2025-12-05 9:47 ` Markus Armbruster
2025-10-10 15:09 ` [PATCH 07/19] rust/qobject: add Serializer (to_qobject) implementation Paolo Bonzini
` (14 subsequent siblings)
20 siblings, 1 reply; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:09 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
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>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/Cargo.lock | 1 +
rust/util/Cargo.toml | 1 +
rust/util/meson.build | 3 +-
rust/util/src/qobject/mod.rs | 4 +-
rust/util/src/qobject/serialize.rs | 59 ++++++++++++++++++++++++++++++
5 files changed, 65 insertions(+), 3 deletions(-)
create mode 100644 rust/util/src/qobject/serialize.rs
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 11085133490..7c9f85d5728 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -445,6 +445,7 @@ dependencies = [
"foreign",
"glib-sys",
"libc",
+ "serde",
]
[[package]]
diff --git a/rust/util/Cargo.toml b/rust/util/Cargo.toml
index 85f91436545..554004816eb 100644
--- a/rust/util/Cargo.toml
+++ b/rust/util/Cargo.toml
@@ -17,6 +17,7 @@ anyhow = { workspace = true }
foreign = { workspace = true }
glib-sys = { workspace = true }
libc = { workspace = true }
+serde = { workspace = true }
common = { path = "../common" }
[lints]
diff --git a/rust/util/meson.build b/rust/util/meson.build
index ce468ea5227..9fafaf76a37 100644
--- a/rust/util/meson.build
+++ b/rust/util/meson.build
@@ -39,10 +39,11 @@ _util_rs = static_library(
{'.': _util_bindings_inc_rs,
'qobject': [
'src/qobject/mod.rs',
+ 'src/qobject/serialize.rs',
]}),
override_options: ['rust_std=2021', 'build.rust_std=2021'],
rust_abi: 'rust',
- dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs, qom, qemuutil],
+ dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs, serde_rs, qom, qemuutil],
)
util_rs = declare_dependency(link_with: [_util_rs])
diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
index 9c6f168d6e1..0913fabc1ee 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},
@@ -230,7 +232,6 @@ fn drop(&mut self) {
}
}
-#[allow(unused)]
macro_rules! match_qobject {
(@internal ($qobj:expr) =>
$(() => $unit:expr,)?
@@ -313,5 +314,4 @@ macro_rules! match_qobject {
$($type $(($val))? => $code,)+)
};
}
-#[allow(unused_imports)]
use match_qobject;
diff --git a/rust/util/src/qobject/serialize.rs b/rust/util/src/qobject/serialize.rs
new file mode 100644
index 00000000000..34ec3847c1d
--- /dev/null
+++ b/rust/util/src/qobject/serialize.rs
@@ -0,0 +1,59 @@
+//! `QObject` serialization
+//!
+//! This module implements the [`Serialize`] trait for `QObject`,
+//! allowing it to be converted to other formats, for example
+//! JSON.
+
+use std::{ffi::CStr, mem::ManuallyDrop, ptr::addr_of};
+
+use serde::ser::{self, Serialize, SerializeMap, SerializeSeq};
+
+use super::{match_qobject, QObject};
+use crate::bindings;
+
+impl Serialize for QObject {
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: ::serde::Serializer,
+ {
+ match_qobject! { (self) =>
+ () => serializer.serialize_unit(),
+ bool(b) => serializer.serialize_bool(b),
+ i64(i) => serializer.serialize_i64(i),
+ u64(u) => serializer.serialize_u64(u),
+ f64(f) => serializer.serialize_f64(f),
+ CStr(cstr) => cstr.to_str().map_or_else(
+ |_| Err(ser::Error::custom("invalid UTF-8 in QString")),
+ |s| serializer.serialize_str(s),
+ ),
+ QList(l) => {
+ let mut node_ptr = unsafe { l.head.tqh_first };
+ let mut state = serializer.serialize_seq(None)?;
+ while !node_ptr.is_null() {
+ let node = unsafe { &*node_ptr };
+ let elem = unsafe { ManuallyDrop::new(QObject::from_raw(addr_of!(*node.value))) };
+ state.serialize_element(&*elem)?;
+ node_ptr = unsafe { node.next.tqe_next };
+ }
+ state.end()
+ },
+ QDict(d) => {
+ let mut state = serializer.serialize_map(Some(d.size))?;
+ let mut e_ptr = unsafe { bindings::qdict_first(d) };
+ while !e_ptr.is_null() {
+ let e = unsafe { &*e_ptr };
+ let key = unsafe { CStr::from_ptr(e.key) };
+ key.to_str().map_or_else(
+ |_| Err(ser::Error::custom("invalid UTF-8 in key")),
+ |k| state.serialize_key(k),
+ )?;
+ let value = unsafe { ManuallyDrop::new(QObject::from_raw(addr_of!(*e.value))) };
+ state.serialize_value(&*value)?;
+ e_ptr = unsafe { bindings::qdict_next(d, e) };
+ }
+ state.end()
+ }
+ }
+ }
+}
--
2.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 07/19] rust/qobject: add Serializer (to_qobject) implementation
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (5 preceding siblings ...)
2025-10-10 15:09 ` [PATCH 06/19] rust/qobject: add Serialize implementation Paolo Bonzini
@ 2025-10-10 15:09 ` Paolo Bonzini
2025-10-10 15:09 ` [PATCH 08/19] rust/qobject: add Deserialize implementation Paolo Bonzini
` (13 subsequent siblings)
20 siblings, 0 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:09 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
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>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/util/meson.build | 2 +
rust/util/src/qobject/error.rs | 52 +++
rust/util/src/qobject/mod.rs | 4 +
rust/util/src/qobject/serializer.rs | 585 ++++++++++++++++++++++++++++
4 files changed, 643 insertions(+)
create mode 100644 rust/util/src/qobject/error.rs
create mode 100644 rust/util/src/qobject/serializer.rs
diff --git a/rust/util/meson.build b/rust/util/meson.build
index 9fafaf76a37..39f427b3456 100644
--- a/rust/util/meson.build
+++ b/rust/util/meson.build
@@ -39,6 +39,8 @@ _util_rs = static_library(
{'.': _util_bindings_inc_rs,
'qobject': [
'src/qobject/mod.rs',
+ 'src/qobject/error.rs',
+ 'src/qobject/serializer.rs',
'src/qobject/serialize.rs',
]}),
override_options: ['rust_std=2021', 'build.rust_std=2021'],
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 0913fabc1ee..f8bd195ca2b 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.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 08/19] rust/qobject: add Deserialize implementation
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (6 preceding siblings ...)
2025-10-10 15:09 ` [PATCH 07/19] rust/qobject: add Serializer (to_qobject) implementation Paolo Bonzini
@ 2025-10-10 15:09 ` Paolo Bonzini
2025-10-10 15:09 ` [PATCH 09/19] rust/qobject: add Deserializer (from_qobject) implementation Paolo Bonzini
` (12 subsequent siblings)
20 siblings, 0 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:09 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
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>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/util/meson.build | 1 +
rust/util/src/qobject/deserialize.rs | 134 +++++++++++++++++++++++++++
rust/util/src/qobject/mod.rs | 1 +
3 files changed, 136 insertions(+)
create mode 100644 rust/util/src/qobject/deserialize.rs
diff --git a/rust/util/meson.build b/rust/util/meson.build
index 39f427b3456..9f8fbd49f00 100644
--- a/rust/util/meson.build
+++ b/rust/util/meson.build
@@ -39,6 +39,7 @@ _util_rs = static_library(
{'.': _util_bindings_inc_rs,
'qobject': [
'src/qobject/mod.rs',
+ 'src/qobject/deserialize.rs',
'src/qobject/error.rs',
'src/qobject/serializer.rs',
'src/qobject/serialize.rs',
diff --git a/rust/util/src/qobject/deserialize.rs b/rust/util/src/qobject/deserialize.rs
new file mode 100644
index 00000000000..280a577b6be
--- /dev/null
+++ b/rust/util/src/qobject/deserialize.rs
@@ -0,0 +1,134 @@
+//! `QObject` deserialization
+//!
+//! This module implements the [`Deserialize`] trait for `QObject`,
+//! allowing it to be created from any serializable format, for
+//! example JSON.
+
+use core::fmt;
+use std::ffi::CString;
+
+use serde::de::{self, Deserialize, MapAccess, SeqAccess, Visitor};
+
+use super::{to_qobject, QObject};
+
+impl<'de> Deserialize<'de> for QObject {
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<QObject, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ struct ValueVisitor;
+
+ impl<'de> Visitor<'de> for ValueVisitor {
+ type Value = QObject;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("any valid JSON value")
+ }
+
+ #[inline]
+ fn visit_bool<E>(self, value: bool) -> Result<QObject, E> {
+ Ok(value.into())
+ }
+
+ #[inline]
+ fn visit_i64<E>(self, value: i64) -> Result<QObject, E> {
+ Ok(value.into())
+ }
+
+ fn visit_i128<E>(self, value: i128) -> Result<QObject, E>
+ where
+ E: serde::de::Error,
+ {
+ to_qobject(value).map_err(|_| de::Error::custom("number out of range"))
+ }
+
+ #[inline]
+ fn visit_u64<E>(self, value: u64) -> Result<QObject, E> {
+ Ok(value.into())
+ }
+
+ fn visit_u128<E>(self, value: u128) -> Result<QObject, E>
+ where
+ E: serde::de::Error,
+ {
+ to_qobject(value).map_err(|_| de::Error::custom("number out of range"))
+ }
+
+ #[inline]
+ fn visit_f64<E>(self, value: f64) -> Result<QObject, E> {
+ Ok(value.into())
+ }
+
+ #[inline]
+ fn visit_str<E>(self, value: &str) -> Result<QObject, E>
+ where
+ E: serde::de::Error,
+ {
+ CString::new(value)
+ .map_err(|_| de::Error::custom("NUL character in string"))
+ .map(QObject::from)
+ }
+
+ #[inline]
+ fn visit_string<E>(self, value: String) -> Result<QObject, E>
+ where
+ E: serde::de::Error,
+ {
+ CString::new(value)
+ .map_err(|_| de::Error::custom("NUL character in string"))
+ .map(QObject::from)
+ }
+
+ #[inline]
+ fn visit_none<E>(self) -> Result<QObject, E> {
+ Ok(().into())
+ }
+
+ #[inline]
+ fn visit_some<D>(self, deserializer: D) -> Result<QObject, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ Deserialize::deserialize(deserializer)
+ }
+
+ #[inline]
+ fn visit_unit<E>(self) -> Result<QObject, E> {
+ Ok(().into())
+ }
+
+ #[inline]
+ fn visit_seq<V>(self, mut visitor: V) -> Result<QObject, V::Error>
+ where
+ V: SeqAccess<'de>,
+ {
+ // TODO: insert elements one at a time
+ let mut vec = Vec::<QObject>::new();
+
+ while let Some(elem) = visitor.next_element()? {
+ vec.push(elem);
+ }
+ Ok(QObject::from_iter(vec))
+ }
+
+ fn visit_map<V>(self, mut visitor: V) -> Result<QObject, V::Error>
+ where
+ V: MapAccess<'de>,
+ {
+ // TODO: insert elements one at a time
+ let mut vec = Vec::<(CString, QObject)>::new();
+
+ if let Some(first_key) = visitor.next_key()? {
+ vec.push((first_key, visitor.next_value()?));
+ while let Some((key, value)) = visitor.next_entry()? {
+ vec.push((key, value));
+ }
+ }
+ Ok(QObject::from_iter(vec))
+ }
+ }
+
+ deserializer.deserialize_any(ValueVisitor)
+ }
+}
diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
index f8bd195ca2b..80c496b8a63 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.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 09/19] rust/qobject: add Deserializer (from_qobject) implementation
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (7 preceding siblings ...)
2025-10-10 15:09 ` [PATCH 08/19] rust/qobject: add Deserialize implementation Paolo Bonzini
@ 2025-10-10 15:09 ` Paolo Bonzini
2025-10-10 15:09 ` [PATCH 10/19] rust/util: replace Error::err_or_unit/err_or_else with Error::with_errp Paolo Bonzini
` (11 subsequent siblings)
20 siblings, 0 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:09 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
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>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
docs/devel/rust.rst | 1 +
rust/util/meson.build | 1 +
rust/util/src/qobject/deserializer.rs | 371 ++++++++++++++++++++++++++
rust/util/src/qobject/error.rs | 8 +-
rust/util/src/qobject/mod.rs | 2 +
5 files changed, 382 insertions(+), 1 deletion(-)
create mode 100644 rust/util/src/qobject/deserializer.rs
diff --git a/docs/devel/rust.rst b/docs/devel/rust.rst
index 2f0ab2e2821..3aadfb78dfd 100644
--- a/docs/devel/rust.rst
+++ b/docs/devel/rust.rst
@@ -161,6 +161,7 @@ module status
``util::error`` stable
``util::log`` proof of concept
``util::module`` complete
+``util::qobject`` stable
``util::timer`` stable
========================== ======================
diff --git a/rust/util/meson.build b/rust/util/meson.build
index 9f8fbd49f00..aff14a41589 100644
--- a/rust/util/meson.build
+++ b/rust/util/meson.build
@@ -39,6 +39,7 @@ _util_rs = static_library(
{'.': _util_bindings_inc_rs,
'qobject': [
'src/qobject/mod.rs',
+ 'src/qobject/deserializer.rs',
'src/qobject/deserialize.rs',
'src/qobject/error.rs',
'src/qobject/serializer.rs',
diff --git a/rust/util/src/qobject/deserializer.rs b/rust/util/src/qobject/deserializer.rs
new file mode 100644
index 00000000000..84a03bd9f1b
--- /dev/null
+++ b/rust/util/src/qobject/deserializer.rs
@@ -0,0 +1,371 @@
+//! `QObject` deserializer
+//!
+//! This module implements a [`Deserializer`](serde::de::Deserializer) that
+//! produces `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 80c496b8a63..e896aba5f3a 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.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 10/19] rust/util: replace Error::err_or_unit/err_or_else with Error::with_errp
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (8 preceding siblings ...)
2025-10-10 15:09 ` [PATCH 09/19] rust/qobject: add Deserializer (from_qobject) implementation Paolo Bonzini
@ 2025-10-10 15:09 ` Paolo Bonzini
2025-10-10 15:09 ` [PATCH 11/19] rust/qobject: add from/to JSON bindings for QObject Paolo Bonzini
` (10 subsequent siblings)
20 siblings, 0 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:09 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
Introduce a simpler function that hides the creation of the Error**.
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/util/src/error.rs | 52 ++++++++++++++++--------------------------
1 file changed, 20 insertions(+), 32 deletions(-)
diff --git a/rust/util/src/error.rs b/rust/util/src/error.rs
index bdbf2634170..5a0dd7786b5 100644
--- a/rust/util/src/error.rs
+++ b/rust/util/src/error.rs
@@ -14,8 +14,7 @@
//! [`ptr_or_propagate`](crate::Error::ptr_or_propagate) can be used to build
//! a C return value while also propagating an error condition
//!
-//! * [`err_or_else`](crate::Error::err_or_else) and
-//! [`err_or_unit`](crate::Error::err_or_unit) can be used to build a `Result`
+//! * [`with_errp`](crate::Error::with_errp) can be used to build a `Result`
//!
//! This module is most commonly used at the boundary between C and Rust code;
//! other code will usually access it through the
@@ -208,35 +207,21 @@ pub unsafe fn propagate(self, errp: *mut *mut bindings::Error) {
}
}
- /// Convert a C `Error*` into a Rust `Result`, using
- /// `Ok(())` if `c_error` is NULL. Free the `Error*`.
+ /// Pass a C `Error*` to the closure, and convert the result
+ /// (either the return value of the closure, or the error)
+ /// into a Rust `Result`.
///
/// # Safety
///
- /// `c_error` must be `NULL` or valid; typically it was initialized
- /// with `ptr::null_mut()` and passed by reference to a C function.
- pub unsafe fn err_or_unit(c_error: *mut bindings::Error) -> Result<()> {
- // SAFETY: caller guarantees c_error is valid
- unsafe { Self::err_or_else(c_error, || ()) }
- }
+ /// One exit from `f`, `c_error` must be unchanged or point to a
+ /// valid C [`struct Error`](bindings::Error).
+ pub unsafe fn with_errp<T, F: FnOnce(&mut *mut bindings::Error) -> T>(f: F) -> Result<T> {
+ let mut c_error: *mut bindings::Error = ptr::null_mut();
- /// Convert a C `Error*` into a Rust `Result`, calling `f()` to
- /// obtain an `Ok` value if `c_error` is NULL. Free the `Error*`.
- ///
- /// # Safety
- ///
- /// `c_error` must be `NULL` or point to a valid C [`struct
- /// Error`](bindings::Error); typically it was initialized with
- /// `ptr::null_mut()` and passed by reference to a C function.
- pub unsafe fn err_or_else<T, F: FnOnce() -> T>(
- c_error: *mut bindings::Error,
- f: F,
- ) -> Result<T> {
- // SAFETY: caller guarantees c_error is valid
- let err = unsafe { Option::<Self>::from_foreign(c_error) };
- match err {
- None => Ok(f()),
- Some(err) => Err(err),
+ // SAFETY: guaranteed by the postcondition of `f`
+ match (f(&mut c_error), unsafe { c_error.into_native() }) {
+ (result, None) => Ok(result),
+ (_, Some(err)) => Err(err),
}
}
}
@@ -421,13 +406,16 @@ fn test_ptr_or_propagate() {
}
#[test]
- fn test_err_or_unit() {
+ fn test_with_errp() {
unsafe {
- let result = Error::err_or_unit(ptr::null_mut());
- assert_match!(result, Ok(()));
+ let result = Error::with_errp(|_errp| true);
+ assert_match!(result, Ok(true));
- let err = error_for_test(c"msg");
- let err = Error::err_or_unit(err.into_inner()).unwrap_err();
+ let err = Error::with_errp(|errp| {
+ *errp = error_for_test(c"msg").into_inner();
+ false
+ })
+ .unwrap_err();
assert_eq!(&*format!("{err}"), "msg");
}
}
--
2.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 11/19] rust/qobject: add from/to JSON bindings for QObject
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (9 preceding siblings ...)
2025-10-10 15:09 ` [PATCH 10/19] rust/util: replace Error::err_or_unit/err_or_else with Error::with_errp Paolo Bonzini
@ 2025-10-10 15:09 ` Paolo Bonzini
2025-12-05 10:04 ` Markus Armbruster
2025-10-10 15:09 ` [PATCH 12/19] rust/qobject: add Display/Debug Paolo Bonzini
` (9 subsequent siblings)
20 siblings, 1 reply; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:09 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
These are used by tests. However it could even be an idea to use
serde_json + transcoding and get rid of the C version...
Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/util/wrapper.h | 1 +
rust/util/src/qobject/mod.rs | 17 +++++++++++++++++
2 files changed, 18 insertions(+)
diff --git a/rust/util/wrapper.h b/rust/util/wrapper.h
index 0907dd59142..c88820a5e5b 100644
--- a/rust/util/wrapper.h
+++ b/rust/util/wrapper.h
@@ -37,3 +37,4 @@ typedef enum memory_order {
#include "qobject/qobject.h"
#include "qobject/qlist.h"
#include "qobject/qdict.h"
+#include "qobject/qjson.h"
diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
index e896aba5f3a..292a3c9c238 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;
@@ -111,6 +112,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))
+ }
+ }
}
impl From<()> for QObject {
--
2.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 12/19] rust/qobject: add Display/Debug
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (10 preceding siblings ...)
2025-10-10 15:09 ` [PATCH 11/19] rust/qobject: add from/to JSON bindings for QObject Paolo Bonzini
@ 2025-10-10 15:09 ` Paolo Bonzini
2025-10-10 15:09 ` [PATCH 13/19] scripts/qapi: add QAPISchemaIfCond.rsgen() Paolo Bonzini
` (8 subsequent siblings)
20 siblings, 0 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:09 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/util/src/qobject/mod.rs | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
index 292a3c9c238..38b98fdb1e8 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},
@@ -256,6 +257,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.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 13/19] scripts/qapi: add QAPISchemaIfCond.rsgen()
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (11 preceding siblings ...)
2025-10-10 15:09 ` [PATCH 12/19] rust/qobject: add Display/Debug Paolo Bonzini
@ 2025-10-10 15:09 ` Paolo Bonzini
2025-12-09 18:43 ` Markus Armbruster
2025-10-10 15:09 ` [PATCH 14/19] scripts/qapi: generate high-level Rust bindings Paolo Bonzini
` (7 subsequent siblings)
20 siblings, 1 reply; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:09 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Generate Rust #[cfg(...)] guards from QAPI 'if' conditions.
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 | 16 ++++++++++++++++
scripts/qapi/schema.py | 4 ++++
2 files changed, 20 insertions(+)
diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
index d7c8aa3365c..f16b9568bb9 100644
--- a/scripts/qapi/common.py
+++ b/scripts/qapi/common.py
@@ -199,6 +199,22 @@ 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
+ if isinstance(ifcond, list):
+ return ', '.join([cfg(c) for c in ifcond])
+ oper, operands = next(iter(ifcond.items()))
+ operands = cfg(operands)
+ return f'{oper}({operands})'
+
+ if not ifcond:
+ return ''
+ return '#[cfg(%s)]' % cfg(ifcond)
+
+
def gen_ifcond(ifcond: Optional[Union[str, Dict[str, Any]]],
cond_fmt: str, not_fmt: str,
all_operator: str, any_operator: str) -> str:
diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 8d88b40de2e..848a7401251 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -37,6 +37,7 @@
docgen_ifcond,
gen_endif,
gen_if,
+ rsgen_ifcond,
)
from .error import QAPIError, QAPISemError, QAPISourceError
from .expr import check_exprs
@@ -63,6 +64,9 @@ def gen_endif(self) -> str:
def docgen(self) -> str:
return docgen_ifcond(self.ifcond)
+ def rsgen(self) -> str:
+ return rsgen_ifcond(self.ifcond)
+
def is_present(self) -> bool:
return bool(self.ifcond)
--
2.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 14/19] scripts/qapi: generate high-level Rust bindings
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (12 preceding siblings ...)
2025-10-10 15:09 ` [PATCH 13/19] scripts/qapi: add QAPISchemaIfCond.rsgen() Paolo Bonzini
@ 2025-10-10 15:09 ` Paolo Bonzini
2025-12-09 10:03 ` Markus Armbruster
2025-10-10 15:10 ` [PATCH 15/19] scripts/qapi: add serde attributes Paolo Bonzini
` (6 subsequent siblings)
20 siblings, 1 reply; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:09 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Generate high-level native Rust declarations for the QAPI types.
- char* is mapped to String, scalars to there corresponding Rust types
- enums are simply aliased from FFI
- has_foo/foo members are mapped to Option<T>
- lists are represented as Vec<T>
- structures have Rust versions, with To/From FFI conversions
- alternate are represented as Rust enum
- unions are represented in a similar way as in C: a struct S with a "u"
member (since S may have extra 'base' fields). However, the discriminant
isn't a member of S, since Rust enum already include it.
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Link: https://lore.kernel.org/r/20210907121943.3498701-21-marcandre.lureau@redhat.com
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
meson.build | 4 +-
scripts/qapi/backend.py | 27 ++-
scripts/qapi/main.py | 4 +-
scripts/qapi/rs.py | 181 +++++++++++++++++++
scripts/qapi/rs_types.py | 365 +++++++++++++++++++++++++++++++++++++++
5 files changed, 577 insertions(+), 4 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 afaefa01722..ce914217c52 100644
--- a/meson.build
+++ b/meson.build
@@ -3571,12 +3571,14 @@ 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/features.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..305b62b514c 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
@@ -36,7 +37,7 @@ def generate(self,
"""
-class QAPICBackend(QAPIBackend):
+class QAPICodeBackend(QAPIBackend):
# pylint: disable=too-few-public-methods
def generate(self,
@@ -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, builtins)
diff --git a/scripts/qapi/main.py b/scripts/qapi/main.py
index 0e2a6ae3f07..4ad75e213f5 100644
--- a/scripts/qapi/main.py
+++ b/scripts/qapi/main.py
@@ -12,7 +12,7 @@
import sys
from typing import Optional
-from .backend import QAPIBackend, QAPICBackend
+from .backend import QAPIBackend, QAPICodeBackend
from .common import must_match
from .error import QAPIError
from .schema import QAPISchema
@@ -27,7 +27,7 @@ def invalid_prefix_char(prefix: str) -> Optional[str]:
def create_backend(path: str) -> QAPIBackend:
if path is None:
- return QAPICBackend()
+ return QAPICodeBackend()
module_path, dot, class_name = path.rpartition('.')
if not dot:
diff --git a/scripts/qapi/rs.py b/scripts/qapi/rs.py
new file mode 100644
index 00000000000..2a9bbcb9f54
--- /dev/null
+++ b/scripts/qapi/rs.py
@@ -0,0 +1,181 @@
+# This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+"""
+QAPI Rust generator
+"""
+
+import os
+import re
+import subprocess
+from typing import NamedTuple, Optional
+
+from .common import POINTER_SUFFIX
+from .gen import QAPIGen
+from .schema import QAPISchemaModule, QAPISchemaVisitor
+
+
+# see to_upper_case()/to_lower_case() below
+snake_case = re.compile(r'((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))')
+
+
+rs_name_trans = str.maketrans('.-', '__')
+
+
+# Map @name to a valid Rust identifier.
+# If @protect, avoid returning certain ticklish identifiers (like
+# keywords) by prepending raw identifier prefix 'r#'.
+def rs_name(name: str, protect: bool = True) -> str:
+ name = name.translate(rs_name_trans)
+ if name[0].isnumeric():
+ name = '_' + name
+ if not protect:
+ return name
+ # based from the list:
+ # https://doc.rust-lang.org/reference/keywords.html
+ if name in ('Self', 'abstract', 'as', 'async',
+ 'await', 'become', 'box', 'break',
+ 'const', 'continue', 'crate', 'do',
+ 'dyn', 'else', 'enum', 'extern',
+ 'false', 'final', 'fn', 'for',
+ 'if', 'impl', 'in', 'let',
+ 'loop', 'macro', 'match', 'mod',
+ 'move', 'mut', 'override', 'priv',
+ 'pub', 'ref', 'return', 'self',
+ 'static', 'struct', 'super', 'trait',
+ 'true', 'try', 'type', 'typeof',
+ 'union', 'unsafe', 'unsized', 'use',
+ 'virtual', 'where', 'while', 'yield'):
+ name = 'r#' + name
+ # avoid some clashes with the standard library
+ if name in ('String',):
+ name = 'Qapi' + name
+
+ return name
+
+
+def rs_type(c_type: str,
+ qapi_ns: str = 'qapi::',
+ optional: bool = False,
+ box: bool = False) -> str:
+ (is_pointer, _, is_list, c_type) = rs_ctype_parse(c_type)
+ to_rs = {
+ 'QNull': '()',
+ 'QObject': 'QObject',
+ 'any': 'QObject',
+ 'bool': 'bool',
+ 'char': 'i8',
+ 'double': 'f64',
+ 'int': 'i64',
+ 'int16': 'i16',
+ 'int16_t': 'i16',
+ 'int32': 'i32',
+ 'int32_t': 'i32',
+ 'int64': 'i64',
+ 'int64_t': 'i64',
+ 'int8': 'i8',
+ 'int8_t': 'i8',
+ 'number': 'f64',
+ 'size': 'u64',
+ 'str': 'String',
+ 'uint16': 'u16',
+ 'uint16_t': 'u16',
+ 'uint32': 'u32',
+ 'uint32_t': 'u32',
+ 'uint64': 'u64',
+ 'uint64_t': 'u64',
+ 'uint8': 'u8',
+ 'uint8_t': 'u8',
+ 'String': 'QapiString',
+ }
+ if is_pointer:
+ to_rs.update({
+ 'char': 'String',
+ })
+
+ if is_list:
+ c_type = c_type[:-4]
+
+ ret = to_rs.get(c_type, qapi_ns + c_type)
+ if is_list:
+ ret = 'Vec<%s>' % ret
+ elif is_pointer and c_type not in to_rs and box:
+ ret = 'Box<%s>' % ret
+ if optional:
+ ret = 'Option<%s>' % ret
+ return ret
+
+
+class CType(NamedTuple):
+ is_pointer: bool
+ is_const: bool
+ is_list: bool
+ c_type: str
+
+
+def rs_ctype_parse(c_type: str) -> CType:
+ is_pointer = False
+ if c_type.endswith(POINTER_SUFFIX):
+ is_pointer = True
+ c_type = c_type[:-len(POINTER_SUFFIX)]
+ is_list = c_type.endswith('List')
+ is_const = False
+ if c_type.startswith('const '):
+ is_const = True
+ c_type = c_type[6:]
+
+ c_type = rs_name(c_type)
+ return CType(is_pointer, is_const, is_list, c_type)
+
+
+def to_camel_case(value: str) -> str:
+ # special case for last enum value
+ if value == '_MAX':
+ return value
+ raw_id = False
+ if value.startswith('r#'):
+ raw_id = True
+ value = value[2:]
+ value = ''.join('_' + word if word[0].isdigit()
+ else word[:1].upper() + word[1:]
+ for word in filter(None, re.split("[-_]+", value)))
+ if raw_id:
+ return 'r#' + value
+ return value
+
+
+def to_upper_case(value: str) -> str:
+ return snake_case.sub(r'_\1', value).upper()
+
+
+def to_lower_case(value: str) -> str:
+ return snake_case.sub(r'_\1', value).lower()
+
+
+class QAPIGenRs(QAPIGen):
+ pass
+
+
+class QAPISchemaRsVisitor(QAPISchemaVisitor):
+
+ def __init__(self, prefix: str, what: str):
+ super().__init__()
+ self._prefix = prefix
+ self._what = what
+ self._gen = QAPIGenRs(self._prefix + self._what + '.rs')
+ self._main_module: Optional[str] = None
+
+ def visit_module(self, name: Optional[str]) -> None:
+ if name is None:
+ return
+ if QAPISchemaModule.is_user_module(name):
+ if self._main_module is None:
+ self._main_module = name
+
+ def write(self, output_dir: str) -> None:
+ self._gen.write(output_dir)
+
+ pathname = os.path.join(output_dir, self._gen.fname)
+ try:
+ subprocess.check_call(['rustfmt', pathname])
+ except FileNotFoundError:
+ pass
diff --git a/scripts/qapi/rs_types.py b/scripts/qapi/rs_types.py
new file mode 100644
index 00000000000..436adcf5be6
--- /dev/null
+++ b/scripts/qapi/rs_types.py
@@ -0,0 +1,365 @@
+# 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
+"""
+
+from typing import List, Optional, Set
+
+from .common import mcgen
+from .rs import (
+ QAPISchemaRsVisitor,
+ rs_name,
+ rs_type,
+ to_camel_case,
+ to_lower_case,
+ to_upper_case,
+)
+from .schema import (
+ QAPISchema,
+ QAPISchemaAlternateType,
+ QAPISchemaArrayType,
+ 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=rs_type(variants.tag_member.type.c_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(to_upper_case(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), False)
+ if type_name == 'q_empty':
+ ret += mcgen('''
+ %(cfg)s
+ %(var_name)s,
+''',
+ cfg=var.ifcond.rsgen(),
+ var_name=var_name)
+ else:
+ c_type = var.type.c_unboxed_type()
+ if c_type.endswith('_wrapper'):
+ c_type = c_type[6:-8] # remove q_obj*-wrapper
+ ret += mcgen('''
+ %(cfg)s
+ %(var_name)s(%(rs_type)s),
+''',
+ cfg=var.ifcond.rsgen(),
+ var_name=var_name,
+ rs_type=rs_type(c_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(to_lower_case(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)
+ if isinstance(memb, QAPISchemaArrayType):
+ return has_recursive_type(memb.element_type, name, visited)
+ return False
+
+
+def gen_struct_members(members: List[QAPISchemaObjectTypeMember],
+ name: str) -> str:
+ ret = ''
+ for memb in members:
+ is_recursive = has_recursive_type(memb.type, name, set())
+ typ = rs_type(memb.type.c_type(), '',
+ optional=memb.optional, box=is_recursive)
+ ret += mcgen('''
+ %(cfg)s
+ pub %(rs_name)s: %(rs_type)s,
+''',
+ cfg=memb.ifcond.rsgen(),
+ rs_type=typ,
+ rs_name=rs_name(to_lower_case(memb.name)))
+ return ret
+
+
+def gen_rs_object(name: str,
+ ifcond: QAPISchemaIfCond,
+ base: Optional[QAPISchemaObjectType],
+ members: List[QAPISchemaObjectTypeMember],
+ variants: Optional[QAPISchemaVariants]) -> str:
+ if name in objects_seen:
+ return ''
+
+ if variants:
+ members = [m for m in members
+ if m.name != variants.tag_member.name]
+
+ ret = ''
+ objects_seen.add(name)
+
+ if variants:
+ ret += gen_rs_variants(name, ifcond, variants)
+
+ ret += mcgen('''
+
+%(cfg)s
+#[derive(Clone, Debug, PartialEq)]
+pub struct %(rs_name)s {
+''',
+ cfg=ifcond.rsgen(),
+ rs_name=rs_name(name))
+
+ if base:
+ if not base.is_implicit():
+ ret += mcgen('''
+ // Members inherited:
+''',
+ c_name=base.c_name())
+ base_members = base.members
+ if variants:
+ base_members = [m for m in base.members
+ if m.name != variants.tag_member.name]
+ ret += gen_struct_members(base_members, name)
+ if not base.is_implicit():
+ ret += mcgen('''
+ // Own members:
+''')
+
+ ret += gen_struct_members(members, name)
+
+ if variants:
+ ret += mcgen('''
+ pub u: %(rs_type)sVariant,
+''', rs_type=rs_name(name))
+ ret += mcgen('''
+}
+''')
+ return ret
+
+
+def gen_rs_enum(name: str,
+ ifcond: QAPISchemaIfCond,
+ members: List[QAPISchemaEnumMember]) -> str:
+ # append automatically generated _max value
+ enum_members = members + [QAPISchemaEnumMember('_MAX', None)]
+
+ ret = mcgen('''
+
+%(cfg)s
+#[repr(u32)]
+#[derive(Copy, Clone, Debug, PartialEq, common::TryInto)]
+pub enum %(rs_name)s {
+''',
+ cfg=ifcond.rsgen(),
+ rs_name=rs_name(name))
+
+ for member in enum_members:
+ ret += mcgen('''
+ %(cfg)s
+ %(c_enum)s,
+''',
+ cfg=member.ifcond.rsgen(),
+ c_enum=rs_name(to_upper_case(member.name)))
+ # picked the first, since that's what malloc0 does
+ default = rs_name(to_upper_case(enum_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
+ is_recursive = has_recursive_type(var.type, name, set())
+ ret += mcgen('''
+ %(cfg)s
+ %(mem_name)s(%(rs_type)s),
+''',
+ cfg=var.ifcond.rsgen(),
+ rs_type=rs_type(var.type.c_unboxed_type(), '',
+ box=is_recursive),
+ mem_name=rs_name(to_camel_case(var.name)))
+
+ ret += mcgen('''
+}
+''')
+ return ret
+
+
+class QAPISchemaGenRsTypeVisitor(QAPISchemaRsVisitor):
+
+ def __init__(self, prefix: str) -> None:
+ super().__init__(prefix, 'qapi-types')
+
+ def visit_begin(self, schema: QAPISchema) -> None:
+ # don't visit the empty type
+ objects_seen.add(schema.the_empty_object_type.name)
+ self._gen.preamble_add(
+ mcgen('''
+// @generated by qapi-gen, DO NOT EDIT
+
+#![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:
+ if 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:
+ 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,
+ builtins: bool) -> None:
+ # pylint: disable=unused-argument
+ # TODO: builtins?
+ vis = QAPISchemaGenRsTypeVisitor(prefix)
+ schema.visit(vis)
+ vis.write(output_dir)
--
2.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 15/19] scripts/qapi: add serde attributes
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (13 preceding siblings ...)
2025-10-10 15:09 ` [PATCH 14/19] scripts/qapi: generate high-level Rust bindings Paolo Bonzini
@ 2025-10-10 15:10 ` Paolo Bonzini
2025-12-09 12:24 ` Markus Armbruster
2025-10-10 15:10 ` [PATCH 16/19] scripts/qapi: strip trailing whitespaces Paolo Bonzini
` (5 subsequent siblings)
20 siblings, 1 reply; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Generate serde attributes to match the serialization format to QAPI's:
- for enums, map Rust enum variants to original QAPI names
- for structs, rejects JSON with extra fields and omit optional fields
(as opposed to serializing them as null)
- for union variants:
- use tagged union format matching QAPI's discriminator,
- map variant names to original QAPI names
- flatten union data into parent struct
- for alternates, use type-based discrimination
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
scripts/qapi/rs_types.py | 40 +++++++++++++++++++++++++++++++---------
1 file changed, 31 insertions(+), 9 deletions(-)
diff --git a/scripts/qapi/rs_types.py b/scripts/qapi/rs_types.py
index 436adcf5be6..f53b419dc2f 100644
--- a/scripts/qapi/rs_types.py
+++ b/scripts/qapi/rs_types.py
@@ -31,6 +31,7 @@
objects_seen = set()
+SERDE_SKIP_NONE = '#[serde(skip_serializing_if = "Option::is_none")]'
def gen_rs_variants_to_tag(name: str,
@@ -77,11 +78,13 @@ def gen_rs_variants(name: str,
ret = mcgen('''
%(cfg)s
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(tag = "%(tag)s")]
pub enum %(rs_name)sVariant {
''',
cfg=ifcond.rsgen(),
- rs_name=rs_name(name))
+ rs_name=rs_name(name),
+ tag=variants.tag_member.name)
for var in variants.variants:
type_name = var.type.name
@@ -89,21 +92,25 @@ 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:
c_type = var.type.c_unboxed_type()
if c_type.endswith('_wrapper'):
c_type = c_type[6:-8] # remove q_obj*-wrapper
ret += mcgen('''
%(cfg)s
+ #[serde(rename = "%(rename)s")]
%(var_name)s(%(rs_type)s),
''',
cfg=var.ifcond.rsgen(),
var_name=var_name,
- rs_type=rs_type(c_type, ''))
+ rs_type=rs_type(c_type, ''),
+ rename=var.name)
ret += mcgen('''
}
@@ -159,9 +166,11 @@ def gen_struct_members(members: List[QAPISchemaObjectTypeMember],
optional=memb.optional, box=is_recursive)
ret += mcgen('''
%(cfg)s
+ %(serde_skip_if)s
pub %(rs_name)s: %(rs_type)s,
''',
cfg=memb.ifcond.rsgen(),
+ serde_skip_if=SERDE_SKIP_NONE if memb.optional else '',
rs_type=typ,
rs_name=rs_name(to_lower_case(memb.name)))
return ret
@@ -182,17 +191,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():
@@ -214,6 +229,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('''
@@ -232,7 +248,8 @@ def gen_rs_enum(name: str,
%(cfg)s
#[repr(u32)]
-#[derive(Copy, Clone, Debug, PartialEq, common::TryInto)]
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize,
+ common::TryInto)]
pub enum %(rs_name)s {
''',
cfg=ifcond.rsgen(),
@@ -241,10 +258,12 @@ def gen_rs_enum(name: str,
for member in enum_members:
ret += mcgen('''
%(cfg)s
+ #[serde(rename = "%(member_name)s")]
%(c_enum)s,
''',
cfg=member.ifcond.rsgen(),
- c_enum=rs_name(to_upper_case(member.name)))
+ c_enum=rs_name(to_upper_case(member.name)),
+ member_name=member.name)
# picked the first, since that's what malloc0 does
default = rs_name(to_upper_case(enum_members[0].name))
ret += mcgen('''
@@ -275,7 +294,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(),
@@ -323,6 +343,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.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 16/19] scripts/qapi: strip trailing whitespaces
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (14 preceding siblings ...)
2025-10-10 15:10 ` [PATCH 15/19] scripts/qapi: add serde attributes Paolo Bonzini
@ 2025-10-10 15:10 ` Paolo Bonzini
2025-12-09 8:48 ` Markus Armbruster
2025-10-10 15:10 ` [PATCH 17/19] scripts/rustc_args: add --no-strict-cfg Paolo Bonzini
` (4 subsequent siblings)
20 siblings, 1 reply; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
From: Marc-André Lureau <marcandre.lureau@redhat.com>
This help workaround a rustfmt issue.
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Link: https://lore.kernel.org/r/20210907121943.3498701-16-marcandre.lureau@redhat.com
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
scripts/qapi/gen.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/scripts/qapi/gen.py b/scripts/qapi/gen.py
index 0c9b8db3b02..c9721545ea7 100644
--- a/scripts/qapi/gen.py
+++ b/scripts/qapi/gen.py
@@ -58,7 +58,11 @@ def add(self, text: str) -> None:
self._body += text
def get_content(self) -> str:
- return self._top() + self._preamble + self._body + self._bottom()
+ content = self._top() + self._preamble + self._body + self._bottom()
+ # delete trailing white-spaces (working around
+ # https://github.com/rust-lang/rustfmt/issues/4248)
+ content = re.sub(r'\s+$', '\n', content, 0, re.M)
+ return content
def _top(self) -> str:
# pylint: disable=no-self-use
--
2.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 17/19] scripts/rustc_args: add --no-strict-cfg
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (15 preceding siblings ...)
2025-10-10 15:10 ` [PATCH 16/19] scripts/qapi: strip trailing whitespaces Paolo Bonzini
@ 2025-10-10 15:10 ` Paolo Bonzini
2025-10-10 15:10 ` [PATCH 18/19] rust/util: build QAPI types Paolo Bonzini
` (3 subsequent siblings)
20 siblings, 0 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Allow to generate all --cfg flags, regardless of Cargo.toml content.
We can't easily list and include all the features used by QAPI types.
Access via #[cfg()] then requires #![allow(unexpected_cfgs)].
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
scripts/rust/rustc_args.py | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/scripts/rust/rustc_args.py b/scripts/rust/rustc_args.py
index 63b0748e0d3..c70b95b8bed 100644
--- a/scripts/rust/rustc_args.py
+++ b/scripts/rust/rustc_args.py
@@ -116,7 +116,7 @@ def generate_lint_flags(cargo_toml: CargoTOML, strict_lints: bool) -> Iterable[s
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:
@@ -125,8 +125,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")
@@ -194,6 +195,13 @@ def main() -> None:
help="apply stricter checks (for nightly Rust)",
default=False,
)
+ 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)
@@ -224,7 +232,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.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 18/19] rust/util: build QAPI types
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (16 preceding siblings ...)
2025-10-10 15:10 ` [PATCH 17/19] scripts/rustc_args: add --no-strict-cfg Paolo Bonzini
@ 2025-10-10 15:10 ` Paolo Bonzini
2025-10-10 15:10 ` [PATCH 19/19] rust/tests: QAPI integration tests Paolo Bonzini
` (2 subsequent siblings)
20 siblings, 0 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
qapi/meson.build | 6 ++++++
rust/Cargo.lock | 1 +
rust/util/Cargo.toml | 1 +
rust/util/meson.build | 16 ++++++++++++++++
4 files changed, 24 insertions(+)
diff --git a/qapi/meson.build b/qapi/meson.build
index a46269b5a0c..888a9adf7ff 100644
--- a/qapi/meson.build
+++ b/qapi/meson.build
@@ -130,3 +130,9 @@ foreach output : qapi_outputs
util_ss.add(qapi_files[i])
i = i + 1
endforeach
+
+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 7c9f85d5728..1074c926b71 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -446,6 +446,7 @@ dependencies = [
"glib-sys",
"libc",
"serde",
+ "serde_derive",
]
[[package]]
diff --git a/rust/util/Cargo.toml b/rust/util/Cargo.toml
index 554004816eb..9f6c52c5acd 100644
--- a/rust/util/Cargo.toml
+++ b/rust/util/Cargo.toml
@@ -18,6 +18,7 @@ foreign = { workspace = true }
glib-sys = { workspace = true }
libc = { workspace = true }
serde = { workspace = true }
+serde_derive = { workspace = true }
common = { path = "../common" }
[lints]
diff --git a/rust/util/meson.build b/rust/util/meson.build
index aff14a41589..fe869ab485e 100644
--- a/rust/util/meson.build
+++ b/rust/util/meson.build
@@ -64,3 +64,19 @@ 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',
+ dependencies: [common_rs, util_rs, serde_rs],
+)
+
+qapi_rs = declare_dependency(link_with: [_qapi_rs])
--
2.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* [PATCH 19/19] rust/tests: QAPI integration tests
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (17 preceding siblings ...)
2025-10-10 15:10 ` [PATCH 18/19] rust/util: build QAPI types Paolo Bonzini
@ 2025-10-10 15:10 ` Paolo Bonzini
2025-10-30 17:13 ` [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
2025-12-05 13:55 ` Markus Armbruster
20 siblings, 0 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-10 15:10 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
rust/tests/meson.build | 25 +-
rust/tests/tests/integration.rs | 2 +
rust/tests/tests/qapi.rs | 444 ++++++++++++++++++++++++++++++++
3 files changed, 468 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 00688c66fb1..663c1e1aafc 100644
--- a/rust/tests/meson.build
+++ b/rust/tests/meson.build
@@ -1,11 +1,30 @@
+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'),
+ files(
+ 'tests/integration.rs',
+ 'tests/vmstate_tests.rs',
+ 'tests/qapi.rs',
+ ),
override_options: ['rust_std=2021', 'build.rust_std=2021'],
- rust_args: ['--test'],
+ 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.51.0
^ permalink raw reply related [flat|nested] 48+ messages in thread
* Re: [PATCH 00/19] rust: QObject and QAPI bindings
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (18 preceding siblings ...)
2025-10-10 15:10 ` [PATCH 19/19] rust/tests: QAPI integration tests Paolo Bonzini
@ 2025-10-30 17:13 ` Paolo Bonzini
2025-12-05 13:55 ` Markus Armbruster
20 siblings, 0 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-10-30 17:13 UTC (permalink / raw)
To: qemu-devel; +Cc: armbru, marcandre.lureau, qemu-rust
On 10/10/25 17:09, Paolo Bonzini wrote:
> This adds two related parts of the Rust bindings:
>
> - QAPI code generator that creates Rust structs from the JSON
> description. The structs are *not* ABI compatible with the
> C ones, instead they use native Rust data types.
>
> - QObject bindings and (de)serialization support, which can be used to
> convert QObjects to and from QAPI structs.
>
> Unfortunately Rust code is not able to use visitors, other than by
> creating an intermediate QObject. This is because of the different
> architecture of serde vs. QAPI visitors, and because visitor's
> dual-purpose functions, where the same function is used by both input and
> output visitors, rely heavily on the structs using the same representation
> as the visitor arguments (for example NUL-terminated strings).
>
> The serde format implementation was co-authored by me and Marc-André.
> Marc-André did all the bug fixing and integration testing.
>
> As an example of how this would be used, the marshaling functions for
> QMP commands would look like this:
>
> fn qmp_marshal_query_stats(args: *mut QDict,
> retp: *mut *mut QObject, errp: *mut *mut Error)
> {
> let qobj = unsafe { QObject::cloned_from_raw(args.cast()) };
>
> let result = from_qobject::<StatsFilter>(qobj)
> .map_err(anyhow::Error::from)
> .and_then(qmp_query_stats)
> .and_then(|ret| to_qobject::<Vec<StatsResult>>(ret).map_err(anyhow::Error::from));
>
> match qmp_marshal_query_stats_rs(qobj) {
> Ok(ret) => unsafe { *retp = ret.into_raw(); },
> Err(e) => unsafe { crate::Error::from(e).propagate(errp) },
> }
> }
>
> As a small extra, patches 1 and 2 rework a bit the error implementation
> so that it is possible to convert any Rust error into a QEMU one. This
> is because we noticed that we had to add several From<> implementations
> to convert e.g. NulError or serde errors into util::Error.
>
> The price is that it's a bit harder to convert *strings* into errors;
> therefore, the first patch adds a macro wrapper for
> "if !cond { return Err(...) }", where the dots build an error from a
> formatted string.
Ping.
Paolo
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 03/19] rust: do not add qemuutil to Rust crates
2025-10-10 15:09 ` [PATCH 03/19] rust: do not add qemuutil to Rust crates Paolo Bonzini
@ 2025-12-05 8:30 ` Markus Armbruster
2025-12-05 12:07 ` Paolo Bonzini
0 siblings, 1 reply; 48+ messages in thread
From: Markus Armbruster @ 2025-12-05 8:30 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, marcandre.lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> This fails due to https://github.com/mesonbuild/meson/pull/15076.
> The config-host.h file from the qemuutil dependency ends up on the
> rustc command line for targets that do not use structured sources.
>
> It will be reverted once Meson 1.9.2 is released, or replaced with
> an update of the minimum supported version of Meson if 1.9.2 is
> released sooner.
I tend to put reminder comments next to such temporary workarounds, so I
don't forget to revert them.
> Reported-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
> rust/chardev/meson.build | 2 +-
> rust/util/meson.build | 2 +-
> 2 files changed, 2 insertions(+), 2 deletions(-)
>
> diff --git a/rust/chardev/meson.build b/rust/chardev/meson.build
> index d365d8dd0f4..36ada7c4546 100644
> --- a/rust/chardev/meson.build
> +++ b/rust/chardev/meson.build
> @@ -39,4 +39,4 @@ _chardev_rs = static_library(
> dependencies: [glib_sys_rs, common_rs, qemu_macros],
> )
>
> -chardev_rs = declare_dependency(link_with: [_chardev_rs], dependencies: [chardev, qemuutil])
> +chardev_rs = declare_dependency(link_with: [_chardev_rs], dependencies: [chardev])
> diff --git a/rust/util/meson.build b/rust/util/meson.build
> index b0b75e93ff6..8ad344dccbd 100644
> --- a/rust/util/meson.build
> +++ b/rust/util/meson.build
> @@ -43,7 +43,7 @@ _util_rs = static_library(
> dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs, qom, qemuutil],
> )
>
> -util_rs = declare_dependency(link_with: [_util_rs], dependencies: [qemuutil, qom])
> +util_rs = declare_dependency(link_with: [_util_rs])
>
> rust.test('rust-util-tests', _util_rs,
> dependencies: [qemuutil, qom],
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 04/19] rust/qobject: add basic bindings
2025-10-10 15:09 ` [PATCH 04/19] rust/qobject: add basic bindings Paolo Bonzini
@ 2025-12-05 9:35 ` Markus Armbruster
2025-12-05 11:27 ` Paolo Bonzini
0 siblings, 1 reply; 48+ messages in thread
From: Markus Armbruster @ 2025-12-05 9:35 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, marcandre.lureau, qemu-rust
Learning opportunity for me, I ask for your patience...
Paolo Bonzini <pbonzini@redhat.com> writes:
> This is only a basic API, intended to be used by the serde traits.
>
> Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
> rust/util/wrapper.h | 7 +
> rust/util/meson.build | 6 +-
> rust/util/src/lib.rs | 2 +
> rust/util/src/qobject/mod.rs | 317 +++++++++++++++++++++++++++++++++++
> 4 files changed, 330 insertions(+), 2 deletions(-)
> create mode 100644 rust/util/src/qobject/mod.rs
>
> diff --git a/rust/util/wrapper.h b/rust/util/wrapper.h
> index b9ed68a01d8..0907dd59142 100644
> --- a/rust/util/wrapper.h
> +++ b/rust/util/wrapper.h
> @@ -30,3 +30,10 @@ typedef enum memory_order {
> #include "qemu/log.h"
> #include "qemu/module.h"
> #include "qemu/timer.h"
> +#include "qobject/qnull.h"
> +#include "qobject/qbool.h"
> +#include "qobject/qnum.h"
> +#include "qobject/qstring.h"
> +#include "qobject/qobject.h"
> +#include "qobject/qlist.h"
> +#include "qobject/qdict.h"
> diff --git a/rust/util/meson.build b/rust/util/meson.build
> index 8ad344dccbd..ce468ea5227 100644
> --- a/rust/util/meson.build
> +++ b/rust/util/meson.build
> @@ -36,8 +36,10 @@ _util_rs = static_library(
> 'src/module.rs',
> 'src/timer.rs',
> ],
> - {'.': _util_bindings_inc_rs}
> - ),
> + {'.': _util_bindings_inc_rs,
> + 'qobject': [
> + 'src/qobject/mod.rs',
> + ]}),
> override_options: ['rust_std=2021', 'build.rust_std=2021'],
> rust_abi: 'rust',
> dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs, qom, qemuutil],
> diff --git a/rust/util/src/lib.rs b/rust/util/src/lib.rs
> index 16c89b95174..fe0128103c8 100644
> --- a/rust/util/src/lib.rs
> +++ b/rust/util/src/lib.rs
> @@ -4,6 +4,8 @@
> pub mod error;
> pub mod log;
> pub mod module;
> +#[macro_use]
> +pub mod qobject;
> pub mod timer;
>
> pub use error::{Error, Result};
> diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
> new file mode 100644
> index 00000000000..9c6f168d6e1
> --- /dev/null
> +++ b/rust/util/src/qobject/mod.rs
> @@ -0,0 +1,317 @@
> +//! `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>);
This defines the Rust QObject. All it contains is a reference (wrapped
in UnsafeCell) self.0 to the C QObject. Correct?
> +
> +// 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.
In other words, we promise never to change a QObject while Rust code
holds a reference, except for the reference counts. Correct?
The reference count is the mutable part of an otherwise immutable
object. Not mentioned here: it is atomic. Therefore, concurrent
updates cannot mess it up. Nothing depends on its value except
deallocation when the last reference drops. I figure that's why the
exception to "aliased XOR mutable" is fine. Correct?
> +unsafe impl Send for QObject {}
> +unsafe impl Sync for QObject {}
> +
> +// Since a QObject can be a floating-point value, and potentially a NaN,
> +// do not implement Eq
> +impl PartialEq for QObject {
> + fn eq(&self, other: &Self) -> bool {
> + unsafe { bindings::qobject_is_equal(self.0.get(), other.0.get()) }
> + }
> +}
> +
> +impl QObject {
> + /// Construct a [`QObject`] from a C `QObjectBase` pointer.
It's spelled QObjectBase_. More of the same below, not flagging again.
Comment next to its definition:
/* Not for use outside include/qobject/ */
We're using it outside now. Do we really need to?
> + /// The caller cedes its reference to the returned struct.
> + ///
> + /// # Safety
> + ///
> + /// The `QObjectBase` must not be changed from C code while
> + /// the Rust `QObject` lives
> + const unsafe fn from_base(p: *const bindings::QObjectBase_) -> Self {
> + QObject(unsafe { &*p.cast() })
> + }
> +
> + /// Construct a [`QObject`] from a C `QObject` pointer.
> + /// The caller cedes its reference to the returned struct.
> + ///
> + /// # Safety
> + ///
> + /// The `QObject` must not be changed from C code while
> + /// the Rust `QObject` lives
> + pub const unsafe fn from_raw(p: *const bindings::QObject) -> Self {
> + QObject(unsafe { &*p.cast() })
> + }
> +
> + /// Obtain a raw C pointer from a reference. `self` is consumed
> + /// and the C `QObject` pointer is leaked.
What exactly do you mean by "leaked"?
> + pub fn into_raw(self) -> *mut bindings::QObject {
> + let src = ManuallyDrop::new(self);
> + src.0.get()
> + }
> +
> + /// Construct a [`QObject`] from a C `QObject` pointer.
Pasto? Isn't it QObjectBase_ here?
> + /// 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)) }
> + }
> +}
> +
> +impl From<()> for QObject {
> + fn from(_null: ()) -> Self {
> + unsafe { QObject::cloned_from_base(addr_of!(bindings::qnull_.base)) }
qnull_ is not meant for use outside qnull.[ch] and its unit test
check-qnull.c. Could we use qnull()?
> + }
> +}
> +
> +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) };
> + unsafe { QObject::from_base(addr_of!(qobj.base)) }
> + }
> +}
> +
> +macro_rules! from_int {
> + ($t:ty) => {
> + impl From<$t> for QObject {
> + fn from(n: $t) -> Self {
> + let qobj = unsafe { &*bindings::qnum_from_int(n.into()) };
> + unsafe { QObject::from_base(addr_of!(qobj.base)) }
> + }
> + }
> + };
> +}
> +
> +from_int!(i8);
> +from_int!(i16);
> +from_int!(i32);
> +from_int!(i64);
> +
> +macro_rules! from_uint {
> + ($t:ty) => {
> + impl From<$t> for QObject {
> + fn from(n: $t) -> Self {
> + let qobj = unsafe { &*bindings::qnum_from_uint(n.into()) };
> + unsafe { QObject::from_base(addr_of!(qobj.base)) }
> + }
> + }
> + };
> +}
> +
> +from_uint!(u8);
> +from_uint!(u16);
> +from_uint!(u32);
> +from_uint!(u64);
> +
> +macro_rules! from_double {
> + ($t:ty) => {
> + impl From<$t> for QObject {
> + fn from(n: $t) -> Self {
> + let qobj = unsafe { &*bindings::qnum_from_double(n.into()) };
> + unsafe { QObject::from_base(addr_of!(qobj.base)) }
> + }
> + }
> + };
> +}
> +
> +from_double!(f32);
Uh, isn't the double in from_double misleading?
> +from_double!(f64);
Can you briefly explain why we need more than i64, u64, and double?
> +
> +impl From<CString> for QObject {
> + fn from(s: CString) -> Self {
> + let qobj = unsafe { &*bindings::qstring_from_str(s.as_ptr()) };
> + unsafe { QObject::from_base(addr_of!(qobj.base)) }
> + }
> +}
> +
> +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);
> + }
> + }
> + unsafe { QObject::from_base(addr_of!(qlist.base)) }
> + }
> +}
> +
> +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);
> + }
> + }
> + unsafe { QObject::from_base(addr_of!(qdict.base)) }
> + }
> +}
> +
> +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());
> + }
> + }
> + }
> +}
> +
Skipping the remainder, it's too much macro magic for poor, ignorant me
:)
> +#[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
> + },)?
> + _ => {}
> + }
> + },
> + $($crate::bindings::QTYPE_QSTRING => break {
> + let qstring__: *mut $crate::bindings::QString = qobj_.cast();
> + let $cstrvar = unsafe { ::core::ffi::CStr::from_ptr((&*qstring__).string) };
> + $cstr
> + },)?
> + $($crate::bindings::QTYPE_QLIST => break {
> + let qlist__: *mut $crate::bindings::QList = qobj_.cast();
> + let $qlistvar = unsafe { &*qlist__ };
> + $qlist
> + },)?
> + $($crate::bindings::QTYPE_QDICT => break {
> + let qdict__: *mut $crate::bindings::QDict = qobj_.cast();
> + let $qdictvar = unsafe { &*qdict__ };
> + $qdict
> + },)?
> + _ => ()
> + };
> + $(break $other;)?
> + #[allow(unreachable_code)]
> + {
> + panic!("unreachable");
> + }
> + }
> + };
> +
> + // first cleanup the syntax a bit, checking that there's at least
> + // one pattern and always adding a trailing comma
> + (($qobj:expr) =>
> + $($type:tt$(($val:tt))? => $code:expr ),+ $(,)?) => {
> + match_qobject!(@internal ($qobj) =>
> + $($type $(($val))? => $code,)+)
> + };
> +}
> +#[allow(unused_imports)]
> +use match_qobject;
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 06/19] rust/qobject: add Serialize implementation
2025-10-10 15:09 ` [PATCH 06/19] rust/qobject: add Serialize implementation Paolo Bonzini
@ 2025-12-05 9:47 ` Markus Armbruster
2025-12-10 17:19 ` Paolo Bonzini
0 siblings, 1 reply; 48+ messages in thread
From: Markus Armbruster @ 2025-12-05 9:47 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, armbru, marcandre.lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> This allows QObject to be converted to other formats, for example
> JSON via serde_json.
>
> This is not too useful, since QObjects are consumed by
> C code or deserialized into structs, but it can be used for testing
> and it is part of the full implementation of a serde format.
>
> Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
> rust/Cargo.lock | 1 +
> rust/util/Cargo.toml | 1 +
> rust/util/meson.build | 3 +-
> rust/util/src/qobject/mod.rs | 4 +-
> rust/util/src/qobject/serialize.rs | 59 ++++++++++++++++++++++++++++++
> 5 files changed, 65 insertions(+), 3 deletions(-)
> create mode 100644 rust/util/src/qobject/serialize.rs
>
> diff --git a/rust/Cargo.lock b/rust/Cargo.lock
> index 11085133490..7c9f85d5728 100644
> --- a/rust/Cargo.lock
> +++ b/rust/Cargo.lock
> @@ -445,6 +445,7 @@ dependencies = [
> "foreign",
> "glib-sys",
> "libc",
> + "serde",
> ]
>
> [[package]]
> diff --git a/rust/util/Cargo.toml b/rust/util/Cargo.toml
> index 85f91436545..554004816eb 100644
> --- a/rust/util/Cargo.toml
> +++ b/rust/util/Cargo.toml
> @@ -17,6 +17,7 @@ anyhow = { workspace = true }
> foreign = { workspace = true }
> glib-sys = { workspace = true }
> libc = { workspace = true }
> +serde = { workspace = true }
> common = { path = "../common" }
>
> [lints]
> diff --git a/rust/util/meson.build b/rust/util/meson.build
> index ce468ea5227..9fafaf76a37 100644
> --- a/rust/util/meson.build
> +++ b/rust/util/meson.build
> @@ -39,10 +39,11 @@ _util_rs = static_library(
> {'.': _util_bindings_inc_rs,
> 'qobject': [
> 'src/qobject/mod.rs',
> + 'src/qobject/serialize.rs',
> ]}),
> override_options: ['rust_std=2021', 'build.rust_std=2021'],
> rust_abi: 'rust',
> - dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs, qom, qemuutil],
> + dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs, serde_rs, qom, qemuutil],
> )
>
> util_rs = declare_dependency(link_with: [_util_rs])
> diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
> index 9c6f168d6e1..0913fabc1ee 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},
> @@ -230,7 +232,6 @@ fn drop(&mut self) {
> }
> }
>
> -#[allow(unused)]
> macro_rules! match_qobject {
> (@internal ($qobj:expr) =>
> $(() => $unit:expr,)?
> @@ -313,5 +314,4 @@ macro_rules! match_qobject {
> $($type $(($val))? => $code,)+)
> };
> }
> -#[allow(unused_imports)]
> use match_qobject;
> diff --git a/rust/util/src/qobject/serialize.rs b/rust/util/src/qobject/serialize.rs
> new file mode 100644
> index 00000000000..34ec3847c1d
> --- /dev/null
> +++ b/rust/util/src/qobject/serialize.rs
> @@ -0,0 +1,59 @@
> +//! `QObject` serialization
> +//!
> +//! This module implements the [`Serialize`] trait for `QObject`,
> +//! allowing it to be converted to other formats, for example
> +//! JSON.
> +
> +use std::{ffi::CStr, mem::ManuallyDrop, ptr::addr_of};
> +
> +use serde::ser::{self, Serialize, SerializeMap, SerializeSeq};
> +
> +use super::{match_qobject, QObject};
> +use crate::bindings;
> +
> +impl Serialize for QObject {
> + #[inline]
> + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
> + where
> + S: ::serde::Serializer,
> + {
> + match_qobject! { (self) =>
> + () => serializer.serialize_unit(),
> + bool(b) => serializer.serialize_bool(b),
> + i64(i) => serializer.serialize_i64(i),
> + u64(u) => serializer.serialize_u64(u),
> + f64(f) => serializer.serialize_f64(f),
> + CStr(cstr) => cstr.to_str().map_or_else(
> + |_| Err(ser::Error::custom("invalid UTF-8 in QString")),
Could this be a programming error? Like flawed input validation?
> + |s| serializer.serialize_str(s),
> + ),
> + QList(l) => {
> + let mut node_ptr = unsafe { l.head.tqh_first };
> + let mut state = serializer.serialize_seq(None)?;
> + while !node_ptr.is_null() {
> + let node = unsafe { &*node_ptr };
> + let elem = unsafe { ManuallyDrop::new(QObject::from_raw(addr_of!(*node.value))) };
> + state.serialize_element(&*elem)?;
> + node_ptr = unsafe { node.next.tqe_next };
> + }
> + state.end()
> + },
> + QDict(d) => {
> + let mut state = serializer.serialize_map(Some(d.size))?;
> + let mut e_ptr = unsafe { bindings::qdict_first(d) };
> + while !e_ptr.is_null() {
> + let e = unsafe { &*e_ptr };
> + let key = unsafe { CStr::from_ptr(e.key) };
> + key.to_str().map_or_else(
> + |_| Err(ser::Error::custom("invalid UTF-8 in key")),
> + |k| state.serialize_key(k),
> + )?;
> + let value = unsafe { ManuallyDrop::new(QObject::from_raw(addr_of!(*e.value))) };
> + state.serialize_value(&*value)?;
> + e_ptr = unsafe { bindings::qdict_next(d, e) };
> + }
> + state.end()
> + }
> + }
> + }
> +}
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 11/19] rust/qobject: add from/to JSON bindings for QObject
2025-10-10 15:09 ` [PATCH 11/19] rust/qobject: add from/to JSON bindings for QObject Paolo Bonzini
@ 2025-12-05 10:04 ` Markus Armbruster
2025-12-05 11:09 ` Paolo Bonzini
0 siblings, 1 reply; 48+ messages in thread
From: Markus Armbruster @ 2025-12-05 10:04 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, marcandre.lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> These are used by tests. However it could even be an idea to use
> serde_json + transcoding and get rid of the C version...
Tell me more!
> Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
> rust/util/wrapper.h | 1 +
> rust/util/src/qobject/mod.rs | 17 +++++++++++++++++
> 2 files changed, 18 insertions(+)
>
> diff --git a/rust/util/wrapper.h b/rust/util/wrapper.h
> index 0907dd59142..c88820a5e5b 100644
> --- a/rust/util/wrapper.h
> +++ b/rust/util/wrapper.h
> @@ -37,3 +37,4 @@ typedef enum memory_order {
> #include "qobject/qobject.h"
> #include "qobject/qlist.h"
> #include "qobject/qdict.h"
> +#include "qobject/qjson.h"
> diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
> index e896aba5f3a..292a3c9c238 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;
> @@ -111,6 +112,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))
> + }
> + }
> }
>
> impl From<()> for QObject {
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 11/19] rust/qobject: add from/to JSON bindings for QObject
2025-12-05 10:04 ` Markus Armbruster
@ 2025-12-05 11:09 ` Paolo Bonzini
2025-12-05 12:16 ` Markus Armbruster
0 siblings, 1 reply; 48+ messages in thread
From: Paolo Bonzini @ 2025-12-05 11:09 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, marcandre.lureau, qemu-rust
On 12/5/25 11:04, Markus Armbruster wrote:
> Paolo Bonzini <pbonzini@redhat.com> writes:
>
>> These are used by tests. However it could even be an idea to use
>> serde_json + transcoding and get rid of the C version...
>
> Tell me more!
QEMU's JSON parser produces a QObject. To obtain the same effect, we
can take JSON-string-to-serde deserialization (implemented by
serde_json) and attach it to serde-to-QObject serialization (the thing
in patch 5). That results in a JSON-string-to-QObject function.
Doing it in the other direction (QObject deserializer + JSON-string
serializer) produces a QObject-to-JSON-string function.
For a little more information see https://serde.rs/transcode.html.
Note however that there is no support for push parsing, therefore this
would not replace the balanced-parentheses machinery in
qobject/json-streamer.c, and therefore QMP would still need a minimal lexer.
Grr... I just remembered about interpolation :/ so no, we still need a
parser for libqmp.c.
Paolo
>> Co-authored-by: Marc-André Lureau <marcandre.lureau@redhat.com>
>> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
>> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
>> ---
>> rust/util/wrapper.h | 1 +
>> rust/util/src/qobject/mod.rs | 17 +++++++++++++++++
>> 2 files changed, 18 insertions(+)
>>
>> diff --git a/rust/util/wrapper.h b/rust/util/wrapper.h
>> index 0907dd59142..c88820a5e5b 100644
>> --- a/rust/util/wrapper.h
>> +++ b/rust/util/wrapper.h
>> @@ -37,3 +37,4 @@ typedef enum memory_order {
>> #include "qobject/qobject.h"
>> #include "qobject/qlist.h"
>> #include "qobject/qdict.h"
>> +#include "qobject/qjson.h"
>> diff --git a/rust/util/src/qobject/mod.rs b/rust/util/src/qobject/mod.rs
>> index e896aba5f3a..292a3c9c238 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;
>> @@ -111,6 +112,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))
>> + }
>> + }
>> }
>>
>> impl From<()> for QObject {
>
>
>
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 04/19] rust/qobject: add basic bindings
2025-12-05 9:35 ` Markus Armbruster
@ 2025-12-05 11:27 ` Paolo Bonzini
2025-12-11 7:21 ` Markus Armbruster
0 siblings, 1 reply; 48+ messages in thread
From: Paolo Bonzini @ 2025-12-05 11:27 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, marcandre.lureau, qemu-rust
On 12/5/25 10:35, Markus Armbruster wrote:
>> +/// 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>);
>
> This defines the Rust QObject. All it contains is a reference (wrapped
> in UnsafeCell) self.0 to the C QObject. Correct?
Correct.
>> +
>> +// 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.
>
> In other words, we promise never to change a QObject while Rust code
> holds a reference, except for the reference counts. Correct?
>
> The reference count is the mutable part of an otherwise immutable
> object. Not mentioned here: it is atomic. Therefore, concurrent
> updates cannot mess it up. Nothing depends on its value except
> deallocation when the last reference drops. I figure that's why the
> exception to "aliased XOR mutable" is fine. Correct?
Yes, it's one of a few exceptions to "aliased XOR mutable" including:
- Mutex (because only one guy can access it at all anyway)
- RefCell (enforces aliased XOR mutable at run-time, enforces
single-thread usage at compile-time)
- atomics (a mini mutex)
- Cell (Mutex:RefCell = atomics:Cell, in other words every access is
independent but also single-thread usage is checked at compile time)
>> +unsafe impl Send for QObject {}
>> +unsafe impl Sync for QObject {}
>> +
>> +// Since a QObject can be a floating-point value, and potentially a NaN,
>> +// do not implement Eq
>> +impl PartialEq for QObject {
>> + fn eq(&self, other: &Self) -> bool {
>> + unsafe { bindings::qobject_is_equal(self.0.get(), other.0.get()) }
>> + }
>> +}
>> +
>> +impl QObject {
>> + /// Construct a [`QObject`] from a C `QObjectBase` pointer.
>
> It's spelled QObjectBase_. More of the same below, not flagging again.
>
> Comment next to its definition:
>
> /* Not for use outside include/qobject/ */
>
> We're using it outside now. Do we really need to?
It's because we're defining equivalents of inline functions in
include/qobject.
I can however replace uses of from_base with a macro similar to QOBJECT()
>> + /// Obtain a raw C pointer from a reference. `self` is consumed
>> + /// and the C `QObject` pointer is leaked.
>
> What exactly do you mean by "leaked"?
s/and the.*/without decreasing the reference count, thus transferring
the reference to the `*mut bindings::QOjbect`/
>> + pub fn into_raw(self) -> *mut bindings::QObject {
>> + let src = ManuallyDrop::new(self);
>> + src.0.get()
>> + }
>> +
>> + /// Construct a [`QObject`] from a C `QObject` pointer.
>
> Pasto? Isn't it QObjectBase_ here?
Yes.
>> +impl From<()> for QObject {
>> + fn from(_null: ()) -> Self {
>> + unsafe { QObject::cloned_from_base(addr_of!(bindings::qnull_.base)) }
>
> qnull_ is not meant for use outside qnull.[ch] and its unit test
> check-qnull.c. Could we use qnull()?
Same as above---it's inline. The above is a translation of
static inline QNull *qnull(void)
{
return qobject_ref(&qnull_);
}
>> +macro_rules! from_double {
>> + ($t:ty) => {
>> + impl From<$t> for QObject {
>> + fn from(n: $t) -> Self {
>> + let qobj = unsafe { &*bindings::qnum_from_double(n.into()) };
>> + unsafe { QObject::from_base(addr_of!(qobj.base)) }
>> + }
>> + }
>> + };
>> +}
>> +
>> +from_double!(f32);
>
> Uh, isn't the double in from_double misleading?
It's a reference to the function that it calls (qnum_from_double). Can
rename it to impl_from_returning_qnum_double.
>> +from_double!(f64);
>
> Can you briefly explain why we need more than i64, u64, and double?
Because Rust doesn't do automatic casts. So it's nicer (and also less
error prone) if the subsequent patches do not have to always convert to
u64 or i64.
> Skipping the remainder, it's too much macro magic for poor, ignorant me
> :)
It's not really hard. The thing to the left of => effectively defines a
parser. Each thing of the shape $IDENT:RULE matches a piece of Rust
grammar; expr is expression an tt is token tree (either a single token
or a parenthesized group). To access $IDENT that appears within $(...)?
on the left of => you must have a similar $(...)? on the right, and the
whole $(...)? on the right will be skipped if the left-side wasn't there.
The macro is used like this:
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,
}
And it produces a "switch" on QObject types, where each "case" extracts
the datum, places it in the variable to the left of "=>" (such as "b"
for bool), and returns the value on the right of "=>" (such as
"Unexpected::Bool(b)"):
>> + ) => {
>> + 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
>> + },)?
(The loop/break is just a syntactic convenience---the loop never rolls
more than once).
Paolo
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 03/19] rust: do not add qemuutil to Rust crates
2025-12-05 8:30 ` Markus Armbruster
@ 2025-12-05 12:07 ` Paolo Bonzini
0 siblings, 0 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-12-05 12:07 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, marcandre.lureau, qemu-rust
On 12/5/25 09:30, Markus Armbruster wrote:
> Paolo Bonzini <pbonzini@redhat.com> writes:
>
>> This fails due to https://github.com/mesonbuild/meson/pull/15076.
>> The config-host.h file from the qemuutil dependency ends up on the
>> rustc command line for targets that do not use structured sources.
>>
>> It will be reverted once Meson 1.9.2 is released, or replaced with
>> an update of the minimum supported version of Meson if 1.9.2 is
>> released sooner.
>
> I tend to put reminder comments next to such temporary workarounds, so I
> don't forget to revert them.
It turns out it wasn't temporary
(https://lore.kernel.org/qemu-devel/20251127132036.84384-9-pbonzini@redhat.com).
I'll post a separate update-meson-and-clean-up-rust/ series once Meson
1.10 is out (which is soon, and I've already tested the rc with QEMU).
Paolo
>> Reported-by: Marc-André Lureau <marcandre.lureau@redhat.com>
>> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
>> ---
>> rust/chardev/meson.build | 2 +-
>> rust/util/meson.build | 2 +-
>> 2 files changed, 2 insertions(+), 2 deletions(-)
>>
>> diff --git a/rust/chardev/meson.build b/rust/chardev/meson.build
>> index d365d8dd0f4..36ada7c4546 100644
>> --- a/rust/chardev/meson.build
>> +++ b/rust/chardev/meson.build
>> @@ -39,4 +39,4 @@ _chardev_rs = static_library(
>> dependencies: [glib_sys_rs, common_rs, qemu_macros],
>> )
>>
>> -chardev_rs = declare_dependency(link_with: [_chardev_rs], dependencies: [chardev, qemuutil])
>> +chardev_rs = declare_dependency(link_with: [_chardev_rs], dependencies: [chardev])
>> diff --git a/rust/util/meson.build b/rust/util/meson.build
>> index b0b75e93ff6..8ad344dccbd 100644
>> --- a/rust/util/meson.build
>> +++ b/rust/util/meson.build
>> @@ -43,7 +43,7 @@ _util_rs = static_library(
>> dependencies: [anyhow_rs, libc_rs, foreign_rs, glib_sys_rs, common_rs, qom, qemuutil],
>> )
>>
>> -util_rs = declare_dependency(link_with: [_util_rs], dependencies: [qemuutil, qom])
>> +util_rs = declare_dependency(link_with: [_util_rs])
>>
>> rust.test('rust-util-tests', _util_rs,
>> dependencies: [qemuutil, qom],
>
>
>
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 11/19] rust/qobject: add from/to JSON bindings for QObject
2025-12-05 11:09 ` Paolo Bonzini
@ 2025-12-05 12:16 ` Markus Armbruster
2025-12-08 7:00 ` Paolo Bonzini
0 siblings, 1 reply; 48+ messages in thread
From: Markus Armbruster @ 2025-12-05 12:16 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, marcandre.lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> On 12/5/25 11:04, Markus Armbruster wrote:
>> Paolo Bonzini <pbonzini@redhat.com> writes:
>>
>>> These are used by tests. However it could even be an idea to use
>>> serde_json + transcoding and get rid of the C version...
>>
>> Tell me more!
>
> QEMU's JSON parser produces a QObject. To obtain the same effect, we
> can take JSON-string-to-serde deserialization (implemented by
> serde_json) and attach it to serde-to-QObject serialization (the thing
> in patch 5). That results in a JSON-string-to-QObject function.
>
> Doing it in the other direction (QObject deserializer + JSON-string
> serializer) produces a QObject-to-JSON-string function.
Yes.
> For a little more information see https://serde.rs/transcode.html.
>
> Note however that there is no support for push parsing, therefore this
> would not replace the balanced-parentheses machinery in
> qobject/json-streamer.c, and therefore QMP would still need a minimal lexer.
That push parser... I never liked it. First, it's half-assed: it's a
push lexer wed to a pull parser with parenthesis counting. Second, why
complicated & half-assed when you can do simple & quarter-assed instead?
We could've required "exactly one complete JSON value per line", or some
expression separator such as an empty line.
> Grr... I just remembered about interpolation :/ so no, we still need a
> parser for libqmp.c.
Right.
Interpolation lets us build QObjects from literal templates with
variable scalars or QObjects interpolated. More concise and much easier
to read than the equivalend nest of constructor calls. Drawback: chains
us to our own, bespoke JSON parser.
Out of curiosity: how would we do better than "nest of constructor
calls" in Rust?
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 00/19] rust: QObject and QAPI bindings
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
` (19 preceding siblings ...)
2025-10-30 17:13 ` [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
@ 2025-12-05 13:55 ` Markus Armbruster
2025-12-09 6:01 ` Markus Armbruster
20 siblings, 1 reply; 48+ messages in thread
From: Markus Armbruster @ 2025-12-05 13:55 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, marcandre.lureau, qemu-rust
I applied, ran make, and it didn't create qapi-types.rs and
test-qapi-types.rs for me. What am I missing?
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 11/19] rust/qobject: add from/to JSON bindings for QObject
2025-12-05 12:16 ` Markus Armbruster
@ 2025-12-08 7:00 ` Paolo Bonzini
2025-12-08 9:17 ` Markus Armbruster
0 siblings, 1 reply; 48+ messages in thread
From: Paolo Bonzini @ 2025-12-08 7:00 UTC (permalink / raw)
To: Markus Armbruster
Cc: qemu-devel, Marc-André Lureau,
open list:Rust-related patc...
[-- Attachment #1: Type: text/plain, Size: 1802 bytes --]
Il ven 5 dic 2025, 13:16 Markus Armbruster <armbru@redhat.com> ha scritto:
> > Note however that there is no support for push parsing, therefore this
> > would not replace the balanced-parentheses machinery in
> > qobject/json-streamer.c, and therefore QMP would still need a minimal
> lexer.
>
> That push parser... I never liked it. First, it's half-assed: it's a
> push lexer wed to a pull parser with parenthesis counting. Second, why
> complicated & half-assed when you can do simple & quarter-assed instead?
> We could've required "exactly one complete JSON value per line", or some
> expression separator such as an empty line.
>
Hmm not sure I agree, actually I think I disagree. It seems simpler but it
is also different.
Push parsing is not rocket science. It would be easy to write a proper one,
it's just that there is no real reason other than cleanliness.
> Grr... I just remembered about interpolation :/ so no, we still need a
> > parser for libqmp.c.
>
> Right.
>
> Interpolation lets us build QObjects from literal templates with
> variable scalars or QObjects interpolated. More concise and much easier
> to read than the equivalend nest of constructor calls. Drawback: chains
> us to our own, bespoke JSON parser.
>
And also, for similar reasons of practicality, single quotes (which IIRC
also became valid QMP, and that's less excusable).
But while it's a pity, we still get a lot from serde, namely making the
automatic generation of visitor code for structs someone else's problem.
Out of curiosity: how would we do better than "nest of constructor
> calls" in Rust?
>
You'd do that with a quoting macro, i.e.
qobject!({"command": cmd})
where the macro compiles to the nest of constructor calls, like
QObject::from_iter([(c"command", to_qobject(cmd))]).
Paolo
>
[-- Attachment #2: Type: text/html, Size: 3325 bytes --]
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 11/19] rust/qobject: add from/to JSON bindings for QObject
2025-12-08 7:00 ` Paolo Bonzini
@ 2025-12-08 9:17 ` Markus Armbruster
2025-12-09 7:34 ` Paolo Bonzini
0 siblings, 1 reply; 48+ messages in thread
From: Markus Armbruster @ 2025-12-08 9:17 UTC (permalink / raw)
To: Paolo Bonzini
Cc: qemu-devel, Marc-André Lureau,
open list:Rust-related patc...
Paolo Bonzini <pbonzini@redhat.com> writes:
> Il ven 5 dic 2025, 13:16 Markus Armbruster <armbru@redhat.com> ha scritto:
>
>> > Note however that there is no support for push parsing, therefore this
>> > would not replace the balanced-parentheses machinery in
>> > qobject/json-streamer.c, and therefore QMP would still need a minimal
>> > lexer.
>>
>> That push parser... I never liked it. First, it's half-assed: it's a
>> push lexer wed to a pull parser with parenthesis counting. Second, why
>> complicated & half-assed when you can do simple & quarter-assed instead?
>> We could've required "exactly one complete JSON value per line", or some
>> expression separator such as an empty line.
>
> Hmm not sure I agree, actually I think I disagree. It seems simpler but it
> is also different.
Management applications would be just fine with the different interface.
Human users would be better off. As is, a missing right parenthesis is
met with silence. You can then input whatever you want, and get more
silence until you somehow close the last parenthesis. Quite confusing
unless you already know.
> Push parsing is not rocket science. It would be easy to write a proper one,
> it's just that there is no real reason other than cleanliness.
Me not liking what we have, when what we have works, is no reason to
rewrite it.
Even the lousy usability for humans doesn't justify a rewrite.
>> > Grr... I just remembered about interpolation :/ so no, we still need a
>> > parser for libqmp.c.
>>
>> Right.
>>
>> Interpolation lets us build QObjects from literal templates with
>> variable scalars or QObjects interpolated. More concise and much easier
>> to read than the equivalend nest of constructor calls. Drawback: chains
>> us to our own, bespoke JSON parser.
>
> And also, for similar reasons of practicality, single quotes (which IIRC
> also became valid QMP, and that's less excusable).
Single quotes should've been restricted to internal use, just like
interpolation.
> But while it's a pity, we still get a lot from serde, namely making the
> automatic generation of visitor code for structs someone else's problem.
>
>> Out of curiosity: how would we do better than "nest of constructor
>> calls" in Rust?
>
> You'd do that with a quoting macro, i.e.
>
> qobject!({"command": cmd})
>
> where the macro compiles to the nest of constructor calls, like
> QObject::from_iter([(c"command", to_qobject(cmd))]).
Thanks!
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 00/19] rust: QObject and QAPI bindings
2025-12-05 13:55 ` Markus Armbruster
@ 2025-12-09 6:01 ` Markus Armbruster
2025-12-10 14:12 ` Paolo Bonzini
0 siblings, 1 reply; 48+ messages in thread
From: Markus Armbruster @ 2025-12-09 6:01 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, marcandre.lureau, qemu-rust
Markus Armbruster <armbru@redhat.com> writes:
> I applied, ran make, and it didn't create qapi-types.rs and
> test-qapi-types.rs for me. What am I missing?
Looks like I have to run with -B qapi.backend.QAPIRsBackend.
-B is meant for out-of-tree backends. Commit dde279925c9 explains:
qapi: pluggable backend code generators
The 'qapi.backend.QAPIBackend' class defines an API contract for code
generators. The current generator is put into a new class
'qapi.backend.QAPICBackend' and made to be the default impl.
A custom generator can be requested using the '-k' arg which takes a
fully qualified python class name
qapi-gen.py -B the.python.module.QAPIMyBackend
This allows out of tree code to use the QAPI generator infrastructure
to create new language bindings for QAPI schemas. This has the caveat
that the QAPI generator APIs are not guaranteed stable, so consumers
of this feature may have to update their code to be compatible with
future QEMU releases.
Using it for the in-tree Rust backend is fine for a prototype.
Mentioning it in a commit message or the cover letter would've saved me
some digging.
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 11/19] rust/qobject: add from/to JSON bindings for QObject
2025-12-08 9:17 ` Markus Armbruster
@ 2025-12-09 7:34 ` Paolo Bonzini
0 siblings, 0 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-12-09 7:34 UTC (permalink / raw)
To: Markus Armbruster
Cc: qemu-devel, Marc-André Lureau,
open list:Rust-related patc...
On 12/8/25 10:17, Markus Armbruster wrote:
> Paolo Bonzini <pbonzini@redhat.com> writes:
>
>> Il ven 5 dic 2025, 13:16 Markus Armbruster <armbru@redhat.com> ha scritto:
>>
>>>> Note however that there is no support for push parsing, therefore this
>>>> would not replace the balanced-parentheses machinery in
>>>> qobject/json-streamer.c, and therefore QMP would still need a minimal
>>>> lexer.
>>>
>>> That push parser... I never liked it. First, it's half-assed: it's a
>>> push lexer wed to a pull parser with parenthesis counting. Second, why
>>> complicated & half-assed when you can do simple & quarter-assed instead?
>>> We could've required "exactly one complete JSON value per line", or some
>>> expression separator such as an empty line.
>>
>> Hmm not sure I agree, actually I think I disagree. It seems simpler but it
>> is also different.
>
> Management applications would be just fine with the different interface.
>
> Human users would be better off. As is, a missing right parenthesis is
> met with silence. You can then input whatever you want, and get more
> silence until you somehow close the last parenthesis. Quite confusing
> unless you already know.
Not being able to add line breaks to a long JSON command (preparing it
in an editor and pasting it into either a "-qmp stdio" terminal or
socat/nc) would be a dealbreaker for me.
JSON says whitespace is irrelevant. "This standard format but..." is a
bad idea that requires extraordinary justification. Not that it matters
now 15 years down the line. :)
Paolo
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 16/19] scripts/qapi: strip trailing whitespaces
2025-10-10 15:10 ` [PATCH 16/19] scripts/qapi: strip trailing whitespaces Paolo Bonzini
@ 2025-12-09 8:48 ` Markus Armbruster
2025-12-10 17:28 ` Paolo Bonzini
0 siblings, 1 reply; 48+ messages in thread
From: Markus Armbruster @ 2025-12-09 8:48 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, armbru, marcandre.lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>
> This help workaround a rustfmt issue.
Which one? Pointer suffices.
>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Link: https://lore.kernel.org/r/20210907121943.3498701-16-marcandre.lureau@redhat.com
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
> scripts/qapi/gen.py | 6 +++++-
> 1 file changed, 5 insertions(+), 1 deletion(-)
>
> diff --git a/scripts/qapi/gen.py b/scripts/qapi/gen.py
> index 0c9b8db3b02..c9721545ea7 100644
> --- a/scripts/qapi/gen.py
> +++ b/scripts/qapi/gen.py
> @@ -58,7 +58,11 @@ def add(self, text: str) -> None:
> self._body += text
>
> def get_content(self) -> str:
> - return self._top() + self._preamble + self._body + self._bottom()
> + content = self._top() + self._preamble + self._body + self._bottom()
> + # delete trailing white-spaces (working around
> + # https://github.com/rust-lang/rustfmt/issues/4248)
> + content = re.sub(r'\s+$', '\n', content, 0, re.M)
> + return content
>
> def _top(self) -> str:
> # pylint: disable=no-self-use
This doesn't just delete trailing whitespace, it also collapses multiple
blank lines into one: \s matches newlines.
We lose the ability to generate multiple blank lines for all generators
based on QAPIGen: C (.c and .h), trace events, Rust. Hmm.
Is collapsing blank lines necessary for working around the rustfmt
issue?
The generators other than the Rust generator do not emit trailing
whitespace. Would that be practical for the Rust generator, too?
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 14/19] scripts/qapi: generate high-level Rust bindings
2025-10-10 15:09 ` [PATCH 14/19] scripts/qapi: generate high-level Rust bindings Paolo Bonzini
@ 2025-12-09 10:03 ` Markus Armbruster
2025-12-10 14:38 ` Paolo Bonzini
0 siblings, 1 reply; 48+ messages in thread
From: Markus Armbruster @ 2025-12-09 10:03 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, marcandre.lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>
> Generate high-level native Rust declarations for the QAPI types.
>
> - char* is mapped to String, scalars to there corresponding Rust types
>
> - enums are simply aliased from FFI
>
> - has_foo/foo members are mapped to Option<T>
>
> - lists are represented as Vec<T>
>
> - structures have Rust versions, with To/From FFI conversions
>
> - alternate are represented as Rust enum
>
> - unions are represented in a similar way as in C: a struct S with a "u"
> member (since S may have extra 'base' fields). However, the discriminant
> isn't a member of S, since Rust enum already include it.
>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Link: https://lore.kernel.org/r/20210907121943.3498701-21-marcandre.lureau@redhat.com
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
I like to look at the generated code before I look at the code
generator. My go-to schema for a first look is example-schema.json from
docs/devel/qapi-code-gen.rst:
{ 'struct': 'UserDefOne',
'data': { 'integer': 'int', '*string': 'str', '*flag': 'bool' } }
{ 'command': 'my-command',
'data': { 'arg1': ['UserDefOne'] },
'returns': 'UserDefOne' }
{ 'event': 'MY_EVENT' }
Generated example-qapi-types.rs:
// @generated by qapi-gen, DO NOT EDIT
#![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;
#[repr(u32)]
#[derive(Copy, Clone, Debug, PartialEq, common::TryInto)]
pub enum QType {
NONE,
QNULL,
QNUM,
QSTRING,
QDICT,
QLIST,
QBOOL,
_MAX,
}
impl Default for QType {
#[inline]
fn default() -> QType {
Self::NONE
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct UserDefOne {
pub integer: i64,
pub string: Option<String>,
pub flag: Option<bool>,
}
Questions / observations:
* Why is util::qobject::QObject needed?
* NONE is an error value, not a valid QType. Having such error values
in enums isn't unusual in C. What about idiomatic Rust? Even if it's
unusual there, we may elect to do it anyway, just to keep generated
Rust closer to C. But it should be a conscious decision, not a blind
port from C to Rust.
* "Default for QType" is NONE. In C, it's zero bytes, which boils down
to QTYPE_NONE.
* QTYPE__MAX is a bit of a headache in C. It's not a valid enum value.
We make it one only because we need to know the largest valid enum
value, e.g. to size arrays, and the easiest way to get that value is
adding an invalid one to the enum. Same for all the other generated
enums. Could we avoid it in Rust?
* Blank lines before the values of enum QType and the members of struct
UserDefOne contain spaces. PATCH 16 will remove the spaces.
* Definitions are separated by two blank lines. PATCH 16 will collapse
them into one.
Compare to example-qapi-types.h:
/* AUTOMATICALLY GENERATED by qapi-gen.py DO NOT MODIFY */
/*
* Schema-defined QAPI types
*
* Copyright IBM, Corp. 2011
* Copyright (c) 2013-2018 Red Hat Inc.
*
* This work is licensed under the terms of the GNU LGPL, version 2.1 or later.
* See the COPYING.LIB file in the top-level directory.
*/
#ifndef EXAMPLE_QAPI_TYPES_H
#define EXAMPLE_QAPI_TYPES_H
#include "qapi/qapi-builtin-types.h"
typedef struct UserDefOne UserDefOne;
typedef struct UserDefOneList UserDefOneList;
typedef struct q_obj_my_command_arg q_obj_my_command_arg;
struct UserDefOne {
int64_t integer;
char *string;
bool has_flag;
bool flag;
};
void qapi_free_UserDefOne(UserDefOne *obj);
G_DEFINE_AUTOPTR_CLEANUP_FUNC(UserDefOne, qapi_free_UserDefOne)
struct UserDefOneList {
UserDefOneList *next;
UserDefOne *value;
};
void qapi_free_UserDefOneList(UserDefOneList *obj);
G_DEFINE_AUTOPTR_CLEANUP_FUNC(UserDefOneList, qapi_free_UserDefOneList)
struct q_obj_my_command_arg {
UserDefOneList *arg1;
};
#endif /* EXAMPLE_QAPI_TYPES_H */
Observations:
* C has a file comment of the form
/*
* One-line description of the file's purpose
*
* Copyright lines
*
* License blurb
*/
I think Rust could use such a comment, too.
* C has built-in types like QType in qapi-builtin-types.h, generated
only with -b. This is a somewhat crude way to let code generated for
multiple schemas coexist: pass -b for exactly one of them. If we
generated code for built-in types unconditionally into qapi-types.h,
the C compiler would choke on duplicate definitions. Why is this not
a problem with Rust?
* The Rust version doesn't have deallocation boilerplate. Deallocation
just works there, I guess.
* The Rust version doesn't have the List type. Lists just work there, I
guess.
* The Rust version doesn't have the implicit type q_obj_my_command_arg,
which is the arguments of my-command as a struct type. C needs it for
marshaling / unmarshaling with visitors. Rust doesn't, because we use
serde. Correct?
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 15/19] scripts/qapi: add serde attributes
2025-10-10 15:10 ` [PATCH 15/19] scripts/qapi: add serde attributes Paolo Bonzini
@ 2025-12-09 12:24 ` Markus Armbruster
0 siblings, 0 replies; 48+ messages in thread
From: Markus Armbruster @ 2025-12-09 12:24 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, armbru, marcandre.lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>
> Generate serde attributes to match the serialization format to QAPI's:
>
> - for enums, map Rust enum variants to original QAPI names
>
> - for structs, rejects JSON with extra fields and omit optional fields
> (as opposed to serializing them as null)
>
> - for union variants:
> - use tagged union format matching QAPI's discriminator,
> - map variant names to original QAPI names
> - flatten union data into parent struct
>
> - for alternates, use type-based discrimination
>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
Again, quick look at the generated code first.
Diff since the previous patch, with comments inline:
diff -rup ex/commit-450ce0c076/example-qapi-types.rs ex/commit-dc1aa3de44/example-qapi-types.rs
--- ex/commit-450ce0c076/example-qapi-types.rs 2025-12-09 08:28:34.167519593 +0100
+++ ex/commit-dc1aa3de44/example-qapi-types.rs 2025-12-09 08:30:13.913250024 +0100
@@ -11,27 +11,38 @@
// that *could* be Eq too.
#![allow(clippy::derive_partial_eq_without_eq)]
+use serde_derive::{Serialize, Deserialize};
+
use util::qobject::QObject;
#[repr(u32)]
-#[derive(Copy, Clone, Debug, PartialEq, common::TryInto)]
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize,
+ common::TryInto)]
pub enum QType {
+ #[serde(rename = "none")]
NONE,
NONE is an error value. It must not occur as type of a QObject (see
qobject_type()'s assertion). It should never occur in serialization /
deserializion. Is there a way to instruct serde accordingly?
Related: generated QType_lookup[] maps QTYPE_NONE to "none", because the
generator special case required to map it to NULL isn't worth the
bother.
+ #[serde(rename = "qnull")]
QNULL,
+ #[serde(rename = "qnum")]
QNUM,
+ #[serde(rename = "qstring")]
QSTRING,
+ #[serde(rename = "qdict")]
QDICT,
+ #[serde(rename = "qlist")]
QLIST,
+ #[serde(rename = "qbool")]
QBOOL,
+ #[serde(rename = "_MAX")]
_MAX,
This one must not occur, either. Generated QType_lookup[] does not have
a value for it.
}
@@ -44,12 +55,16 @@ impl Default for QType {
}
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
pub struct UserDefOne {
+
Funny extra blank line, next patch will revert it.
pub integer: i64,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub string: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub flag: Option<bool>,
}
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 13/19] scripts/qapi: add QAPISchemaIfCond.rsgen()
2025-10-10 15:09 ` [PATCH 13/19] scripts/qapi: add QAPISchemaIfCond.rsgen() Paolo Bonzini
@ 2025-12-09 18:43 ` Markus Armbruster
0 siblings, 0 replies; 48+ messages in thread
From: Markus Armbruster @ 2025-12-09 18:43 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, marcandre.lureau, qemu-rust, John Snow
Paolo Bonzini <pbonzini@redhat.com> writes:
> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>
> Generate Rust #[cfg(...)] guards from QAPI 'if' conditions.
Please mention that this isn't used, yet. I commonly write something
like "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 | 16 ++++++++++++++++
> scripts/qapi/schema.py | 4 ++++
> 2 files changed, 20 insertions(+)
>
> diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
> index d7c8aa3365c..f16b9568bb9 100644
> --- a/scripts/qapi/common.py
> +++ b/scripts/qapi/common.py
> @@ -199,6 +199,22 @@ def guardend(name: str) -> str:
> name=c_fname(name).upper())
>
>
> +def rsgen_ifcond(ifcond: Optional[Union[str, Dict[str, Any]]]) -> str:
This is the Rust-generating cousin of cgen_ifcond().
The argument is None or a tree. The tree's leaves are str, and its
inner nodes look like
{'all': [sub-tree, ...]}
{'any': [sub-tree, ...]}
{'not': sub-tree}
mypy doesn't do recursive types, so we approximate the tree as
Union[str, Dict[str, Any]].
> +
> + def cfg(ifcond: Union[str, Dict[str, Any]]) -> str:
The argument's type is wrong. It should be
Union[str, List[Dict[str, Any]], Dict[str, Any]]
We'll see below why mypy doesn't complain, and where the List[...]
comes from.
The name @ifcond is misleading. This isn't an if condition, it's the
union of if condition and list of if conditions.
I needed John Snow's help to figure this out. Thanks, John!
Case 1: the code for str, i.e. a tree leaf:
> + if isinstance(ifcond, str):
> + return ifcond
Case 2: the code for List[Dict[str, Any]]:
> + if isinstance(ifcond, list):
> + return ', '.join([cfg(c) for c in ifcond])
Case 3: the code for Dict[str, Any]:
gen_ifcond() below has
assert isinstance(ifcond, dict) and len(ifcond) == 1
right here to make this crystal clear.
> + oper, operands = next(iter(ifcond.items()))
Recall @ifcond is either
{'all': [sub-tree, ...]}
{'any': [sub-tree, ...]}
{'not': sub-tree}
The next(...) wizardry returns the first element of the @ifcond
dictionary. Actually *the* element, since @ifcond has just one.
Thus:
@oper is 'all', 'any', or 'not'
@operands is a sub-tree when @oper is 'not', else a [sub-tree, ...],
i.e. Dict[str, Any] or List[Dict[str, Any]]
> + operands = cfg(operands)
We pass @operands to cfg(). That's where the List[...] comes from.
> + return f'{oper}({operands})'
> +
> + if not ifcond:
> + return ''
> + return '#[cfg(%s)]' % cfg(ifcond)
So, cfg(ifcond) returns the argument to interpolate into '#[cfg(%s)]'.
When @ifcond is str, it's @ifcond itself. This is case 1.
When @ifcond is {'not': COND}, it's 'not(CC)', where CC is cfg(COND).
This is case 3 and case 2 with a non-list argument.
When @ifcond is {'all': [COND, ...]}, it's 'all(CC, ...)', where the CC
are cfg(COND). This is case 3 and case 2 with a list argument.
Likewise for {'any': [COND, ...]}.
Okay apart from the incorrect type hint and the misleading name.
Less clever code would've saved me quite some review time.
But why doesn't mypy scream? Consider again
oper, operands = next(iter(ifcond.items()))
@ifcond's static type is Dict[str, Any]. Therefore @oper's static type
is str, and @operands is Any. Any suppresses type checking! The call
cfg(operands) is therefore *not* checked, and we get away with passing a
list to cfg() even though its type hint doesn't allow it.
> +
> +
> def gen_ifcond(ifcond: Optional[Union[str, Dict[str, Any]]],
> cond_fmt: str, not_fmt: str,
> all_operator: str, any_operator: str) -> str:
> diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
> index 8d88b40de2e..848a7401251 100644
> --- a/scripts/qapi/schema.py
> +++ b/scripts/qapi/schema.py
> @@ -37,6 +37,7 @@
> docgen_ifcond,
> gen_endif,
> gen_if,
> + rsgen_ifcond,
> )
> from .error import QAPIError, QAPISemError, QAPISourceError
> from .expr import check_exprs
> @@ -63,6 +64,9 @@ def gen_endif(self) -> str:
> def docgen(self) -> str:
> return docgen_ifcond(self.ifcond)
>
> + def rsgen(self) -> str:
> + return rsgen_ifcond(self.ifcond)
> +
> def is_present(self) -> bool:
> return bool(self.ifcond)
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 00/19] rust: QObject and QAPI bindings
2025-12-09 6:01 ` Markus Armbruster
@ 2025-12-10 14:12 ` Paolo Bonzini
2025-12-10 14:50 ` Markus Armbruster
0 siblings, 1 reply; 48+ messages in thread
From: Paolo Bonzini @ 2025-12-10 14:12 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, marcandre.lureau, qemu-rust
On 12/9/25 07:01, Markus Armbruster wrote:
> Markus Armbruster <armbru@redhat.com> writes:
>
>> I applied, ran make, and it didn't create qapi-types.rs and
>> test-qapi-types.rs for me. What am I missing?
>
> Looks like I have to run with -B qapi.backend.QAPIRsBackend.
>
> -B is meant for out-of-tree backends. Commit dde279925c9 explains:
>
> qapi: pluggable backend code generators
>
> The 'qapi.backend.QAPIBackend' class defines an API contract for code
> generators. The current generator is put into a new class
> 'qapi.backend.QAPICBackend' and made to be the default impl.
>
> A custom generator can be requested using the '-k' arg which takes a
> fully qualified python class name
>
> qapi-gen.py -B the.python.module.QAPIMyBackend
>
> This allows out of tree code to use the QAPI generator infrastructure
> to create new language bindings for QAPI schemas. This has the caveat
> that the QAPI generator APIs are not guaranteed stable, so consumers
> of this feature may have to update their code to be compatible with
> future QEMU releases.
>
> Using it for the in-tree Rust backend is fine for a prototype.
> Mentioning it in a commit message or the cover letter would've saved me
> some digging.
Well, it wasn't intentional - right now it does this:
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 ])
so "make rust/tests/test-qapi-types.rs" will work, and so will "make" if
you have --enable-rust.
Let us know what you'd prefer and we'll switch. Alternatively,
retconning -B's meaning so that it applies to Rust will work too. :)
Paolo
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 14/19] scripts/qapi: generate high-level Rust bindings
2025-12-09 10:03 ` Markus Armbruster
@ 2025-12-10 14:38 ` Paolo Bonzini
2025-12-11 7:25 ` Markus Armbruster
0 siblings, 1 reply; 48+ messages in thread
From: Paolo Bonzini @ 2025-12-10 14:38 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, marcandre.lureau, qemu-rust
On 12/9/25 11:03, Markus Armbruster wrote:
> * Why is util::qobject::QObject needed?
>
> * NONE is an error value, not a valid QType. Having such error values
> in enums isn't unusual in C. What about idiomatic Rust? Even if it's
> unusual there, we may elect to do it anyway, just to keep generated
> Rust closer to C. But it should be a conscious decision, not a blind
> port from C to Rust.
For QType we don't need to keep it closer, but actually ABI-compatible:
QType is defined by QAPI but is used (almost exclusively) by QObject.
We use the C version in the QObject bindings, for example:
$($crate::bindings::QTYPE_QNULL => break $unit,)?
> * "Default for QType" is NONE. In C, it's zero bytes, which boils down
> to QTYPE_NONE.
>
> * QTYPE__MAX is a bit of a headache in C. It's not a valid enum value.
> We make it one only because we need to know the largest valid enum
> value, e.g. to size arrays, and the easiest way to get that value is
> adding an invalid one to the enum. Same for all the other generated
> enums. Could we avoid it in Rust?
Yes, I think so.
> * C has a file comment of the form
>
> /*
> * One-line description of the file's purpose
> *
> * Copyright lines
> *
> * License blurb
> */
>
> I think Rust could use such a comment, too.
Ok.
> * C has built-in types like QType in qapi-builtin-types.h, generated
> only with -b. This is a somewhat crude way to let code generated for
> multiple schemas coexist: pass -b for exactly one of them. If we
> generated code for built-in types unconditionally into qapi-types.h,
> the C compiler would choke on duplicate definitions. Why is this not
> a problem with Rust?
Because there's better namespacing, so it's okay to define the builtin
types in more than one place. However, do we need at all the builtin
types in Rust? QType is only defined in QAPI to have the nice enum
lookup tables, and we can get it via FFI bindings. Lists, as you say
below, are not needed, and they are also a part of qapi-builtin-types.h.
So I think Rust does not need built-in types at all, which I think
solves all your problems here (other than _MAX which can be removed).
>
> * The Rust version doesn't have deallocation boilerplate. Deallocation
> just works there, I guess.
>
> * The Rust version doesn't have the List type. Lists just work there, I
> guess.
Yep.
> * The Rust version doesn't have the implicit type q_obj_my_command_arg,
> which is the arguments of my-command as a struct type. C needs it for
> marshaling / unmarshaling with visitors. Rust doesn't, because we use
> serde. Correct?
Commands are not supported at all yet.
Paolo
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 00/19] rust: QObject and QAPI bindings
2025-12-10 14:12 ` Paolo Bonzini
@ 2025-12-10 14:50 ` Markus Armbruster
2025-12-10 16:01 ` Paolo Bonzini
0 siblings, 1 reply; 48+ messages in thread
From: Markus Armbruster @ 2025-12-10 14:50 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, marcandre.lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> On 12/9/25 07:01, Markus Armbruster wrote:
>> Markus Armbruster <armbru@redhat.com> writes:
>>
>>> I applied, ran make, and it didn't create qapi-types.rs and
>>> test-qapi-types.rs for me. What am I missing?
>>
>> Looks like I have to run with -B qapi.backend.QAPIRsBackend.
>>
>> -B is meant for out-of-tree backends. Commit dde279925c9 explains:
>>
>> qapi: pluggable backend code generators
>>
>> The 'qapi.backend.QAPIBackend' class defines an API contract for code
>> generators. The current generator is put into a new class
>> 'qapi.backend.QAPICBackend' and made to be the default impl.
>>
>> A custom generator can be requested using the '-k' arg which takes a
>> fully qualified python class name
>>
>> qapi-gen.py -B the.python.module.QAPIMyBackend
>>
>> This allows out of tree code to use the QAPI generator infrastructure
>> to create new language bindings for QAPI schemas. This has the caveat
>> that the QAPI generator APIs are not guaranteed stable, so consumers
>> of this feature may have to update their code to be compatible with
>> future QEMU releases.
>>
>> Using it for the in-tree Rust backend is fine for a prototype.
>> Mentioning it in a commit message or the cover letter would've saved me
>> some digging.
>
> Well, it wasn't intentional - right now it does this:
>
> 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 ])
>
> so "make rust/tests/test-qapi-types.rs" will work, and so will "make" if
> you have --enable-rust.
>
> Let us know what you'd prefer and we'll switch. Alternatively,
> retconning -B's meaning so that it applies to Rust will work too. :)
Any particular reason *not* to generate Rust unconditionally along with
C?
To do it, stick gen_rs_types() into QAPICodeBackend.generate(). Then
the build runs qapi-gen at most once[*].
[*] Lie. Sphinx runs the *frontend* another time.
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 00/19] rust: QObject and QAPI bindings
2025-12-10 14:50 ` Markus Armbruster
@ 2025-12-10 16:01 ` Paolo Bonzini
2025-12-10 16:59 ` Markus Armbruster
0 siblings, 1 reply; 48+ messages in thread
From: Paolo Bonzini @ 2025-12-10 16:01 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, marcandre.lureau, qemu-rust
On 12/10/25 15:50, Markus Armbruster wrote:
>> Let us know what you'd prefer and we'll switch. Alternatively,
>> retconning -B's meaning so that it applies to Rust will work too. :)
>
> Any particular reason *not* to generate Rust unconditionally along with
> C?
Two reasons I can think of:
1) the exact layout is still in flux (not modular, no commands or events).
2) Rust prefers to have all files under a common path ("mod xyz" directs
the compiler to look at xyz.rs or xyz/mod.rs), which doesn't work well
if they are created in qapi/meson.build. There's plenty of workarounds,
but I'd rather not pick one until the layout is fixed.
So maybe for now we can use -B and not pollute qapi/meson.build, and
when the generator is more complete we can move Rust generation there?
Paolo
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 00/19] rust: QObject and QAPI bindings
2025-12-10 16:01 ` Paolo Bonzini
@ 2025-12-10 16:59 ` Markus Armbruster
0 siblings, 0 replies; 48+ messages in thread
From: Markus Armbruster @ 2025-12-10 16:59 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, marcandre.lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> On 12/10/25 15:50, Markus Armbruster wrote:
>>> Let us know what you'd prefer and we'll switch. Alternatively,
>>> retconning -B's meaning so that it applies to Rust will work too. :)
>>
>> Any particular reason *not* to generate Rust unconditionally along with
>> C?
>
> Two reasons I can think of:
>
> 1) the exact layout is still in flux (not modular, no commands or events).
>
> 2) Rust prefers to have all files under a common path ("mod xyz" directs
> the compiler to look at xyz.rs or xyz/mod.rs), which doesn't work well
> if they are created in qapi/meson.build. There's plenty of workarounds,
> but I'd rather not pick one until the layout is fixed.
>
> So maybe for now we can use -B and not pollute qapi/meson.build, and
> when the generator is more complete we can move Rust generation there?
>
> Paolo
Using -B for an in-tree prototype is fine with me. I'd like a suitable
comment, though.
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 06/19] rust/qobject: add Serialize implementation
2025-12-05 9:47 ` Markus Armbruster
@ 2025-12-10 17:19 ` Paolo Bonzini
2025-12-11 7:07 ` Markus Armbruster
0 siblings, 1 reply; 48+ messages in thread
From: Paolo Bonzini @ 2025-12-10 17:19 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, marcandre.lureau, qemu-rust
On 12/5/25 10:47, Markus Armbruster wrote:
>> + match_qobject! { (self) =>
>> + () => serializer.serialize_unit(),
>> + bool(b) => serializer.serialize_bool(b),
>> + i64(i) => serializer.serialize_i64(i),
>> + u64(u) => serializer.serialize_u64(u),
>> + f64(f) => serializer.serialize_f64(f),
>> + CStr(cstr) => cstr.to_str().map_or_else(
>> + |_| Err(ser::Error::custom("invalid UTF-8 in QString")),
>
> Could this be a programming error? Like flawed input validation?
Possibly, but given that you have to create a custom error type anyway,
I'd rather not special case this into the only abort (see the
"#![deny(clippy::unwrap_used)]" in patch 4).
Paolo
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 16/19] scripts/qapi: strip trailing whitespaces
2025-12-09 8:48 ` Markus Armbruster
@ 2025-12-10 17:28 ` Paolo Bonzini
0 siblings, 0 replies; 48+ messages in thread
From: Paolo Bonzini @ 2025-12-10 17:28 UTC (permalink / raw)
To: Markus Armbruster; +Cc: qemu-devel, marcandre.lureau, qemu-rust
On 12/9/25 09:48, Markus Armbruster wrote:
> Paolo Bonzini <pbonzini@redhat.com> writes:
>
>> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>>
>> This help workaround a rustfmt issue.
>
> Which one? Pointer suffices.
>
>>
>> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
>> Link: https://lore.kernel.org/r/20210907121943.3498701-16-marcandre.lureau@redhat.com
>> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
>> ---
>> scripts/qapi/gen.py | 6 +++++-
>> 1 file changed, 5 insertions(+), 1 deletion(-)
>>
>> diff --git a/scripts/qapi/gen.py b/scripts/qapi/gen.py
>> index 0c9b8db3b02..c9721545ea7 100644
>> --- a/scripts/qapi/gen.py
>> +++ b/scripts/qapi/gen.py
>> @@ -58,7 +58,11 @@ def add(self, text: str) -> None:
>> self._body += text
>>
>> def get_content(self) -> str:
>> - return self._top() + self._preamble + self._body + self._bottom()
>> + content = self._top() + self._preamble + self._body + self._bottom()
>> + # delete trailing white-spaces (working around
>> + # https://github.com/rust-lang/rustfmt/issues/4248)
>> + content = re.sub(r'\s+$', '\n', content, 0, re.M)
>> + return content
>>
>> def _top(self) -> str:
>> # pylint: disable=no-self-use
>
> This doesn't just delete trailing whitespace, it also collapses multiple
> blank lines into one: \s matches newlines.
>
> We lose the ability to generate multiple blank lines for all generators
> based on QAPIGen: C (.c and .h), trace events, Rust. Hmm.
>
> Is collapsing blank lines necessary for working around the rustfmt
> issue?
>
> The generators other than the Rust generator do not emit trailing
> whitespace. Would that be practical for the Rust generator, too?
The main source is stuff like %(cfg)s and %(serde_skip_if)s. Rust's
syntax makes it more natural to write in this style, compared to C where
you have "#ifdef"..."#endif", but it results in empty lines. I'll see
if there's another way to clean up the output.
Paolo
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 06/19] rust/qobject: add Serialize implementation
2025-12-10 17:19 ` Paolo Bonzini
@ 2025-12-11 7:07 ` Markus Armbruster
0 siblings, 0 replies; 48+ messages in thread
From: Markus Armbruster @ 2025-12-11 7:07 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: qemu-devel, marcandre.lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> On 12/5/25 10:47, Markus Armbruster wrote:
>>> + match_qobject! { (self) =>
>>> + () => serializer.serialize_unit(),
>>> + bool(b) => serializer.serialize_bool(b),
>>> + i64(i) => serializer.serialize_i64(i),
>>> + u64(u) => serializer.serialize_u64(u),
>>> + f64(f) => serializer.serialize_f64(f),
>>> + CStr(cstr) => cstr.to_str().map_or_else(
>>> + |_| Err(ser::Error::custom("invalid UTF-8 in QString")),
>>
>> Could this be a programming error? Like flawed input validation?
qobject's JSON parser validates, see parse_string().
> Possibly, but given that you have to create a custom error type anyway,
> I'd rather not special case this into the only abort (see the
> "#![deny(clippy::unwrap_used)]" in patch 4).
We *should* abort on programming error.
.to_str() fails when its argument is invalid UTF-8. It returns "an
error with details of where UTF-8 validation failed."[1]
Why are we replacing this error with a custom one? I guess we add the
clue "in QString". We also lose the details of where. Feels like a
questionable trade.
Can we use .expect()? It panics "if the value is an Err, with a panic
message including the passed message, and the content of the Err."[2]
If we decide this isn't a programming error (because QString may contain
arbitrary zero-terminated byte sequences[3]): can we combine all the
readily available information like .expect() does?
[1] https://doc.rust-lang.org/std/ffi/struct.CStr.html#method.to_str
[2] https://doc.rust-lang.org/std/result/enum.Result.html#method.expect
[3] Feels like a problematic idea to me.
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 04/19] rust/qobject: add basic bindings
2025-12-05 11:27 ` Paolo Bonzini
@ 2025-12-11 7:21 ` Markus Armbruster
0 siblings, 0 replies; 48+ messages in thread
From: Markus Armbruster @ 2025-12-11 7:21 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: Markus Armbruster, qemu-devel, marcandre.lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> On 12/5/25 10:35, Markus Armbruster wrote:
>>> +/// 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>);
>>
>> This defines the Rust QObject. All it contains is a reference (wrapped
>> in UnsafeCell) self.0 to the C QObject. Correct?
>
> Correct.
>
>>> +
>>> +// 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.
>>
>> In other words, we promise never to change a QObject while Rust code
>> holds a reference, except for the reference counts. Correct?
>>
>> The reference count is the mutable part of an otherwise immutable
>> object. Not mentioned here: it is atomic. Therefore, concurrent
>> updates cannot mess it up. Nothing depends on its value except
>> deallocation when the last reference drops. I figure that's why the
>> exception to "aliased XOR mutable" is fine. Correct?
>
> Yes, it's one of a few exceptions to "aliased XOR mutable" including:
>
> - Mutex (because only one guy can access it at all anyway)
>
> - RefCell (enforces aliased XOR mutable at run-time, enforces
> single-thread usage at compile-time)
>
> - atomics (a mini mutex)
>
> - Cell (Mutex:RefCell = atomics:Cell, in other words every access is
> independent but also single-thread usage is checked at compile time)
>
>>> +unsafe impl Send for QObject {}
>>> +unsafe impl Sync for QObject {}
>>> +
>>> +// Since a QObject can be a floating-point value, and potentially a NaN,
>>> +// do not implement Eq
>>> +impl PartialEq for QObject {
>>> + fn eq(&self, other: &Self) -> bool {
>>> + unsafe { bindings::qobject_is_equal(self.0.get(), other.0.get()) }
>>> + }
>>> +}
>>> +
>>> +impl QObject {
>>> + /// Construct a [`QObject`] from a C `QObjectBase` pointer.
>>
>> It's spelled QObjectBase_. More of the same below, not flagging again.
>>
>> Comment next to its definition:
>>
>> /* Not for use outside include/qobject/ */
>>
>> We're using it outside now. Do we really need to?
>
> It's because we're defining equivalents of inline functions in
> include/qobject.
Fair, but we should update comments when we make them wrong :)
> I can however replace uses of from_base with a macro similar to QOBJECT()
Might be cleaner.
>>> + /// Obtain a raw C pointer from a reference. `self` is consumed
>>> + /// and the C `QObject` pointer is leaked.
>>
>> What exactly do you mean by "leaked"?
>
> s/and the.*/without decreasing the reference count, thus transferring
> the reference to the `*mut bindings::QOjbect`/
Much clearer.
>>> + pub fn into_raw(self) -> *mut bindings::QObject {
>>> + let src = ManuallyDrop::new(self);
>>> + src.0.get()
>>> + }
>>> +
>>> + /// Construct a [`QObject`] from a C `QObject` pointer.
>>
>> Pasto? Isn't it QObjectBase_ here?
>
> Yes.
>
>>> +impl From<()> for QObject {
>>> + fn from(_null: ()) -> Self {
>>> + unsafe { QObject::cloned_from_base(addr_of!(bindings::qnull_.base)) }
>>
>> qnull_ is not meant for use outside qnull.[ch] and its unit test
>> check-qnull.c. Could we use qnull()?
>
> Same as above---it's inline. The above is a translation of
>
> static inline QNull *qnull(void)
> {
> return qobject_ref(&qnull_);
> }
Could we call C qnull() instead?
>>> +macro_rules! from_double {
>>> + ($t:ty) => {
>>> + impl From<$t> for QObject {
>>> + fn from(n: $t) -> Self {
>>> + let qobj = unsafe { &*bindings::qnum_from_double(n.into()) };
>>> + unsafe { QObject::from_base(addr_of!(qobj.base)) }
>>> + }
>>> + }
>>> + };
>>> +}
>>> +
>>> +from_double!(f32);
>>
>> Uh, isn't the double in from_double misleading?
>
> It's a reference to the function that it calls (qnum_from_double). Can
> rename it to impl_from_returning_qnum_double.
>
>>> +from_double!(f64);
>>
>> Can you briefly explain why we need more than i64, u64, and double?
>
> Because Rust doesn't do automatic casts. So it's nicer (and also less
> error prone) if the subsequent patches do not have to always convert to
> u64 or i64.
Okay.
>> Skipping the remainder, it's too much macro magic for poor, ignorant me
>> :)
>
> It's not really hard. The thing to the left of => effectively defines a
> parser. Each thing of the shape $IDENT:RULE matches a piece of Rust
> grammar; expr is expression an tt is token tree (either a single token
> or a parenthesized group). To access $IDENT that appears within $(...)?
> on the left of => you must have a similar $(...)? on the right, and the
> whole $(...)? on the right will be skipped if the left-side wasn't there.
>
> The macro is used like this:
>
> 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,
> }
>
> And it produces a "switch" on QObject types, where each "case" extracts
> the datum, places it in the variable to the left of "=>" (such as "b"
> for bool), and returns the value on the right of "=>" (such as
> "Unexpected::Bool(b)"):
>
>
>>> + ) => {
>>> + 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
>>> + },)?
>
> (The loop/break is just a syntactic convenience---the loop never rolls
> more than once).
>
> Paolo
Thanks for your help!
^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH 14/19] scripts/qapi: generate high-level Rust bindings
2025-12-10 14:38 ` Paolo Bonzini
@ 2025-12-11 7:25 ` Markus Armbruster
0 siblings, 0 replies; 48+ messages in thread
From: Markus Armbruster @ 2025-12-11 7:25 UTC (permalink / raw)
To: Paolo Bonzini; +Cc: Markus Armbruster, qemu-devel, marcandre.lureau, qemu-rust
Paolo Bonzini <pbonzini@redhat.com> writes:
> On 12/9/25 11:03, Markus Armbruster wrote:
>> * Why is util::qobject::QObject needed?
>>
>> * NONE is an error value, not a valid QType. Having such error values
>> in enums isn't unusual in C. What about idiomatic Rust? Even if it's
>> unusual there, we may elect to do it anyway, just to keep generated
>> Rust closer to C. But it should be a conscious decision, not a blind
>> port from C to Rust.
>
> For QType we don't need to keep it closer, but actually ABI-compatible:
> QType is defined by QAPI but is used (almost exclusively) by QObject.
> We use the C version in the QObject bindings, for example:
>
> $($crate::bindings::QTYPE_QNULL => break $unit,)?
I see. Worth a comment.
>> * "Default for QType" is NONE. In C, it's zero bytes, which boils down
>> to QTYPE_NONE.
>>
>> * QTYPE__MAX is a bit of a headache in C. It's not a valid enum value.
>> We make it one only because we need to know the largest valid enum
>> value, e.g. to size arrays, and the easiest way to get that value is
>> adding an invalid one to the enum. Same for all the other generated
>> enums. Could we avoid it in Rust?
>
> Yes, I think so.
>
>> * C has a file comment of the form
>>
>> /*
>> * One-line description of the file's purpose
>> *
>> * Copyright lines
>> *
>> * License blurb
>> */
>>
>> I think Rust could use such a comment, too.
>
> Ok.
>
>> * C has built-in types like QType in qapi-builtin-types.h, generated
>> only with -b. This is a somewhat crude way to let code generated for
>> multiple schemas coexist: pass -b for exactly one of them. If we
>> generated code for built-in types unconditionally into qapi-types.h,
>> the C compiler would choke on duplicate definitions. Why is this not
>> a problem with Rust?
>
> Because there's better namespacing, so it's okay to define the builtin
> types in more than one place. However, do we need at all the builtin
> types in Rust? QType is only defined in QAPI to have the nice enum
> lookup tables, and we can get it via FFI bindings. Lists, as you say
> below, are not needed, and they are also a part of qapi-builtin-types.h.
>
> So I think Rust does not need built-in types at all, which I think
> solves all your problems here (other than _MAX which can be removed).
Let's try this.
>> * The Rust version doesn't have deallocation boilerplate. Deallocation
>> just works there, I guess.
>>
>> * The Rust version doesn't have the List type. Lists just work there, I
>> guess.
>
> Yep.
>
>> * The Rust version doesn't have the implicit type q_obj_my_command_arg,
>> which is the arguments of my-command as a struct type. C needs it for
>> marshaling / unmarshaling with visitors. Rust doesn't, because we use
>> serde. Correct?
>
> Commands are not supported at all yet.
>
> Paolo
Thanks!
^ permalink raw reply [flat|nested] 48+ messages in thread
end of thread, other threads:[~2025-12-11 7:25 UTC | newest]
Thread overview: 48+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-10-10 15:09 [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
2025-10-10 15:09 ` [PATCH 01/19] util: add ensure macro Paolo Bonzini
2025-10-10 15:09 ` [PATCH 02/19] rust/util: use anyhow's native chaining capabilities Paolo Bonzini
2025-10-10 15:09 ` [PATCH 03/19] rust: do not add qemuutil to Rust crates Paolo Bonzini
2025-12-05 8:30 ` Markus Armbruster
2025-12-05 12:07 ` Paolo Bonzini
2025-10-10 15:09 ` [PATCH 04/19] rust/qobject: add basic bindings Paolo Bonzini
2025-12-05 9:35 ` Markus Armbruster
2025-12-05 11:27 ` Paolo Bonzini
2025-12-11 7:21 ` Markus Armbruster
2025-10-10 15:09 ` [PATCH 05/19] subprojects: add serde Paolo Bonzini
2025-10-10 15:09 ` [PATCH 06/19] rust/qobject: add Serialize implementation Paolo Bonzini
2025-12-05 9:47 ` Markus Armbruster
2025-12-10 17:19 ` Paolo Bonzini
2025-12-11 7:07 ` Markus Armbruster
2025-10-10 15:09 ` [PATCH 07/19] rust/qobject: add Serializer (to_qobject) implementation Paolo Bonzini
2025-10-10 15:09 ` [PATCH 08/19] rust/qobject: add Deserialize implementation Paolo Bonzini
2025-10-10 15:09 ` [PATCH 09/19] rust/qobject: add Deserializer (from_qobject) implementation Paolo Bonzini
2025-10-10 15:09 ` [PATCH 10/19] rust/util: replace Error::err_or_unit/err_or_else with Error::with_errp Paolo Bonzini
2025-10-10 15:09 ` [PATCH 11/19] rust/qobject: add from/to JSON bindings for QObject Paolo Bonzini
2025-12-05 10:04 ` Markus Armbruster
2025-12-05 11:09 ` Paolo Bonzini
2025-12-05 12:16 ` Markus Armbruster
2025-12-08 7:00 ` Paolo Bonzini
2025-12-08 9:17 ` Markus Armbruster
2025-12-09 7:34 ` Paolo Bonzini
2025-10-10 15:09 ` [PATCH 12/19] rust/qobject: add Display/Debug Paolo Bonzini
2025-10-10 15:09 ` [PATCH 13/19] scripts/qapi: add QAPISchemaIfCond.rsgen() Paolo Bonzini
2025-12-09 18:43 ` Markus Armbruster
2025-10-10 15:09 ` [PATCH 14/19] scripts/qapi: generate high-level Rust bindings Paolo Bonzini
2025-12-09 10:03 ` Markus Armbruster
2025-12-10 14:38 ` Paolo Bonzini
2025-12-11 7:25 ` Markus Armbruster
2025-10-10 15:10 ` [PATCH 15/19] scripts/qapi: add serde attributes Paolo Bonzini
2025-12-09 12:24 ` Markus Armbruster
2025-10-10 15:10 ` [PATCH 16/19] scripts/qapi: strip trailing whitespaces Paolo Bonzini
2025-12-09 8:48 ` Markus Armbruster
2025-12-10 17:28 ` Paolo Bonzini
2025-10-10 15:10 ` [PATCH 17/19] scripts/rustc_args: add --no-strict-cfg Paolo Bonzini
2025-10-10 15:10 ` [PATCH 18/19] rust/util: build QAPI types Paolo Bonzini
2025-10-10 15:10 ` [PATCH 19/19] rust/tests: QAPI integration tests Paolo Bonzini
2025-10-30 17:13 ` [PATCH 00/19] rust: QObject and QAPI bindings Paolo Bonzini
2025-12-05 13:55 ` Markus Armbruster
2025-12-09 6:01 ` Markus Armbruster
2025-12-10 14:12 ` Paolo Bonzini
2025-12-10 14:50 ` Markus Armbruster
2025-12-10 16:01 ` Paolo Bonzini
2025-12-10 16:59 ` Markus Armbruster
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).