From: Paolo Bonzini <pbonzini@redhat.com>
To: qemu-devel@nongnu.org
Cc: armbru@redhat.com, marcandre.lureau@redhat.com, qemu-rust@nongnu.org
Subject: [PATCH 09/19] rust/qobject: add Deserializer (from_qobject) implementation
Date: Fri, 10 Oct 2025 17:09:54 +0200 [thread overview]
Message-ID: <20251010151006.791038-10-pbonzini@redhat.com> (raw)
In-Reply-To: <20251010151006.791038-1-pbonzini@redhat.com>
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
next prev parent reply other threads:[~2025-10-10 15:12 UTC|newest]
Thread overview: 20+ messages / expand[flat|nested] mbox.gz Atom feed top
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-10-10 15:09 ` [PATCH 04/19] rust/qobject: add basic bindings Paolo Bonzini
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-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 ` Paolo Bonzini [this message]
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-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-10-10 15:09 ` [PATCH 14/19] scripts/qapi: generate high-level Rust bindings Paolo Bonzini
2025-10-10 15:10 ` [PATCH 15/19] scripts/qapi: add serde attributes Paolo Bonzini
2025-10-10 15:10 ` [PATCH 16/19] scripts/qapi: strip trailing whitespaces 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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20251010151006.791038-10-pbonzini@redhat.com \
--to=pbonzini@redhat.com \
--cc=armbru@redhat.com \
--cc=marcandre.lureau@redhat.com \
--cc=qemu-devel@nongnu.org \
--cc=qemu-rust@nongnu.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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).