From nobody Fri Nov 14 19:42:42 2025 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=quarantine dis=none) header.from=redhat.com ARC-Seal: i=1; a=rsa-sha256; t=1760109164; cv=none; d=zohomail.com; s=zohoarc; b=AKkA5DaLFq6NLobyRTlKAhSfq52nupI15wYYdnETa2VRhz0QYfZcY278RMw5tERjtF8sAp7N1HttqpKO7k0xlkKxgrWChBC3qDkfGNzwNqjr1KK503Z+yuwlzoq7MJQH88R+6xr6i73nWiY7gqLtF5WgTKfm2JoqusnJHZoiqEs= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1760109164; h=Content-Type:Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=dXiTTUbNHDbzCZW92MIQx4Ti2Mz3wQylh/TTI5lqmoM=; b=Wir9zKCi58mhNTIl4wuJOMM89VeSy6NiezUB3Da/PWNejUqsGzOuxhff6rh8DfGaheYW2km+SuwMTEktNBeT429EfmC9je1qj0fp1bdC4eCO+IlnHFwbjceJ/hcHk+ApGr4kOL5RqCSRaWerKQtXx3vDBwAvvZOQLMcHrmOR1vI= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=quarantine dis=none) Return-Path: Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1760109164435740.1970890284906; Fri, 10 Oct 2025 08:12:44 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1v7Elr-00009n-1Z; Fri, 10 Oct 2025 11:11:03 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1v7Ell-00007q-Ci for qemu-devel@nongnu.org; Fri, 10 Oct 2025 11:10:57 -0400 Received: from us-smtp-delivery-124.mimecast.com ([170.10.129.124]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1v7ElR-00047p-CW for qemu-devel@nongnu.org; Fri, 10 Oct 2025 11:10:57 -0400 Received: from mail-ej1-f72.google.com (mail-ej1-f72.google.com [209.85.218.72]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-531-ChmhUmekM5qdlUFwRh93eg-1; Fri, 10 Oct 2025 11:10:32 -0400 Received: by mail-ej1-f72.google.com with SMTP id a640c23a62f3a-afcb72a8816so179533066b.0 for ; Fri, 10 Oct 2025 08:10:32 -0700 (PDT) Received: from [192.168.10.48] ([151.49.231.162]) by smtp.gmail.com with ESMTPSA id a640c23a62f3a-b55d8c12a62sm257055966b.58.2025.10.10.08.10.28 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 10 Oct 2025 08:10:28 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1760109034; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=dXiTTUbNHDbzCZW92MIQx4Ti2Mz3wQylh/TTI5lqmoM=; b=HspXSoKizhwcpUbbOxBh/dHvSBRdGzbGK2BkhFSH24tRzg33dl0Y+kmdLFiV7G3rl2M9jf n8c0T8b+cVnnbnuyKXfBeiwi2/LNI2rz79lfkRi4V0ndDqB49iIJYHCpkRhq31QQdHw57Q kW57rgIuy7eoCjC/p07VArBDCSKU6lY= X-MC-Unique: ChmhUmekM5qdlUFwRh93eg-1 X-Mimecast-MFC-AGG-ID: ChmhUmekM5qdlUFwRh93eg_1760109032 X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1760109031; x=1760713831; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=dXiTTUbNHDbzCZW92MIQx4Ti2Mz3wQylh/TTI5lqmoM=; b=NlIjhJM5qvUqa53A5ahaEgQsBkGqS8Unv49Vs6pJvEF6W/aqJWwrQLF8gjvCrRml4o 8hatMCbmJBGjgdGHffuqYzETfUx/nguftKcac82Sd4Gf1DYv1oYsPvks9Qx6t5zyf8c8 NkFeVc1XcVe/l+gNlB38ynULMw5rZKugfGGsRvsulDsZBeFnEzT0OHiXJgERw2tpsA2R aWu9OekGrkReC56BicWtsKRkLZPYOUBviS5+t9BnBjqoLj0gzkRpAe71j/yoFQRVr3uj iKM8ZUZHFlGWfzZ5orLHcZPt0a6IWBB/xxKvTI7A5xg8bInhp1mqu/1nQ4KQS7xYhqBV 7t2A== X-Gm-Message-State: AOJu0YxEZohtHSvbwLb73Onvfy0niamovbBdFgUvuV/Qscrv4ZqqB6Li QGOedpjnB+av9bKxPFAmKUzzMdDetLnfaDpA19JAt/SAONlbR5QEvfeLFMzIeT4AooRzaMOKy6X glj6QPaH4JYay9zKyoMGYfApgjsyKWndKo7xeJXRT6BQ/0XtkrK4dvT+80u+pYLM+UhsvwiKV4s 3UbQdR7WefBddfrZOdGMOPh0NRV0pRSu4RQTEQD648 X-Gm-Gg: ASbGncuYg/0JgtzWYVa/XzTEVXXM0KZusX5nMzFa9/CsvII3qBuuO5d7hqM13cR/lDf 8AWjxFb6k5iGxIIh2V5aXCUKFvC8AsAdYaNWZvZEhwJbbbzqXF/vmGNzE4Omc4CpNaF//ybo0EZ 5Sou1kc90B7RdxRQ2y8GCN6FK3xiuZJ/rYXmoMY0tPWcorr0whMo0hEY90g8kqQS4J0Xb07YbZL O7AWkZHmP5LrR2Q7li6JNZ0U71DFYdDYHAr0jMRi/nVHXEA8uSRmGMpLHSaRcKV19tDbZ9zTonT 2OniWwI/46aAbciUN3Doi3N1mMA+01L1PGecJINQBzaQkA0kTp+b6fKO19AhMq5Obu3xRhuuiVk sllCjQke6RyXjo0Z3NcbXZESryDiEGBYcGbsm1WxqrtFV X-Received: by 2002:a17:906:c10f:b0:b3c:f0a4:b324 with SMTP id a640c23a62f3a-b50aaa9fe39mr1191002066b.21.1760109030626; Fri, 10 Oct 2025 08:10:30 -0700 (PDT) X-Google-Smtp-Source: AGHT+IFaT6YihrPLKX768RhgQzNMhDp+ObmC/dt7GsizT9K8qPSVfMTMpLjiAuWRXiQePn/74olbCA== X-Received: by 2002:a17:906:c10f:b0:b3c:f0a4:b324 with SMTP id a640c23a62f3a-b50aaa9fe39mr1190998766b.21.1760109030103; Fri, 10 Oct 2025 08:10:30 -0700 (PDT) From: Paolo Bonzini 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 Message-ID: <20251010151006.791038-10-pbonzini@redhat.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20251010151006.791038-1-pbonzini@redhat.com> References: <20251010151006.791038-1-pbonzini@redhat.com> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=170.10.129.124; envelope-from=pbonzini@redhat.com; helo=us-smtp-delivery-124.mimecast.com X-Spam_score_int: -24 X-Spam_score: -2.5 X-Spam_bar: -- X-Spam_report: (-2.5 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.441, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_DNSWL_NONE=-0.0001, RCVD_IN_MSPIKE_H3=0.001, RCVD_IN_MSPIKE_WL=0.001, RCVD_IN_VALIDITY_CERTIFIED_BLOCKED=0.001, RCVD_IN_VALIDITY_RPBL_BLOCKED=0.001, SPF_HELO_PASS=-0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @redhat.com) X-ZM-MESSAGEID: 1760109166389154100 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=C3=A9 Lureau Signed-off-by: Marc-Andr=C3=A9 Lureau Signed-off-by: Paolo Bonzini --- 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 =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D =20 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 =3D 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 da= ta +//! structures (such as primitive data types, or structs that implement +//! `Deserialize`). + +use std::ffi::CStr; + +use serde::de::{ + self, value::StrDeserializer, DeserializeSeed, Expected, IntoDeseriali= zer, MapAccess, + SeqAccess, Unexpected, Visitor, +}; + +use super::{ + error::{Error, Result}, + match_qobject, QObject, +}; +use crate::bindings; + +impl QObject { + #[cold] + fn invalid_type(&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) =3D> + () =3D> Unexpected::Unit, + bool(b) =3D> Unexpected::Bool(b), + i64(n) =3D> Unexpected::Signed(n), + u64(n) =3D> Unexpected::Unsigned(n), + f64(n) =3D> Unexpected::Float(n), + CStr(s) =3D> s.to_str().map_or_else( + |_| Unexpected::Other("string with invalid UTF-8"), + Unexpected::Str), + QList(_) =3D> Unexpected::Seq, + QDict(_) =3D> Unexpected::Map, + } + } +} + +fn visit_qlist_ref<'de, V>(qlist: &'de bindings::QList, visitor: V) -> Res= ult +where + V: Visitor<'de>, +{ + struct QListDeserializer(*mut bindings::QListEntry, usize); + + impl<'de> SeqAccess<'de> for QListDeserializer { + type Error =3D Error; + + fn next_element_seed(&mut self, seed: T) -> Result> + where + T: DeserializeSeed<'de>, + { + if self.0.is_null() { + return Ok(None); + } + + let e =3D unsafe { &*self.0 }; + // increment the reference count because deserialize consumes = `value`. + let value =3D unsafe { QObject::cloned_from_raw(e.value.cast_c= onst()) }; + let result =3D seed.deserialize(value); + self.0 =3D unsafe { e.next.tqe_next }; + self.1 +=3D 1; + result.map(Some) + } + } + + let mut deserializer =3D QListDeserializer(unsafe { qlist.head.tqh_fir= st }, 0); + let seq =3D 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) -> Res= ult +where + V: Visitor<'de>, +{ + struct QDictDeserializer(*mut bindings::QDict, *const bindings::QDictE= ntry); + + impl<'de> MapAccess<'de> for QDictDeserializer { + type Error =3D Error; + + fn next_key_seed(&mut self, seed: T) -> Result> + where + T: DeserializeSeed<'de>, + { + if self.1.is_null() { + return Ok(None); + } + + let e =3D unsafe { &*self.1 }; + let key =3D unsafe { CStr::from_ptr(e.key) }; + let key_de =3D StrDeserializer::new(key.to_str()?); + seed.deserialize(key_de).map(Some) + } + + fn next_value_seed(&mut self, seed: T) -> Result + where + T: DeserializeSeed<'de>, + { + if self.1.is_null() { + panic!("next_key must have returned None"); + } + + let e =3D unsafe { &*self.1 }; + // increment the reference count because deserialize consumes = `value`. + let value =3D unsafe { QObject::cloned_from_raw(e.value) }; + let result =3D seed.deserialize(value); + self.1 =3D unsafe { bindings::qdict_next(self.0, self.1) }; + result + } + } + + let qdict =3D (qdict as *const bindings::QDict).cast_mut(); + let e =3D unsafe { bindings::qdict_first(qdict) }; + let mut deserializer =3D QDictDeserializer(qdict, e); + let map =3D 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 +where + V: Visitor<'de>, +{ + match_qobject! { (qnum) =3D> + i64(n) =3D> visitor.visit_i64(n), + u64(n) =3D> visitor.visit_u64(n), + f64(n) =3D> visitor.visit_f64(n), + _ =3D> Err(qnum.invalid_type(&"number")), + } +} + +macro_rules! deserialize_number { + ($method:ident) =3D> { + fn $method(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visit_qnum_ref(self, visitor) + } + }; +} + +impl<'de> serde::Deserializer<'de> for QObject { + type Error =3D Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match_qobject! { (self) =3D> + () =3D> visitor.visit_unit(), + bool(v) =3D> visitor.visit_bool(v), + i64(n) =3D> visitor.visit_i64(n), + u64(n) =3D> visitor.visit_u64(n), + f64(n) =3D> visitor.visit_f64(n), + CStr(cstr) =3D> visitor.visit_str(cstr.to_str()?), + QList(qlist) =3D> visit_qlist_ref(qlist, visitor), + QDict(qdict) =3D> 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(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match_qobject! { (self) =3D> + () =3D> visitor.visit_none(), + _ =3D> visitor.visit_some(self), + } + } + + fn deserialize_enum( + self, + _name: &'static str, + _variants: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + match_qobject! { (self) =3D> + CStr(cstr) =3D> visitor.visit_enum(cstr.to_str()?.into_deseria= lizer()), + _ =3D> Err(self.invalid_type(&"string")), + } + } + + fn deserialize_newtype_struct(self, _name: &'static str, visitor: V= ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_bool(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match_qobject! { (self) =3D> + bool(v) =3D> visitor.visit_bool(v), + _ =3D> Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_char(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + self.deserialize_str(visitor) + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match_qobject! { (self) =3D> + CStr(cstr) =3D> visitor.visit_str(cstr.to_str()?), + _ =3D> Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_string(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + self.deserialize_str(visitor) + } + + fn deserialize_bytes(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match_qobject! { (self) =3D> + CStr(cstr) =3D> visitor.visit_str(cstr.to_str()?), + QList(qlist) =3D> visit_qlist_ref(qlist, visitor), + _ =3D> Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_byte_buf(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + self.deserialize_bytes(visitor) + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match_qobject! { (self) =3D> + () =3D> visitor.visit_unit(), + _ =3D> Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_unit_struct(self, _name: &'static str, visitor: V) -= > Result + where + V: Visitor<'de>, + { + self.deserialize_unit(visitor) + } + + fn deserialize_seq(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match_qobject! { (self) =3D> + QList(qlist) =3D> visit_qlist_ref(qlist, visitor), + _ =3D> Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_tuple(self, _len: usize, visitor: V) -> Result + where + V: Visitor<'de>, + { + self.deserialize_seq(visitor) + } + + fn deserialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + self.deserialize_seq(visitor) + } + + fn deserialize_map(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match_qobject! { (self) =3D> + QDict(qdict) =3D> visit_qdict_ref(qdict, visitor), + _ =3D> Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_struct( + self, + _name: &'static str, + _fields: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + match_qobject! { (self) =3D> + QList(qlist) =3D> visit_qlist_ref(qlist, visitor), + QDict(qdict) =3D> visit_qdict_ref(qdict, visitor), + _ =3D> Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_identifier(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + self.deserialize_str(visitor) + } + + fn deserialize_ignored_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } +} + +pub fn from_qobject(value: QObject) -> Result +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, }; =20 -use serde::ser; +use serde::{de, ser}; =20 #[derive(Debug)] pub enum Error { @@ -23,6 +23,12 @@ fn custom(msg: T) -> Self { } } =20 +impl de::Error for Error { + fn custom(msg: T) -> Self { + Error::Custom(msg.to_string()) + } +} + impl From 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)] =20 mod deserialize; +mod deserializer; mod error; mod serialize; mod serializer; @@ -20,6 +21,7 @@ }; =20 use common::assert_field_type; +pub use deserializer::from_qobject; pub use error::{Error, Result}; pub use serializer::to_qobject; =20 --=20 2.51.0