Re: [PATCH v2 12/16] scripts/qapi: generate high-level Rust bindings

Paolo Bonzini
2026-03-03T13:18:05Z

On 3/3/26 10:19, Markus Armbruster wrote:
> Paolo Bonzini <pbonzini@redhat.com> writes:
>
>> From: Marc-André Lureau <marcandre.lureau@redhat.com>
>>
>> Generate high-level native Rust declarations for the QAPI types.
>>
>> - char* is mapped to String, scalars to there corresponding Rust types
>>
>> - enums use #[repr(u32)] and can be transmuted to their C counterparts
>>
>> - has_foo/foo members are mapped to Option<T>
>>
>> - lists are represented as Vec<T>
>>
>> - structures map fields 1:1 to Rust
>>
>> - alternate are represented as Rust enum, each variant being a 1-element
>> tuple
>>
>> - unions are represented in a similar way as in C: a struct S with a "u"
>> member (since S may have extra 'base' fields). The discriminant
>> isn't a member of S, since Rust enum already include it, but it can be
>> recovered with "mystruct.u.into()"
>>
>> Anything that includes a recursive struct puts it in a Box. Lists are
>> not considered recursive, because Vec breaks the recursion (it's possible
>> to construct an object containing an empty Vec of its own type).
>>
>> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
>> Link: https://lore.kernel.org/r/20210907121943.3498701-21-marcandre.lureau@redhat.com
>> [Paolo: rewrite conversion of schema types to Rust types]
>> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
>
> Let's look at the generated Rust. I'm going to ignore formatting issues
> like blank lines. My test inputs are adapted from
> tests/qapi-schema/qapi-schema-test.json.
>
> = Boilerplate and built-in stuff =
>
> The qapi-types.rs generated for an empty schema is basically empty:
>
> // @generated by qapi-gen, DO NOT EDIT
> //!
> //! Schema-defined QAPI types
> //!
> //! Copyright (c) 2025 Red Hat, Inc.
> //!
> //! This work is licensed under the terms of the GNU LGPL, version 2.1 or
> //! later. See the COPYING.LIB file in the top-level directory.
> #![allow(unexpected_cfgs)]
> #![allow(non_camel_case_types)]
> #![allow(clippy::empty_structs_with_brackets)]
> #![allow(clippy::large_enum_variant)]
> #![allow(clippy::pub_underscore_fields)]
> // Because QAPI structs can contain float, for simplicity we never
> // derive Eq. Clippy however would complain for those structs
> // that *could* be Eq too.
> #![allow(clippy::derive_partial_eq_without_eq)]
> use util::qobject::QObject;
>
> Okay.
>
> Note we generate nothing for the built-in types. For C, we generate
> qapi-builtin-types.[ch], which provides lists of built-in types, and
> enum QType. Rust gives us lists for free, and QType we don't need right
> now. Good.
>
> = Enum types =
>
> Input:
>
> { 'enum': 'TestIfEnum',
> 'data': [ 'foo', { 'name' : 'bar', 'if': 'TEST_IF_ENUM_MEMBER' } ],
> 'if': 'TEST_IF_UNION' }
>
> Output without the boilerplate:
>
> #[cfg(TEST_IF_UNION)]
> #[derive(Copy, Clone, Debug, PartialEq)]
>
> Ignoring such lines for now.
>
> #[repr(u32)]
>
> As explained in the commit message.
>
> #[derive(common::TryInto)]
>
> Ignoring these, too.
>
> pub enum TestIfEnum {
>
> FOO,
> #[cfg(TEST_IF_ENUM_MEMBER)]
> BAR,
> }
>
> The obvious part :)
>
> #[cfg(TEST_IF_UNION)]
> impl Default for TestIfEnum {
> #[inline]
> fn default() -> TestIfEnum {
> Self::FOO
> }
> }
>
> This specifies the enum type's default value. Hmm... Modified input:
>
> { 'enum': 'TestIfEnum',
> 'data': [ { 'name' : 'bar', 'if': 'TEST_IF_ENUM_MEMBER' }, 'foo' ],
> 'if': 'TEST_IF_UNION' }
>
> Output:
>
> #[cfg(TEST_IF_UNION)]
> #[derive(Copy, Clone, Debug, PartialEq)]
> #[repr(u32)]
> #[derive(common::TryInto)]
> pub enum TestIfEnum {
> #[cfg(TEST_IF_ENUM_MEMBER)]
> BAR,
>
> FOO,
> }
>
> #[cfg(TEST_IF_UNION)]
> impl Default for TestIfEnum {
> #[inline]
> fn default() -> TestIfEnum {
> Self::BAR
> }
> }
>
> If TEST_IF_ENUM_MEMBER is off, member BAR does not exist. default()
> uses it anyway. Bug?

Nice catch. I can make it produce the first element (as in C) with a
tiny bit of cheating

unsafe { ::std::mem::transmute_copy(&0u32) }

or some reliance on the optimizer

0.try_into().unwrap()

However, I'm inclined to handle this generically. For example, an enum
where the first element is configuration dependent could be usable only
if optional. And then the Rust code can skip the "impl Default". Let
me check how often it would trigger.

Paolo