2 QAPI introspection generator
4 Copyright (C) 2015-2021 Red Hat, Inc.
7 Markus Armbruster <armbru@redhat.com>
8 John Snow <jsnow@redhat.com>
10 This work is licensed under the terms of the GNU GPL, version 2.
11 See the COPYING file in the top-level directory.
25 from .common
import c_name
, mcgen
26 from .gen
import QAPISchemaMonolithicCVisitor
30 QAPISchemaBuiltinType
,
36 QAPISchemaObjectTypeMember
,
41 from .source
import QAPISourceInfo
44 # This module constructs a tree data structure that is used to
45 # generate the introspection information for QEMU. It is shaped
48 # A complexity over JSON is that our values may or may not be annotated.
50 # Un-annotated values may be:
51 # Scalar: str, bool, None.
52 # Non-scalar: List, Dict
53 # _value = Union[str, bool, None, Dict[str, JSONValue], List[JSONValue]]
55 # With optional annotations, the type of all values is:
56 # JSONValue = Union[_Value, Annotated[_Value]]
58 # Sadly, mypy does not support recursive types; so the _Stub alias is used to
59 # mark the imprecision in the type model where we'd otherwise use JSONValue.
61 _Scalar
= Union
[str, bool, None]
62 _NonScalar
= Union
[Dict
[str, _Stub
], List
[_Stub
]]
63 _Value
= Union
[_Scalar
, _NonScalar
]
64 JSONValue
= Union
[_Value
, 'Annotated[_Value]']
66 # These types are based on structures defined in QEMU's schema, so we
67 # lack precise types for them here. Python 3.6 does not offer
68 # TypedDict constructs, so they are broadly typed here as simple
70 SchemaInfo
= Dict
[str, object]
71 SchemaInfoEnumMember
= Dict
[str, object]
72 SchemaInfoObject
= Dict
[str, object]
73 SchemaInfoObjectVariant
= Dict
[str, object]
74 SchemaInfoObjectMember
= Dict
[str, object]
75 SchemaInfoCommand
= Dict
[str, object]
78 _ValueT
= TypeVar('_ValueT', bound
=_Value
)
81 class Annotated(Generic
[_ValueT
]):
83 Annotated generally contains a SchemaInfo-like type (as a dict),
84 But it also used to wrap comments/ifconds around scalar leaf values,
85 for the benefit of features and enums.
87 # TODO: Remove after Python 3.7 adds @dataclass:
88 # pylint: disable=too-few-public-methods
89 def __init__(self
, value
: _ValueT
, ifcond
: QAPISchemaIfCond
,
90 comment
: Optional
[str] = None):
92 self
.comment
: Optional
[str] = comment
96 def _tree_to_qlit(obj
: JSONValue
,
98 dict_value
: bool = False) -> str:
100 Convert the type tree into a QLIT C string, recursively.
102 :param obj: The value to convert.
103 This value may not be Annotated when dict_value is True.
104 :param level: The indentation level for this particular value.
105 :param dict_value: True when the value being processed belongs to a
106 dict key; which suppresses the output indent.
109 def indent(level
: int) -> str:
110 return level
* 4 * ' '
112 if isinstance(obj
, Annotated
):
113 # NB: _tree_to_qlit is called recursively on the values of a
114 # key:value pair; those values can't be decorated with
115 # comments or conditionals.
116 msg
= "dict values cannot have attached comments or if-conditionals."
117 assert not dict_value
, msg
121 ret
+= indent(level
) + f
"/* {obj.comment} */\n"
122 if obj
.ifcond
.is_present():
123 ret
+= obj
.ifcond
.gen_if()
124 ret
+= _tree_to_qlit(obj
.value
, level
)
125 if obj
.ifcond
.is_present():
126 ret
+= '\n' + obj
.ifcond
.gen_endif()
136 elif isinstance(obj
, str):
137 ret
+= f
"QLIT_QSTR({to_c_string(obj)})"
138 elif isinstance(obj
, bool):
139 ret
+= f
"QLIT_QBOOL({str(obj).lower()})"
142 elif isinstance(obj
, list):
143 ret
+= 'QLIT_QLIST(((QLitObject[]) {\n'
145 ret
+= _tree_to_qlit(value
, level
+ 1).strip('\n') + '\n'
146 ret
+= indent(level
+ 1) + '{}\n'
147 ret
+= indent(level
) + '}))'
148 elif isinstance(obj
, dict):
149 ret
+= 'QLIT_QDICT(((QLitDictEntry[]) {\n'
150 for key
, value
in sorted(obj
.items()):
151 ret
+= indent(level
+ 1) + "{{ {:s}, {:s} }},\n".format(
153 _tree_to_qlit(value
, level
+ 1, dict_value
=True)
155 ret
+= indent(level
+ 1) + '{}\n'
156 ret
+= indent(level
) + '}))'
158 raise NotImplementedError(
159 f
"type '{type(obj).__name__}' not implemented"
167 def to_c_string(string
: str) -> str:
168 return '"' + string
.replace('\\', r
'\\').replace('"', r
'\"') + '"'
171 class QAPISchemaGenIntrospectVisitor(QAPISchemaMonolithicCVisitor
):
173 def __init__(self
, prefix
: str, unmask
: bool):
175 prefix
, 'qapi-introspect',
176 ' * QAPI/QMP schema introspection', __doc__
)
177 self
._unmask
= unmask
178 self
._schema
: Optional
[QAPISchema
] = None
179 self
._trees
: List
[Annotated
[SchemaInfo
]] = []
180 self
._used
_types
: List
[QAPISchemaType
] = []
181 self
._name
_map
: Dict
[str, str] = {}
182 self
._genc
.add(mcgen('''
183 #include "qemu/osdep.h"
184 #include "%(prefix)sqapi-introspect.h"
189 def visit_begin(self
, schema
: QAPISchema
) -> None:
190 self
._schema
= schema
192 def visit_end(self
) -> None:
193 # visit the types that are actually used
194 for typ
in self
._used
_types
:
197 name
= c_name(self
._prefix
, protect
=False) + 'qmp_schema_qlit'
198 self
._genh
.add(mcgen('''
199 #include "qapi/qmp/qlit.h"
201 extern const QLitObject %(c_name)s;
203 c_name
=c_name(name
)))
204 self
._genc
.add(mcgen('''
205 const QLitObject %(c_name)s = %(c_string)s;
208 c_string
=_tree_to_qlit(self
._trees
)))
211 self
._used
_types
= []
214 def visit_needed(self
, entity
: QAPISchemaEntity
) -> bool:
215 # Ignore types on first pass; visit_end() will pick up used types
216 return not isinstance(entity
, QAPISchemaType
)
218 def _name(self
, name
: str) -> str:
221 if name
not in self
._name
_map
:
222 self
._name
_map
[name
] = '%d' % len(self
._name
_map
)
223 return self
._name
_map
[name
]
225 def _use_type(self
, typ
: QAPISchemaType
) -> str:
226 assert self
._schema
is not None
228 # Map the various integer types to plain int
229 if typ
.json_type() == 'int':
230 typ
= self
._schema
.lookup_type('int')
231 elif (isinstance(typ
, QAPISchemaArrayType
) and
232 typ
.element_type
.json_type() == 'int'):
233 typ
= self
._schema
.lookup_type('intList')
234 # Add type to work queue if new
235 if typ
not in self
._used
_types
:
236 self
._used
_types
.append(typ
)
237 # Clients should examine commands and events, not types. Hide
238 # type names as integers to reduce the temptation. Also, it
239 # saves a few characters on the wire.
240 if isinstance(typ
, QAPISchemaBuiltinType
):
242 if isinstance(typ
, QAPISchemaArrayType
):
243 return '[' + self
._use
_type
(typ
.element_type
) + ']'
244 return self
._name
(typ
.name
)
247 def _gen_features(features
: Sequence
[QAPISchemaFeature
]
248 ) -> List
[Annotated
[str]]:
249 return [Annotated(f
.name
, f
.ifcond
) for f
in features
]
251 def _gen_tree(self
, name
: str, mtype
: str, obj
: Dict
[str, object],
252 ifcond
: QAPISchemaIfCond
= QAPISchemaIfCond(),
253 features
: Sequence
[QAPISchemaFeature
] = ()) -> None:
255 Build and append a SchemaInfo object to self._trees.
257 :param name: The SchemaInfo's name.
258 :param mtype: The SchemaInfo's meta-type.
259 :param obj: Additional SchemaInfo members, as appropriate for
261 :param ifcond: Conditionals to apply to the SchemaInfo.
262 :param features: The SchemaInfo's features.
263 Will be omitted from the output if empty.
265 comment
: Optional
[str] = None
266 if mtype
not in ('command', 'event', 'builtin', 'array'):
268 # Output a comment to make it easy to map masked names
269 # back to the source when reading the generated output.
270 comment
= f
'"{self._name(name)}" = {name}'
271 name
= self
._name
(name
)
273 obj
['meta-type'] = mtype
275 obj
['features'] = self
._gen
_features
(features
)
276 self
._trees
.append(Annotated(obj
, ifcond
, comment
))
278 def _gen_enum_member(self
, member
: QAPISchemaEnumMember
279 ) -> Annotated
[SchemaInfoEnumMember
]:
280 obj
: SchemaInfoEnumMember
= {
284 obj
['features'] = self
._gen
_features
(member
.features
)
285 return Annotated(obj
, member
.ifcond
)
287 def _gen_object_member(self
, member
: QAPISchemaObjectTypeMember
288 ) -> Annotated
[SchemaInfoObjectMember
]:
289 obj
: SchemaInfoObjectMember
= {
291 'type': self
._use
_type
(member
.type)
294 obj
['default'] = None
296 obj
['features'] = self
._gen
_features
(member
.features
)
297 return Annotated(obj
, member
.ifcond
)
299 def _gen_variant(self
, variant
: QAPISchemaVariant
300 ) -> Annotated
[SchemaInfoObjectVariant
]:
301 obj
: SchemaInfoObjectVariant
= {
302 'case': variant
.name
,
303 'type': self
._use
_type
(variant
.type)
305 return Annotated(obj
, variant
.ifcond
)
307 def visit_builtin_type(self
, name
: str, info
: Optional
[QAPISourceInfo
],
308 json_type
: str) -> None:
309 self
._gen
_tree
(name
, 'builtin', {'json-type': json_type
})
311 def visit_enum_type(self
, name
: str, info
: Optional
[QAPISourceInfo
],
312 ifcond
: QAPISchemaIfCond
,
313 features
: List
[QAPISchemaFeature
],
314 members
: List
[QAPISchemaEnumMember
],
315 prefix
: Optional
[str]) -> None:
318 {'members': [self
._gen
_enum
_member
(m
) for m
in members
],
319 'values': [Annotated(m
.name
, m
.ifcond
) for m
in members
]},
323 def visit_array_type(self
, name
: str, info
: Optional
[QAPISourceInfo
],
324 ifcond
: QAPISchemaIfCond
,
325 element_type
: QAPISchemaType
) -> None:
326 element
= self
._use
_type
(element_type
)
327 self
._gen
_tree
('[' + element
+ ']', 'array', {'element-type': element
},
330 def visit_object_type_flat(self
, name
: str, info
: Optional
[QAPISourceInfo
],
331 ifcond
: QAPISchemaIfCond
,
332 features
: List
[QAPISchemaFeature
],
333 members
: List
[QAPISchemaObjectTypeMember
],
334 variants
: Optional
[QAPISchemaVariants
]) -> None:
335 obj
: SchemaInfoObject
= {
336 'members': [self
._gen
_object
_member
(m
) for m
in members
]
339 obj
['tag'] = variants
.tag_member
.name
340 obj
['variants'] = [self
._gen
_variant
(v
) for v
in variants
.variants
]
341 self
._gen
_tree
(name
, 'object', obj
, ifcond
, features
)
343 def visit_alternate_type(self
, name
: str, info
: Optional
[QAPISourceInfo
],
344 ifcond
: QAPISchemaIfCond
,
345 features
: List
[QAPISchemaFeature
],
346 variants
: QAPISchemaVariants
) -> None:
349 {'members': [Annotated({'type': self
._use
_type
(m
.type)},
351 for m
in variants
.variants
]},
355 def visit_command(self
, name
: str, info
: Optional
[QAPISourceInfo
],
356 ifcond
: QAPISchemaIfCond
,
357 features
: List
[QAPISchemaFeature
],
358 arg_type
: Optional
[QAPISchemaObjectType
],
359 ret_type
: Optional
[QAPISchemaType
], gen
: bool,
360 success_response
: bool, boxed
: bool, allow_oob
: bool,
361 allow_preconfig
: bool, coroutine
: bool) -> None:
362 assert self
._schema
is not None
364 arg_type
= arg_type
or self
._schema
.the_empty_object_type
365 ret_type
= ret_type
or self
._schema
.the_empty_object_type
366 obj
: SchemaInfoCommand
= {
367 'arg-type': self
._use
_type
(arg_type
),
368 'ret-type': self
._use
_type
(ret_type
)
371 obj
['allow-oob'] = allow_oob
372 self
._gen
_tree
(name
, 'command', obj
, ifcond
, features
)
374 def visit_event(self
, name
: str, info
: Optional
[QAPISourceInfo
],
375 ifcond
: QAPISchemaIfCond
,
376 features
: List
[QAPISchemaFeature
],
377 arg_type
: Optional
[QAPISchemaObjectType
],
378 boxed
: bool) -> None:
379 assert self
._schema
is not None
381 arg_type
= arg_type
or self
._schema
.the_empty_object_type
382 self
._gen
_tree
(name
, 'event', {'arg-type': self
._use
_type
(arg_type
)},
386 def gen_introspect(schema
: QAPISchema
, output_dir
: str, prefix
: str,
387 opt_unmask
: bool) -> None:
388 vis
= QAPISchemaGenIntrospectVisitor(prefix
, opt_unmask
)
390 vis
.write(output_dir
)