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