qemu/scripts/qapi/gen.py
Markus Armbruster 44ea9d9be3 qapi: Start to elide redundant has_FOO in generated C
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>
2022-12-13 18:31:37 +01:00

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))