44ea9d9be3
In QAPI, absent optional members are distinct from any present value. We thus represent an optional schema member FOO as two C members: a FOO with the member's type, and a bool has_FOO. Likewise for function arguments. However, has_FOO is actually redundant for a pointer-valued FOO, which can be null only when has_FOO is false, i.e. has_FOO == !!FOO. Except for arrays, where we a null FOO can also be a present empty array. The redundant has_FOO are a nuisance to work with. Improve the generator to elide them. Uses of has_FOO need to be replaced as follows. Tests of has_FOO become the equivalent comparison of FOO with null. For brevity, this is commonly done by implicit conversion to bool. Assignments to has_FOO get dropped. Likewise for arguments to has_FOO parameters. Beware: code may violate the invariant has_FOO == !!FOO before the transformation, and get away with it. The above transformation can then break things. Two cases: * Absent: if code ignores FOO entirely when !has_FOO (except for freeing it if necessary), even non-null / uninitialized FOO works. Such code is known to exist. * Present: if code ignores FOO entirely when has_FOO, even null FOO works. Such code should not exist. In both cases, replacing tests of has_FOO by FOO reverts their sense. We have to fix the value of FOO then. To facilitate review of the necessary updates to handwritten code, add means to opt out of this change, and opt out for all QAPI schema modules where the change requires updates to handwritten code. The next few commits will remove these opt-outs in reviewable chunks, then drop the means to opt out. Signed-off-by: Markus Armbruster <armbru@redhat.com> Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Message-Id: <20221104160712.3005652-5-armbru@redhat.com>
363 lines
11 KiB
Python
363 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# QAPI code generation
|
|
#
|
|
# Copyright (c) 2015-2019 Red Hat Inc.
|
|
#
|
|
# Authors:
|
|
# Markus Armbruster <armbru@redhat.com>
|
|
# Marc-André Lureau <marcandre.lureau@redhat.com>
|
|
#
|
|
# This work is licensed under the terms of the GNU GPL, version 2.
|
|
# See the COPYING file in the top-level directory.
|
|
|
|
from contextlib import contextmanager
|
|
import os
|
|
import re
|
|
from typing import (
|
|
Dict,
|
|
Iterator,
|
|
Optional,
|
|
Sequence,
|
|
Tuple,
|
|
)
|
|
|
|
from .common import (
|
|
c_fname,
|
|
c_name,
|
|
guardend,
|
|
guardstart,
|
|
mcgen,
|
|
)
|
|
from .schema import (
|
|
QAPISchemaFeature,
|
|
QAPISchemaIfCond,
|
|
QAPISchemaModule,
|
|
QAPISchemaObjectType,
|
|
QAPISchemaVisitor,
|
|
)
|
|
from .source import QAPISourceInfo
|
|
|
|
|
|
def gen_special_features(features: Sequence[QAPISchemaFeature]) -> str:
|
|
special_features = [f"1u << QAPI_{feat.name.upper()}"
|
|
for feat in features if feat.is_special()]
|
|
return ' | '.join(special_features) or '0'
|
|
|
|
|
|
class QAPIGen:
|
|
def __init__(self, fname: str):
|
|
self.fname = fname
|
|
self._preamble = ''
|
|
self._body = ''
|
|
|
|
def preamble_add(self, text: str) -> None:
|
|
self._preamble += text
|
|
|
|
def add(self, text: str) -> None:
|
|
self._body += text
|
|
|
|
def get_content(self) -> str:
|
|
return self._top() + self._preamble + self._body + self._bottom()
|
|
|
|
def _top(self) -> str:
|
|
# pylint: disable=no-self-use
|
|
return ''
|
|
|
|
def _bottom(self) -> str:
|
|
# pylint: disable=no-self-use
|
|
return ''
|
|
|
|
def write(self, output_dir: str) -> None:
|
|
# Include paths starting with ../ are used to reuse modules of the main
|
|
# schema in specialised schemas. Don't overwrite the files that are
|
|
# already generated for the main schema.
|
|
if self.fname.startswith('../'):
|
|
return
|
|
pathname = os.path.join(output_dir, self.fname)
|
|
odir = os.path.dirname(pathname)
|
|
|
|
if odir:
|
|
os.makedirs(odir, exist_ok=True)
|
|
|
|
# use os.open for O_CREAT to create and read a non-existant file
|
|
fd = os.open(pathname, os.O_RDWR | os.O_CREAT, 0o666)
|
|
with os.fdopen(fd, 'r+', encoding='utf-8') as fp:
|
|
text = self.get_content()
|
|
oldtext = fp.read(len(text) + 1)
|
|
if text != oldtext:
|
|
fp.seek(0)
|
|
fp.truncate(0)
|
|
fp.write(text)
|
|
|
|
|
|
def _wrap_ifcond(ifcond: QAPISchemaIfCond, before: str, after: str) -> str:
|
|
if before == after:
|
|
return after # suppress empty #if ... #endif
|
|
|
|
assert after.startswith(before)
|
|
out = before
|
|
added = after[len(before):]
|
|
if added[0] == '\n':
|
|
out += '\n'
|
|
added = added[1:]
|
|
out += ifcond.gen_if()
|
|
out += added
|
|
out += ifcond.gen_endif()
|
|
return out
|
|
|
|
|
|
def build_params(arg_type: Optional[QAPISchemaObjectType],
|
|
boxed: bool,
|
|
extra: Optional[str] = None) -> str:
|
|
ret = ''
|
|
sep = ''
|
|
if boxed:
|
|
assert arg_type
|
|
ret += '%s arg' % arg_type.c_param_type()
|
|
sep = ', '
|
|
elif arg_type:
|
|
assert not arg_type.variants
|
|
for memb in arg_type.members:
|
|
ret += sep
|
|
sep = ', '
|
|
if memb.need_has():
|
|
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)
|
|
|
|
|
|
class QAPIGenTrace(QAPIGen):
|
|
def _top(self) -> str:
|
|
return super()._top() + '# AUTOMATICALLY GENERATED, DO NOT MODIFY\n\n'
|
|
|
|
|
|
@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,
|
|
gen_tracing: bool = False):
|
|
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,
|
|
Optional[QAPIGenTrace]]] = {}
|
|
self._main_module: Optional[str] = None
|
|
self._gen_tracing = gen_tracing
|
|
|
|
@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]
|
|
|
|
@property
|
|
def _gen_trace_events(self) -> QAPIGenTrace:
|
|
assert self._gen_tracing
|
|
assert self._current_module is not None
|
|
gent = self._module[self._current_module][2]
|
|
assert gent is not None
|
|
return gent
|
|
|
|
@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)
|
|
|
|
gent: Optional[QAPIGenTrace] = None
|
|
if self._gen_tracing:
|
|
gent = QAPIGenTrace(basename + '.trace-events')
|
|
|
|
self._module[name] = (genc, genh, gent)
|
|
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, gent) in self._module.items():
|
|
if QAPISchemaModule.is_builtin_module(name) and not opt_builtins:
|
|
continue
|
|
genc.write(output_dir)
|
|
genh.write(output_dir)
|
|
if gent is not None:
|
|
gent.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))
|