aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/qapi
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/qapi')
-rw-r--r--scripts/qapi/.flake82
-rw-r--r--scripts/qapi/.isort.cfg7
-rw-r--r--scripts/qapi/__init__.py0
-rw-r--r--scripts/qapi/commands.py338
-rw-r--r--scripts/qapi/common.py251
-rw-r--r--scripts/qapi/error.py50
-rw-r--r--scripts/qapi/events.py252
-rw-r--r--scripts/qapi/expr.py694
-rw-r--r--scripts/qapi/gen.py339
-rw-r--r--scripts/qapi/introspect.py390
-rw-r--r--scripts/qapi/main.py95
-rw-r--r--scripts/qapi/mypy.ini9
-rw-r--r--scripts/qapi/parser.py810
-rw-r--r--scripts/qapi/pylintrc69
-rw-r--r--scripts/qapi/schema.py1185
-rw-r--r--scripts/qapi/source.py71
-rw-r--r--scripts/qapi/types.py383
-rw-r--r--scripts/qapi/visit.py410
18 files changed, 5355 insertions, 0 deletions
diff --git a/scripts/qapi/.flake8 b/scripts/qapi/.flake8
new file mode 100644
index 000000000..6b158c68b
--- /dev/null
+++ b/scripts/qapi/.flake8
@@ -0,0 +1,2 @@
+[flake8]
+extend-ignore = E722 # Prefer pylint's bare-except checks to flake8's
diff --git a/scripts/qapi/.isort.cfg b/scripts/qapi/.isort.cfg
new file mode 100644
index 000000000..643caa1fb
--- /dev/null
+++ b/scripts/qapi/.isort.cfg
@@ -0,0 +1,7 @@
+[settings]
+force_grid_wrap=4
+force_sort_within_sections=True
+include_trailing_comma=True
+line_length=72
+lines_after_imports=2
+multi_line_output=3
diff --git a/scripts/qapi/__init__.py b/scripts/qapi/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/scripts/qapi/__init__.py
diff --git a/scripts/qapi/commands.py b/scripts/qapi/commands.py
new file mode 100644
index 000000000..21001bbd6
--- /dev/null
+++ b/scripts/qapi/commands.py
@@ -0,0 +1,338 @@
+"""
+QAPI command marshaller generator
+
+Copyright IBM, Corp. 2011
+Copyright (C) 2014-2018 Red Hat, Inc.
+
+Authors:
+ Anthony Liguori <aliguori@us.ibm.com>
+ Michael Roth <mdroth@linux.vnet.ibm.com>
+ Markus Armbruster <armbru@redhat.com>
+
+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 (
+ Dict,
+ List,
+ Optional,
+ Set,
+)
+
+from .common import c_name, mcgen
+from .gen import (
+ QAPIGenC,
+ QAPISchemaModularCVisitor,
+ build_params,
+ ifcontext,
+ gen_special_features,
+)
+from .schema import (
+ QAPISchema,
+ QAPISchemaFeature,
+ QAPISchemaIfCond,
+ QAPISchemaObjectType,
+ QAPISchemaType,
+)
+from .source import QAPISourceInfo
+
+
+def gen_command_decl(name: str,
+ arg_type: Optional[QAPISchemaObjectType],
+ boxed: bool,
+ ret_type: Optional[QAPISchemaType]) -> str:
+ return mcgen('''
+%(c_type)s qmp_%(c_name)s(%(params)s);
+''',
+ c_type=(ret_type and ret_type.c_type()) or 'void',
+ c_name=c_name(name),
+ params=build_params(arg_type, boxed, 'Error **errp'))
+
+
+def gen_call(name: str,
+ arg_type: Optional[QAPISchemaObjectType],
+ boxed: bool,
+ ret_type: Optional[QAPISchemaType]) -> str:
+ ret = ''
+
+ argstr = ''
+ if boxed:
+ assert arg_type
+ argstr = '&arg, '
+ elif arg_type:
+ assert not arg_type.variants
+ for memb in arg_type.members:
+ if memb.optional:
+ argstr += 'arg.has_%s, ' % c_name(memb.name)
+ argstr += 'arg.%s, ' % c_name(memb.name)
+
+ lhs = ''
+ if ret_type:
+ lhs = 'retval = '
+
+ ret = mcgen('''
+
+ %(lhs)sqmp_%(c_name)s(%(args)s&err);
+ error_propagate(errp, err);
+''',
+ c_name=c_name(name), args=argstr, lhs=lhs)
+ if ret_type:
+ ret += mcgen('''
+ if (err) {
+ goto out;
+ }
+
+ qmp_marshal_output_%(c_name)s(retval, ret, errp);
+''',
+ c_name=ret_type.c_name())
+ return ret
+
+
+def gen_marshal_output(ret_type: QAPISchemaType) -> str:
+ return mcgen('''
+
+static void qmp_marshal_output_%(c_name)s(%(c_type)s ret_in,
+ QObject **ret_out, Error **errp)
+{
+ Visitor *v;
+
+ v = qobject_output_visitor_new_qmp(ret_out);
+ if (visit_type_%(c_name)s(v, "unused", &ret_in, errp)) {
+ visit_complete(v, ret_out);
+ }
+ visit_free(v);
+ v = qapi_dealloc_visitor_new();
+ visit_type_%(c_name)s(v, "unused", &ret_in, NULL);
+ visit_free(v);
+}
+''',
+ c_type=ret_type.c_type(), c_name=ret_type.c_name())
+
+
+def build_marshal_proto(name: str) -> str:
+ return ('void qmp_marshal_%s(QDict *args, QObject **ret, Error **errp)'
+ % c_name(name))
+
+
+def gen_marshal_decl(name: str) -> str:
+ return mcgen('''
+%(proto)s;
+''',
+ proto=build_marshal_proto(name))
+
+
+def gen_marshal(name: str,
+ arg_type: Optional[QAPISchemaObjectType],
+ boxed: bool,
+ ret_type: Optional[QAPISchemaType]) -> str:
+ have_args = boxed or (arg_type and not arg_type.is_empty())
+ if have_args:
+ assert arg_type is not None
+ arg_type_c_name = arg_type.c_name()
+
+ ret = mcgen('''
+
+%(proto)s
+{
+ Error *err = NULL;
+ bool ok = false;
+ Visitor *v;
+''',
+ proto=build_marshal_proto(name))
+
+ if ret_type:
+ ret += mcgen('''
+ %(c_type)s retval;
+''',
+ c_type=ret_type.c_type())
+
+ if have_args:
+ ret += mcgen('''
+ %(c_name)s arg = {0};
+''',
+ c_name=arg_type_c_name)
+
+ ret += mcgen('''
+
+ v = qobject_input_visitor_new_qmp(QOBJECT(args));
+ if (!visit_start_struct(v, NULL, NULL, 0, errp)) {
+ goto out;
+ }
+''')
+
+ if have_args:
+ ret += mcgen('''
+ if (visit_type_%(c_arg_type)s_members(v, &arg, errp)) {
+ ok = visit_check_struct(v, errp);
+ }
+''',
+ c_arg_type=arg_type_c_name)
+ else:
+ ret += mcgen('''
+ ok = visit_check_struct(v, errp);
+''')
+
+ ret += mcgen('''
+ visit_end_struct(v, NULL);
+ if (!ok) {
+ goto out;
+ }
+''')
+
+ ret += gen_call(name, arg_type, boxed, ret_type)
+
+ ret += mcgen('''
+
+out:
+ visit_free(v);
+''')
+
+ ret += mcgen('''
+ v = qapi_dealloc_visitor_new();
+ visit_start_struct(v, NULL, NULL, 0, NULL);
+''')
+
+ if have_args:
+ ret += mcgen('''
+ visit_type_%(c_arg_type)s_members(v, &arg, NULL);
+''',
+ c_arg_type=arg_type_c_name)
+
+ ret += mcgen('''
+ visit_end_struct(v, NULL);
+ visit_free(v);
+''')
+
+ ret += mcgen('''
+}
+''')
+ return ret
+
+
+def gen_register_command(name: str,
+ features: List[QAPISchemaFeature],
+ success_response: bool,
+ allow_oob: bool,
+ allow_preconfig: bool,
+ coroutine: bool) -> str:
+ options = []
+
+ if not success_response:
+ options += ['QCO_NO_SUCCESS_RESP']
+ if allow_oob:
+ options += ['QCO_ALLOW_OOB']
+ if allow_preconfig:
+ options += ['QCO_ALLOW_PRECONFIG']
+ if coroutine:
+ options += ['QCO_COROUTINE']
+
+ ret = mcgen('''
+ qmp_register_command(cmds, "%(name)s",
+ qmp_marshal_%(c_name)s, %(opts)s, %(feats)s);
+''',
+ name=name, c_name=c_name(name),
+ opts=' | '.join(options) or 0,
+ feats=gen_special_features(features))
+ return ret
+
+
+class QAPISchemaGenCommandVisitor(QAPISchemaModularCVisitor):
+ def __init__(self, prefix: str):
+ super().__init__(
+ prefix, 'qapi-commands',
+ ' * Schema-defined QAPI/QMP commands', None, __doc__)
+ self._visited_ret_types: Dict[QAPIGenC, Set[QAPISchemaType]] = {}
+
+ def _begin_user_module(self, name: str) -> None:
+ self._visited_ret_types[self._genc] = set()
+ commands = self._module_basename('qapi-commands', name)
+ types = self._module_basename('qapi-types', name)
+ visit = self._module_basename('qapi-visit', name)
+ self._genc.add(mcgen('''
+#include "qemu/osdep.h"
+#include "qapi/compat-policy.h"
+#include "qapi/visitor.h"
+#include "qapi/qmp/qdict.h"
+#include "qapi/dealloc-visitor.h"
+#include "qapi/error.h"
+#include "%(visit)s.h"
+#include "%(commands)s.h"
+
+''',
+ commands=commands, visit=visit))
+ self._genh.add(mcgen('''
+#include "%(types)s.h"
+
+''',
+ types=types))
+
+ def visit_begin(self, schema: QAPISchema) -> None:
+ self._add_module('./init', ' * QAPI Commands initialization')
+ self._genh.add(mcgen('''
+#include "qapi/qmp/dispatch.h"
+
+void %(c_prefix)sqmp_init_marshal(QmpCommandList *cmds);
+''',
+ c_prefix=c_name(self._prefix, protect=False)))
+ self._genc.add(mcgen('''
+#include "qemu/osdep.h"
+#include "%(prefix)sqapi-commands.h"
+#include "%(prefix)sqapi-init-commands.h"
+
+void %(c_prefix)sqmp_init_marshal(QmpCommandList *cmds)
+{
+ QTAILQ_INIT(cmds);
+
+''',
+ prefix=self._prefix,
+ c_prefix=c_name(self._prefix, protect=False)))
+
+ def visit_end(self) -> None:
+ with self._temp_module('./init'):
+ self._genc.add(mcgen('''
+}
+'''))
+
+ def visit_command(self,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ arg_type: Optional[QAPISchemaObjectType],
+ ret_type: Optional[QAPISchemaType],
+ gen: bool,
+ success_response: bool,
+ boxed: bool,
+ allow_oob: bool,
+ allow_preconfig: bool,
+ coroutine: bool) -> None:
+ if not gen:
+ return
+ # FIXME: If T is a user-defined type, the user is responsible
+ # for making this work, i.e. to make T's condition the
+ # conjunction of the T-returning commands' conditions. If T
+ # is a built-in type, this isn't possible: the
+ # qmp_marshal_output_T() will be generated unconditionally.
+ if ret_type and ret_type not in self._visited_ret_types[self._genc]:
+ self._visited_ret_types[self._genc].add(ret_type)
+ with ifcontext(ret_type.ifcond,
+ self._genh, self._genc):
+ self._genc.add(gen_marshal_output(ret_type))
+ with ifcontext(ifcond, self._genh, self._genc):
+ self._genh.add(gen_command_decl(name, arg_type, boxed, ret_type))
+ self._genh.add(gen_marshal_decl(name))
+ self._genc.add(gen_marshal(name, arg_type, boxed, ret_type))
+ with self._temp_module('./init'):
+ with ifcontext(ifcond, self._genh, self._genc):
+ self._genc.add(gen_register_command(
+ name, features, success_response, allow_oob,
+ allow_preconfig, coroutine))
+
+
+def gen_commands(schema: QAPISchema,
+ output_dir: str,
+ prefix: str) -> None:
+ vis = QAPISchemaGenCommandVisitor(prefix)
+ schema.visit(vis)
+ vis.write(output_dir)
diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
new file mode 100644
index 000000000..489273574
--- /dev/null
+++ b/scripts/qapi/common.py
@@ -0,0 +1,251 @@
+#
+# QAPI helper library
+#
+# Copyright IBM, Corp. 2011
+# Copyright (c) 2013-2018 Red Hat Inc.
+#
+# Authors:
+# Anthony Liguori <aliguori@us.ibm.com>
+# Markus Armbruster <armbru@redhat.com>
+#
+# This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+
+import re
+from typing import (
+ Any,
+ Dict,
+ Match,
+ Optional,
+ Sequence,
+ Union,
+)
+
+
+#: Magic string that gets removed along with all space to its right.
+EATSPACE = '\033EATSPACE.'
+POINTER_SUFFIX = ' *' + EATSPACE
+
+
+def camel_to_upper(value: str) -> str:
+ """
+ Converts CamelCase to CAMEL_CASE.
+
+ Examples::
+
+ ENUMName -> ENUM_NAME
+ EnumName1 -> ENUM_NAME1
+ ENUM_NAME -> ENUM_NAME
+ ENUM_NAME1 -> ENUM_NAME1
+ ENUM_Name2 -> ENUM_NAME2
+ ENUM24_Name -> ENUM24_NAME
+ """
+ c_fun_str = c_name(value, False)
+ if value.isupper():
+ return c_fun_str
+
+ new_name = ''
+ length = len(c_fun_str)
+ for i in range(length):
+ char = c_fun_str[i]
+ # When char is upper case and no '_' appears before, do more checks
+ if char.isupper() and (i > 0) and c_fun_str[i - 1] != '_':
+ if i < length - 1 and c_fun_str[i + 1].islower():
+ new_name += '_'
+ elif c_fun_str[i - 1].isdigit():
+ new_name += '_'
+ new_name += char
+ return new_name.lstrip('_').upper()
+
+
+def c_enum_const(type_name: str,
+ const_name: str,
+ prefix: Optional[str] = None) -> str:
+ """
+ Generate a C enumeration constant name.
+
+ :param type_name: The name of the enumeration.
+ :param const_name: The name of this constant.
+ :param prefix: Optional, prefix that overrides the type_name.
+ """
+ if prefix is not None:
+ type_name = prefix
+ return camel_to_upper(type_name) + '_' + c_name(const_name, False).upper()
+
+
+def c_name(name: str, protect: bool = True) -> str:
+ """
+ Map ``name`` to a valid C identifier.
+
+ Used for converting 'name' from a 'name':'type' qapi definition
+ into a generated struct member, as well as converting type names
+ into substrings of a generated C function name.
+
+ '__a.b_c' -> '__a_b_c', 'x-foo' -> 'x_foo'
+ protect=True: 'int' -> 'q_int'; protect=False: 'int' -> 'int'
+
+ :param name: The name to map.
+ :param protect: If true, avoid returning certain ticklish identifiers
+ (like C keywords) by prepending ``q_``.
+ """
+ # ANSI X3J11/88-090, 3.1.1
+ c89_words = set(['auto', 'break', 'case', 'char', 'const', 'continue',
+ 'default', 'do', 'double', 'else', 'enum', 'extern',
+ 'float', 'for', 'goto', 'if', 'int', 'long', 'register',
+ 'return', 'short', 'signed', 'sizeof', 'static',
+ 'struct', 'switch', 'typedef', 'union', 'unsigned',
+ 'void', 'volatile', 'while'])
+ # ISO/IEC 9899:1999, 6.4.1
+ c99_words = set(['inline', 'restrict', '_Bool', '_Complex', '_Imaginary'])
+ # ISO/IEC 9899:2011, 6.4.1
+ c11_words = set(['_Alignas', '_Alignof', '_Atomic', '_Generic',
+ '_Noreturn', '_Static_assert', '_Thread_local'])
+ # GCC http://gcc.gnu.org/onlinedocs/gcc-4.7.1/gcc/C-Extensions.html
+ # excluding _.*
+ gcc_words = set(['asm', 'typeof'])
+ # C++ ISO/IEC 14882:2003 2.11
+ cpp_words = set(['bool', 'catch', 'class', 'const_cast', 'delete',
+ 'dynamic_cast', 'explicit', 'false', 'friend', 'mutable',
+ 'namespace', 'new', 'operator', 'private', 'protected',
+ 'public', 'reinterpret_cast', 'static_cast', 'template',
+ 'this', 'throw', 'true', 'try', 'typeid', 'typename',
+ 'using', 'virtual', 'wchar_t',
+ # alternative representations
+ 'and', 'and_eq', 'bitand', 'bitor', 'compl', 'not',
+ 'not_eq', 'or', 'or_eq', 'xor', 'xor_eq'])
+ # namespace pollution:
+ polluted_words = set(['unix', 'errno', 'mips', 'sparc', 'i386'])
+ name = re.sub(r'[^A-Za-z0-9_]', '_', name)
+ if protect and (name in (c89_words | c99_words | c11_words | gcc_words
+ | cpp_words | polluted_words)
+ or name[0].isdigit()):
+ return 'q_' + name
+ return name
+
+
+class Indentation:
+ """
+ Indentation level management.
+
+ :param initial: Initial number of spaces, default 0.
+ """
+ def __init__(self, initial: int = 0) -> None:
+ self._level = initial
+
+ def __repr__(self) -> str:
+ return "{}({:d})".format(type(self).__name__, self._level)
+
+ def __str__(self) -> str:
+ """Return the current indentation as a string of spaces."""
+ return ' ' * self._level
+
+ def increase(self, amount: int = 4) -> None:
+ """Increase the indentation level by ``amount``, default 4."""
+ self._level += amount
+
+ def decrease(self, amount: int = 4) -> None:
+ """Decrease the indentation level by ``amount``, default 4."""
+ assert amount <= self._level
+ self._level -= amount
+
+
+#: Global, current indent level for code generation.
+indent = Indentation()
+
+
+def cgen(code: str, **kwds: object) -> str:
+ """
+ Generate ``code`` with ``kwds`` interpolated.
+
+ Obey `indent`, and strip `EATSPACE`.
+ """
+ raw = code % kwds
+ pfx = str(indent)
+ if pfx:
+ raw = re.sub(r'^(?!(#|$))', pfx, raw, flags=re.MULTILINE)
+ return re.sub(re.escape(EATSPACE) + r' *', '', raw)
+
+
+def mcgen(code: str, **kwds: object) -> str:
+ if code[0] == '\n':
+ code = code[1:]
+ return cgen(code, **kwds)
+
+
+def c_fname(filename: str) -> str:
+ return re.sub(r'[^A-Za-z0-9_]', '_', filename)
+
+
+def guardstart(name: str) -> str:
+ return mcgen('''
+#ifndef %(name)s
+#define %(name)s
+
+''',
+ name=c_fname(name).upper())
+
+
+def guardend(name: str) -> str:
+ return mcgen('''
+
+#endif /* %(name)s */
+''',
+ name=c_fname(name).upper())
+
+
+def gen_ifcond(ifcond: Optional[Union[str, Dict[str, Any]]],
+ cond_fmt: str, not_fmt: str,
+ all_operator: str, any_operator: str) -> str:
+
+ def do_gen(ifcond: Union[str, Dict[str, Any]],
+ need_parens: bool) -> str:
+ if isinstance(ifcond, str):
+ return cond_fmt % ifcond
+ assert isinstance(ifcond, dict) and len(ifcond) == 1
+ if 'not' in ifcond:
+ return not_fmt % do_gen(ifcond['not'], True)
+ if 'all' in ifcond:
+ gen = gen_infix(all_operator, ifcond['all'])
+ else:
+ gen = gen_infix(any_operator, ifcond['any'])
+ if need_parens:
+ gen = '(' + gen + ')'
+ return gen
+
+ def gen_infix(operator: str, operands: Sequence[Any]) -> str:
+ return operator.join([do_gen(o, True) for o in operands])
+
+ if not ifcond:
+ return ''
+ return do_gen(ifcond, False)
+
+
+def cgen_ifcond(ifcond: Optional[Union[str, Dict[str, Any]]]) -> str:
+ return gen_ifcond(ifcond, 'defined(%s)', '!%s', ' && ', ' || ')
+
+
+def docgen_ifcond(ifcond: Optional[Union[str, Dict[str, Any]]]) -> str:
+ # TODO Doc generated for conditions needs polish
+ return gen_ifcond(ifcond, '%s', 'not %s', ' and ', ' or ')
+
+
+def gen_if(cond: str) -> str:
+ if not cond:
+ return ''
+ return mcgen('''
+#if %(cond)s
+''', cond=cond)
+
+
+def gen_endif(cond: str) -> str:
+ if not cond:
+ return ''
+ return mcgen('''
+#endif /* %(cond)s */
+''', cond=cond)
+
+
+def must_match(pattern: str, string: str) -> Match[str]:
+ match = re.match(pattern, string)
+ assert match is not None
+ return match
diff --git a/scripts/qapi/error.py b/scripts/qapi/error.py
new file mode 100644
index 000000000..e35e4ddb2
--- /dev/null
+++ b/scripts/qapi/error.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017-2019 Red Hat Inc.
+#
+# Authors:
+# Markus Armbruster <armbru@redhat.com>
+# Marc-André Lureau <marcandre.lureau@redhat.com>
+#
+# This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+
+"""
+QAPI error classes
+
+Common error classes used throughout the package. Additional errors may
+be defined in other modules. At present, `QAPIParseError` is defined in
+parser.py.
+"""
+
+from typing import Optional
+
+from .source import QAPISourceInfo
+
+
+class QAPIError(Exception):
+ """Base class for all exceptions from the QAPI package."""
+
+
+class QAPISourceError(QAPIError):
+ """Error class for all exceptions identifying a source location."""
+ def __init__(self,
+ info: Optional[QAPISourceInfo],
+ msg: str,
+ col: Optional[int] = None):
+ super().__init__()
+ self.info = info
+ self.msg = msg
+ self.col = col
+
+ def __str__(self) -> str:
+ assert self.info is not None
+ loc = str(self.info)
+ if self.col is not None:
+ assert self.info.line is not None
+ loc += ':%s' % self.col
+ return loc + ': ' + self.msg
+
+
+class QAPISemError(QAPISourceError):
+ """Error class for semantic QAPI errors."""
diff --git a/scripts/qapi/events.py b/scripts/qapi/events.py
new file mode 100644
index 000000000..27b44c49f
--- /dev/null
+++ b/scripts/qapi/events.py
@@ -0,0 +1,252 @@
+"""
+QAPI event generator
+
+Copyright (c) 2014 Wenchao Xia
+Copyright (c) 2015-2018 Red Hat Inc.
+
+Authors:
+ Wenchao Xia <wenchaoqemu@gmail.com>
+ Markus Armbruster <armbru@redhat.com>
+
+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
+
+from .common import c_enum_const, c_name, mcgen
+from .gen import QAPISchemaModularCVisitor, build_params, ifcontext
+from .schema import (
+ QAPISchema,
+ QAPISchemaEnumMember,
+ QAPISchemaFeature,
+ QAPISchemaIfCond,
+ QAPISchemaObjectType,
+)
+from .source import QAPISourceInfo
+from .types import gen_enum, gen_enum_lookup
+
+
+def build_event_send_proto(name: str,
+ arg_type: Optional[QAPISchemaObjectType],
+ boxed: bool) -> str:
+ return 'void qapi_event_send_%(c_name)s(%(param)s)' % {
+ 'c_name': c_name(name.lower()),
+ 'param': build_params(arg_type, boxed)}
+
+
+def gen_event_send_decl(name: str,
+ arg_type: Optional[QAPISchemaObjectType],
+ boxed: bool) -> str:
+ return mcgen('''
+
+%(proto)s;
+''',
+ proto=build_event_send_proto(name, arg_type, boxed))
+
+
+def gen_param_var(typ: QAPISchemaObjectType) -> str:
+ """
+ Generate a struct variable holding the event parameters.
+
+ Initialize it with the function arguments defined in `gen_event_send`.
+ """
+ assert not typ.variants
+ ret = mcgen('''
+ %(c_name)s param = {
+''',
+ c_name=typ.c_name())
+ sep = ' '
+ for memb in typ.members:
+ ret += sep
+ sep = ', '
+ if memb.optional:
+ ret += 'has_' + c_name(memb.name) + sep
+ if memb.type.name == 'str':
+ # Cast away const added in build_params()
+ ret += '(char *)'
+ ret += c_name(memb.name)
+ ret += mcgen('''
+
+ };
+''')
+ if not typ.is_implicit():
+ ret += mcgen('''
+ %(c_name)s *arg = &param;
+''',
+ c_name=typ.c_name())
+ return ret
+
+
+def gen_event_send(name: str,
+ arg_type: Optional[QAPISchemaObjectType],
+ features: List[QAPISchemaFeature],
+ boxed: bool,
+ event_enum_name: str,
+ event_emit: str) -> str:
+ # FIXME: Our declaration of local variables (and of 'errp' in the
+ # parameter list) can collide with exploded members of the event's
+ # data type passed in as parameters. If this collision ever hits in
+ # practice, we can rename our local variables with a leading _ prefix,
+ # or split the code into a wrapper function that creates a boxed
+ # 'param' object then calls another to do the real work.
+ have_args = boxed or (arg_type and not arg_type.is_empty())
+
+ ret = mcgen('''
+
+%(proto)s
+{
+ QDict *qmp;
+''',
+ proto=build_event_send_proto(name, arg_type, boxed))
+
+ if have_args:
+ assert arg_type is not None
+ ret += mcgen('''
+ QObject *obj;
+ Visitor *v;
+''')
+ if not boxed:
+ ret += gen_param_var(arg_type)
+
+ for f in features:
+ if f.is_special():
+ ret += mcgen('''
+
+ if (compat_policy.%(feat)s_output == COMPAT_POLICY_OUTPUT_HIDE) {
+ return;
+ }
+''',
+ feat=f.name)
+
+ ret += mcgen('''
+
+ qmp = qmp_event_build_dict("%(name)s");
+
+''',
+ name=name)
+
+ if have_args:
+ assert arg_type is not None
+ ret += mcgen('''
+ v = qobject_output_visitor_new_qmp(&obj);
+''')
+ if not arg_type.is_implicit():
+ ret += mcgen('''
+ visit_type_%(c_name)s(v, "%(name)s", &arg, &error_abort);
+''',
+ name=name, c_name=arg_type.c_name())
+ else:
+ ret += mcgen('''
+
+ visit_start_struct(v, "%(name)s", NULL, 0, &error_abort);
+ visit_type_%(c_name)s_members(v, &param, &error_abort);
+ visit_check_struct(v, &error_abort);
+ visit_end_struct(v, NULL);
+''',
+ name=name, c_name=arg_type.c_name())
+ ret += mcgen('''
+
+ visit_complete(v, &obj);
+ if (qdict_size(qobject_to(QDict, obj))) {
+ qdict_put_obj(qmp, "data", obj);
+ } else {
+ qobject_unref(obj);
+ }
+''')
+
+ ret += mcgen('''
+ %(event_emit)s(%(c_enum)s, qmp);
+
+''',
+ event_emit=event_emit,
+ c_enum=c_enum_const(event_enum_name, name))
+
+ if have_args:
+ ret += mcgen('''
+ visit_free(v);
+''')
+ ret += mcgen('''
+ qobject_unref(qmp);
+}
+''')
+ return ret
+
+
+class QAPISchemaGenEventVisitor(QAPISchemaModularCVisitor):
+
+ def __init__(self, prefix: str):
+ super().__init__(
+ prefix, 'qapi-events',
+ ' * Schema-defined QAPI/QMP events', None, __doc__)
+ self._event_enum_name = c_name(prefix + 'QAPIEvent', protect=False)
+ self._event_enum_members: List[QAPISchemaEnumMember] = []
+ self._event_emit_name = c_name(prefix + 'qapi_event_emit')
+
+ def _begin_user_module(self, name: str) -> None:
+ events = self._module_basename('qapi-events', name)
+ types = self._module_basename('qapi-types', name)
+ visit = self._module_basename('qapi-visit', name)
+ self._genc.add(mcgen('''
+#include "qemu/osdep.h"
+#include "%(prefix)sqapi-emit-events.h"
+#include "%(events)s.h"
+#include "%(visit)s.h"
+#include "qapi/compat-policy.h"
+#include "qapi/error.h"
+#include "qapi/qmp/qdict.h"
+#include "qapi/qmp-event.h"
+
+''',
+ events=events, visit=visit,
+ prefix=self._prefix))
+ self._genh.add(mcgen('''
+#include "qapi/util.h"
+#include "%(types)s.h"
+''',
+ types=types))
+
+ def visit_end(self) -> None:
+ self._add_module('./emit', ' * QAPI Events emission')
+ self._genc.preamble_add(mcgen('''
+#include "qemu/osdep.h"
+#include "%(prefix)sqapi-emit-events.h"
+''',
+ prefix=self._prefix))
+ self._genh.preamble_add(mcgen('''
+#include "qapi/util.h"
+'''))
+ self._genh.add(gen_enum(self._event_enum_name,
+ self._event_enum_members))
+ self._genc.add(gen_enum_lookup(self._event_enum_name,
+ self._event_enum_members))
+ self._genh.add(mcgen('''
+
+void %(event_emit)s(%(event_enum)s event, QDict *qdict);
+''',
+ event_emit=self._event_emit_name,
+ event_enum=self._event_enum_name))
+
+ def visit_event(self,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ arg_type: Optional[QAPISchemaObjectType],
+ boxed: bool) -> None:
+ with ifcontext(ifcond, self._genh, self._genc):
+ self._genh.add(gen_event_send_decl(name, arg_type, boxed))
+ self._genc.add(gen_event_send(name, arg_type, features, boxed,
+ self._event_enum_name,
+ self._event_emit_name))
+ # Note: we generate the enum member regardless of @ifcond, to
+ # keep the enumeration usable in target-independent code.
+ self._event_enum_members.append(QAPISchemaEnumMember(name, None))
+
+
+def gen_events(schema: QAPISchema,
+ output_dir: str,
+ prefix: str) -> None:
+ vis = QAPISchemaGenEventVisitor(prefix)
+ schema.visit(vis)
+ vis.write(output_dir)
diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
new file mode 100644
index 000000000..3cb389e87
--- /dev/null
+++ b/scripts/qapi/expr.py
@@ -0,0 +1,694 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright IBM, Corp. 2011
+# Copyright (c) 2013-2021 Red Hat Inc.
+#
+# Authors:
+# Anthony Liguori <aliguori@us.ibm.com>
+# Markus Armbruster <armbru@redhat.com>
+# Eric Blake <eblake@redhat.com>
+# Marc-André Lureau <marcandre.lureau@redhat.com>
+# John Snow <jsnow@redhat.com>
+#
+# This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+
+"""
+Normalize and validate (context-free) QAPI schema expression structures.
+
+`QAPISchemaParser` parses a QAPI schema into abstract syntax trees
+consisting of dict, list, str, bool, and int nodes. This module ensures
+that these nested structures have the correct type(s) and key(s) where
+appropriate for the QAPI context-free grammar.
+
+The QAPI schema expression language allows for certain syntactic sugar;
+this module also handles the normalization process of these nested
+structures.
+
+See `check_exprs` for the main entry point.
+
+See `schema.QAPISchema` for processing into native Python data
+structures and contextual semantic validation.
+"""
+
+import re
+from typing import (
+ Collection,
+ Dict,
+ Iterable,
+ List,
+ Optional,
+ Union,
+ cast,
+)
+
+from .common import c_name
+from .error import QAPISemError
+from .parser import QAPIDoc
+from .source import QAPISourceInfo
+
+
+# Deserialized JSON objects as returned by the parser.
+# The values of this mapping are not necessary to exhaustively type
+# here (and also not practical as long as mypy lacks recursive
+# types), because the purpose of this module is to interrogate that
+# type.
+_JSONObject = Dict[str, object]
+
+
+# See check_name_str(), below.
+valid_name = re.compile(r'(__[a-z0-9.-]+_)?'
+ r'(x-)?'
+ r'([a-z][a-z0-9_-]*)$', re.IGNORECASE)
+
+
+def check_name_is_str(name: object,
+ info: QAPISourceInfo,
+ source: str) -> None:
+ """
+ Ensure that ``name`` is a ``str``.
+
+ :raise QAPISemError: When ``name`` fails validation.
+ """
+ if not isinstance(name, str):
+ raise QAPISemError(info, "%s requires a string name" % source)
+
+
+def check_name_str(name: str, info: QAPISourceInfo, source: str) -> str:
+ """
+ Ensure that ``name`` is a valid QAPI name.
+
+ A valid name consists of ASCII letters, digits, ``-``, and ``_``,
+ starting with a letter. It may be prefixed by a downstream prefix
+ of the form __RFQDN_, or the experimental prefix ``x-``. If both
+ prefixes are present, the __RFDQN_ prefix goes first.
+
+ A valid name cannot start with ``q_``, which is reserved.
+
+ :param name: Name to check.
+ :param info: QAPI schema source file information.
+ :param source: Error string describing what ``name`` belongs to.
+
+ :raise QAPISemError: When ``name`` fails validation.
+ :return: The stem of the valid name, with no prefixes.
+ """
+ # Reserve the entire 'q_' namespace for c_name(), and for 'q_empty'
+ # and 'q_obj_*' implicit type names.
+ match = valid_name.match(name)
+ if not match or c_name(name, False).startswith('q_'):
+ raise QAPISemError(info, "%s has an invalid name" % source)
+ return match.group(3)
+
+
+def check_name_upper(name: str, info: QAPISourceInfo, source: str) -> None:
+ """
+ Ensure that ``name`` is a valid event name.
+
+ This means it must be a valid QAPI name as checked by
+ `check_name_str()`, but where the stem prohibits lowercase
+ characters and ``-``.
+
+ :param name: Name to check.
+ :param info: QAPI schema source file information.
+ :param source: Error string describing what ``name`` belongs to.
+
+ :raise QAPISemError: When ``name`` fails validation.
+ """
+ stem = check_name_str(name, info, source)
+ if re.search(r'[a-z-]', stem):
+ raise QAPISemError(
+ info, "name of %s must not use lowercase or '-'" % source)
+
+
+def check_name_lower(name: str, info: QAPISourceInfo, source: str,
+ permit_upper: bool = False,
+ permit_underscore: bool = False) -> None:
+ """
+ Ensure that ``name`` is a valid command or member name.
+
+ This means it must be a valid QAPI name as checked by
+ `check_name_str()`, but where the stem prohibits uppercase
+ characters and ``_``.
+
+ :param name: Name to check.
+ :param info: QAPI schema source file information.
+ :param source: Error string describing what ``name`` belongs to.
+ :param permit_upper: Additionally permit uppercase.
+ :param permit_underscore: Additionally permit ``_``.
+
+ :raise QAPISemError: When ``name`` fails validation.
+ """
+ stem = check_name_str(name, info, source)
+ if ((not permit_upper and re.search(r'[A-Z]', stem))
+ or (not permit_underscore and '_' in stem)):
+ raise QAPISemError(
+ info, "name of %s must not use uppercase or '_'" % source)
+
+
+def check_name_camel(name: str, info: QAPISourceInfo, source: str) -> None:
+ """
+ Ensure that ``name`` is a valid user-defined type name.
+
+ This means it must be a valid QAPI name as checked by
+ `check_name_str()`, but where the stem must be in CamelCase.
+
+ :param name: Name to check.
+ :param info: QAPI schema source file information.
+ :param source: Error string describing what ``name`` belongs to.
+
+ :raise QAPISemError: When ``name`` fails validation.
+ """
+ stem = check_name_str(name, info, source)
+ if not re.match(r'[A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*$', stem):
+ raise QAPISemError(info, "name of %s must use CamelCase" % source)
+
+
+def check_defn_name_str(name: str, info: QAPISourceInfo, meta: str) -> None:
+ """
+ Ensure that ``name`` is a valid definition name.
+
+ Based on the value of ``meta``, this means that:
+ - 'event' names adhere to `check_name_upper()`.
+ - 'command' names adhere to `check_name_lower()`.
+ - Else, meta is a type, and must pass `check_name_camel()`.
+ These names must not end with ``List``.
+
+ :param name: Name to check.
+ :param info: QAPI schema source file information.
+ :param meta: Meta-type name of the QAPI expression.
+
+ :raise QAPISemError: When ``name`` fails validation.
+ """
+ if meta == 'event':
+ check_name_upper(name, info, meta)
+ elif meta == 'command':
+ check_name_lower(
+ name, info, meta,
+ permit_underscore=name in info.pragma.command_name_exceptions)
+ else:
+ check_name_camel(name, info, meta)
+ if name.endswith('List'):
+ raise QAPISemError(
+ info, "%s name should not end in 'List'" % meta)
+
+
+def check_keys(value: _JSONObject,
+ info: QAPISourceInfo,
+ source: str,
+ required: Collection[str],
+ optional: Collection[str]) -> None:
+ """
+ Ensure that a dict has a specific set of keys.
+
+ :param value: The dict to check.
+ :param info: QAPI schema source file information.
+ :param source: Error string describing this ``value``.
+ :param required: Keys that *must* be present.
+ :param optional: Keys that *may* be present.
+
+ :raise QAPISemError: When unknown keys are present.
+ """
+
+ def pprint(elems: Iterable[str]) -> str:
+ return ', '.join("'" + e + "'" for e in sorted(elems))
+
+ missing = set(required) - set(value)
+ if missing:
+ raise QAPISemError(
+ info,
+ "%s misses key%s %s"
+ % (source, 's' if len(missing) > 1 else '',
+ pprint(missing)))
+ allowed = set(required) | set(optional)
+ unknown = set(value) - allowed
+ if unknown:
+ raise QAPISemError(
+ info,
+ "%s has unknown key%s %s\nValid keys are %s."
+ % (source, 's' if len(unknown) > 1 else '',
+ pprint(unknown), pprint(allowed)))
+
+
+def check_flags(expr: _JSONObject, info: QAPISourceInfo) -> None:
+ """
+ Ensure flag members (if present) have valid values.
+
+ :param expr: The expression to validate.
+ :param info: QAPI schema source file information.
+
+ :raise QAPISemError:
+ When certain flags have an invalid value, or when
+ incompatible flags are present.
+ """
+ for key in ('gen', 'success-response'):
+ if key in expr and expr[key] is not False:
+ raise QAPISemError(
+ info, "flag '%s' may only use false value" % key)
+ for key in ('boxed', 'allow-oob', 'allow-preconfig', 'coroutine'):
+ if key in expr and expr[key] is not True:
+ raise QAPISemError(
+ info, "flag '%s' may only use true value" % key)
+ if 'allow-oob' in expr and 'coroutine' in expr:
+ # This is not necessarily a fundamental incompatibility, but
+ # we don't have a use case and the desired semantics isn't
+ # obvious. The simplest solution is to forbid it until we get
+ # a use case for it.
+ raise QAPISemError(info, "flags 'allow-oob' and 'coroutine' "
+ "are incompatible")
+
+
+def check_if(expr: _JSONObject, info: QAPISourceInfo, source: str) -> None:
+ """
+ Validate the ``if`` member of an object.
+
+ The ``if`` member may be either a ``str`` or a dict.
+
+ :param expr: The expression containing the ``if`` member to validate.
+ :param info: QAPI schema source file information.
+ :param source: Error string describing ``expr``.
+
+ :raise QAPISemError:
+ When the "if" member fails validation, or when there are no
+ non-empty conditions.
+ :return: None
+ """
+
+ def _check_if(cond: Union[str, object]) -> None:
+ if isinstance(cond, str):
+ if not re.fullmatch(r'[A-Z][A-Z0-9_]*', cond):
+ raise QAPISemError(
+ info,
+ "'if' condition '%s' of %s is not a valid identifier"
+ % (cond, source))
+ return
+
+ if not isinstance(cond, dict):
+ raise QAPISemError(
+ info,
+ "'if' condition of %s must be a string or an object" % source)
+ check_keys(cond, info, "'if' condition of %s" % source, [],
+ ["all", "any", "not"])
+ if len(cond) != 1:
+ raise QAPISemError(
+ info,
+ "'if' condition of %s has conflicting keys" % source)
+
+ if 'not' in cond:
+ _check_if(cond['not'])
+ elif 'all' in cond:
+ _check_infix('all', cond['all'])
+ else:
+ _check_infix('any', cond['any'])
+
+ def _check_infix(operator: str, operands: object) -> None:
+ if not isinstance(operands, list):
+ raise QAPISemError(
+ info,
+ "'%s' condition of %s must be an array"
+ % (operator, source))
+ if not operands:
+ raise QAPISemError(
+ info, "'if' condition [] of %s is useless" % source)
+ for operand in operands:
+ _check_if(operand)
+
+ ifcond = expr.get('if')
+ if ifcond is None:
+ return
+
+ _check_if(ifcond)
+
+
+def normalize_members(members: object) -> None:
+ """
+ Normalize a "members" value.
+
+ If ``members`` is a dict, for every value in that dict, if that
+ value is not itself already a dict, normalize it to
+ ``{'type': value}``.
+
+ :forms:
+ :sugared: ``Dict[str, Union[str, TypeRef]]``
+ :canonical: ``Dict[str, TypeRef]``
+
+ :param members: The members value to normalize.
+
+ :return: None, ``members`` is normalized in-place as needed.
+ """
+ if isinstance(members, dict):
+ for key, arg in members.items():
+ if isinstance(arg, dict):
+ continue
+ members[key] = {'type': arg}
+
+
+def check_type(value: Optional[object],
+ info: QAPISourceInfo,
+ source: str,
+ allow_array: bool = False,
+ allow_dict: Union[bool, str] = False) -> None:
+ """
+ Normalize and validate the QAPI type of ``value``.
+
+ Python types of ``str`` or ``None`` are always allowed.
+
+ :param value: The value to check.
+ :param info: QAPI schema source file information.
+ :param source: Error string describing this ``value``.
+ :param allow_array:
+ Allow a ``List[str]`` of length 1, which indicates an array of
+ the type named by the list element.
+ :param allow_dict:
+ Allow a dict. Its members can be struct type members or union
+ branches. When the value of ``allow_dict`` is in pragma
+ ``member-name-exceptions``, the dict's keys may violate the
+ member naming rules. The dict members are normalized in place.
+
+ :raise QAPISemError: When ``value`` fails validation.
+ :return: None, ``value`` is normalized in-place as needed.
+ """
+ if value is None:
+ return
+
+ # Type name
+ if isinstance(value, str):
+ return
+
+ # Array type
+ if isinstance(value, list):
+ if not allow_array:
+ raise QAPISemError(info, "%s cannot be an array" % source)
+ if len(value) != 1 or not isinstance(value[0], str):
+ raise QAPISemError(info,
+ "%s: array type must contain single type name" %
+ source)
+ return
+
+ # Anonymous type
+
+ if not allow_dict:
+ raise QAPISemError(info, "%s should be a type name" % source)
+
+ if not isinstance(value, dict):
+ raise QAPISemError(info,
+ "%s should be an object or type name" % source)
+
+ permissive = False
+ if isinstance(allow_dict, str):
+ permissive = allow_dict in info.pragma.member_name_exceptions
+
+ # value is a dictionary, check that each member is okay
+ for (key, arg) in value.items():
+ key_source = "%s member '%s'" % (source, key)
+ if key.startswith('*'):
+ key = key[1:]
+ check_name_lower(key, info, key_source,
+ permit_upper=permissive,
+ permit_underscore=permissive)
+ if c_name(key, False) == 'u' or c_name(key, False).startswith('has_'):
+ raise QAPISemError(info, "%s uses reserved name" % key_source)
+ check_keys(arg, info, key_source, ['type'], ['if', 'features'])
+ check_if(arg, info, key_source)
+ check_features(arg.get('features'), info)
+ check_type(arg['type'], info, key_source, allow_array=True)
+
+
+def check_features(features: Optional[object],
+ info: QAPISourceInfo) -> None:
+ """
+ Normalize and validate the ``features`` member.
+
+ ``features`` may be a ``list`` of either ``str`` or ``dict``.
+ Any ``str`` element will be normalized to ``{'name': element}``.
+
+ :forms:
+ :sugared: ``List[Union[str, Feature]]``
+ :canonical: ``List[Feature]``
+
+ :param features: The features member value to validate.
+ :param info: QAPI schema source file information.
+
+ :raise QAPISemError: When ``features`` fails validation.
+ :return: None, ``features`` is normalized in-place as needed.
+ """
+ if features is None:
+ return
+ if not isinstance(features, list):
+ raise QAPISemError(info, "'features' must be an array")
+ features[:] = [f if isinstance(f, dict) else {'name': f}
+ for f in features]
+ for feat in features:
+ source = "'features' member"
+ assert isinstance(feat, dict)
+ check_keys(feat, info, source, ['name'], ['if'])
+ check_name_is_str(feat['name'], info, source)
+ source = "%s '%s'" % (source, feat['name'])
+ check_name_str(feat['name'], info, source)
+ check_if(feat, info, source)
+
+
+def check_enum(expr: _JSONObject, info: QAPISourceInfo) -> None:
+ """
+ Normalize and validate this expression as an ``enum`` definition.
+
+ :param expr: The expression to validate.
+ :param info: QAPI schema source file information.
+
+ :raise QAPISemError: When ``expr`` is not a valid ``enum``.
+ :return: None, ``expr`` is normalized in-place as needed.
+ """
+ name = expr['enum']
+ members = expr['data']
+ prefix = expr.get('prefix')
+
+ if not isinstance(members, list):
+ raise QAPISemError(info, "'data' must be an array")
+ if prefix is not None and not isinstance(prefix, str):
+ raise QAPISemError(info, "'prefix' must be a string")
+
+ permissive = name in info.pragma.member_name_exceptions
+
+ members[:] = [m if isinstance(m, dict) else {'name': m}
+ for m in members]
+ for member in members:
+ source = "'data' member"
+ check_keys(member, info, source, ['name'], ['if', 'features'])
+ member_name = member['name']
+ check_name_is_str(member_name, info, source)
+ source = "%s '%s'" % (source, member_name)
+ # Enum members may start with a digit
+ if member_name[0].isdigit():
+ member_name = 'd' + member_name # Hack: hide the digit
+ check_name_lower(member_name, info, source,
+ permit_upper=permissive,
+ permit_underscore=permissive)
+ check_if(member, info, source)
+ check_features(member.get('features'), info)
+
+
+def check_struct(expr: _JSONObject, info: QAPISourceInfo) -> None:
+ """
+ Normalize and validate this expression as a ``struct`` definition.
+
+ :param expr: The expression to validate.
+ :param info: QAPI schema source file information.
+
+ :raise QAPISemError: When ``expr`` is not a valid ``struct``.
+ :return: None, ``expr`` is normalized in-place as needed.
+ """
+ name = cast(str, expr['struct']) # Checked in check_exprs
+ members = expr['data']
+
+ check_type(members, info, "'data'", allow_dict=name)
+ check_type(expr.get('base'), info, "'base'")
+
+
+def check_union(expr: _JSONObject, info: QAPISourceInfo) -> None:
+ """
+ Normalize and validate this expression as a ``union`` definition.
+
+ :param expr: The expression to validate.
+ :param info: QAPI schema source file information.
+
+ :raise QAPISemError: when ``expr`` is not a valid ``union``.
+ :return: None, ``expr`` is normalized in-place as needed.
+ """
+ name = cast(str, expr['union']) # Checked in check_exprs
+ base = expr['base']
+ discriminator = expr['discriminator']
+ members = expr['data']
+
+ check_type(base, info, "'base'", allow_dict=name)
+ check_name_is_str(discriminator, info, "'discriminator'")
+
+ if not isinstance(members, dict):
+ raise QAPISemError(info, "'data' must be an object")
+
+ for (key, value) in members.items():
+ source = "'data' member '%s'" % key
+ check_keys(value, info, source, ['type'], ['if'])
+ check_if(value, info, source)
+ check_type(value['type'], info, source, allow_array=not base)
+
+
+def check_alternate(expr: _JSONObject, info: QAPISourceInfo) -> None:
+ """
+ Normalize and validate this expression as an ``alternate`` definition.
+
+ :param expr: The expression to validate.
+ :param info: QAPI schema source file information.
+
+ :raise QAPISemError: When ``expr`` is not a valid ``alternate``.
+ :return: None, ``expr`` is normalized in-place as needed.
+ """
+ members = expr['data']
+
+ if not members:
+ raise QAPISemError(info, "'data' must not be empty")
+
+ if not isinstance(members, dict):
+ raise QAPISemError(info, "'data' must be an object")
+
+ for (key, value) in members.items():
+ source = "'data' member '%s'" % key
+ check_name_lower(key, info, source)
+ check_keys(value, info, source, ['type'], ['if'])
+ check_if(value, info, source)
+ check_type(value['type'], info, source)
+
+
+def check_command(expr: _JSONObject, info: QAPISourceInfo) -> None:
+ """
+ Normalize and validate this expression as a ``command`` definition.
+
+ :param expr: The expression to validate.
+ :param info: QAPI schema source file information.
+
+ :raise QAPISemError: When ``expr`` is not a valid ``command``.
+ :return: None, ``expr`` is normalized in-place as needed.
+ """
+ args = expr.get('data')
+ rets = expr.get('returns')
+ boxed = expr.get('boxed', False)
+
+ if boxed and args is None:
+ raise QAPISemError(info, "'boxed': true requires 'data'")
+ check_type(args, info, "'data'", allow_dict=not boxed)
+ check_type(rets, info, "'returns'", allow_array=True)
+
+
+def check_event(expr: _JSONObject, info: QAPISourceInfo) -> None:
+ """
+ Normalize and validate this expression as an ``event`` definition.
+
+ :param expr: The expression to validate.
+ :param info: QAPI schema source file information.
+
+ :raise QAPISemError: When ``expr`` is not a valid ``event``.
+ :return: None, ``expr`` is normalized in-place as needed.
+ """
+ args = expr.get('data')
+ boxed = expr.get('boxed', False)
+
+ if boxed and args is None:
+ raise QAPISemError(info, "'boxed': true requires 'data'")
+ check_type(args, info, "'data'", allow_dict=not boxed)
+
+
+def check_exprs(exprs: List[_JSONObject]) -> List[_JSONObject]:
+ """
+ Validate and normalize a list of parsed QAPI schema expressions.
+
+ This function accepts a list of expressions and metadata as returned
+ by the parser. It destructively normalizes the expressions in-place.
+
+ :param exprs: The list of expressions to normalize and validate.
+
+ :raise QAPISemError: When any expression fails validation.
+ :return: The same list of expressions (now modified).
+ """
+ for expr_elem in exprs:
+ # Expression
+ assert isinstance(expr_elem['expr'], dict)
+ for key in expr_elem['expr'].keys():
+ assert isinstance(key, str)
+ expr: _JSONObject = expr_elem['expr']
+
+ # QAPISourceInfo
+ assert isinstance(expr_elem['info'], QAPISourceInfo)
+ info: QAPISourceInfo = expr_elem['info']
+
+ # Optional[QAPIDoc]
+ tmp = expr_elem.get('doc')
+ assert tmp is None or isinstance(tmp, QAPIDoc)
+ doc: Optional[QAPIDoc] = tmp
+
+ if 'include' in expr:
+ continue
+
+ metas = expr.keys() & {'enum', 'struct', 'union', 'alternate',
+ 'command', 'event'}
+ if len(metas) != 1:
+ raise QAPISemError(
+ info,
+ "expression must have exactly one key"
+ " 'enum', 'struct', 'union', 'alternate',"
+ " 'command', 'event'")
+ meta = metas.pop()
+
+ check_name_is_str(expr[meta], info, "'%s'" % meta)
+ name = cast(str, expr[meta])
+ info.set_defn(meta, name)
+ check_defn_name_str(name, info, meta)
+
+ if doc:
+ if doc.symbol != name:
+ raise QAPISemError(
+ info, "documentation comment is for '%s'" % doc.symbol)
+ doc.check_expr(expr)
+ elif info.pragma.doc_required:
+ raise QAPISemError(info,
+ "documentation comment required")
+
+ if meta == 'enum':
+ check_keys(expr, info, meta,
+ ['enum', 'data'], ['if', 'features', 'prefix'])
+ check_enum(expr, info)
+ elif meta == 'union':
+ check_keys(expr, info, meta,
+ ['union', 'base', 'discriminator', 'data'],
+ ['if', 'features'])
+ normalize_members(expr.get('base'))
+ normalize_members(expr['data'])
+ check_union(expr, info)
+ elif meta == 'alternate':
+ check_keys(expr, info, meta,
+ ['alternate', 'data'], ['if', 'features'])
+ normalize_members(expr['data'])
+ check_alternate(expr, info)
+ elif meta == 'struct':
+ check_keys(expr, info, meta,
+ ['struct', 'data'], ['base', 'if', 'features'])
+ normalize_members(expr['data'])
+ check_struct(expr, info)
+ elif meta == 'command':
+ check_keys(expr, info, meta,
+ ['command'],
+ ['data', 'returns', 'boxed', 'if', 'features',
+ 'gen', 'success-response', 'allow-oob',
+ 'allow-preconfig', 'coroutine'])
+ normalize_members(expr.get('data'))
+ check_command(expr, info)
+ elif meta == 'event':
+ check_keys(expr, info, meta,
+ ['event'], ['data', 'boxed', 'if', 'features'])
+ normalize_members(expr.get('data'))
+ check_event(expr, info)
+ else:
+ assert False, 'unexpected meta type'
+
+ check_if(expr, info, meta)
+ check_features(expr.get('features'), info)
+ check_flags(expr, info)
+
+ return exprs
diff --git a/scripts/qapi/gen.py b/scripts/qapi/gen.py
new file mode 100644
index 000000000..995a97d2b
--- /dev/null
+++ b/scripts/qapi/gen.py
@@ -0,0 +1,339 @@
+# -*- coding: utf-8 -*-
+#
+# QAPI code generation
+#
+# Copyright (c) 2015-2019 Red Hat Inc.
+#
+# Authors:
+# Markus Armbruster <armbru@redhat.com>
+# Marc-André Lureau <marcandre.lureau@redhat.com>
+#
+# This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+
+from contextlib import contextmanager
+import os
+import re
+from typing import (
+ Dict,
+ Iterator,
+ Optional,
+ Sequence,
+ Tuple,
+)
+
+from .common import (
+ c_fname,
+ c_name,
+ guardend,
+ guardstart,
+ mcgen,
+)
+from .schema import (
+ QAPISchemaFeature,
+ QAPISchemaIfCond,
+ QAPISchemaModule,
+ QAPISchemaObjectType,
+ QAPISchemaVisitor,
+)
+from .source import QAPISourceInfo
+
+
+def gen_special_features(features: Sequence[QAPISchemaFeature]) -> str:
+ special_features = [f"1u << QAPI_{feat.name.upper()}"
+ for feat in features if feat.is_special()]
+ return ' | '.join(special_features) or '0'
+
+
+class QAPIGen:
+ def __init__(self, fname: str):
+ self.fname = fname
+ self._preamble = ''
+ self._body = ''
+
+ def preamble_add(self, text: str) -> None:
+ self._preamble += text
+
+ def add(self, text: str) -> None:
+ self._body += text
+
+ def get_content(self) -> str:
+ return self._top() + self._preamble + self._body + self._bottom()
+
+ def _top(self) -> str:
+ # pylint: disable=no-self-use
+ return ''
+
+ def _bottom(self) -> str:
+ # pylint: disable=no-self-use
+ return ''
+
+ def write(self, output_dir: str) -> None:
+ # Include paths starting with ../ are used to reuse modules of the main
+ # schema in specialised schemas. Don't overwrite the files that are
+ # already generated for the main schema.
+ if self.fname.startswith('../'):
+ return
+ pathname = os.path.join(output_dir, self.fname)
+ odir = os.path.dirname(pathname)
+
+ if odir:
+ os.makedirs(odir, exist_ok=True)
+
+ # use os.open for O_CREAT to create and read a non-existant file
+ fd = os.open(pathname, os.O_RDWR | os.O_CREAT, 0o666)
+ with os.fdopen(fd, 'r+', encoding='utf-8') as fp:
+ text = self.get_content()
+ oldtext = fp.read(len(text) + 1)
+ if text != oldtext:
+ fp.seek(0)
+ fp.truncate(0)
+ fp.write(text)
+
+
+def _wrap_ifcond(ifcond: QAPISchemaIfCond, before: str, after: str) -> str:
+ if before == after:
+ return after # suppress empty #if ... #endif
+
+ assert after.startswith(before)
+ out = before
+ added = after[len(before):]
+ if added[0] == '\n':
+ out += '\n'
+ added = added[1:]
+ out += ifcond.gen_if()
+ out += added
+ out += ifcond.gen_endif()
+ return out
+
+
+def build_params(arg_type: Optional[QAPISchemaObjectType],
+ boxed: bool,
+ extra: Optional[str] = None) -> str:
+ ret = ''
+ sep = ''
+ if boxed:
+ assert arg_type
+ ret += '%s arg' % arg_type.c_param_type()
+ sep = ', '
+ elif arg_type:
+ assert not arg_type.variants
+ for memb in arg_type.members:
+ ret += sep
+ sep = ', '
+ if memb.optional:
+ ret += 'bool has_%s, ' % c_name(memb.name)
+ ret += '%s %s' % (memb.type.c_param_type(),
+ c_name(memb.name))
+ if extra:
+ ret += sep + extra
+ return ret if ret else 'void'
+
+
+class QAPIGenCCode(QAPIGen):
+ def __init__(self, fname: str):
+ super().__init__(fname)
+ self._start_if: Optional[Tuple[QAPISchemaIfCond, str, str]] = None
+
+ def start_if(self, ifcond: QAPISchemaIfCond) -> None:
+ assert self._start_if is None
+ self._start_if = (ifcond, self._body, self._preamble)
+
+ def end_if(self) -> None:
+ assert self._start_if is not None
+ self._body = _wrap_ifcond(self._start_if[0],
+ self._start_if[1], self._body)
+ self._preamble = _wrap_ifcond(self._start_if[0],
+ self._start_if[2], self._preamble)
+ self._start_if = None
+
+ def get_content(self) -> str:
+ assert self._start_if is None
+ return super().get_content()
+
+
+class QAPIGenC(QAPIGenCCode):
+ 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('''
+/* AUTOMATICALLY GENERATED, DO NOT MODIFY */
+
+/*
+%(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.
+ */
+
+''',
+ blurb=self._blurb, copyright=self._copyright)
+
+ def _bottom(self) -> str:
+ return mcgen('''
+
+/* Dummy declaration to prevent empty .o file */
+char qapi_dummy_%(name)s;
+''',
+ name=c_fname(self.fname))
+
+
+class QAPIGenH(QAPIGenC):
+ def _top(self) -> str:
+ return super()._top() + guardstart(self.fname)
+
+ def _bottom(self) -> str:
+ return guardend(self.fname)
+
+
+@contextmanager
+def ifcontext(ifcond: QAPISchemaIfCond, *args: QAPIGenCCode) -> Iterator[None]:
+ """
+ A with-statement context manager that wraps with `start_if()` / `end_if()`.
+
+ :param ifcond: A sequence of conditionals, passed to `start_if()`.
+ :param args: any number of `QAPIGenCCode`.
+
+ Example::
+
+ with ifcontext(ifcond, self._genh, self._genc):
+ modify self._genh and self._genc ...
+
+ Is equivalent to calling::
+
+ self._genh.start_if(ifcond)
+ self._genc.start_if(ifcond)
+ modify self._genh and self._genc ...
+ self._genh.end_if()
+ self._genc.end_if()
+ """
+ for arg in args:
+ arg.start_if(ifcond)
+ yield
+ for arg in args:
+ arg.end_if()
+
+
+class QAPISchemaMonolithicCVisitor(QAPISchemaVisitor):
+ def __init__(self,
+ prefix: str,
+ what: str,
+ blurb: str,
+ pydoc: str):
+ self._prefix = prefix
+ self._what = what
+ self._genc = QAPIGenC(self._prefix + self._what + '.c',
+ blurb, pydoc)
+ self._genh = QAPIGenH(self._prefix + self._what + '.h',
+ blurb, pydoc)
+
+ def write(self, output_dir: str) -> None:
+ self._genc.write(output_dir)
+ self._genh.write(output_dir)
+
+
+class QAPISchemaModularCVisitor(QAPISchemaVisitor):
+ def __init__(self,
+ prefix: str,
+ what: str,
+ user_blurb: str,
+ builtin_blurb: Optional[str],
+ pydoc: str):
+ self._prefix = prefix
+ self._what = what
+ self._user_blurb = user_blurb
+ self._builtin_blurb = builtin_blurb
+ self._pydoc = pydoc
+ self._current_module: Optional[str] = None
+ self._module: Dict[str, Tuple[QAPIGenC, QAPIGenH]] = {}
+ self._main_module: Optional[str] = None
+
+ @property
+ def _genc(self) -> QAPIGenC:
+ assert self._current_module is not None
+ return self._module[self._current_module][0]
+
+ @property
+ def _genh(self) -> QAPIGenH:
+ assert self._current_module is not None
+ return self._module[self._current_module][1]
+
+ @staticmethod
+ def _module_dirname(name: str) -> str:
+ if QAPISchemaModule.is_user_module(name):
+ return os.path.dirname(name)
+ return ''
+
+ def _module_basename(self, what: str, name: str) -> str:
+ ret = '' if QAPISchemaModule.is_builtin_module(name) else self._prefix
+ if QAPISchemaModule.is_user_module(name):
+ basename = os.path.basename(name)
+ ret += what
+ if name != self._main_module:
+ ret += '-' + os.path.splitext(basename)[0]
+ else:
+ assert QAPISchemaModule.is_system_module(name)
+ ret += re.sub(r'-', '-' + name[2:] + '-', what)
+ return ret
+
+ def _module_filename(self, what: str, name: str) -> str:
+ return os.path.join(self._module_dirname(name),
+ self._module_basename(what, name))
+
+ def _add_module(self, name: str, blurb: str) -> None:
+ if QAPISchemaModule.is_user_module(name):
+ if self._main_module is None:
+ self._main_module = name
+ basename = self._module_filename(self._what, name)
+ genc = QAPIGenC(basename + '.c', blurb, self._pydoc)
+ genh = QAPIGenH(basename + '.h', blurb, self._pydoc)
+ self._module[name] = (genc, genh)
+ self._current_module = name
+
+ @contextmanager
+ def _temp_module(self, name: str) -> Iterator[None]:
+ old_module = self._current_module
+ self._current_module = name
+ yield
+ self._current_module = old_module
+
+ def write(self, output_dir: str, opt_builtins: bool = False) -> None:
+ for name, (genc, genh) in self._module.items():
+ if QAPISchemaModule.is_builtin_module(name) and not opt_builtins:
+ continue
+ genc.write(output_dir)
+ genh.write(output_dir)
+
+ def _begin_builtin_module(self) -> None:
+ pass
+
+ def _begin_user_module(self, name: str) -> None:
+ pass
+
+ def visit_module(self, name: str) -> None:
+ if QAPISchemaModule.is_builtin_module(name):
+ if self._builtin_blurb:
+ self._add_module(name, self._builtin_blurb)
+ self._begin_builtin_module()
+ else:
+ # The built-in module has not been created. No code may
+ # be generated.
+ self._current_module = None
+ else:
+ assert QAPISchemaModule.is_user_module(name)
+ self._add_module(name, self._user_blurb)
+ self._begin_user_module(name)
+
+ def visit_include(self, name: str, info: Optional[QAPISourceInfo]) -> None:
+ relname = os.path.relpath(self._module_filename(self._what, name),
+ os.path.dirname(self._genh.fname))
+ self._genh.preamble_add(mcgen('''
+#include "%(relname)s.h"
+''',
+ relname=relname))
diff --git a/scripts/qapi/introspect.py b/scripts/qapi/introspect.py
new file mode 100644
index 000000000..67c7d89aa
--- /dev/null
+++ b/scripts/qapi/introspect.py
@@ -0,0 +1,390 @@
+"""
+QAPI introspection generator
+
+Copyright (C) 2015-2021 Red Hat, Inc.
+
+Authors:
+ Markus Armbruster <armbru@redhat.com>
+ John Snow <jsnow@redhat.com>
+
+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 (
+ Any,
+ Dict,
+ Generic,
+ List,
+ Optional,
+ Sequence,
+ TypeVar,
+ Union,
+)
+
+from .common import c_name, mcgen
+from .gen import QAPISchemaMonolithicCVisitor
+from .schema import (
+ QAPISchema,
+ QAPISchemaArrayType,
+ QAPISchemaBuiltinType,
+ QAPISchemaEntity,
+ QAPISchemaEnumMember,
+ QAPISchemaFeature,
+ QAPISchemaIfCond,
+ QAPISchemaObjectType,
+ QAPISchemaObjectTypeMember,
+ QAPISchemaType,
+ QAPISchemaVariant,
+ QAPISchemaVariants,
+)
+from .source import QAPISourceInfo
+
+
+# This module constructs a tree data structure that is used to
+# generate the introspection information for QEMU. It is shaped
+# like a JSON value.
+#
+# A complexity over JSON is that our values may or may not be annotated.
+#
+# Un-annotated values may be:
+# Scalar: str, bool, None.
+# Non-scalar: List, Dict
+# _value = Union[str, bool, None, Dict[str, JSONValue], List[JSONValue]]
+#
+# With optional annotations, the type of all values is:
+# JSONValue = Union[_Value, Annotated[_Value]]
+#
+# Sadly, mypy does not support recursive types; so the _Stub alias is used to
+# mark the imprecision in the type model where we'd otherwise use JSONValue.
+_Stub = Any
+_Scalar = Union[str, bool, None]
+_NonScalar = Union[Dict[str, _Stub], List[_Stub]]
+_Value = Union[_Scalar, _NonScalar]
+JSONValue = Union[_Value, 'Annotated[_Value]']
+
+# These types are based on structures defined in QEMU's schema, so we
+# lack precise types for them here. Python 3.6 does not offer
+# TypedDict constructs, so they are broadly typed here as simple
+# Python Dicts.
+SchemaInfo = Dict[str, object]
+SchemaInfoEnumMember = Dict[str, object]
+SchemaInfoObject = Dict[str, object]
+SchemaInfoObjectVariant = Dict[str, object]
+SchemaInfoObjectMember = Dict[str, object]
+SchemaInfoCommand = Dict[str, object]
+
+
+_ValueT = TypeVar('_ValueT', bound=_Value)
+
+
+class Annotated(Generic[_ValueT]):
+ """
+ Annotated generally contains a SchemaInfo-like type (as a dict),
+ But it also used to wrap comments/ifconds around scalar leaf values,
+ for the benefit of features and enums.
+ """
+ # TODO: Remove after Python 3.7 adds @dataclass:
+ # pylint: disable=too-few-public-methods
+ def __init__(self, value: _ValueT, ifcond: QAPISchemaIfCond,
+ comment: Optional[str] = None):
+ self.value = value
+ self.comment: Optional[str] = comment
+ self.ifcond = ifcond
+
+
+def _tree_to_qlit(obj: JSONValue,
+ level: int = 0,
+ dict_value: bool = False) -> str:
+ """
+ Convert the type tree into a QLIT C string, recursively.
+
+ :param obj: The value to convert.
+ This value may not be Annotated when dict_value is True.
+ :param level: The indentation level for this particular value.
+ :param dict_value: True when the value being processed belongs to a
+ dict key; which suppresses the output indent.
+ """
+
+ def indent(level: int) -> str:
+ return level * 4 * ' '
+
+ if isinstance(obj, Annotated):
+ # NB: _tree_to_qlit is called recursively on the values of a
+ # key:value pair; those values can't be decorated with
+ # comments or conditionals.
+ msg = "dict values cannot have attached comments or if-conditionals."
+ assert not dict_value, msg
+
+ ret = ''
+ if obj.comment:
+ ret += indent(level) + f"/* {obj.comment} */\n"
+ if obj.ifcond.is_present():
+ ret += obj.ifcond.gen_if()
+ ret += _tree_to_qlit(obj.value, level)
+ if obj.ifcond.is_present():
+ ret += '\n' + obj.ifcond.gen_endif()
+ return ret
+
+ ret = ''
+ if not dict_value:
+ ret += indent(level)
+
+ # Scalars:
+ if obj is None:
+ ret += 'QLIT_QNULL'
+ elif isinstance(obj, str):
+ ret += f"QLIT_QSTR({to_c_string(obj)})"
+ elif isinstance(obj, bool):
+ ret += f"QLIT_QBOOL({str(obj).lower()})"
+
+ # Non-scalars:
+ elif isinstance(obj, list):
+ ret += 'QLIT_QLIST(((QLitObject[]) {\n'
+ for value in obj:
+ ret += _tree_to_qlit(value, level + 1).strip('\n') + '\n'
+ ret += indent(level + 1) + '{}\n'
+ ret += indent(level) + '}))'
+ elif isinstance(obj, dict):
+ ret += 'QLIT_QDICT(((QLitDictEntry[]) {\n'
+ for key, value in sorted(obj.items()):
+ ret += indent(level + 1) + "{{ {:s}, {:s} }},\n".format(
+ to_c_string(key),
+ _tree_to_qlit(value, level + 1, dict_value=True)
+ )
+ ret += indent(level + 1) + '{}\n'
+ ret += indent(level) + '}))'
+ else:
+ raise NotImplementedError(
+ f"type '{type(obj).__name__}' not implemented"
+ )
+
+ if level > 0:
+ ret += ','
+ return ret
+
+
+def to_c_string(string: str) -> str:
+ return '"' + string.replace('\\', r'\\').replace('"', r'\"') + '"'
+
+
+class QAPISchemaGenIntrospectVisitor(QAPISchemaMonolithicCVisitor):
+
+ def __init__(self, prefix: str, unmask: bool):
+ super().__init__(
+ prefix, 'qapi-introspect',
+ ' * QAPI/QMP schema introspection', __doc__)
+ self._unmask = unmask
+ self._schema: Optional[QAPISchema] = None
+ self._trees: List[Annotated[SchemaInfo]] = []
+ self._used_types: List[QAPISchemaType] = []
+ self._name_map: Dict[str, str] = {}
+ self._genc.add(mcgen('''
+#include "qemu/osdep.h"
+#include "%(prefix)sqapi-introspect.h"
+
+''',
+ prefix=prefix))
+
+ def visit_begin(self, schema: QAPISchema) -> None:
+ self._schema = schema
+
+ def visit_end(self) -> None:
+ # visit the types that are actually used
+ for typ in self._used_types:
+ typ.visit(self)
+ # generate C
+ name = c_name(self._prefix, protect=False) + 'qmp_schema_qlit'
+ self._genh.add(mcgen('''
+#include "qapi/qmp/qlit.h"
+
+extern const QLitObject %(c_name)s;
+''',
+ c_name=c_name(name)))
+ self._genc.add(mcgen('''
+const QLitObject %(c_name)s = %(c_string)s;
+''',
+ c_name=c_name(name),
+ c_string=_tree_to_qlit(self._trees)))
+ self._schema = None
+ self._trees = []
+ self._used_types = []
+ self._name_map = {}
+
+ def visit_needed(self, entity: QAPISchemaEntity) -> bool:
+ # Ignore types on first pass; visit_end() will pick up used types
+ return not isinstance(entity, QAPISchemaType)
+
+ def _name(self, name: str) -> str:
+ if self._unmask:
+ return name
+ if name not in self._name_map:
+ self._name_map[name] = '%d' % len(self._name_map)
+ return self._name_map[name]
+
+ def _use_type(self, typ: QAPISchemaType) -> str:
+ assert self._schema is not None
+
+ # Map the various integer types to plain int
+ if typ.json_type() == 'int':
+ typ = self._schema.lookup_type('int')
+ elif (isinstance(typ, QAPISchemaArrayType) and
+ typ.element_type.json_type() == 'int'):
+ typ = self._schema.lookup_type('intList')
+ # Add type to work queue if new
+ if typ not in self._used_types:
+ self._used_types.append(typ)
+ # Clients should examine commands and events, not types. Hide
+ # type names as integers to reduce the temptation. Also, it
+ # saves a few characters on the wire.
+ if isinstance(typ, QAPISchemaBuiltinType):
+ return typ.name
+ if isinstance(typ, QAPISchemaArrayType):
+ return '[' + self._use_type(typ.element_type) + ']'
+ return self._name(typ.name)
+
+ @staticmethod
+ def _gen_features(features: Sequence[QAPISchemaFeature]
+ ) -> List[Annotated[str]]:
+ return [Annotated(f.name, f.ifcond) for f in features]
+
+ def _gen_tree(self, name: str, mtype: str, obj: Dict[str, object],
+ ifcond: QAPISchemaIfCond = QAPISchemaIfCond(),
+ features: Sequence[QAPISchemaFeature] = ()) -> None:
+ """
+ Build and append a SchemaInfo object to self._trees.
+
+ :param name: The SchemaInfo's name.
+ :param mtype: The SchemaInfo's meta-type.
+ :param obj: Additional SchemaInfo members, as appropriate for
+ the meta-type.
+ :param ifcond: Conditionals to apply to the SchemaInfo.
+ :param features: The SchemaInfo's features.
+ Will be omitted from the output if empty.
+ """
+ comment: Optional[str] = None
+ if mtype not in ('command', 'event', 'builtin', 'array'):
+ if not self._unmask:
+ # Output a comment to make it easy to map masked names
+ # back to the source when reading the generated output.
+ comment = f'"{self._name(name)}" = {name}'
+ name = self._name(name)
+ obj['name'] = name
+ obj['meta-type'] = mtype
+ if features:
+ obj['features'] = self._gen_features(features)
+ self._trees.append(Annotated(obj, ifcond, comment))
+
+ def _gen_enum_member(self, member: QAPISchemaEnumMember
+ ) -> Annotated[SchemaInfoEnumMember]:
+ obj: SchemaInfoEnumMember = {
+ 'name': member.name,
+ }
+ if member.features:
+ obj['features'] = self._gen_features(member.features)
+ return Annotated(obj, member.ifcond)
+
+ def _gen_object_member(self, member: QAPISchemaObjectTypeMember
+ ) -> Annotated[SchemaInfoObjectMember]:
+ obj: SchemaInfoObjectMember = {
+ 'name': member.name,
+ 'type': self._use_type(member.type)
+ }
+ if member.optional:
+ obj['default'] = None
+ if member.features:
+ obj['features'] = self._gen_features(member.features)
+ return Annotated(obj, member.ifcond)
+
+ def _gen_variant(self, variant: QAPISchemaVariant
+ ) -> Annotated[SchemaInfoObjectVariant]:
+ obj: SchemaInfoObjectVariant = {
+ 'case': variant.name,
+ 'type': self._use_type(variant.type)
+ }
+ return Annotated(obj, variant.ifcond)
+
+ def visit_builtin_type(self, name: str, info: Optional[QAPISourceInfo],
+ json_type: str) -> None:
+ self._gen_tree(name, 'builtin', {'json-type': json_type})
+
+ def visit_enum_type(self, name: str, info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ members: List[QAPISchemaEnumMember],
+ prefix: Optional[str]) -> None:
+ self._gen_tree(
+ name, 'enum',
+ {'members': [self._gen_enum_member(m) for m in members],
+ 'values': [Annotated(m.name, m.ifcond) for m in members]},
+ ifcond, features
+ )
+
+ def visit_array_type(self, name: str, info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ element_type: QAPISchemaType) -> None:
+ element = self._use_type(element_type)
+ self._gen_tree('[' + element + ']', 'array', {'element-type': element},
+ ifcond)
+
+ def visit_object_type_flat(self, name: str, info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ members: List[QAPISchemaObjectTypeMember],
+ variants: Optional[QAPISchemaVariants]) -> None:
+ obj: SchemaInfoObject = {
+ 'members': [self._gen_object_member(m) for m in members]
+ }
+ if variants:
+ obj['tag'] = variants.tag_member.name
+ obj['variants'] = [self._gen_variant(v) for v in variants.variants]
+ self._gen_tree(name, 'object', obj, ifcond, features)
+
+ def visit_alternate_type(self, name: str, info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ variants: QAPISchemaVariants) -> None:
+ self._gen_tree(
+ name, 'alternate',
+ {'members': [Annotated({'type': self._use_type(m.type)},
+ m.ifcond)
+ for m in variants.variants]},
+ ifcond, features
+ )
+
+ def visit_command(self, name: str, info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ arg_type: Optional[QAPISchemaObjectType],
+ ret_type: Optional[QAPISchemaType], gen: bool,
+ success_response: bool, boxed: bool, allow_oob: bool,
+ allow_preconfig: bool, coroutine: bool) -> None:
+ assert self._schema is not None
+
+ arg_type = arg_type or self._schema.the_empty_object_type
+ ret_type = ret_type or self._schema.the_empty_object_type
+ obj: SchemaInfoCommand = {
+ 'arg-type': self._use_type(arg_type),
+ 'ret-type': self._use_type(ret_type)
+ }
+ if allow_oob:
+ obj['allow-oob'] = allow_oob
+ self._gen_tree(name, 'command', obj, ifcond, features)
+
+ def visit_event(self, name: str, info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ arg_type: Optional[QAPISchemaObjectType],
+ boxed: bool) -> None:
+ assert self._schema is not None
+
+ arg_type = arg_type or self._schema.the_empty_object_type
+ self._gen_tree(name, 'event', {'arg-type': self._use_type(arg_type)},
+ ifcond, features)
+
+
+def gen_introspect(schema: QAPISchema, output_dir: str, prefix: str,
+ opt_unmask: bool) -> None:
+ vis = QAPISchemaGenIntrospectVisitor(prefix, opt_unmask)
+ schema.visit(vis)
+ vis.write(output_dir)
diff --git a/scripts/qapi/main.py b/scripts/qapi/main.py
new file mode 100644
index 000000000..f2ea6e0ce
--- /dev/null
+++ b/scripts/qapi/main.py
@@ -0,0 +1,95 @@
+# This work is licensed under the terms of the GNU GPL, version 2 or later.
+# See the COPYING file in the top-level directory.
+
+"""
+QAPI Generator
+
+This is the main entry point for generating C code from the QAPI schema.
+"""
+
+import argparse
+import sys
+from typing import Optional
+
+from .commands import gen_commands
+from .common import must_match
+from .error import QAPIError
+from .events import gen_events
+from .introspect import gen_introspect
+from .schema import QAPISchema
+from .types import gen_types
+from .visit import gen_visit
+
+
+def invalid_prefix_char(prefix: str) -> Optional[str]:
+ match = must_match(r'([A-Za-z_.-][A-Za-z0-9_.-]*)?', prefix)
+ if match.end() != len(prefix):
+ return prefix[match.end()]
+ return None
+
+
+def generate(schema_file: str,
+ output_dir: str,
+ prefix: str,
+ unmask: bool = False,
+ builtins: bool = False) -> None:
+ """
+ Generate C 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.
+ """
+ assert invalid_prefix_char(prefix) is None
+
+ schema = QAPISchema(schema_file)
+ gen_types(schema, output_dir, prefix, builtins)
+ gen_visit(schema, output_dir, prefix, builtins)
+ gen_commands(schema, output_dir, prefix)
+ gen_events(schema, output_dir, prefix)
+ gen_introspect(schema, output_dir, prefix, unmask)
+
+
+def main() -> int:
+ """
+ gapi-gen executable entry point.
+ Expects arguments via sys.argv, see --help for details.
+
+ :return: int, 0 on success, 1 on failure.
+ """
+ parser = argparse.ArgumentParser(
+ description='Generate code from a QAPI schema')
+ parser.add_argument('-b', '--builtins', action='store_true',
+ help="generate code for built-in types")
+ parser.add_argument('-o', '--output-dir', action='store',
+ default='',
+ help="write output to directory OUTPUT_DIR")
+ parser.add_argument('-p', '--prefix', action='store',
+ default='',
+ help="prefix for symbols")
+ parser.add_argument('-u', '--unmask-non-abi-names', action='store_true',
+ dest='unmask',
+ help="expose non-ABI names in introspection")
+ parser.add_argument('schema', action='store')
+ args = parser.parse_args()
+
+ funny_char = invalid_prefix_char(args.prefix)
+ if funny_char:
+ msg = f"funny character '{funny_char}' in argument of --prefix"
+ print(f"{sys.argv[0]}: {msg}", file=sys.stderr)
+ return 1
+
+ try:
+ generate(args.schema,
+ output_dir=args.output_dir,
+ prefix=args.prefix,
+ unmask=args.unmask,
+ builtins=args.builtins)
+ except QAPIError as err:
+ print(f"{sys.argv[0]}: {str(err)}", file=sys.stderr)
+ return 1
+ return 0
diff --git a/scripts/qapi/mypy.ini b/scripts/qapi/mypy.ini
new file mode 100644
index 000000000..662535642
--- /dev/null
+++ b/scripts/qapi/mypy.ini
@@ -0,0 +1,9 @@
+[mypy]
+strict = True
+disallow_untyped_calls = False
+python_version = 3.6
+
+[mypy-qapi.schema]
+disallow_untyped_defs = False
+disallow_incomplete_defs = False
+check_untyped_defs = False
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
new file mode 100644
index 000000000..1b006cdc1
--- /dev/null
+++ b/scripts/qapi/parser.py
@@ -0,0 +1,810 @@
+# -*- coding: utf-8 -*-
+#
+# QAPI schema parser
+#
+# Copyright IBM, Corp. 2011
+# Copyright (c) 2013-2019 Red Hat Inc.
+#
+# Authors:
+# Anthony Liguori <aliguori@us.ibm.com>
+# Markus Armbruster <armbru@redhat.com>
+# Marc-André Lureau <marcandre.lureau@redhat.com>
+# Kevin Wolf <kwolf@redhat.com>
+#
+# This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+
+from collections import OrderedDict
+import os
+import re
+from typing import (
+ TYPE_CHECKING,
+ Dict,
+ List,
+ Optional,
+ Set,
+ Union,
+)
+
+from .common import must_match
+from .error import QAPISemError, QAPISourceError
+from .source import QAPISourceInfo
+
+
+if TYPE_CHECKING:
+ # pylint: disable=cyclic-import
+ # TODO: Remove cycle. [schema -> expr -> parser -> schema]
+ from .schema import QAPISchemaFeature, QAPISchemaMember
+
+
+#: Represents a single Top Level QAPI schema expression.
+TopLevelExpr = Dict[str, object]
+
+# Return value alias for get_expr().
+_ExprValue = Union[List[object], Dict[str, object], str, bool]
+
+# FIXME: Consolidate and centralize definitions for TopLevelExpr,
+# _ExprValue, _JSONValue, and _JSONObject; currently scattered across
+# several modules.
+
+
+class QAPIParseError(QAPISourceError):
+ """Error class for all QAPI schema parsing errors."""
+ def __init__(self, parser: 'QAPISchemaParser', msg: str):
+ col = 1
+ for ch in parser.src[parser.line_pos:parser.pos]:
+ if ch == '\t':
+ col = (col + 7) % 8 + 1
+ else:
+ col += 1
+ super().__init__(parser.info, msg, col)
+
+
+class QAPISchemaParser:
+ """
+ Parse QAPI schema source.
+
+ Parse a JSON-esque schema file and process directives. See
+ qapi-code-gen.txt section "Schema Syntax" for the exact syntax.
+ Grammatical validation is handled later by `expr.check_exprs()`.
+
+ :param fname: Source file name.
+ :param previously_included:
+ The absolute names of previously included source files,
+ if being invoked from another parser.
+ :param incl_info:
+ `QAPISourceInfo` belonging to the parent module.
+ ``None`` implies this is the root module.
+
+ :ivar exprs: Resulting parsed expressions.
+ :ivar docs: Resulting parsed documentation blocks.
+
+ :raise OSError: For problems reading the root schema document.
+ :raise QAPIError: For errors in the schema source.
+ """
+ def __init__(self,
+ fname: str,
+ previously_included: Optional[Set[str]] = None,
+ incl_info: Optional[QAPISourceInfo] = None):
+ self._fname = fname
+ self._included = previously_included or set()
+ self._included.add(os.path.abspath(self._fname))
+ self.src = ''
+
+ # Lexer state (see `accept` for details):
+ self.info = QAPISourceInfo(self._fname, incl_info)
+ self.tok: Union[None, str] = None
+ self.pos = 0
+ self.cursor = 0
+ self.val: Optional[Union[bool, str]] = None
+ self.line_pos = 0
+
+ # Parser output:
+ self.exprs: List[Dict[str, object]] = []
+ self.docs: List[QAPIDoc] = []
+
+ # Showtime!
+ self._parse()
+
+ def _parse(self) -> None:
+ """
+ Parse the QAPI schema document.
+
+ :return: None. Results are stored in ``.exprs`` and ``.docs``.
+ """
+ cur_doc = None
+
+ # May raise OSError; allow the caller to handle it.
+ with open(self._fname, 'r', encoding='utf-8') as fp:
+ self.src = fp.read()
+ if self.src == '' or self.src[-1] != '\n':
+ self.src += '\n'
+
+ # Prime the lexer:
+ self.accept()
+
+ # Parse until done:
+ while self.tok is not None:
+ info = self.info
+ if self.tok == '#':
+ self.reject_expr_doc(cur_doc)
+ for cur_doc in self.get_doc(info):
+ self.docs.append(cur_doc)
+ continue
+
+ expr = self.get_expr()
+ if not isinstance(expr, dict):
+ raise QAPISemError(
+ info, "top-level expression must be an object")
+
+ if 'include' in expr:
+ self.reject_expr_doc(cur_doc)
+ if len(expr) != 1:
+ raise QAPISemError(info, "invalid 'include' directive")
+ include = expr['include']
+ if not isinstance(include, str):
+ raise QAPISemError(info,
+ "value of 'include' must be a string")
+ incl_fname = os.path.join(os.path.dirname(self._fname),
+ include)
+ self.exprs.append({'expr': {'include': incl_fname},
+ 'info': info})
+ exprs_include = self._include(include, info, incl_fname,
+ self._included)
+ if exprs_include:
+ self.exprs.extend(exprs_include.exprs)
+ self.docs.extend(exprs_include.docs)
+ elif "pragma" in expr:
+ self.reject_expr_doc(cur_doc)
+ if len(expr) != 1:
+ raise QAPISemError(info, "invalid 'pragma' directive")
+ pragma = expr['pragma']
+ if not isinstance(pragma, dict):
+ raise QAPISemError(
+ info, "value of 'pragma' must be an object")
+ for name, value in pragma.items():
+ self._pragma(name, value, info)
+ else:
+ expr_elem = {'expr': expr,
+ 'info': info}
+ if cur_doc:
+ if not cur_doc.symbol:
+ raise QAPISemError(
+ cur_doc.info, "definition documentation required")
+ expr_elem['doc'] = cur_doc
+ self.exprs.append(expr_elem)
+ cur_doc = None
+ self.reject_expr_doc(cur_doc)
+
+ @staticmethod
+ def reject_expr_doc(doc: Optional['QAPIDoc']) -> None:
+ if doc and doc.symbol:
+ raise QAPISemError(
+ doc.info,
+ "documentation for '%s' is not followed by the definition"
+ % doc.symbol)
+
+ @staticmethod
+ def _include(include: str,
+ info: QAPISourceInfo,
+ incl_fname: str,
+ previously_included: Set[str]
+ ) -> Optional['QAPISchemaParser']:
+ incl_abs_fname = os.path.abspath(incl_fname)
+ # catch inclusion cycle
+ inf: Optional[QAPISourceInfo] = info
+ while inf:
+ if incl_abs_fname == os.path.abspath(inf.fname):
+ raise QAPISemError(info, "inclusion loop for %s" % include)
+ inf = inf.parent
+
+ # skip multiple include of the same file
+ if incl_abs_fname in previously_included:
+ return None
+
+ try:
+ return QAPISchemaParser(incl_fname, previously_included, info)
+ except OSError as err:
+ raise QAPISemError(
+ info,
+ f"can't read include file '{incl_fname}': {err.strerror}"
+ ) from err
+
+ @staticmethod
+ def _pragma(name: str, value: object, info: QAPISourceInfo) -> None:
+
+ def check_list_str(name: str, value: object) -> List[str]:
+ if (not isinstance(value, list) or
+ any(not isinstance(elt, str) for elt in value)):
+ raise QAPISemError(
+ info,
+ "pragma %s must be a list of strings" % name)
+ return value
+
+ pragma = info.pragma
+
+ if name == 'doc-required':
+ if not isinstance(value, bool):
+ raise QAPISemError(info,
+ "pragma 'doc-required' must be boolean")
+ pragma.doc_required = value
+ elif name == 'command-name-exceptions':
+ pragma.command_name_exceptions = check_list_str(name, value)
+ elif name == 'command-returns-exceptions':
+ pragma.command_returns_exceptions = check_list_str(name, value)
+ elif name == 'member-name-exceptions':
+ pragma.member_name_exceptions = check_list_str(name, value)
+ else:
+ raise QAPISemError(info, "unknown pragma '%s'" % name)
+
+ def accept(self, skip_comment: bool = True) -> None:
+ """
+ Read and store the next token.
+
+ :param skip_comment:
+ When false, return COMMENT tokens ("#").
+ This is used when reading documentation blocks.
+
+ :return:
+ None. Several instance attributes are updated instead:
+
+ - ``.tok`` represents the token type. See below for values.
+ - ``.info`` describes the token's source location.
+ - ``.val`` is the token's value, if any. See below.
+ - ``.pos`` is the buffer index of the first character of
+ the token.
+
+ * Single-character tokens:
+
+ These are "{", "}", ":", ",", "[", and "]".
+ ``.tok`` holds the single character and ``.val`` is None.
+
+ * Multi-character tokens:
+
+ * COMMENT:
+
+ This token is not normally returned by the lexer, but it can
+ be when ``skip_comment`` is False. ``.tok`` is "#", and
+ ``.val`` is a string including all chars until end-of-line,
+ including the "#" itself.
+
+ * STRING:
+
+ ``.tok`` is "'", the single quote. ``.val`` contains the
+ string, excluding the surrounding quotes.
+
+ * TRUE and FALSE:
+
+ ``.tok`` is either "t" or "f", ``.val`` will be the
+ corresponding bool value.
+
+ * EOF:
+
+ ``.tok`` and ``.val`` will both be None at EOF.
+ """
+ while True:
+ self.tok = self.src[self.cursor]
+ self.pos = self.cursor
+ self.cursor += 1
+ self.val = None
+
+ if self.tok == '#':
+ if self.src[self.cursor] == '#':
+ # Start of doc comment
+ skip_comment = False
+ self.cursor = self.src.find('\n', self.cursor)
+ if not skip_comment:
+ self.val = self.src[self.pos:self.cursor]
+ return
+ elif self.tok in '{}:,[]':
+ return
+ elif self.tok == "'":
+ # Note: we accept only printable ASCII
+ string = ''
+ esc = False
+ while True:
+ ch = self.src[self.cursor]
+ self.cursor += 1
+ if ch == '\n':
+ raise QAPIParseError(self, "missing terminating \"'\"")
+ if esc:
+ # Note: we recognize only \\ because we have
+ # no use for funny characters in strings
+ if ch != '\\':
+ raise QAPIParseError(self,
+ "unknown escape \\%s" % ch)
+ esc = False
+ elif ch == '\\':
+ esc = True
+ continue
+ elif ch == "'":
+ self.val = string
+ return
+ if ord(ch) < 32 or ord(ch) >= 127:
+ raise QAPIParseError(
+ self, "funny character in string")
+ string += ch
+ elif self.src.startswith('true', self.pos):
+ self.val = True
+ self.cursor += 3
+ return
+ elif self.src.startswith('false', self.pos):
+ self.val = False
+ self.cursor += 4
+ return
+ elif self.tok == '\n':
+ if self.cursor == len(self.src):
+ self.tok = None
+ return
+ self.info = self.info.next_line()
+ self.line_pos = self.cursor
+ elif not self.tok.isspace():
+ # Show up to next structural, whitespace or quote
+ # character
+ match = must_match('[^[\\]{}:,\\s\'"]+',
+ self.src[self.cursor-1:])
+ raise QAPIParseError(self, "stray '%s'" % match.group(0))
+
+ def get_members(self) -> Dict[str, object]:
+ expr: Dict[str, object] = OrderedDict()
+ if self.tok == '}':
+ self.accept()
+ return expr
+ if self.tok != "'":
+ raise QAPIParseError(self, "expected string or '}'")
+ while True:
+ key = self.val
+ assert isinstance(key, str) # Guaranteed by tok == "'"
+
+ self.accept()
+ if self.tok != ':':
+ raise QAPIParseError(self, "expected ':'")
+ self.accept()
+ if key in expr:
+ raise QAPIParseError(self, "duplicate key '%s'" % key)
+ expr[key] = self.get_expr()
+ if self.tok == '}':
+ self.accept()
+ return expr
+ if self.tok != ',':
+ raise QAPIParseError(self, "expected ',' or '}'")
+ self.accept()
+ if self.tok != "'":
+ raise QAPIParseError(self, "expected string")
+
+ def get_values(self) -> List[object]:
+ expr: List[object] = []
+ if self.tok == ']':
+ self.accept()
+ return expr
+ if self.tok not in tuple("{['tf"):
+ raise QAPIParseError(
+ self, "expected '{', '[', ']', string, or boolean")
+ while True:
+ expr.append(self.get_expr())
+ if self.tok == ']':
+ self.accept()
+ return expr
+ if self.tok != ',':
+ raise QAPIParseError(self, "expected ',' or ']'")
+ self.accept()
+
+ def get_expr(self) -> _ExprValue:
+ expr: _ExprValue
+ if self.tok == '{':
+ self.accept()
+ expr = self.get_members()
+ elif self.tok == '[':
+ self.accept()
+ expr = self.get_values()
+ elif self.tok in tuple("'tf"):
+ assert isinstance(self.val, (str, bool))
+ expr = self.val
+ self.accept()
+ else:
+ raise QAPIParseError(
+ self, "expected '{', '[', string, or boolean")
+ return expr
+
+ def get_doc(self, info: QAPISourceInfo) -> List['QAPIDoc']:
+ if self.val != '##':
+ raise QAPIParseError(
+ self, "junk after '##' at start of documentation comment")
+
+ docs = []
+ cur_doc = QAPIDoc(self, info)
+ self.accept(False)
+ while self.tok == '#':
+ assert isinstance(self.val, str)
+ if self.val.startswith('##'):
+ # End of doc comment
+ if self.val != '##':
+ raise QAPIParseError(
+ self,
+ "junk after '##' at end of documentation comment")
+ cur_doc.end_comment()
+ docs.append(cur_doc)
+ self.accept()
+ return docs
+ if self.val.startswith('# ='):
+ if cur_doc.symbol:
+ raise QAPIParseError(
+ self,
+ "unexpected '=' markup in definition documentation")
+ if cur_doc.body.text:
+ cur_doc.end_comment()
+ docs.append(cur_doc)
+ cur_doc = QAPIDoc(self, info)
+ cur_doc.append(self.val)
+ self.accept(False)
+
+ raise QAPIParseError(self, "documentation comment must end with '##'")
+
+
+class QAPIDoc:
+ """
+ A documentation comment block, either definition or free-form
+
+ Definition documentation blocks consist of
+
+ * a body section: one line naming the definition, followed by an
+ overview (any number of lines)
+
+ * argument sections: a description of each argument (for commands
+ and events) or member (for structs, unions and alternates)
+
+ * features sections: a description of each feature flag
+
+ * additional (non-argument) sections, possibly tagged
+
+ Free-form documentation blocks consist only of a body section.
+ """
+
+ class Section:
+ # pylint: disable=too-few-public-methods
+ def __init__(self, parser: QAPISchemaParser,
+ name: Optional[str] = None, indent: int = 0):
+
+ # parser, for error messages about indentation
+ self._parser = parser
+ # optional section name (argument/member or section name)
+ self.name = name
+ self.text = ''
+ # the expected indent level of the text of this section
+ self._indent = indent
+
+ def append(self, line: str) -> None:
+ # Strip leading spaces corresponding to the expected indent level
+ # Blank lines are always OK.
+ if line:
+ indent = must_match(r'\s*', line).end()
+ if indent < self._indent:
+ raise QAPIParseError(
+ self._parser,
+ "unexpected de-indent (expected at least %d spaces)" %
+ self._indent)
+ line = line[self._indent:]
+
+ self.text += line.rstrip() + '\n'
+
+ class ArgSection(Section):
+ def __init__(self, parser: QAPISchemaParser,
+ name: str, indent: int = 0):
+ super().__init__(parser, name, indent)
+ self.member: Optional['QAPISchemaMember'] = None
+
+ def connect(self, member: 'QAPISchemaMember') -> None:
+ self.member = member
+
+ class NullSection(Section):
+ """
+ Immutable dummy section for use at the end of a doc block.
+ """
+ # pylint: disable=too-few-public-methods
+ def append(self, line: str) -> None:
+ assert False, "Text appended after end_comment() called."
+
+ def __init__(self, parser: QAPISchemaParser, info: QAPISourceInfo):
+ # self._parser is used to report errors with QAPIParseError. The
+ # resulting error position depends on the state of the parser.
+ # It happens to be the beginning of the comment. More or less
+ # servicable, but action at a distance.
+ self._parser = parser
+ self.info = info
+ self.symbol: Optional[str] = None
+ self.body = QAPIDoc.Section(parser)
+ # dicts mapping parameter/feature names to their ArgSection
+ self.args: Dict[str, QAPIDoc.ArgSection] = OrderedDict()
+ self.features: Dict[str, QAPIDoc.ArgSection] = OrderedDict()
+ self.sections: List[QAPIDoc.Section] = []
+ # the current section
+ self._section = self.body
+ self._append_line = self._append_body_line
+
+ def has_section(self, name: str) -> bool:
+ """Return True if we have a section with this name."""
+ for i in self.sections:
+ if i.name == name:
+ return True
+ return False
+
+ def append(self, line: str) -> None:
+ """
+ Parse a comment line and add it to the documentation.
+
+ The way that the line is dealt with depends on which part of
+ the documentation we're parsing right now:
+ * The body section: ._append_line is ._append_body_line
+ * An argument section: ._append_line is ._append_args_line
+ * A features section: ._append_line is ._append_features_line
+ * An additional section: ._append_line is ._append_various_line
+ """
+ line = line[1:]
+ if not line:
+ self._append_freeform(line)
+ return
+
+ if line[0] != ' ':
+ raise QAPIParseError(self._parser, "missing space after #")
+ line = line[1:]
+ self._append_line(line)
+
+ def end_comment(self) -> None:
+ self._switch_section(QAPIDoc.NullSection(self._parser))
+
+ @staticmethod
+ def _is_section_tag(name: str) -> bool:
+ return name in ('Returns:', 'Since:',
+ # those are often singular or plural
+ 'Note:', 'Notes:',
+ 'Example:', 'Examples:',
+ 'TODO:')
+
+ def _append_body_line(self, line: str) -> None:
+ """
+ Process a line of documentation text in the body section.
+
+ If this a symbol line and it is the section's first line, this
+ is a definition documentation block for that symbol.
+
+ If it's a definition documentation block, another symbol line
+ begins the argument section for the argument named by it, and
+ a section tag begins an additional section. Start that
+ section and append the line to it.
+
+ Else, append the line to the current section.
+ """
+ name = line.split(' ', 1)[0]
+ # FIXME not nice: things like '# @foo:' and '# @foo: ' aren't
+ # recognized, and get silently treated as ordinary text
+ if not self.symbol and not self.body.text and line.startswith('@'):
+ if not line.endswith(':'):
+ raise QAPIParseError(self._parser, "line should end with ':'")
+ self.symbol = line[1:-1]
+ # Invalid names are not checked here, but the name provided MUST
+ # match the following definition, which *is* validated in expr.py.
+ if not self.symbol:
+ raise QAPIParseError(
+ self._parser, "name required after '@'")
+ elif self.symbol:
+ # This is a definition documentation block
+ if name.startswith('@') and name.endswith(':'):
+ self._append_line = self._append_args_line
+ self._append_args_line(line)
+ elif line == 'Features:':
+ self._append_line = self._append_features_line
+ elif self._is_section_tag(name):
+ self._append_line = self._append_various_line
+ self._append_various_line(line)
+ else:
+ self._append_freeform(line)
+ else:
+ # This is a free-form documentation block
+ self._append_freeform(line)
+
+ def _append_args_line(self, line: str) -> None:
+ """
+ Process a line of documentation text in an argument section.
+
+ A symbol line begins the next argument section, a section tag
+ section or a non-indented line after a blank line begins an
+ additional section. Start that section and append the line to
+ it.
+
+ Else, append the line to the current section.
+
+ """
+ name = line.split(' ', 1)[0]
+
+ if name.startswith('@') and name.endswith(':'):
+ # If line is "@arg: first line of description", find
+ # the index of 'f', which is the indent we expect for any
+ # following lines. We then remove the leading "@arg:"
+ # from line and replace it with spaces so that 'f' has the
+ # same index as it did in the original line and can be
+ # handled the same way we will handle following lines.
+ indent = must_match(r'@\S*:\s*', line).end()
+ line = line[indent:]
+ if not line:
+ # Line was just the "@arg:" header; following lines
+ # are not indented
+ indent = 0
+ else:
+ line = ' ' * indent + line
+ self._start_args_section(name[1:-1], indent)
+ elif self._is_section_tag(name):
+ self._append_line = self._append_various_line
+ self._append_various_line(line)
+ return
+ elif (self._section.text.endswith('\n\n')
+ and line and not line[0].isspace()):
+ if line == 'Features:':
+ self._append_line = self._append_features_line
+ else:
+ self._start_section()
+ self._append_line = self._append_various_line
+ self._append_various_line(line)
+ return
+
+ self._append_freeform(line)
+
+ def _append_features_line(self, line: str) -> None:
+ name = line.split(' ', 1)[0]
+
+ if name.startswith('@') and name.endswith(':'):
+ # If line is "@arg: first line of description", find
+ # the index of 'f', which is the indent we expect for any
+ # following lines. We then remove the leading "@arg:"
+ # from line and replace it with spaces so that 'f' has the
+ # same index as it did in the original line and can be
+ # handled the same way we will handle following lines.
+ indent = must_match(r'@\S*:\s*', line).end()
+ line = line[indent:]
+ if not line:
+ # Line was just the "@arg:" header; following lines
+ # are not indented
+ indent = 0
+ else:
+ line = ' ' * indent + line
+ self._start_features_section(name[1:-1], indent)
+ elif self._is_section_tag(name):
+ self._append_line = self._append_various_line
+ self._append_various_line(line)
+ return
+ elif (self._section.text.endswith('\n\n')
+ and line and not line[0].isspace()):
+ self._start_section()
+ self._append_line = self._append_various_line
+ self._append_various_line(line)
+ return
+
+ self._append_freeform(line)
+
+ def _append_various_line(self, line: str) -> None:
+ """
+ Process a line of documentation text in an additional section.
+
+ A symbol line is an error.
+
+ A section tag begins an additional section. Start that
+ section and append the line to it.
+
+ Else, append the line to the current section.
+ """
+ name = line.split(' ', 1)[0]
+
+ if name.startswith('@') and name.endswith(':'):
+ raise QAPIParseError(self._parser,
+ "'%s' can't follow '%s' section"
+ % (name, self.sections[0].name))
+ if self._is_section_tag(name):
+ # If line is "Section: first line of description", find
+ # the index of 'f', which is the indent we expect for any
+ # following lines. We then remove the leading "Section:"
+ # from line and replace it with spaces so that 'f' has the
+ # same index as it did in the original line and can be
+ # handled the same way we will handle following lines.
+ indent = must_match(r'\S*:\s*', line).end()
+ line = line[indent:]
+ if not line:
+ # Line was just the "Section:" header; following lines
+ # are not indented
+ indent = 0
+ else:
+ line = ' ' * indent + line
+ self._start_section(name[:-1], indent)
+
+ self._append_freeform(line)
+
+ def _start_symbol_section(
+ self,
+ symbols_dict: Dict[str, 'QAPIDoc.ArgSection'],
+ name: str,
+ indent: int) -> None:
+ # FIXME invalid names other than the empty string aren't flagged
+ if not name:
+ raise QAPIParseError(self._parser, "invalid parameter name")
+ if name in symbols_dict:
+ raise QAPIParseError(self._parser,
+ "'%s' parameter name duplicated" % name)
+ assert not self.sections
+ new_section = QAPIDoc.ArgSection(self._parser, name, indent)
+ self._switch_section(new_section)
+ symbols_dict[name] = new_section
+
+ def _start_args_section(self, name: str, indent: int) -> None:
+ self._start_symbol_section(self.args, name, indent)
+
+ def _start_features_section(self, name: str, indent: int) -> None:
+ self._start_symbol_section(self.features, name, indent)
+
+ def _start_section(self, name: Optional[str] = None,
+ indent: int = 0) -> None:
+ if name in ('Returns', 'Since') and self.has_section(name):
+ raise QAPIParseError(self._parser,
+ "duplicated '%s' section" % name)
+ new_section = QAPIDoc.Section(self._parser, name, indent)
+ self._switch_section(new_section)
+ self.sections.append(new_section)
+
+ def _switch_section(self, new_section: 'QAPIDoc.Section') -> None:
+ text = self._section.text = self._section.text.strip()
+
+ # Only the 'body' section is allowed to have an empty body.
+ # All other sections, including anonymous ones, must have text.
+ if self._section != self.body and not text:
+ # We do not create anonymous sections unless there is
+ # something to put in them; this is a parser bug.
+ assert self._section.name
+ raise QAPIParseError(
+ self._parser,
+ "empty doc section '%s'" % self._section.name)
+
+ self._section = new_section
+
+ def _append_freeform(self, line: str) -> None:
+ match = re.match(r'(@\S+:)', line)
+ if match:
+ raise QAPIParseError(self._parser,
+ "'%s' not allowed in free-form documentation"
+ % match.group(1))
+ self._section.append(line)
+
+ def connect_member(self, member: 'QAPISchemaMember') -> None:
+ if member.name not in self.args:
+ # Undocumented TODO outlaw
+ self.args[member.name] = QAPIDoc.ArgSection(self._parser,
+ member.name)
+ self.args[member.name].connect(member)
+
+ def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
+ if feature.name not in self.features:
+ raise QAPISemError(feature.info,
+ "feature '%s' lacks documentation"
+ % feature.name)
+ self.features[feature.name].connect(feature)
+
+ def check_expr(self, expr: TopLevelExpr) -> None:
+ if self.has_section('Returns') and 'command' not in expr:
+ raise QAPISemError(self.info,
+ "'Returns:' is only valid for commands")
+
+ def check(self) -> None:
+
+ def check_args_section(
+ args: Dict[str, QAPIDoc.ArgSection], what: str
+ ) -> None:
+ bogus = [name for name, section in args.items()
+ if not section.member]
+ if bogus:
+ raise QAPISemError(
+ self.info,
+ "documented %s%s '%s' %s not exist" % (
+ what,
+ "s" if len(bogus) > 1 else "",
+ "', '".join(bogus),
+ "do" if len(bogus) > 1 else "does"
+ ))
+
+ check_args_section(self.args, 'member')
+ check_args_section(self.features, 'feature')
diff --git a/scripts/qapi/pylintrc b/scripts/qapi/pylintrc
new file mode 100644
index 000000000..b259531a7
--- /dev/null
+++ b/scripts/qapi/pylintrc
@@ -0,0 +1,69 @@
+[MASTER]
+
+# Add files or directories matching the regex patterns to the ignore list.
+# The regex matches against base names, not paths.
+ignore-patterns=schema.py,
+
+
+[MESSAGES CONTROL]
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once). You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use "--disable=all --enable=classes
+# --disable=W".
+disable=fixme,
+ missing-docstring,
+ too-many-arguments,
+ too-many-branches,
+ too-many-statements,
+ too-many-instance-attributes,
+ consider-using-f-string,
+
+[REPORTS]
+
+[REFACTORING]
+
+[MISCELLANEOUS]
+
+[LOGGING]
+
+[BASIC]
+
+# Good variable names which should always be accepted, separated by a comma.
+good-names=i,
+ j,
+ k,
+ ex,
+ Run,
+ _,
+ fp, # fp = open(...)
+ fd, # fd = os.open(...)
+ ch,
+
+[VARIABLES]
+
+[STRING]
+
+[SPELLING]
+
+[FORMAT]
+
+[SIMILARITIES]
+
+# Ignore import statements themselves when computing similarities.
+ignore-imports=yes
+
+[TYPECHECK]
+
+[CLASSES]
+
+[IMPORTS]
+
+[DESIGN]
+
+[EXCEPTIONS]
diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
new file mode 100644
index 000000000..b7b3fc0ce
--- /dev/null
+++ b/scripts/qapi/schema.py
@@ -0,0 +1,1185 @@
+# -*- coding: utf-8 -*-
+#
+# QAPI schema internal representation
+#
+# Copyright (c) 2015-2019 Red Hat Inc.
+#
+# Authors:
+# Markus Armbruster <armbru@redhat.com>
+# Eric Blake <eblake@redhat.com>
+# Marc-André Lureau <marcandre.lureau@redhat.com>
+#
+# This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+
+# TODO catching name collisions in generated code would be nice
+
+from collections import OrderedDict
+import os
+import re
+from typing import Optional
+
+from .common import (
+ POINTER_SUFFIX,
+ c_name,
+ cgen_ifcond,
+ docgen_ifcond,
+ gen_endif,
+ gen_if,
+)
+from .error import QAPIError, QAPISemError, QAPISourceError
+from .expr import check_exprs
+from .parser import QAPISchemaParser
+
+
+class QAPISchemaIfCond:
+ def __init__(self, ifcond=None):
+ self.ifcond = ifcond
+
+ def _cgen(self):
+ return cgen_ifcond(self.ifcond)
+
+ def gen_if(self):
+ return gen_if(self._cgen())
+
+ def gen_endif(self):
+ return gen_endif(self._cgen())
+
+ def docgen(self):
+ return docgen_ifcond(self.ifcond)
+
+ def is_present(self):
+ return bool(self.ifcond)
+
+
+class QAPISchemaEntity:
+ meta: Optional[str] = None
+
+ def __init__(self, name: str, info, doc, ifcond=None, features=None):
+ assert name is None or isinstance(name, str)
+ for f in features or []:
+ assert isinstance(f, QAPISchemaFeature)
+ f.set_defined_in(name)
+ self.name = name
+ self._module = None
+ # For explicitly defined entities, info points to the (explicit)
+ # definition. For builtins (and their arrays), info is None.
+ # For implicitly defined entities, info points to a place that
+ # triggered the implicit definition (there may be more than one
+ # such place).
+ self.info = info
+ self.doc = doc
+ self._ifcond = ifcond or QAPISchemaIfCond()
+ self.features = features or []
+ self._checked = False
+
+ def c_name(self):
+ return c_name(self.name)
+
+ def check(self, schema):
+ assert not self._checked
+ seen = {}
+ for f in self.features:
+ f.check_clash(self.info, seen)
+ self._checked = True
+
+ def connect_doc(self, doc=None):
+ doc = doc or self.doc
+ if doc:
+ for f in self.features:
+ doc.connect_feature(f)
+
+ def check_doc(self):
+ if self.doc:
+ self.doc.check()
+
+ def _set_module(self, schema, info):
+ assert self._checked
+ fname = info.fname if info else QAPISchemaModule.BUILTIN_MODULE_NAME
+ self._module = schema.module_by_fname(fname)
+ self._module.add_entity(self)
+
+ def set_module(self, schema):
+ self._set_module(schema, self.info)
+
+ @property
+ def ifcond(self):
+ assert self._checked
+ return self._ifcond
+
+ def is_implicit(self):
+ return not self.info
+
+ def visit(self, visitor):
+ assert self._checked
+
+ def describe(self):
+ assert self.meta
+ return "%s '%s'" % (self.meta, self.name)
+
+
+class QAPISchemaVisitor:
+ def visit_begin(self, schema):
+ pass
+
+ def visit_end(self):
+ pass
+
+ def visit_module(self, name):
+ pass
+
+ def visit_needed(self, entity):
+ # Default to visiting everything
+ return True
+
+ def visit_include(self, name, info):
+ pass
+
+ def visit_builtin_type(self, name, info, json_type):
+ pass
+
+ def visit_enum_type(self, name, info, ifcond, features, members, prefix):
+ pass
+
+ def visit_array_type(self, name, info, ifcond, element_type):
+ pass
+
+ def visit_object_type(self, name, info, ifcond, features,
+ base, members, variants):
+ pass
+
+ def visit_object_type_flat(self, name, info, ifcond, features,
+ members, variants):
+ pass
+
+ def visit_alternate_type(self, name, info, ifcond, features, variants):
+ pass
+
+ def visit_command(self, name, info, ifcond, features,
+ arg_type, ret_type, gen, success_response, boxed,
+ allow_oob, allow_preconfig, coroutine):
+ pass
+
+ def visit_event(self, name, info, ifcond, features, arg_type, boxed):
+ pass
+
+
+class QAPISchemaModule:
+
+ BUILTIN_MODULE_NAME = './builtin'
+
+ def __init__(self, name):
+ self.name = name
+ self._entity_list = []
+
+ @staticmethod
+ def is_system_module(name: str) -> bool:
+ """
+ System modules are internally defined modules.
+
+ Their names start with the "./" prefix.
+ """
+ return name.startswith('./')
+
+ @classmethod
+ def is_user_module(cls, name: str) -> bool:
+ """
+ User modules are those defined by the user in qapi JSON files.
+
+ They do not start with the "./" prefix.
+ """
+ return not cls.is_system_module(name)
+
+ @classmethod
+ def is_builtin_module(cls, name: str) -> bool:
+ """
+ The built-in module is a single System module for the built-in types.
+
+ It is always "./builtin".
+ """
+ return name == cls.BUILTIN_MODULE_NAME
+
+ def add_entity(self, ent):
+ self._entity_list.append(ent)
+
+ def visit(self, visitor):
+ visitor.visit_module(self.name)
+ for entity in self._entity_list:
+ if visitor.visit_needed(entity):
+ entity.visit(visitor)
+
+
+class QAPISchemaInclude(QAPISchemaEntity):
+ def __init__(self, sub_module, info):
+ super().__init__(None, info, None)
+ self._sub_module = sub_module
+
+ def visit(self, visitor):
+ super().visit(visitor)
+ visitor.visit_include(self._sub_module.name, self.info)
+
+
+class QAPISchemaType(QAPISchemaEntity):
+ # Return the C type for common use.
+ # For the types we commonly box, this is a pointer type.
+ def c_type(self):
+ pass
+
+ # Return the C type to be used in a parameter list.
+ def c_param_type(self):
+ return self.c_type()
+
+ # Return the C type to be used where we suppress boxing.
+ def c_unboxed_type(self):
+ return self.c_type()
+
+ def json_type(self):
+ pass
+
+ def alternate_qtype(self):
+ json2qtype = {
+ 'null': 'QTYPE_QNULL',
+ 'string': 'QTYPE_QSTRING',
+ 'number': 'QTYPE_QNUM',
+ 'int': 'QTYPE_QNUM',
+ 'boolean': 'QTYPE_QBOOL',
+ 'object': 'QTYPE_QDICT'
+ }
+ return json2qtype.get(self.json_type())
+
+ def doc_type(self):
+ if self.is_implicit():
+ return None
+ return self.name
+
+ def check(self, schema):
+ QAPISchemaEntity.check(self, schema)
+ for feat in self.features:
+ if feat.is_special():
+ raise QAPISemError(
+ self.info,
+ f"feature '{feat.name}' is not supported for types")
+
+ def describe(self):
+ assert self.meta
+ return "%s type '%s'" % (self.meta, self.name)
+
+
+class QAPISchemaBuiltinType(QAPISchemaType):
+ meta = 'built-in'
+
+ def __init__(self, name, json_type, c_type):
+ super().__init__(name, None, None)
+ assert not c_type or isinstance(c_type, str)
+ assert json_type in ('string', 'number', 'int', 'boolean', 'null',
+ 'value')
+ self._json_type_name = json_type
+ self._c_type_name = c_type
+
+ def c_name(self):
+ return self.name
+
+ def c_type(self):
+ return self._c_type_name
+
+ def c_param_type(self):
+ if self.name == 'str':
+ return 'const ' + self._c_type_name
+ return self._c_type_name
+
+ def json_type(self):
+ return self._json_type_name
+
+ def doc_type(self):
+ return self.json_type()
+
+ def visit(self, visitor):
+ super().visit(visitor)
+ visitor.visit_builtin_type(self.name, self.info, self.json_type())
+
+
+class QAPISchemaEnumType(QAPISchemaType):
+ meta = 'enum'
+
+ def __init__(self, name, info, doc, ifcond, features, members, prefix):
+ super().__init__(name, info, doc, ifcond, features)
+ for m in members:
+ assert isinstance(m, QAPISchemaEnumMember)
+ m.set_defined_in(name)
+ assert prefix is None or isinstance(prefix, str)
+ self.members = members
+ self.prefix = prefix
+
+ def check(self, schema):
+ super().check(schema)
+ seen = {}
+ for m in self.members:
+ m.check_clash(self.info, seen)
+
+ def connect_doc(self, doc=None):
+ super().connect_doc(doc)
+ doc = doc or self.doc
+ for m in self.members:
+ m.connect_doc(doc)
+
+ def is_implicit(self):
+ # See QAPISchema._def_predefineds()
+ return self.name == 'QType'
+
+ def c_type(self):
+ return c_name(self.name)
+
+ def member_names(self):
+ return [m.name for m in self.members]
+
+ def json_type(self):
+ return 'string'
+
+ def visit(self, visitor):
+ super().visit(visitor)
+ visitor.visit_enum_type(
+ self.name, self.info, self.ifcond, self.features,
+ self.members, self.prefix)
+
+
+class QAPISchemaArrayType(QAPISchemaType):
+ meta = 'array'
+
+ def __init__(self, name, info, element_type):
+ super().__init__(name, info, None)
+ assert isinstance(element_type, str)
+ self._element_type_name = element_type
+ self.element_type = None
+
+ def check(self, schema):
+ super().check(schema)
+ self.element_type = schema.resolve_type(
+ self._element_type_name, self.info,
+ self.info and self.info.defn_meta)
+ assert not isinstance(self.element_type, QAPISchemaArrayType)
+
+ def set_module(self, schema):
+ self._set_module(schema, self.element_type.info)
+
+ @property
+ def ifcond(self):
+ assert self._checked
+ return self.element_type.ifcond
+
+ def is_implicit(self):
+ return True
+
+ def c_type(self):
+ return c_name(self.name) + POINTER_SUFFIX
+
+ def json_type(self):
+ return 'array'
+
+ def doc_type(self):
+ elt_doc_type = self.element_type.doc_type()
+ if not elt_doc_type:
+ return None
+ return 'array of ' + elt_doc_type
+
+ def visit(self, visitor):
+ super().visit(visitor)
+ visitor.visit_array_type(self.name, self.info, self.ifcond,
+ self.element_type)
+
+ def describe(self):
+ assert self.meta
+ return "%s type ['%s']" % (self.meta, self._element_type_name)
+
+
+class QAPISchemaObjectType(QAPISchemaType):
+ def __init__(self, name, info, doc, ifcond, features,
+ base, local_members, variants):
+ # struct has local_members, optional base, and no variants
+ # union has base, variants, and no local_members
+ super().__init__(name, info, doc, ifcond, features)
+ self.meta = 'union' if variants else 'struct'
+ assert base is None or isinstance(base, str)
+ for m in local_members:
+ assert isinstance(m, QAPISchemaObjectTypeMember)
+ m.set_defined_in(name)
+ if variants is not None:
+ assert isinstance(variants, QAPISchemaVariants)
+ variants.set_defined_in(name)
+ self._base_name = base
+ self.base = None
+ self.local_members = local_members
+ self.variants = variants
+ self.members = None
+
+ def check(self, schema):
+ # This calls another type T's .check() exactly when the C
+ # struct emitted by gen_object() contains that T's C struct
+ # (pointers don't count).
+ if self.members is not None:
+ # A previous .check() completed: nothing to do
+ return
+ if self._checked:
+ # Recursed: C struct contains itself
+ raise QAPISemError(self.info,
+ "object %s contains itself" % self.name)
+
+ super().check(schema)
+ assert self._checked and self.members is None
+
+ seen = OrderedDict()
+ if self._base_name:
+ self.base = schema.resolve_type(self._base_name, self.info,
+ "'base'")
+ if (not isinstance(self.base, QAPISchemaObjectType)
+ or self.base.variants):
+ raise QAPISemError(
+ self.info,
+ "'base' requires a struct type, %s isn't"
+ % self.base.describe())
+ self.base.check(schema)
+ self.base.check_clash(self.info, seen)
+ for m in self.local_members:
+ m.check(schema)
+ m.check_clash(self.info, seen)
+ members = seen.values()
+
+ if self.variants:
+ self.variants.check(schema, seen)
+ self.variants.check_clash(self.info, seen)
+
+ self.members = members # mark completed
+
+ # Check that the members of this type do not cause duplicate JSON members,
+ # and update seen to track the members seen so far. Report any errors
+ # on behalf of info, which is not necessarily self.info
+ def check_clash(self, info, seen):
+ assert self._checked
+ assert not self.variants # not implemented
+ for m in self.members:
+ m.check_clash(info, seen)
+
+ def connect_doc(self, doc=None):
+ super().connect_doc(doc)
+ doc = doc or self.doc
+ if self.base and self.base.is_implicit():
+ self.base.connect_doc(doc)
+ for m in self.local_members:
+ m.connect_doc(doc)
+
+ def is_implicit(self):
+ # See QAPISchema._make_implicit_object_type(), as well as
+ # _def_predefineds()
+ return self.name.startswith('q_')
+
+ def is_empty(self):
+ assert self.members is not None
+ return not self.members and not self.variants
+
+ def c_name(self):
+ assert self.name != 'q_empty'
+ return super().c_name()
+
+ def c_type(self):
+ assert not self.is_implicit()
+ return c_name(self.name) + POINTER_SUFFIX
+
+ def c_unboxed_type(self):
+ return c_name(self.name)
+
+ def json_type(self):
+ return 'object'
+
+ def visit(self, visitor):
+ super().visit(visitor)
+ visitor.visit_object_type(
+ self.name, self.info, self.ifcond, self.features,
+ self.base, self.local_members, self.variants)
+ visitor.visit_object_type_flat(
+ self.name, self.info, self.ifcond, self.features,
+ self.members, self.variants)
+
+
+class QAPISchemaAlternateType(QAPISchemaType):
+ meta = 'alternate'
+
+ def __init__(self, name, info, doc, ifcond, features, variants):
+ super().__init__(name, info, doc, ifcond, features)
+ assert isinstance(variants, QAPISchemaVariants)
+ assert variants.tag_member
+ variants.set_defined_in(name)
+ variants.tag_member.set_defined_in(self.name)
+ self.variants = variants
+
+ def check(self, schema):
+ super().check(schema)
+ self.variants.tag_member.check(schema)
+ # Not calling self.variants.check_clash(), because there's nothing
+ # to clash with
+ self.variants.check(schema, {})
+ # Alternate branch names have no relation to the tag enum values;
+ # so we have to check for potential name collisions ourselves.
+ seen = {}
+ types_seen = {}
+ for v in self.variants.variants:
+ v.check_clash(self.info, seen)
+ qtype = v.type.alternate_qtype()
+ if not qtype:
+ raise QAPISemError(
+ self.info,
+ "%s cannot use %s"
+ % (v.describe(self.info), v.type.describe()))
+ conflicting = set([qtype])
+ if qtype == 'QTYPE_QSTRING':
+ if isinstance(v.type, QAPISchemaEnumType):
+ for m in v.type.members:
+ if m.name in ['on', 'off']:
+ conflicting.add('QTYPE_QBOOL')
+ if re.match(r'[-+0-9.]', m.name):
+ # lazy, could be tightened
+ conflicting.add('QTYPE_QNUM')
+ else:
+ conflicting.add('QTYPE_QNUM')
+ conflicting.add('QTYPE_QBOOL')
+ for qt in conflicting:
+ if qt in types_seen:
+ raise QAPISemError(
+ self.info,
+ "%s can't be distinguished from '%s'"
+ % (v.describe(self.info), types_seen[qt]))
+ types_seen[qt] = v.name
+
+ def connect_doc(self, doc=None):
+ super().connect_doc(doc)
+ doc = doc or self.doc
+ for v in self.variants.variants:
+ v.connect_doc(doc)
+
+ def c_type(self):
+ return c_name(self.name) + POINTER_SUFFIX
+
+ def json_type(self):
+ return 'value'
+
+ def visit(self, visitor):
+ super().visit(visitor)
+ visitor.visit_alternate_type(
+ self.name, self.info, self.ifcond, self.features, self.variants)
+
+
+class QAPISchemaVariants:
+ def __init__(self, tag_name, info, tag_member, variants):
+ # Unions pass tag_name but not tag_member.
+ # Alternates pass tag_member but not tag_name.
+ # After check(), tag_member is always set.
+ assert bool(tag_member) != bool(tag_name)
+ assert (isinstance(tag_name, str) or
+ isinstance(tag_member, QAPISchemaObjectTypeMember))
+ for v in variants:
+ assert isinstance(v, QAPISchemaVariant)
+ self._tag_name = tag_name
+ self.info = info
+ self.tag_member = tag_member
+ self.variants = variants
+
+ def set_defined_in(self, name):
+ for v in self.variants:
+ v.set_defined_in(name)
+
+ def check(self, schema, seen):
+ if self._tag_name: # union
+ self.tag_member = seen.get(c_name(self._tag_name))
+ base = "'base'"
+ # Pointing to the base type when not implicit would be
+ # nice, but we don't know it here
+ if not self.tag_member or self._tag_name != self.tag_member.name:
+ raise QAPISemError(
+ self.info,
+ "discriminator '%s' is not a member of %s"
+ % (self._tag_name, base))
+ # Here we do:
+ base_type = schema.lookup_type(self.tag_member.defined_in)
+ assert base_type
+ if not base_type.is_implicit():
+ base = "base type '%s'" % self.tag_member.defined_in
+ if not isinstance(self.tag_member.type, QAPISchemaEnumType):
+ raise QAPISemError(
+ self.info,
+ "discriminator member '%s' of %s must be of enum type"
+ % (self._tag_name, base))
+ if self.tag_member.optional:
+ raise QAPISemError(
+ self.info,
+ "discriminator member '%s' of %s must not be optional"
+ % (self._tag_name, base))
+ if self.tag_member.ifcond.is_present():
+ raise QAPISemError(
+ self.info,
+ "discriminator member '%s' of %s must not be conditional"
+ % (self._tag_name, base))
+ else: # alternate
+ assert isinstance(self.tag_member.type, QAPISchemaEnumType)
+ assert not self.tag_member.optional
+ assert not self.tag_member.ifcond.is_present()
+ if self._tag_name: # union
+ # branches that are not explicitly covered get an empty type
+ cases = {v.name for v in self.variants}
+ for m in self.tag_member.type.members:
+ if m.name not in cases:
+ v = QAPISchemaVariant(m.name, self.info,
+ 'q_empty', m.ifcond)
+ v.set_defined_in(self.tag_member.defined_in)
+ self.variants.append(v)
+ if not self.variants:
+ raise QAPISemError(self.info, "union has no branches")
+ for v in self.variants:
+ v.check(schema)
+ # Union names must match enum values; alternate names are
+ # checked separately. Use 'seen' to tell the two apart.
+ if seen:
+ if v.name not in self.tag_member.type.member_names():
+ raise QAPISemError(
+ self.info,
+ "branch '%s' is not a value of %s"
+ % (v.name, self.tag_member.type.describe()))
+ if (not isinstance(v.type, QAPISchemaObjectType)
+ or v.type.variants):
+ raise QAPISemError(
+ self.info,
+ "%s cannot use %s"
+ % (v.describe(self.info), v.type.describe()))
+ v.type.check(schema)
+
+ def check_clash(self, info, seen):
+ for v in self.variants:
+ # Reset seen map for each variant, since qapi names from one
+ # branch do not affect another branch
+ v.type.check_clash(info, dict(seen))
+
+
+class QAPISchemaMember:
+ """ Represents object members, enum members and features """
+ role = 'member'
+
+ def __init__(self, name, info, ifcond=None):
+ assert isinstance(name, str)
+ self.name = name
+ self.info = info
+ self.ifcond = ifcond or QAPISchemaIfCond()
+ self.defined_in = None
+
+ def set_defined_in(self, name):
+ assert not self.defined_in
+ self.defined_in = name
+
+ def check_clash(self, info, seen):
+ cname = c_name(self.name)
+ if cname in seen:
+ raise QAPISemError(
+ info,
+ "%s collides with %s"
+ % (self.describe(info), seen[cname].describe(info)))
+ seen[cname] = self
+
+ def connect_doc(self, doc):
+ if doc:
+ doc.connect_member(self)
+
+ def describe(self, info):
+ role = self.role
+ defined_in = self.defined_in
+ assert defined_in
+
+ if defined_in.startswith('q_obj_'):
+ # See QAPISchema._make_implicit_object_type() - reverse the
+ # mapping there to create a nice human-readable description
+ defined_in = defined_in[6:]
+ if defined_in.endswith('-arg'):
+ # Implicit type created for a command's dict 'data'
+ assert role == 'member'
+ role = 'parameter'
+ elif defined_in.endswith('-base'):
+ # Implicit type created for a union's dict 'base'
+ role = 'base ' + role
+ else:
+ assert False
+ elif defined_in != info.defn_name:
+ return "%s '%s' of type '%s'" % (role, self.name, defined_in)
+ return "%s '%s'" % (role, self.name)
+
+
+class QAPISchemaEnumMember(QAPISchemaMember):
+ role = 'value'
+
+ def __init__(self, name, info, ifcond=None, features=None):
+ super().__init__(name, info, ifcond)
+ for f in features or []:
+ assert isinstance(f, QAPISchemaFeature)
+ f.set_defined_in(name)
+ self.features = features or []
+
+ def connect_doc(self, doc):
+ super().connect_doc(doc)
+ if doc:
+ for f in self.features:
+ doc.connect_feature(f)
+
+
+class QAPISchemaFeature(QAPISchemaMember):
+ role = 'feature'
+
+ def is_special(self):
+ return self.name in ('deprecated', 'unstable')
+
+
+class QAPISchemaObjectTypeMember(QAPISchemaMember):
+ def __init__(self, name, info, typ, optional, ifcond=None, features=None):
+ super().__init__(name, info, ifcond)
+ assert isinstance(typ, str)
+ assert isinstance(optional, bool)
+ for f in features or []:
+ assert isinstance(f, QAPISchemaFeature)
+ f.set_defined_in(name)
+ self._type_name = typ
+ self.type = None
+ self.optional = optional
+ self.features = features or []
+
+ def check(self, schema):
+ assert self.defined_in
+ self.type = schema.resolve_type(self._type_name, self.info,
+ self.describe)
+ seen = {}
+ for f in self.features:
+ f.check_clash(self.info, seen)
+
+ def connect_doc(self, doc):
+ super().connect_doc(doc)
+ if doc:
+ for f in self.features:
+ doc.connect_feature(f)
+
+
+class QAPISchemaVariant(QAPISchemaObjectTypeMember):
+ role = 'branch'
+
+ def __init__(self, name, info, typ, ifcond=None):
+ super().__init__(name, info, typ, False, ifcond)
+
+
+class QAPISchemaCommand(QAPISchemaEntity):
+ meta = 'command'
+
+ def __init__(self, name, info, doc, ifcond, features,
+ arg_type, ret_type,
+ gen, success_response, boxed, allow_oob, allow_preconfig,
+ coroutine):
+ super().__init__(name, info, doc, ifcond, features)
+ assert not arg_type or isinstance(arg_type, str)
+ assert not ret_type or isinstance(ret_type, str)
+ self._arg_type_name = arg_type
+ self.arg_type = None
+ self._ret_type_name = ret_type
+ self.ret_type = None
+ self.gen = gen
+ self.success_response = success_response
+ self.boxed = boxed
+ self.allow_oob = allow_oob
+ self.allow_preconfig = allow_preconfig
+ self.coroutine = coroutine
+
+ def check(self, schema):
+ super().check(schema)
+ if self._arg_type_name:
+ self.arg_type = schema.resolve_type(
+ self._arg_type_name, self.info, "command's 'data'")
+ if not isinstance(self.arg_type, QAPISchemaObjectType):
+ raise QAPISemError(
+ self.info,
+ "command's 'data' cannot take %s"
+ % self.arg_type.describe())
+ if self.arg_type.variants and not self.boxed:
+ raise QAPISemError(
+ self.info,
+ "command's 'data' can take %s only with 'boxed': true"
+ % self.arg_type.describe())
+ if self._ret_type_name:
+ self.ret_type = schema.resolve_type(
+ self._ret_type_name, self.info, "command's 'returns'")
+ if self.name not in self.info.pragma.command_returns_exceptions:
+ typ = self.ret_type
+ if isinstance(typ, QAPISchemaArrayType):
+ typ = self.ret_type.element_type
+ assert typ
+ if not isinstance(typ, QAPISchemaObjectType):
+ raise QAPISemError(
+ self.info,
+ "command's 'returns' cannot take %s"
+ % self.ret_type.describe())
+
+ def connect_doc(self, doc=None):
+ super().connect_doc(doc)
+ doc = doc or self.doc
+ if doc:
+ if self.arg_type and self.arg_type.is_implicit():
+ self.arg_type.connect_doc(doc)
+
+ def visit(self, visitor):
+ super().visit(visitor)
+ visitor.visit_command(
+ self.name, self.info, self.ifcond, self.features,
+ self.arg_type, self.ret_type, self.gen, self.success_response,
+ self.boxed, self.allow_oob, self.allow_preconfig,
+ self.coroutine)
+
+
+class QAPISchemaEvent(QAPISchemaEntity):
+ meta = 'event'
+
+ def __init__(self, name, info, doc, ifcond, features, arg_type, boxed):
+ super().__init__(name, info, doc, ifcond, features)
+ assert not arg_type or isinstance(arg_type, str)
+ self._arg_type_name = arg_type
+ self.arg_type = None
+ self.boxed = boxed
+
+ def check(self, schema):
+ super().check(schema)
+ if self._arg_type_name:
+ self.arg_type = schema.resolve_type(
+ self._arg_type_name, self.info, "event's 'data'")
+ if not isinstance(self.arg_type, QAPISchemaObjectType):
+ raise QAPISemError(
+ self.info,
+ "event's 'data' cannot take %s"
+ % self.arg_type.describe())
+ if self.arg_type.variants and not self.boxed:
+ raise QAPISemError(
+ self.info,
+ "event's 'data' can take %s only with 'boxed': true"
+ % self.arg_type.describe())
+
+ def connect_doc(self, doc=None):
+ super().connect_doc(doc)
+ doc = doc or self.doc
+ if doc:
+ if self.arg_type and self.arg_type.is_implicit():
+ self.arg_type.connect_doc(doc)
+
+ def visit(self, visitor):
+ super().visit(visitor)
+ visitor.visit_event(
+ self.name, self.info, self.ifcond, self.features,
+ self.arg_type, self.boxed)
+
+
+class QAPISchema:
+ def __init__(self, fname):
+ self.fname = fname
+
+ try:
+ parser = QAPISchemaParser(fname)
+ except OSError as err:
+ raise QAPIError(
+ f"can't read schema file '{fname}': {err.strerror}"
+ ) from err
+
+ exprs = check_exprs(parser.exprs)
+ self.docs = parser.docs
+ self._entity_list = []
+ self._entity_dict = {}
+ self._module_dict = OrderedDict()
+ self._schema_dir = os.path.dirname(fname)
+ self._make_module(QAPISchemaModule.BUILTIN_MODULE_NAME)
+ self._make_module(fname)
+ self._predefining = True
+ self._def_predefineds()
+ self._predefining = False
+ self._def_exprs(exprs)
+ self.check()
+
+ def _def_entity(self, ent):
+ # Only the predefined types are allowed to not have info
+ assert ent.info or self._predefining
+ self._entity_list.append(ent)
+ if ent.name is None:
+ return
+ # TODO reject names that differ only in '_' vs. '.' vs. '-',
+ # because they're liable to clash in generated C.
+ other_ent = self._entity_dict.get(ent.name)
+ if other_ent:
+ if other_ent.info:
+ where = QAPISourceError(other_ent.info, "previous definition")
+ raise QAPISemError(
+ ent.info,
+ "'%s' is already defined\n%s" % (ent.name, where))
+ raise QAPISemError(
+ ent.info, "%s is already defined" % other_ent.describe())
+ self._entity_dict[ent.name] = ent
+
+ def lookup_entity(self, name, typ=None):
+ ent = self._entity_dict.get(name)
+ if typ and not isinstance(ent, typ):
+ return None
+ return ent
+
+ def lookup_type(self, name):
+ return self.lookup_entity(name, QAPISchemaType)
+
+ def resolve_type(self, name, info, what):
+ typ = self.lookup_type(name)
+ if not typ:
+ if callable(what):
+ what = what(info)
+ raise QAPISemError(
+ info, "%s uses unknown type '%s'" % (what, name))
+ return typ
+
+ def _module_name(self, fname: str) -> str:
+ if QAPISchemaModule.is_system_module(fname):
+ return fname
+ return os.path.relpath(fname, self._schema_dir)
+
+ def _make_module(self, fname):
+ name = self._module_name(fname)
+ if name not in self._module_dict:
+ self._module_dict[name] = QAPISchemaModule(name)
+ return self._module_dict[name]
+
+ def module_by_fname(self, fname):
+ name = self._module_name(fname)
+ return self._module_dict[name]
+
+ def _def_include(self, expr, info, doc):
+ include = expr['include']
+ assert doc is None
+ self._def_entity(QAPISchemaInclude(self._make_module(include), info))
+
+ def _def_builtin_type(self, name, json_type, c_type):
+ self._def_entity(QAPISchemaBuiltinType(name, json_type, c_type))
+ # 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
+ # schema.
+ self._make_array_type(name, None)
+
+ def _def_predefineds(self):
+ 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)]:
+ self._def_builtin_type(*t)
+ self.the_empty_object_type = QAPISchemaObjectType(
+ 'q_empty', None, None, None, None, None, [], None)
+ self._def_entity(self.the_empty_object_type)
+
+ qtypes = ['none', 'qnull', 'qnum', 'qstring', 'qdict', 'qlist',
+ 'qbool']
+ qtype_values = self._make_enum_members(
+ [{'name': n} for n in qtypes], None)
+
+ self._def_entity(QAPISchemaEnumType('QType', None, None, None, None,
+ qtype_values, 'QTYPE'))
+
+ def _make_features(self, features, info):
+ if features is None:
+ return []
+ return [QAPISchemaFeature(f['name'], info,
+ QAPISchemaIfCond(f.get('if')))
+ for f in features]
+
+ def _make_enum_member(self, name, ifcond, features, info):
+ return QAPISchemaEnumMember(name, info,
+ QAPISchemaIfCond(ifcond),
+ self._make_features(features, info))
+
+ def _make_enum_members(self, values, info):
+ return [self._make_enum_member(v['name'], v.get('if'),
+ v.get('features'), info)
+ for v in values]
+
+ def _make_array_type(self, element_type, info):
+ name = element_type + 'List' # reserved by check_defn_name_str()
+ if not self.lookup_type(name):
+ self._def_entity(QAPISchemaArrayType(name, info, element_type))
+ return name
+
+ def _make_implicit_object_type(self, name, info, ifcond, role, members):
+ if not members:
+ return None
+ # See also QAPISchemaObjectTypeMember.describe()
+ name = 'q_obj_%s-%s' % (name, role)
+ typ = self.lookup_entity(name, QAPISchemaObjectType)
+ if typ:
+ # The implicit object type has multiple users. This can
+ # only be a duplicate definition, which will be flagged
+ # later.
+ pass
+ else:
+ self._def_entity(QAPISchemaObjectType(
+ name, info, None, ifcond, None, None, members, None))
+ return name
+
+ def _def_enum_type(self, expr, info, doc):
+ name = expr['enum']
+ data = expr['data']
+ prefix = expr.get('prefix')
+ ifcond = QAPISchemaIfCond(expr.get('if'))
+ features = self._make_features(expr.get('features'), info)
+ self._def_entity(QAPISchemaEnumType(
+ name, info, doc, ifcond, features,
+ self._make_enum_members(data, info), prefix))
+
+ def _make_member(self, name, typ, ifcond, features, info):
+ optional = False
+ if name.startswith('*'):
+ name = name[1:]
+ optional = True
+ if isinstance(typ, list):
+ assert len(typ) == 1
+ typ = self._make_array_type(typ[0], info)
+ return QAPISchemaObjectTypeMember(name, info, typ, optional, ifcond,
+ self._make_features(features, info))
+
+ def _make_members(self, data, info):
+ return [self._make_member(key, value['type'],
+ QAPISchemaIfCond(value.get('if')),
+ value.get('features'), info)
+ for (key, value) in data.items()]
+
+ def _def_struct_type(self, expr, info, doc):
+ name = expr['struct']
+ base = expr.get('base')
+ data = expr['data']
+ ifcond = QAPISchemaIfCond(expr.get('if'))
+ features = self._make_features(expr.get('features'), info)
+ self._def_entity(QAPISchemaObjectType(
+ name, info, doc, ifcond, features, base,
+ self._make_members(data, info),
+ None))
+
+ def _make_variant(self, case, typ, ifcond, info):
+ return QAPISchemaVariant(case, info, typ, ifcond)
+
+ def _def_union_type(self, expr, info, doc):
+ name = expr['union']
+ base = expr['base']
+ tag_name = expr['discriminator']
+ data = expr['data']
+ ifcond = QAPISchemaIfCond(expr.get('if'))
+ features = self._make_features(expr.get('features'), info)
+ if isinstance(base, dict):
+ base = self._make_implicit_object_type(
+ name, info, ifcond,
+ 'base', self._make_members(base, info))
+ variants = [
+ self._make_variant(key, value['type'],
+ QAPISchemaIfCond(value.get('if')),
+ info)
+ for (key, value) in data.items()]
+ members = []
+ self._def_entity(
+ QAPISchemaObjectType(name, info, doc, ifcond, features,
+ base, members,
+ QAPISchemaVariants(
+ tag_name, info, None, variants)))
+
+ def _def_alternate_type(self, expr, info, doc):
+ name = expr['alternate']
+ data = expr['data']
+ ifcond = QAPISchemaIfCond(expr.get('if'))
+ features = self._make_features(expr.get('features'), info)
+ variants = [
+ self._make_variant(key, value['type'],
+ QAPISchemaIfCond(value.get('if')),
+ info)
+ for (key, value) in data.items()]
+ tag_member = QAPISchemaObjectTypeMember('type', info, 'QType', False)
+ self._def_entity(
+ QAPISchemaAlternateType(name, info, doc, ifcond, features,
+ QAPISchemaVariants(
+ None, info, tag_member, variants)))
+
+ def _def_command(self, expr, info, doc):
+ name = expr['command']
+ data = expr.get('data')
+ rets = expr.get('returns')
+ gen = expr.get('gen', True)
+ success_response = expr.get('success-response', True)
+ boxed = expr.get('boxed', False)
+ allow_oob = expr.get('allow-oob', False)
+ allow_preconfig = expr.get('allow-preconfig', False)
+ coroutine = expr.get('coroutine', False)
+ ifcond = QAPISchemaIfCond(expr.get('if'))
+ features = self._make_features(expr.get('features'), info)
+ if isinstance(data, OrderedDict):
+ data = self._make_implicit_object_type(
+ name, info, ifcond,
+ 'arg', self._make_members(data, info))
+ if isinstance(rets, list):
+ assert len(rets) == 1
+ rets = self._make_array_type(rets[0], info)
+ self._def_entity(QAPISchemaCommand(name, info, doc, ifcond, features,
+ data, rets,
+ gen, success_response,
+ boxed, allow_oob, allow_preconfig,
+ coroutine))
+
+ def _def_event(self, expr, info, doc):
+ name = expr['event']
+ data = expr.get('data')
+ boxed = expr.get('boxed', False)
+ ifcond = QAPISchemaIfCond(expr.get('if'))
+ features = self._make_features(expr.get('features'), info)
+ if isinstance(data, OrderedDict):
+ data = self._make_implicit_object_type(
+ name, info, ifcond,
+ 'arg', self._make_members(data, info))
+ self._def_entity(QAPISchemaEvent(name, info, doc, ifcond, features,
+ data, boxed))
+
+ def _def_exprs(self, exprs):
+ for expr_elem in exprs:
+ expr = expr_elem['expr']
+ info = expr_elem['info']
+ doc = expr_elem.get('doc')
+ if 'enum' in expr:
+ self._def_enum_type(expr, info, doc)
+ elif 'struct' in expr:
+ self._def_struct_type(expr, info, doc)
+ elif 'union' in expr:
+ self._def_union_type(expr, info, doc)
+ elif 'alternate' in expr:
+ self._def_alternate_type(expr, info, doc)
+ elif 'command' in expr:
+ self._def_command(expr, info, doc)
+ elif 'event' in expr:
+ self._def_event(expr, info, doc)
+ elif 'include' in expr:
+ self._def_include(expr, info, doc)
+ else:
+ assert False
+
+ def check(self):
+ for ent in self._entity_list:
+ ent.check(self)
+ ent.connect_doc()
+ ent.check_doc()
+ for ent in self._entity_list:
+ ent.set_module(self)
+
+ def visit(self, visitor):
+ visitor.visit_begin(self)
+ for mod in self._module_dict.values():
+ mod.visit(visitor)
+ visitor.visit_end()
diff --git a/scripts/qapi/source.py b/scripts/qapi/source.py
new file mode 100644
index 000000000..04193cc96
--- /dev/null
+++ b/scripts/qapi/source.py
@@ -0,0 +1,71 @@
+#
+# QAPI frontend source file info
+#
+# Copyright (c) 2019 Red Hat Inc.
+#
+# Authors:
+# Markus Armbruster <armbru@redhat.com>
+#
+# This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+
+import copy
+from typing import List, Optional, TypeVar
+
+
+class QAPISchemaPragma:
+ # Replace with @dataclass in Python 3.7+
+ # pylint: disable=too-few-public-methods
+
+ def __init__(self) -> None:
+ # Are documentation comments required?
+ self.doc_required = False
+ # Commands whose names may use '_'
+ self.command_name_exceptions: List[str] = []
+ # Commands allowed to return a non-dictionary
+ self.command_returns_exceptions: List[str] = []
+ # Types whose member names may violate case conventions
+ self.member_name_exceptions: List[str] = []
+
+
+class QAPISourceInfo:
+ T = TypeVar('T', bound='QAPISourceInfo')
+
+ def __init__(self, fname: str, parent: Optional['QAPISourceInfo']):
+ self.fname = fname
+ self.line = 1
+ self.parent = parent
+ self.pragma: QAPISchemaPragma = (
+ parent.pragma if parent else QAPISchemaPragma()
+ )
+ self.defn_meta: Optional[str] = None
+ self.defn_name: Optional[str] = None
+
+ def set_defn(self, meta: str, name: str) -> None:
+ self.defn_meta = meta
+ self.defn_name = name
+
+ def next_line(self: T) -> T:
+ info = copy.copy(self)
+ info.line += 1
+ return info
+
+ def loc(self) -> str:
+ return f"{self.fname}:{self.line}"
+
+ def in_defn(self) -> str:
+ if self.defn_name:
+ return "%s: In %s '%s':\n" % (self.fname,
+ self.defn_meta, self.defn_name)
+ return ''
+
+ def include_path(self) -> str:
+ ret = ''
+ parent = self.parent
+ while parent:
+ ret = 'In file included from %s:\n' % parent.loc() + ret
+ parent = parent.parent
+ return ret
+
+ def __str__(self) -> str:
+ return self.include_path() + self.in_defn() + self.loc()
diff --git a/scripts/qapi/types.py b/scripts/qapi/types.py
new file mode 100644
index 000000000..3013329c2
--- /dev/null
+++ b/scripts/qapi/types.py
@@ -0,0 +1,383 @@
+"""
+QAPI types generator
+
+Copyright IBM, Corp. 2011
+Copyright (c) 2013-2018 Red Hat Inc.
+
+Authors:
+ Anthony Liguori <aliguori@us.ibm.com>
+ Michael Roth <mdroth@linux.vnet.ibm.com>
+ Markus Armbruster <armbru@redhat.com>
+
+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
+
+from .common import c_enum_const, c_name, mcgen
+from .gen import QAPISchemaModularCVisitor, gen_special_features, ifcontext
+from .schema import (
+ QAPISchema,
+ QAPISchemaEnumMember,
+ QAPISchemaFeature,
+ QAPISchemaIfCond,
+ QAPISchemaObjectType,
+ QAPISchemaObjectTypeMember,
+ QAPISchemaType,
+ QAPISchemaVariants,
+)
+from .source import QAPISourceInfo
+
+
+# variants must be emitted before their container; track what has already
+# been output
+objects_seen = set()
+
+
+def gen_enum_lookup(name: str,
+ members: List[QAPISchemaEnumMember],
+ prefix: Optional[str] = None) -> str:
+ max_index = c_enum_const(name, '_MAX', prefix)
+ feats = ''
+ ret = mcgen('''
+
+const QEnumLookup %(c_name)s_lookup = {
+ .array = (const char *const[]) {
+''',
+ c_name=c_name(name))
+ for memb in members:
+ ret += memb.ifcond.gen_if()
+ index = c_enum_const(name, memb.name, prefix)
+ ret += mcgen('''
+ [%(index)s] = "%(name)s",
+''',
+ index=index, name=memb.name)
+ ret += memb.ifcond.gen_endif()
+
+ special_features = gen_special_features(memb.features)
+ if special_features != '0':
+ feats += mcgen('''
+ [%(index)s] = %(special_features)s,
+''',
+ index=index, special_features=special_features)
+
+ if feats:
+ ret += mcgen('''
+ },
+ .special_features = (const unsigned char[%(max_index)s]) {
+''',
+ max_index=max_index)
+ ret += feats
+
+ ret += mcgen('''
+ },
+ .size = %(max_index)s
+};
+''',
+ max_index=max_index)
+ return ret
+
+
+def gen_enum(name: str,
+ members: List[QAPISchemaEnumMember],
+ prefix: Optional[str] = None) -> str:
+ # append automatically generated _MAX value
+ enum_members = members + [QAPISchemaEnumMember('_MAX', None)]
+
+ ret = mcgen('''
+
+typedef enum %(c_name)s {
+''',
+ c_name=c_name(name))
+
+ for memb in enum_members:
+ ret += memb.ifcond.gen_if()
+ ret += mcgen('''
+ %(c_enum)s,
+''',
+ c_enum=c_enum_const(name, memb.name, prefix))
+ ret += memb.ifcond.gen_endif()
+
+ ret += mcgen('''
+} %(c_name)s;
+''',
+ c_name=c_name(name))
+
+ ret += mcgen('''
+
+#define %(c_name)s_str(val) \\
+ qapi_enum_lookup(&%(c_name)s_lookup, (val))
+
+extern const QEnumLookup %(c_name)s_lookup;
+''',
+ c_name=c_name(name))
+ return ret
+
+
+def gen_fwd_object_or_array(name: str) -> str:
+ return mcgen('''
+
+typedef struct %(c_name)s %(c_name)s;
+''',
+ c_name=c_name(name))
+
+
+def gen_array(name: str, element_type: QAPISchemaType) -> str:
+ return mcgen('''
+
+struct %(c_name)s {
+ %(c_name)s *next;
+ %(c_type)s value;
+};
+''',
+ c_name=c_name(name), c_type=element_type.c_type())
+
+
+def gen_struct_members(members: List[QAPISchemaObjectTypeMember]) -> str:
+ ret = ''
+ for memb in members:
+ ret += memb.ifcond.gen_if()
+ if memb.optional:
+ ret += mcgen('''
+ bool has_%(c_name)s;
+''',
+ c_name=c_name(memb.name))
+ ret += mcgen('''
+ %(c_type)s %(c_name)s;
+''',
+ c_type=memb.type.c_type(), c_name=c_name(memb.name))
+ ret += memb.ifcond.gen_endif()
+ return ret
+
+
+def gen_object(name: str, ifcond: QAPISchemaIfCond,
+ base: Optional[QAPISchemaObjectType],
+ members: List[QAPISchemaObjectTypeMember],
+ variants: Optional[QAPISchemaVariants]) -> str:
+ if name in objects_seen:
+ return ''
+ objects_seen.add(name)
+
+ ret = ''
+ for var in variants.variants if variants else ():
+ obj = var.type
+ if not isinstance(obj, QAPISchemaObjectType):
+ continue
+ ret += gen_object(obj.name, obj.ifcond, obj.base,
+ obj.local_members, obj.variants)
+
+ ret += mcgen('''
+
+''')
+ ret += ifcond.gen_if()
+ ret += mcgen('''
+struct %(c_name)s {
+''',
+ c_name=c_name(name))
+
+ if base:
+ if not base.is_implicit():
+ ret += mcgen('''
+ /* Members inherited from %(c_name)s: */
+''',
+ c_name=base.c_name())
+ ret += gen_struct_members(base.members)
+ if not base.is_implicit():
+ ret += mcgen('''
+ /* Own members: */
+''')
+ ret += gen_struct_members(members)
+
+ if variants:
+ ret += gen_variants(variants)
+
+ # Make sure that all structs have at least one member; this avoids
+ # potential issues with attempting to malloc space for zero-length
+ # structs in C, and also incompatibility with C++ (where an empty
+ # struct is size 1).
+ if (not base or base.is_empty()) and not members and not variants:
+ ret += mcgen('''
+ char qapi_dummy_for_empty_struct;
+''')
+
+ ret += mcgen('''
+};
+''')
+ ret += ifcond.gen_endif()
+
+ return ret
+
+
+def gen_upcast(name: str, base: QAPISchemaObjectType) -> str:
+ # C makes const-correctness ugly. We have to cast away const to let
+ # this function work for both const and non-const obj.
+ return mcgen('''
+
+static inline %(base)s *qapi_%(c_name)s_base(const %(c_name)s *obj)
+{
+ return (%(base)s *)obj;
+}
+''',
+ c_name=c_name(name), base=base.c_name())
+
+
+def gen_variants(variants: QAPISchemaVariants) -> str:
+ ret = mcgen('''
+ union { /* union tag is @%(c_name)s */
+''',
+ c_name=c_name(variants.tag_member.name))
+
+ for var in variants.variants:
+ if var.type.name == 'q_empty':
+ continue
+ ret += var.ifcond.gen_if()
+ ret += mcgen('''
+ %(c_type)s %(c_name)s;
+''',
+ c_type=var.type.c_unboxed_type(),
+ c_name=c_name(var.name))
+ ret += var.ifcond.gen_endif()
+
+ ret += mcgen('''
+ } u;
+''')
+
+ return ret
+
+
+def gen_type_cleanup_decl(name: str) -> str:
+ ret = mcgen('''
+
+void qapi_free_%(c_name)s(%(c_name)s *obj);
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(%(c_name)s, qapi_free_%(c_name)s)
+''',
+ c_name=c_name(name))
+ return ret
+
+
+def gen_type_cleanup(name: str) -> str:
+ ret = mcgen('''
+
+void qapi_free_%(c_name)s(%(c_name)s *obj)
+{
+ Visitor *v;
+
+ if (!obj) {
+ return;
+ }
+
+ v = qapi_dealloc_visitor_new();
+ visit_type_%(c_name)s(v, NULL, &obj, NULL);
+ visit_free(v);
+}
+''',
+ c_name=c_name(name))
+ return ret
+
+
+class QAPISchemaGenTypeVisitor(QAPISchemaModularCVisitor):
+
+ def __init__(self, prefix: str):
+ super().__init__(
+ prefix, 'qapi-types', ' * Schema-defined QAPI types',
+ ' * Built-in QAPI types', __doc__)
+
+ def _begin_builtin_module(self) -> None:
+ self._genc.preamble_add(mcgen('''
+#include "qemu/osdep.h"
+#include "qapi/dealloc-visitor.h"
+#include "qapi/qapi-builtin-types.h"
+#include "qapi/qapi-builtin-visit.h"
+'''))
+ self._genh.preamble_add(mcgen('''
+#include "qapi/util.h"
+'''))
+
+ def _begin_user_module(self, name: str) -> None:
+ types = self._module_basename('qapi-types', name)
+ visit = self._module_basename('qapi-visit', name)
+ self._genc.preamble_add(mcgen('''
+#include "qemu/osdep.h"
+#include "qapi/dealloc-visitor.h"
+#include "%(types)s.h"
+#include "%(visit)s.h"
+''',
+ types=types, visit=visit))
+ self._genh.preamble_add(mcgen('''
+#include "qapi/qapi-builtin-types.h"
+'''))
+
+ def visit_begin(self, schema: QAPISchema) -> None:
+ # gen_object() is recursive, ensure it doesn't visit the empty type
+ objects_seen.add(schema.the_empty_object_type.name)
+
+ def _gen_type_cleanup(self, name: str) -> None:
+ self._genh.add(gen_type_cleanup_decl(name))
+ self._genc.add(gen_type_cleanup(name))
+
+ def visit_enum_type(self,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ members: List[QAPISchemaEnumMember],
+ prefix: Optional[str]) -> None:
+ with ifcontext(ifcond, self._genh, self._genc):
+ self._genh.preamble_add(gen_enum(name, members, prefix))
+ self._genc.add(gen_enum_lookup(name, members, prefix))
+
+ def visit_array_type(self,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ element_type: QAPISchemaType) -> None:
+ with ifcontext(ifcond, self._genh, self._genc):
+ self._genh.preamble_add(gen_fwd_object_or_array(name))
+ self._genh.add(gen_array(name, element_type))
+ self._gen_type_cleanup(name)
+
+ def visit_object_type(self,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ base: Optional[QAPISchemaObjectType],
+ members: List[QAPISchemaObjectTypeMember],
+ variants: Optional[QAPISchemaVariants]) -> None:
+ # Nothing to do for the special empty builtin
+ if name == 'q_empty':
+ return
+ with ifcontext(ifcond, self._genh):
+ self._genh.preamble_add(gen_fwd_object_or_array(name))
+ self._genh.add(gen_object(name, ifcond, base, members, variants))
+ with ifcontext(ifcond, self._genh, self._genc):
+ if base and not base.is_implicit():
+ self._genh.add(gen_upcast(name, base))
+ # TODO Worth changing the visitor signature, so we could
+ # directly use rather than repeat type.is_implicit()?
+ if not name.startswith('q_'):
+ # implicit types won't be directly allocated/freed
+ self._gen_type_cleanup(name)
+
+ def visit_alternate_type(self,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ variants: QAPISchemaVariants) -> None:
+ with ifcontext(ifcond, self._genh):
+ self._genh.preamble_add(gen_fwd_object_or_array(name))
+ self._genh.add(gen_object(name, ifcond, None,
+ [variants.tag_member], variants))
+ with ifcontext(ifcond, self._genh, self._genc):
+ self._gen_type_cleanup(name)
+
+
+def gen_types(schema: QAPISchema,
+ output_dir: str,
+ prefix: str,
+ opt_builtins: bool) -> None:
+ vis = QAPISchemaGenTypeVisitor(prefix)
+ schema.visit(vis)
+ vis.write(output_dir, opt_builtins)
diff --git a/scripts/qapi/visit.py b/scripts/qapi/visit.py
new file mode 100644
index 000000000..e13bbe429
--- /dev/null
+++ b/scripts/qapi/visit.py
@@ -0,0 +1,410 @@
+"""
+QAPI visitor generator
+
+Copyright IBM, Corp. 2011
+Copyright (C) 2014-2018 Red Hat, Inc.
+
+Authors:
+ Anthony Liguori <aliguori@us.ibm.com>
+ Michael Roth <mdroth@linux.vnet.ibm.com>
+ Markus Armbruster <armbru@redhat.com>
+
+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
+
+from .common import (
+ c_enum_const,
+ c_name,
+ indent,
+ mcgen,
+)
+from .gen import QAPISchemaModularCVisitor, gen_special_features, ifcontext
+from .schema import (
+ QAPISchema,
+ QAPISchemaEnumMember,
+ QAPISchemaEnumType,
+ QAPISchemaFeature,
+ QAPISchemaIfCond,
+ QAPISchemaObjectType,
+ QAPISchemaObjectTypeMember,
+ QAPISchemaType,
+ QAPISchemaVariants,
+)
+from .source import QAPISourceInfo
+
+
+def gen_visit_decl(name: str, scalar: bool = False) -> str:
+ c_type = c_name(name) + ' *'
+ if not scalar:
+ c_type += '*'
+ return mcgen('''
+
+bool visit_type_%(c_name)s(Visitor *v, const char *name,
+ %(c_type)sobj, Error **errp);
+''',
+ c_name=c_name(name), c_type=c_type)
+
+
+def gen_visit_members_decl(name: str) -> str:
+ return mcgen('''
+
+bool visit_type_%(c_name)s_members(Visitor *v, %(c_name)s *obj, Error **errp);
+''',
+ c_name=c_name(name))
+
+
+def gen_visit_object_members(name: str,
+ base: Optional[QAPISchemaObjectType],
+ members: List[QAPISchemaObjectTypeMember],
+ variants: Optional[QAPISchemaVariants]) -> str:
+ ret = mcgen('''
+
+bool visit_type_%(c_name)s_members(Visitor *v, %(c_name)s *obj, Error **errp)
+{
+''',
+ c_name=c_name(name))
+
+ if base:
+ ret += mcgen('''
+ if (!visit_type_%(c_type)s_members(v, (%(c_type)s *)obj, errp)) {
+ return false;
+ }
+''',
+ c_type=base.c_name())
+
+ for memb in members:
+ ret += memb.ifcond.gen_if()
+ if memb.optional:
+ ret += mcgen('''
+ if (visit_optional(v, "%(name)s", &obj->has_%(c_name)s)) {
+''',
+ name=memb.name, c_name=c_name(memb.name))
+ indent.increase()
+ special_features = gen_special_features(memb.features)
+ if special_features != '0':
+ ret += mcgen('''
+ if (visit_policy_reject(v, "%(name)s", %(special_features)s, errp)) {
+ return false;
+ }
+ if (!visit_policy_skip(v, "%(name)s", %(special_features)s)) {
+''',
+ name=memb.name, special_features=special_features)
+ indent.increase()
+ ret += mcgen('''
+ if (!visit_type_%(c_type)s(v, "%(name)s", &obj->%(c_name)s, errp)) {
+ return false;
+ }
+''',
+ c_type=memb.type.c_name(), name=memb.name,
+ c_name=c_name(memb.name))
+ if special_features != '0':
+ indent.decrease()
+ ret += mcgen('''
+ }
+''')
+ if memb.optional:
+ indent.decrease()
+ ret += mcgen('''
+ }
+''')
+ ret += memb.ifcond.gen_endif()
+
+ if variants:
+ tag_member = variants.tag_member
+ assert isinstance(tag_member.type, QAPISchemaEnumType)
+
+ ret += mcgen('''
+ switch (obj->%(c_name)s) {
+''',
+ c_name=c_name(tag_member.name))
+
+ for var in variants.variants:
+ case_str = c_enum_const(tag_member.type.name, var.name,
+ tag_member.type.prefix)
+ ret += var.ifcond.gen_if()
+ if var.type.name == 'q_empty':
+ # valid variant and nothing to do
+ ret += mcgen('''
+ case %(case)s:
+ break;
+''',
+ case=case_str)
+ else:
+ ret += mcgen('''
+ case %(case)s:
+ return visit_type_%(c_type)s_members(v, &obj->u.%(c_name)s, errp);
+''',
+ case=case_str,
+ c_type=var.type.c_name(), c_name=c_name(var.name))
+
+ ret += var.ifcond.gen_endif()
+ ret += mcgen('''
+ default:
+ abort();
+ }
+''')
+
+ ret += mcgen('''
+ return true;
+}
+''')
+ return ret
+
+
+def gen_visit_list(name: str, element_type: QAPISchemaType) -> str:
+ return mcgen('''
+
+bool visit_type_%(c_name)s(Visitor *v, const char *name,
+ %(c_name)s **obj, Error **errp)
+{
+ bool ok = false;
+ %(c_name)s *tail;
+ size_t size = sizeof(**obj);
+
+ if (!visit_start_list(v, name, (GenericList **)obj, size, errp)) {
+ return false;
+ }
+
+ for (tail = *obj; tail;
+ tail = (%(c_name)s *)visit_next_list(v, (GenericList *)tail, size)) {
+ if (!visit_type_%(c_elt_type)s(v, NULL, &tail->value, errp)) {
+ goto out_obj;
+ }
+ }
+
+ ok = visit_check_list(v, errp);
+out_obj:
+ visit_end_list(v, (void **)obj);
+ if (!ok && visit_is_input(v)) {
+ qapi_free_%(c_name)s(*obj);
+ *obj = NULL;
+ }
+ return ok;
+}
+''',
+ c_name=c_name(name), c_elt_type=element_type.c_name())
+
+
+def gen_visit_enum(name: str) -> str:
+ return mcgen('''
+
+bool visit_type_%(c_name)s(Visitor *v, const char *name,
+ %(c_name)s *obj, Error **errp)
+{
+ int value = *obj;
+ bool ok = visit_type_enum(v, name, &value, &%(c_name)s_lookup, errp);
+ *obj = value;
+ return ok;
+}
+''',
+ c_name=c_name(name))
+
+
+def gen_visit_alternate(name: str, variants: QAPISchemaVariants) -> str:
+ ret = mcgen('''
+
+bool visit_type_%(c_name)s(Visitor *v, const char *name,
+ %(c_name)s **obj, Error **errp)
+{
+ bool ok = false;
+
+ if (!visit_start_alternate(v, name, (GenericAlternate **)obj,
+ sizeof(**obj), errp)) {
+ return false;
+ }
+ if (!*obj) {
+ /* incomplete */
+ assert(visit_is_dealloc(v));
+ ok = true;
+ goto out_obj;
+ }
+ switch ((*obj)->type) {
+''',
+ c_name=c_name(name))
+
+ for var in variants.variants:
+ ret += var.ifcond.gen_if()
+ ret += mcgen('''
+ case %(case)s:
+''',
+ case=var.type.alternate_qtype())
+ if isinstance(var.type, QAPISchemaObjectType):
+ ret += mcgen('''
+ if (!visit_start_struct(v, name, NULL, 0, errp)) {
+ break;
+ }
+ if (visit_type_%(c_type)s_members(v, &(*obj)->u.%(c_name)s, errp)) {
+ ok = visit_check_struct(v, errp);
+ }
+ visit_end_struct(v, NULL);
+''',
+ c_type=var.type.c_name(),
+ c_name=c_name(var.name))
+ else:
+ ret += mcgen('''
+ ok = visit_type_%(c_type)s(v, name, &(*obj)->u.%(c_name)s, errp);
+''',
+ c_type=var.type.c_name(),
+ c_name=c_name(var.name))
+ ret += mcgen('''
+ break;
+''')
+ ret += var.ifcond.gen_endif()
+
+ ret += mcgen('''
+ case QTYPE_NONE:
+ abort();
+ default:
+ assert(visit_is_input(v));
+ error_setg(errp, QERR_INVALID_PARAMETER_TYPE, name ? name : "null",
+ "%(name)s");
+ /* Avoid passing invalid *obj to qapi_free_%(c_name)s() */
+ g_free(*obj);
+ *obj = NULL;
+ }
+out_obj:
+ visit_end_alternate(v, (void **)obj);
+ if (!ok && visit_is_input(v)) {
+ qapi_free_%(c_name)s(*obj);
+ *obj = NULL;
+ }
+ return ok;
+}
+''',
+ name=name, c_name=c_name(name))
+
+ return ret
+
+
+def gen_visit_object(name: str) -> str:
+ return mcgen('''
+
+bool visit_type_%(c_name)s(Visitor *v, const char *name,
+ %(c_name)s **obj, Error **errp)
+{
+ bool ok = false;
+
+ if (!visit_start_struct(v, name, (void **)obj, sizeof(%(c_name)s), errp)) {
+ return false;
+ }
+ if (!*obj) {
+ /* incomplete */
+ assert(visit_is_dealloc(v));
+ ok = true;
+ goto out_obj;
+ }
+ if (!visit_type_%(c_name)s_members(v, *obj, errp)) {
+ goto out_obj;
+ }
+ ok = visit_check_struct(v, errp);
+out_obj:
+ visit_end_struct(v, (void **)obj);
+ if (!ok && visit_is_input(v)) {
+ qapi_free_%(c_name)s(*obj);
+ *obj = NULL;
+ }
+ return ok;
+}
+''',
+ c_name=c_name(name))
+
+
+class QAPISchemaGenVisitVisitor(QAPISchemaModularCVisitor):
+
+ def __init__(self, prefix: str):
+ super().__init__(
+ prefix, 'qapi-visit', ' * Schema-defined QAPI visitors',
+ ' * Built-in QAPI visitors', __doc__)
+
+ def _begin_builtin_module(self) -> None:
+ self._genc.preamble_add(mcgen('''
+#include "qemu/osdep.h"
+#include "qapi/error.h"
+#include "qapi/qapi-builtin-visit.h"
+'''))
+ self._genh.preamble_add(mcgen('''
+#include "qapi/visitor.h"
+#include "qapi/qapi-builtin-types.h"
+
+'''))
+
+ def _begin_user_module(self, name: str) -> None:
+ types = self._module_basename('qapi-types', name)
+ visit = self._module_basename('qapi-visit', name)
+ self._genc.preamble_add(mcgen('''
+#include "qemu/osdep.h"
+#include "qapi/error.h"
+#include "qapi/qmp/qerror.h"
+#include "%(visit)s.h"
+''',
+ visit=visit))
+ self._genh.preamble_add(mcgen('''
+#include "qapi/qapi-builtin-visit.h"
+#include "%(types)s.h"
+
+''',
+ types=types))
+
+ def visit_enum_type(self,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ members: List[QAPISchemaEnumMember],
+ prefix: Optional[str]) -> None:
+ with ifcontext(ifcond, self._genh, self._genc):
+ self._genh.add(gen_visit_decl(name, scalar=True))
+ self._genc.add(gen_visit_enum(name))
+
+ def visit_array_type(self,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ element_type: QAPISchemaType) -> None:
+ with ifcontext(ifcond, self._genh, self._genc):
+ self._genh.add(gen_visit_decl(name))
+ self._genc.add(gen_visit_list(name, element_type))
+
+ def visit_object_type(self,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ base: Optional[QAPISchemaObjectType],
+ members: List[QAPISchemaObjectTypeMember],
+ variants: Optional[QAPISchemaVariants]) -> None:
+ # Nothing to do for the special empty builtin
+ if name == 'q_empty':
+ return
+ with ifcontext(ifcond, self._genh, self._genc):
+ self._genh.add(gen_visit_members_decl(name))
+ self._genc.add(gen_visit_object_members(name, base,
+ members, variants))
+ # TODO Worth changing the visitor signature, so we could
+ # directly use rather than repeat type.is_implicit()?
+ if not name.startswith('q_'):
+ # only explicit types need an allocating visit
+ self._genh.add(gen_visit_decl(name))
+ self._genc.add(gen_visit_object(name))
+
+ def visit_alternate_type(self,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ variants: QAPISchemaVariants) -> None:
+ with ifcontext(ifcond, self._genh, self._genc):
+ self._genh.add(gen_visit_decl(name))
+ self._genc.add(gen_visit_alternate(name, variants))
+
+
+def gen_visit(schema: QAPISchema,
+ output_dir: str,
+ prefix: str,
+ opt_builtins: bool) -> None:
+ vis = QAPISchemaGenVisitVisitor(prefix)
+ schema.visit(vis)
+ vis.write(output_dir, opt_builtins)