xref: /qemu/scripts/qapi/gen.py (revision 370ed600)
1# -*- coding: utf-8 -*-
2#
3# QAPI code generation
4#
5# Copyright (c) 2015-2019 Red Hat Inc.
6#
7# Authors:
8#  Markus Armbruster <armbru@redhat.com>
9#  Marc-André Lureau <marcandre.lureau@redhat.com>
10#
11# This work is licensed under the terms of the GNU GPL, version 2.
12# See the COPYING file in the top-level directory.
13
14from contextlib import contextmanager
15import os
16import re
17from typing import (
18    Dict,
19    Iterator,
20    Optional,
21    Sequence,
22    Tuple,
23)
24
25from .common import (
26    c_fname,
27    c_name,
28    guardend,
29    guardstart,
30    mcgen,
31)
32from .schema import (
33    QAPISchemaFeature,
34    QAPISchemaIfCond,
35    QAPISchemaModule,
36    QAPISchemaObjectType,
37    QAPISchemaVisitor,
38)
39from .source import QAPISourceInfo
40
41
42def gen_special_features(features: Sequence[QAPISchemaFeature]) -> str:
43    special_features = [f"1u << QAPI_{feat.name.upper()}"
44                        for feat in features if feat.is_special()]
45    return ' | '.join(special_features) or '0'
46
47
48class QAPIGen:
49    def __init__(self, fname: str):
50        self.fname = fname
51        self._preamble = ''
52        self._body = ''
53
54    def preamble_add(self, text: str) -> None:
55        self._preamble += text
56
57    def add(self, text: str) -> None:
58        self._body += text
59
60    def get_content(self) -> str:
61        return self._top() + self._preamble + self._body + self._bottom()
62
63    def _top(self) -> str:
64        # pylint: disable=no-self-use
65        return ''
66
67    def _bottom(self) -> str:
68        # pylint: disable=no-self-use
69        return ''
70
71    def write(self, output_dir: str) -> None:
72        # Include paths starting with ../ are used to reuse modules of the main
73        # schema in specialised schemas. Don't overwrite the files that are
74        # already generated for the main schema.
75        if self.fname.startswith('../'):
76            return
77        pathname = os.path.join(output_dir, self.fname)
78        odir = os.path.dirname(pathname)
79
80        if odir:
81            os.makedirs(odir, exist_ok=True)
82
83        # use os.open for O_CREAT to create and read a non-existant file
84        fd = os.open(pathname, os.O_RDWR | os.O_CREAT, 0o666)
85        with os.fdopen(fd, 'r+', encoding='utf-8') as fp:
86            text = self.get_content()
87            oldtext = fp.read(len(text) + 1)
88            if text != oldtext:
89                fp.seek(0)
90                fp.truncate(0)
91                fp.write(text)
92
93
94def _wrap_ifcond(ifcond: QAPISchemaIfCond, before: str, after: str) -> str:
95    if before == after:
96        return after   # suppress empty #if ... #endif
97
98    assert after.startswith(before)
99    out = before
100    added = after[len(before):]
101    if added[0] == '\n':
102        out += '\n'
103        added = added[1:]
104    out += ifcond.gen_if()
105    out += added
106    out += ifcond.gen_endif()
107    return out
108
109
110def build_params(arg_type: Optional[QAPISchemaObjectType],
111                 boxed: bool,
112                 extra: Optional[str] = None) -> str:
113    ret = ''
114    sep = ''
115    if boxed:
116        assert arg_type
117        ret += '%s arg' % arg_type.c_param_type()
118        sep = ', '
119    elif arg_type:
120        assert not arg_type.variants
121        for memb in arg_type.members:
122            assert not memb.ifcond.is_present()
123            ret += sep
124            sep = ', '
125            if memb.need_has():
126                ret += 'bool has_%s, ' % c_name(memb.name)
127            ret += '%s %s' % (memb.type.c_param_type(),
128                              c_name(memb.name))
129    if extra:
130        ret += sep + extra
131    return ret if ret else 'void'
132
133
134class QAPIGenCCode(QAPIGen):
135    def __init__(self, fname: str):
136        super().__init__(fname)
137        self._start_if: Optional[Tuple[QAPISchemaIfCond, str, str]] = None
138
139    def start_if(self, ifcond: QAPISchemaIfCond) -> None:
140        assert self._start_if is None
141        self._start_if = (ifcond, self._body, self._preamble)
142
143    def end_if(self) -> None:
144        assert self._start_if is not None
145        self._body = _wrap_ifcond(self._start_if[0],
146                                  self._start_if[1], self._body)
147        self._preamble = _wrap_ifcond(self._start_if[0],
148                                      self._start_if[2], self._preamble)
149        self._start_if = None
150
151    def get_content(self) -> str:
152        assert self._start_if is None
153        return super().get_content()
154
155
156class QAPIGenC(QAPIGenCCode):
157    def __init__(self, fname: str, blurb: str, pydoc: str):
158        super().__init__(fname)
159        self._blurb = blurb
160        self._copyright = '\n * '.join(re.findall(r'^Copyright .*', pydoc,
161                                                  re.MULTILINE))
162
163    def _top(self) -> str:
164        return mcgen('''
165/* AUTOMATICALLY GENERATED, DO NOT MODIFY */
166
167/*
168%(blurb)s
169 *
170 * %(copyright)s
171 *
172 * This work is licensed under the terms of the GNU LGPL, version 2.1 or later.
173 * See the COPYING.LIB file in the top-level directory.
174 */
175
176''',
177                     blurb=self._blurb, copyright=self._copyright)
178
179    def _bottom(self) -> str:
180        return mcgen('''
181
182/* Dummy declaration to prevent empty .o file */
183char qapi_dummy_%(name)s;
184''',
185                     name=c_fname(self.fname))
186
187
188class QAPIGenH(QAPIGenC):
189    def _top(self) -> str:
190        return super()._top() + guardstart(self.fname)
191
192    def _bottom(self) -> str:
193        return guardend(self.fname)
194
195
196class QAPIGenTrace(QAPIGen):
197    def _top(self) -> str:
198        return super()._top() + '# AUTOMATICALLY GENERATED, DO NOT MODIFY\n\n'
199
200
201@contextmanager
202def ifcontext(ifcond: QAPISchemaIfCond, *args: QAPIGenCCode) -> Iterator[None]:
203    """
204    A with-statement context manager that wraps with `start_if()` / `end_if()`.
205
206    :param ifcond: A sequence of conditionals, passed to `start_if()`.
207    :param args: any number of `QAPIGenCCode`.
208
209    Example::
210
211        with ifcontext(ifcond, self._genh, self._genc):
212            modify self._genh and self._genc ...
213
214    Is equivalent to calling::
215
216        self._genh.start_if(ifcond)
217        self._genc.start_if(ifcond)
218        modify self._genh and self._genc ...
219        self._genh.end_if()
220        self._genc.end_if()
221    """
222    for arg in args:
223        arg.start_if(ifcond)
224    yield
225    for arg in args:
226        arg.end_if()
227
228
229class QAPISchemaMonolithicCVisitor(QAPISchemaVisitor):
230    def __init__(self,
231                 prefix: str,
232                 what: str,
233                 blurb: str,
234                 pydoc: str):
235        self._prefix = prefix
236        self._what = what
237        self._genc = QAPIGenC(self._prefix + self._what + '.c',
238                              blurb, pydoc)
239        self._genh = QAPIGenH(self._prefix + self._what + '.h',
240                              blurb, pydoc)
241
242    def write(self, output_dir: str) -> None:
243        self._genc.write(output_dir)
244        self._genh.write(output_dir)
245
246
247class QAPISchemaModularCVisitor(QAPISchemaVisitor):
248    def __init__(self,
249                 prefix: str,
250                 what: str,
251                 user_blurb: str,
252                 builtin_blurb: Optional[str],
253                 pydoc: str,
254                 gen_tracing: bool = False):
255        self._prefix = prefix
256        self._what = what
257        self._user_blurb = user_blurb
258        self._builtin_blurb = builtin_blurb
259        self._pydoc = pydoc
260        self._current_module: Optional[str] = None
261        self._module: Dict[str, Tuple[QAPIGenC, QAPIGenH,
262                                      Optional[QAPIGenTrace]]] = {}
263        self._main_module: Optional[str] = None
264        self._gen_tracing = gen_tracing
265
266    @property
267    def _genc(self) -> QAPIGenC:
268        assert self._current_module is not None
269        return self._module[self._current_module][0]
270
271    @property
272    def _genh(self) -> QAPIGenH:
273        assert self._current_module is not None
274        return self._module[self._current_module][1]
275
276    @property
277    def _gen_trace_events(self) -> QAPIGenTrace:
278        assert self._gen_tracing
279        assert self._current_module is not None
280        gent = self._module[self._current_module][2]
281        assert gent is not None
282        return gent
283
284    @staticmethod
285    def _module_dirname(name: str) -> str:
286        if QAPISchemaModule.is_user_module(name):
287            return os.path.dirname(name)
288        return ''
289
290    def _module_basename(self, what: str, name: str) -> str:
291        ret = '' if QAPISchemaModule.is_builtin_module(name) else self._prefix
292        if QAPISchemaModule.is_user_module(name):
293            basename = os.path.basename(name)
294            ret += what
295            if name != self._main_module:
296                ret += '-' + os.path.splitext(basename)[0]
297        else:
298            assert QAPISchemaModule.is_system_module(name)
299            ret += re.sub(r'-', '-' + name[2:] + '-', what)
300        return ret
301
302    def _module_filename(self, what: str, name: str) -> str:
303        return os.path.join(self._module_dirname(name),
304                            self._module_basename(what, name))
305
306    def _add_module(self, name: str, blurb: str) -> None:
307        if QAPISchemaModule.is_user_module(name):
308            if self._main_module is None:
309                self._main_module = name
310        basename = self._module_filename(self._what, name)
311        genc = QAPIGenC(basename + '.c', blurb, self._pydoc)
312        genh = QAPIGenH(basename + '.h', blurb, self._pydoc)
313
314        gent: Optional[QAPIGenTrace] = None
315        if self._gen_tracing:
316            gent = QAPIGenTrace(basename + '.trace-events')
317
318        self._module[name] = (genc, genh, gent)
319        self._current_module = name
320
321    @contextmanager
322    def _temp_module(self, name: str) -> Iterator[None]:
323        old_module = self._current_module
324        self._current_module = name
325        yield
326        self._current_module = old_module
327
328    def write(self, output_dir: str, opt_builtins: bool = False) -> None:
329        for name, (genc, genh, gent) in self._module.items():
330            if QAPISchemaModule.is_builtin_module(name) and not opt_builtins:
331                continue
332            genc.write(output_dir)
333            genh.write(output_dir)
334            if gent is not None:
335                gent.write(output_dir)
336
337    def _begin_builtin_module(self) -> None:
338        pass
339
340    def _begin_user_module(self, name: str) -> None:
341        pass
342
343    def visit_module(self, name: str) -> None:
344        if QAPISchemaModule.is_builtin_module(name):
345            if self._builtin_blurb:
346                self._add_module(name, self._builtin_blurb)
347                self._begin_builtin_module()
348            else:
349                # The built-in module has not been created.  No code may
350                # be generated.
351                self._current_module = None
352        else:
353            assert QAPISchemaModule.is_user_module(name)
354            self._add_module(name, self._user_blurb)
355            self._begin_user_module(name)
356
357    def visit_include(self, name: str, info: Optional[QAPISourceInfo]) -> None:
358        relname = os.path.relpath(self._module_filename(self._what, name),
359                                  os.path.dirname(self._genh.fname))
360        self._genh.preamble_add(mcgen('''
361#include "%(relname)s.h"
362''',
363                                      relname=relname))
364