From e02cda008591317b1625707ff8e115a4841aa889 Mon Sep 17 00:00:00 2001 From: Timos Ampelikiotis Date: Tue, 10 Oct 2023 11:40:56 +0000 Subject: Introduce Virtio-loopback epsilon release: Epsilon release introduces a new compatibility layer which make virtio-loopback design to work with QEMU and rust-vmm vhost-user backend without require any changes. Signed-off-by: Timos Ampelikiotis Change-Id: I52e57563e08a7d0bdc002f8e928ee61ba0c53dd9 --- scripts/qapi/.flake8 | 2 + scripts/qapi/.isort.cfg | 7 + scripts/qapi/__init__.py | 0 scripts/qapi/commands.py | 338 +++++++++++++ scripts/qapi/common.py | 251 ++++++++++ scripts/qapi/error.py | 50 ++ scripts/qapi/events.py | 252 ++++++++++ scripts/qapi/expr.py | 694 ++++++++++++++++++++++++++ scripts/qapi/gen.py | 339 +++++++++++++ scripts/qapi/introspect.py | 390 +++++++++++++++ scripts/qapi/main.py | 95 ++++ scripts/qapi/mypy.ini | 9 + scripts/qapi/parser.py | 810 ++++++++++++++++++++++++++++++ scripts/qapi/pylintrc | 69 +++ scripts/qapi/schema.py | 1185 ++++++++++++++++++++++++++++++++++++++++++++ scripts/qapi/source.py | 71 +++ scripts/qapi/types.py | 383 ++++++++++++++ scripts/qapi/visit.py | 410 +++++++++++++++ 18 files changed, 5355 insertions(+) create mode 100644 scripts/qapi/.flake8 create mode 100644 scripts/qapi/.isort.cfg create mode 100644 scripts/qapi/__init__.py create mode 100644 scripts/qapi/commands.py create mode 100644 scripts/qapi/common.py create mode 100644 scripts/qapi/error.py create mode 100644 scripts/qapi/events.py create mode 100644 scripts/qapi/expr.py create mode 100644 scripts/qapi/gen.py create mode 100644 scripts/qapi/introspect.py create mode 100644 scripts/qapi/main.py create mode 100644 scripts/qapi/mypy.ini create mode 100644 scripts/qapi/parser.py create mode 100644 scripts/qapi/pylintrc create mode 100644 scripts/qapi/schema.py create mode 100644 scripts/qapi/source.py create mode 100644 scripts/qapi/types.py create mode 100644 scripts/qapi/visit.py (limited to 'scripts/qapi') 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 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 + Michael Roth + Markus Armbruster + +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 +# Markus Armbruster +# +# 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 +# Marc-André Lureau +# +# 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 + Markus Armbruster + +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 = ¶m; +''', + 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, ¶m, &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 +# Markus Armbruster +# Eric Blake +# Marc-André Lureau +# John Snow +# +# 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 +# Marc-André Lureau +# +# 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 + John Snow + +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 +# Markus Armbruster +# Marc-André Lureau +# Kevin Wolf +# +# 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 +# Eric Blake +# Marc-André Lureau +# +# 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 +# +# 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 + Michael Roth + Markus Armbruster + +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 + Michael Roth + Markus Armbruster + +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) -- cgit