From: Marc-André Lureau <[email protected]> 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). Given the experimental nature of Rust, and the incompleteness of the backend (it lacks commands and events), QAPIRsBackend is not modular and is not built together with the C and trace-event files. It can be used by specifying "-B qapi.backend.QAPIRsBackend" on the qapi-gen command line. Signed-off-by: Marc-André Lureau <[email protected]> Link: https://lore.kernel.org/r/[email protected] [Paolo: rewrite conversion of leaf types] Signed-off-by: Paolo Bonzini <[email protected]> --- meson.build | 4 +- scripts/qapi/backend.py | 25 +++ scripts/qapi/common.py | 49 ++++++ scripts/qapi/rs.py | 50 ++++++ scripts/qapi/rs_types.py | 372 +++++++++++++++++++++++++++++++++++++++ scripts/qapi/schema.py | 59 +++++-- 6 files changed, 540 insertions(+), 19 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 eb074918193..bda5180c436 100644 --- a/meson.build +++ b/meson.build @@ -3485,11 +3485,13 @@ 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/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..8023acce0d6 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 @@ -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) diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py index 44229c2e366..67a4b11e4f3 100644 --- a/scripts/qapi/common.py +++ b/scripts/qapi/common.py @@ -64,6 +64,13 @@ def camel_to_upper(value: str) -> str: return ret.upper() +def camel_to_lower(value: str) -> str: + """ + Converts CamelCase to camel_case. + """ + return camel_to_upper(value).lower() + + def c_enum_const(type_name: str, const_name: str, prefix: Optional[str] = None) -> str: @@ -129,6 +136,48 @@ def c_name(name: str, protect: bool = True) -> str: return name +def rs_name(name: str) -> str: + """ + Map @name to a valid, possibly raw Rust identifier. + """ + name = re.sub(r'[^A-Za-z0-9_]', '_', name) + if name[0].isnumeric(): + name = '_' + 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 + + return name + + +def to_camel_case(value: str) -> str: + result = '' + for p in re.split(r'[-_]+', value): + if not p: + pass + elif p[0].isalpha(): + result += p[0].upper() + p[1:] + elif result and result[-1].isalpha(): + result += p + else: + # digit_digit, or digit at start of value + result += '_' + p + return result + + class Indentation: """ Indentation level management. diff --git a/scripts/qapi/rs.py b/scripts/qapi/rs.py new file mode 100644 index 00000000000..ad32b527cd6 --- /dev/null +++ b/scripts/qapi/rs.py @@ -0,0 +1,50 @@ +# 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 sys + +from .common import mcgen +from .gen import QAPIGen +from .schema import QAPISchemaVisitor + + +class QAPIGenRs(QAPIGen): + def __init__(self, fname: str, blurb: str, pydoc: str): + super().__init__(fname) + self._blurb = blurb + self._copyright = '\n//! '.join(re.findall(r'^Copyright .*', pydoc, + re.MULTILINE)) + + def _top(self) -> str: + return mcgen(''' +// @generated by qapi-gen, DO NOT EDIT + +//! +//! %(blurb)s +//! +//! %(copyright)s +//! +//! 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. + +''', + tool=os.path.basename(sys.argv[0]), + blurb=self._blurb, copyright=self._copyright) + + +class QAPISchemaRsVisitor(QAPISchemaVisitor): + + def __init__(self, prefix: str, what: str, + blurb: str, pydoc: str): + super().__init__() + self._prefix = prefix + self._what = what + self._gen = QAPIGenRs(self._prefix + self._what + '.rs', blurb, pydoc) + + def write(self, output_dir: str) -> None: + self._gen.write(output_dir) diff --git a/scripts/qapi/rs_types.py b/scripts/qapi/rs_types.py new file mode 100644 index 00000000000..874ebdbfa41 --- /dev/null +++ b/scripts/qapi/rs_types.py @@ -0,0 +1,372 @@ +# 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 + +Copyright (c) 2025 Red Hat, Inc. + +This work is licensed under the terms of the GNU GPL, version 2. +See the COPYING file in the top-level directory. +""" + +from typing import List, Optional, Set + +from .common import ( + camel_to_lower, + camel_to_upper, + mcgen, + rs_name, + to_camel_case, +) +from .rs import QAPISchemaRsVisitor +from .schema import ( + QAPISchema, + QAPISchemaAlternateType, + 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=variants.tag_member.type.rs_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(camel_to_upper(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)) + if type_name == 'q_empty': + ret += mcgen(''' + %(cfg)s''', ''' + %(var_name)s, +''', + cfg=var.ifcond.rsgen(), + var_name=var_name) + else: + ret += mcgen(''' + %(cfg)s''', ''' + %(var_name)s(%(rs_type)s), +''', + cfg=var.ifcond.rsgen(), + var_name=var_name, + rs_type=var.type.rs_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(camel_to_lower(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) + return False + + +def gen_struct_members(members: List[QAPISchemaObjectTypeMember], + name: str) -> str: + ret = [] + for memb in members: + typ = memb.type.rs_type() + if has_recursive_type(memb.type, name, set()): + typ = 'Box<%s>' % typ + if memb.optional: + typ = 'Option<%s>' % typ + ret.append(mcgen(''' +%(cfg)s''', ''' + pub %(rs_name)s: %(rs_type)s, +''', + cfg=memb.ifcond.rsgen(), + rs_type=typ, + rs_name=rs_name(camel_to_lower(memb.name)))) + return '\n'.join(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: + ret = mcgen(''' + +%(cfg)s''', ''' +#[derive(Copy, Clone, Debug, PartialEq)] +''', + cfg=ifcond.rsgen()) + + if members: + ret += '''#[repr(u32)] +#[derive(common::TryInto)] +''' + ret += mcgen(''' +pub enum %(rs_name)s {''', + rs_name=rs_name(name)) + + for member in members: + ret += mcgen(''' + %(cfg)s''', ''' + %(c_enum)s, +''', + cfg=member.ifcond.rsgen(), + c_enum=rs_name(camel_to_upper(member.name))) + ret += '''} + +''' + + # pick the first, since that's what malloc0 does + if not members[0].ifcond.is_present(): + default = rs_name(camel_to_upper(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 + typ = var.type.rs_type() + if has_recursive_type(var.type, name, set()): + typ = 'Box<%s>' % typ + ret += mcgen(''' + %(cfg)s''', ''' + %(mem_name)s(%(rs_type)s), +''', + cfg=var.ifcond.rsgen(), + rs_type=typ, + mem_name=rs_name(to_camel_case(var.name))) + + ret += mcgen(''' +} +''') + return ret + + +class QAPISchemaGenRsTypeVisitor(QAPISchemaRsVisitor): + _schema: Optional[QAPISchema] + + def __init__(self, prefix: str) -> None: + super().__init__(prefix, 'qapi-types', + 'Schema-defined QAPI types', __doc__) + + def visit_begin(self, schema: QAPISchema) -> None: + self._schema = schema + objects_seen.add(schema.the_empty_object_type.name) + + self._gen.preamble_add( + mcgen(''' +#![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: + assert self._schema is not None + if self._schema.is_predefined(name) or 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: + assert self._schema is not None + if self._schema.is_predefined(name): + return + 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) -> None: + vis = QAPISchemaGenRsTypeVisitor(prefix) + schema.visit(vis) + vis.write(output_dir) diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py index 3459b8038e5..3cfe9bbc21d 100644 --- a/scripts/qapi/schema.py +++ b/scripts/qapi/schema.py @@ -37,6 +37,7 @@ docgen_ifcond, gen_endif, gen_if, + rs_name, rsgen_ifcond, ) from .error import QAPIError, QAPISemError, QAPISourceError @@ -341,6 +342,11 @@ def c_param_type(self) -> str: def c_unboxed_type(self) -> str: return self.c_type() + # Return the Rust type for common use + @abstractmethod + def rs_type(self) -> str: + pass + @abstractmethod def json_type(self) -> str: pass @@ -382,11 +388,12 @@ def describe(self) -> str: class QAPISchemaBuiltinType(QAPISchemaType): meta = 'built-in' - def __init__(self, name: str, json_type: str, c_type: str): + def __init__(self, name: str, json_type: str, rs_type: str, c_type: str): super().__init__(name, None, None) assert json_type in ('string', 'number', 'int', 'boolean', 'null', 'value') self._json_type_name = json_type + self._rs_type_name = rs_type self._c_type_name = c_type def c_name(self) -> str: @@ -406,6 +413,9 @@ def json_type(self) -> str: def doc_type(self) -> str: return self.json_type() + def rs_type(self) -> str: + return self._rs_type_name + def visit(self, visitor: QAPISchemaVisitor) -> None: super().visit(visitor) visitor.visit_builtin_type(self.name, self.info, self.json_type()) @@ -453,6 +463,9 @@ def is_implicit(self) -> bool: def c_type(self) -> str: return c_name(self.name) + def rs_type(self) -> str: + return rs_name(self.name) + def member_names(self) -> List[str]: return [m.name for m in self.members] @@ -502,6 +515,9 @@ def is_implicit(self) -> bool: def c_type(self) -> str: return c_name(self.name) + POINTER_SUFFIX + def rs_type(self) -> str: + return 'Vec<%s>' % self.element_type.rs_type() + def json_type(self) -> str: return 'array' @@ -634,6 +650,9 @@ def c_type(self) -> str: def c_unboxed_type(self) -> str: return c_name(self.name) + def rs_type(self) -> str: + return rs_name(self.name) + def json_type(self) -> str: return 'object' @@ -715,6 +734,9 @@ def c_type(self) -> str: def json_type(self) -> str: return 'value' + def rs_type(self) -> str: + return rs_name(self.name) + def visit(self, visitor: QAPISchemaVisitor) -> None: super().visit(visitor) visitor.visit_alternate_type( @@ -1246,9 +1268,10 @@ def _def_include(self, expr: QAPIExpression) -> None: QAPISchemaInclude(self._make_module(include), expr.info)) def _def_builtin_type( - self, name: str, json_type: str, c_type: str + self, name: str, json_type: str, rs_type: str, c_type: str ) -> None: - self._def_definition(QAPISchemaBuiltinType(name, json_type, c_type)) + builtin = QAPISchemaBuiltinType(name, json_type, rs_type, c_type) + self._def_definition(builtin) # Instantiating only the arrays that are actually used would # be nice, but we can't as long as their generated code # (qapi-builtin-types.[ch]) may be shared by some other @@ -1267,21 +1290,21 @@ def is_predefined(self, name: str) -> bool: return False def _def_predefineds(self) -> None: - for t in [('str', 'string', 'char' + POINTER_SUFFIX), - ('number', 'number', 'double'), - ('int', 'int', 'int64_t'), - ('int8', 'int', 'int8_t'), - ('int16', 'int', 'int16_t'), - ('int32', 'int', 'int32_t'), - ('int64', 'int', 'int64_t'), - ('uint8', 'int', 'uint8_t'), - ('uint16', 'int', 'uint16_t'), - ('uint32', 'int', 'uint32_t'), - ('uint64', 'int', 'uint64_t'), - ('size', 'int', 'uint64_t'), - ('bool', 'boolean', 'bool'), - ('any', 'value', 'QObject' + POINTER_SUFFIX), - ('null', 'null', 'QNull' + POINTER_SUFFIX)]: + for t in [('str', 'string', 'String', 'char' + POINTER_SUFFIX), + ('number', 'number', 'f64', 'double'), + ('int', 'int', 'i64', 'int64_t'), + ('int8', 'int', 'i8', 'int8_t'), + ('int16', 'int', 'i16', 'int16_t'), + ('int32', 'int', 'i32', 'int32_t'), + ('int64', 'int', 'i64', 'int64_t'), + ('uint8', 'int', 'u8', 'uint8_t'), + ('uint16', 'int', 'u16', 'uint16_t'), + ('uint32', 'int', 'u32', 'uint32_t'), + ('uint64', 'int', 'u64', 'uint64_t'), + ('size', 'int', 'u64', 'uint64_t'), + ('bool', 'boolean', 'bool', 'bool'), + ('any', 'value', 'QObject', 'QObject' + POINTER_SUFFIX), + ('null', 'null', '()', 'QNull' + POINTER_SUFFIX)]: self._def_builtin_type(*t) self.the_empty_object_type = QAPISchemaObjectType( 'q_empty', None, None, None, None, None, [], None) -- 2.54.0
