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

Markus Armbruster
2026-03-03T09:39:33Z

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?

Not tested: enum definition's optional 'prefix' member, because Rust
generation has no need for it and does not use it. Good.

= Object types =

Input:

{ 'struct': 'UserDefOne',
'base': 'UserDefZero',
'data': { 'string': 'str',
'*number': 'number' } }

{ 'struct': 'UserDefZero',
'data': { 'integer': 'int' } }

Output:

#[derive(Clone, Debug, PartialEq)]

Ignoring such lines for now.

pub struct UserDefOne {
// Members inherited:

pub integer: i64,
// Own members:

pub string: String,

pub number: Option<f64>,
}

#[derive(Clone, Debug, PartialEq)]
pub struct UserDefZero {

pub integer: i64,
}

Obvious enough.

More input:

{ 'union': 'TestIfUnion',
'base': { 'type': 'TestIfEnum' },
'discriminator': 'type',
'data': { 'foo': 'UserDefOne',
'bar': { 'type': 'UserDefZero', 'if': 'TEST_IF_ENUM_MEMBER'} },
'if': { 'all': ['TEST_IF_UNION', 'TEST_IF_STRUCT'] } }

Output:

#[cfg(all(TEST_IF_UNION, TEST_IF_STRUCT))]
#[derive(Clone, Debug, PartialEq)]
pub enum TestIfUnionVariant {

Possible trouble if somebody defines a QAPI type name ending with
'Variant'.

Code generated for a QAPI schema name named 'Frob' typically defines a
'Frob'. Sometimes, we transform 'Frob' in (hopefully obvious) ways, say
to adhere to naming rules. For instance, QAPI enum member 'foo' of
'TestIfEnum' becomes FOO in generated Rust. This risks naming
collisions in generated code. Hasn't been much of a problem in
practice.

We also need names for helpers. These we commonly derive from QAPI
schema names, and we reserve names for the purpose:

* Names beginning with 'q_'

* Type names ending with 'List'

* Member name 'u' and names starting with 'has_'

Source: docs/devel/qapi-code-gen.rst section "Naming rules and reserved
names".

We could reserve names ending with 'Variant'. Or we use a 'q_' prefix
here.

Foo(UserDefOne),
#[cfg(TEST_IF_ENUM_MEMBER)]
Bar(UserDefZero),
}

#[cfg(all(TEST_IF_UNION, TEST_IF_STRUCT))]
impl From<&TestIfUnionVariant> for TestIfEnum {
fn from(e: &TestIfUnionVariant) -> Self {
match e {

TestIfUnionVariant::Foo(_) => Self::FOO,
#[cfg(TEST_IF_ENUM_MEMBER)]
TestIfUnionVariant::Bar(_) => Self::BAR,
}
}
}

#[cfg(all(TEST_IF_UNION, TEST_IF_STRUCT))]
#[derive(Clone, Debug, PartialEq)]
pub struct TestIfUnion {
pub u: TestIfUnionVariant,
}

Why do we need to wrap TestIfUnionVariant in a struct? Hmm, we need it
in case we have more common members. Modified input to demonstrate:

{ 'union': 'TestIfUnion',
'base': { 'type': 'TestIfEnum', 'common': 'int' },
'discriminator': 'type',
'data': { 'foo': 'UserDefOne',
'bar': { 'type': 'UserDefZero', 'if': 'TEST_IF_ENUM_MEMBER'} },
'if': { 'all': ['TEST_IF_UNION', 'TEST_IF_STRUCT'] } }

Output:

#[cfg(all(TEST_IF_UNION, TEST_IF_STRUCT))]
#[derive(Clone, Debug, PartialEq)]
pub enum TestIfUnionVariant {

Foo(UserDefOne),
#[cfg(TEST_IF_ENUM_MEMBER)]
Bar(UserDefZero),
}

#[cfg(all(TEST_IF_UNION, TEST_IF_STRUCT))]
impl From<&TestIfUnionVariant> for TestIfEnum {
fn from(e: &TestIfUnionVariant) -> Self {
match e {

TestIfUnionVariant::Foo(_) => Self::FOO,
#[cfg(TEST_IF_ENUM_MEMBER)]
TestIfUnionVariant::Bar(_) => Self::BAR,
}
}
}

#[cfg(all(TEST_IF_UNION, TEST_IF_STRUCT))]
#[derive(Clone, Debug, PartialEq)]
pub struct TestIfUnion {

pub common: i64,
pub u: TestIfUnionVariant,
}

Okay.

= Array types =

Input:

{ 'struct': 'ArrayStruct',
'data': { 'integer': ['int'],
'*any': ['any'] } }

Output:

#[derive(Clone, Debug, PartialEq)]
pub struct ArrayStruct {

pub integer: Vec<i64>,

pub any: Option<Vec<QObject>>,
}

Okay.

= Alternate types =

Input:

{ 'alternate': 'TestIfAlternate',
'data': { 'foo': 'int',
'bar': { 'type': 'UserDefZero', 'if': 'TEST_IF_ALT_MEMBER'} },
'if': { 'all': ['TEST_IF_ALT', 'TEST_IF_STRUCT'] } }

Output:

#[cfg(all(TEST_IF_ALT, TEST_IF_STRUCT))]
#[derive(Clone, Debug, PartialEq)]
pub enum TestIfAlternate {

Foo(i64),
#[cfg(TEST_IF_ALT_MEMBER)]
Bar(UserDefZero),
}

Doesn't use QType for the tag like C does. Okay.