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.
31 from .gen
import QAPISchemaMonolithicCVisitor
35 QAPISchemaBuiltinType
,
41 QAPISchemaObjectTypeMember
,
46 from .source
import QAPISourceInfo
49 # This module constructs a tree data structure that is used to
50 # generate the introspection information for QEMU. It is shaped
53 # A complexity over JSON is that our values may or may not be annotated.
55 # Un-annotated values may be:
56 # Scalar: str, bool, None.
57 # Non-scalar: List, Dict
58 # _value = Union[str, bool, None, Dict[str, JSONValue], List[JSONValue]]
60 # With optional annotations, the type of all values is:
61 # JSONValue = Union[_Value, Annotated[_Value]]
63 # Sadly, mypy does not support recursive types; so the _Stub alias is used to
64 # mark the imprecision in the type model where we'd otherwise use JSONValue.
66 _Scalar
= Union
[str, bool, None]
67 _NonScalar
= Union
[Dict
[str, _Stub
], List
[_Stub
]]
68 _Value
= Union
[_Scalar
, _NonScalar
]
69 JSONValue
= Union
[_Value
, 'Annotated[_Value]']
71 # These types are based on structures defined in QEMU's schema, so we
72 # lack precise types for them here. Python 3.6 does not offer
73 # TypedDict constructs, so they are broadly typed here as simple
75 SchemaInfo
= Dict
[str, object]
76 SchemaInfoObject
= Dict
[str, object]
77 SchemaInfoObjectVariant
= Dict
[str, object]
78 SchemaInfoObjectMember
= Dict
[str, object]
79 SchemaInfoCommand
= Dict
[str, object]
82 _ValueT
= TypeVar('_ValueT', bound
=_Value
)
85 class Annotated(Generic
[_ValueT
]):
87 Annotated generally contains a SchemaInfo-like type (as a dict),
88 But it also used to wrap comments/ifconds around scalar leaf values,
89 for the benefit of features and enums.
91 # TODO: Remove after Python 3.7 adds @dataclass:
92 # pylint: disable=too-few-public-methods
93 def __init__(self
, value
: _ValueT
, ifcond
: QAPISchemaIfCond
,
94 comment
: Optional
[str] = None):
96 self
.comment
: Optional
[str] = comment
100 def _tree_to_qlit(obj
: JSONValue
,
102 dict_value
: bool = False) -> str:
104 Convert the type tree into a QLIT C string, recursively.
106 :param obj: The value to convert.
107 This value may not be Annotated when dict_value is True.
108 :param level: The indentation level for this particular value.
109 :param dict_value: True when the value being processed belongs to a
110 dict key; which suppresses the output indent.
113 def indent(level
: int) -> str:
114 return level
* 4 * ' '
116 if isinstance(obj
, Annotated
):
117 # NB: _tree_to_qlit is called recursively on the values of a
118 # key:value pair; those values can't be decorated with
119 # comments or conditionals.
120 msg
= "dict values cannot have attached comments or if-conditionals."
121 assert not dict_value
, msg
125 ret
+= indent(level
) + f
"/* {obj.comment} */\n"
126 if obj
.ifcond
.is_present():
127 ret
+= gen_if(obj
.ifcond
.cgen())
128 ret
+= _tree_to_qlit(obj
.value
, level
)
129 if obj
.ifcond
.is_present():
130 ret
+= '\n' + gen_endif(obj
.ifcond
.cgen())
140 elif isinstance(obj
, str):
141 ret
+= f
"QLIT_QSTR({to_c_string(obj)})"
142 elif isinstance(obj
, bool):
143 ret
+= f
"QLIT_QBOOL({str(obj).lower()})"
146 elif isinstance(obj
, list):
147 ret
+= 'QLIT_QLIST(((QLitObject[]) {\n'
149 ret
+= _tree_to_qlit(value
, level
+ 1).strip('\n') + '\n'
150 ret
+= indent(level
+ 1) + '{}\n'
151 ret
+= indent(level
) + '}))'
152 elif isinstance(obj
, dict):
153 ret
+= 'QLIT_QDICT(((QLitDictEntry[]) {\n'
154 for key
, value
in sorted(obj
.items()):
155 ret
+= indent(level
+ 1) + "{{ {:s}, {:s} }},\n".format(
157 _tree_to_qlit(value
, level
+ 1, dict_value
=True)
159 ret
+= indent(level
+ 1) + '{}\n'
160 ret
+= indent(level
) + '}))'
162 raise NotImplementedError(
163 f
"type '{type(obj).__name__}' not implemented"
171 def to_c_string(string
: str) -> str:
172 return '"' + string
.replace('\\', r
'\\').replace('"', r
'\"') + '"'
175 class QAPISchemaGenIntrospectVisitor(QAPISchemaMonolithicCVisitor
):
177 def __init__(self
, prefix
: str, unmask
: bool):
179 prefix
, 'qapi-introspect',
180 ' * QAPI/QMP schema introspection', __doc__
)
181 self
._unmask
= unmask
182 self
._schema
: Optional
[QAPISchema
] = None
183 self
._trees
: List
[Annotated
[SchemaInfo
]] = []
184 self
._used
_types
: List
[QAPISchemaType
] = []
185 self
._name
_map
: Dict
[str, str] = {}
186 self
._genc
.add(mcgen('''
187 #include "qemu/osdep.h"
188 #include "%(prefix)sqapi-introspect.h"
193 def visit_begin(self
, schema
: QAPISchema
) -> None:
194 self
._schema
= schema
196 def visit_end(self
) -> None:
197 # visit the types that are actually used
198 for typ
in self
._used
_types
:
201 name
= c_name(self
._prefix
, protect
=False) + 'qmp_schema_qlit'
202 self
._genh
.add(mcgen('''
203 #include "qapi/qmp/qlit.h"
205 extern const QLitObject %(c_name)s;
207 c_name
=c_name(name
)))
208 self
._genc
.add(mcgen('''
209 const QLitObject %(c_name)s = %(c_string)s;
212 c_string
=_tree_to_qlit(self
._trees
)))
215 self
._used
_types
= []
218 def visit_needed(self
, entity
: QAPISchemaEntity
) -> bool:
219 # Ignore types on first pass; visit_end() will pick up used types
220 return not isinstance(entity
, QAPISchemaType
)
222 def _name(self
, name
: str) -> str:
225 if name
not in self
._name
_map
:
226 self
._name
_map
[name
] = '%d' % len(self
._name
_map
)
227 return self
._name
_map
[name
]
229 def _use_type(self
, typ
: QAPISchemaType
) -> str:
230 assert self
._schema
is not None
232 # Map the various integer types to plain int
233 if typ
.json_type() == 'int':
234 typ
= self
._schema
.lookup_type('int')
235 elif (isinstance(typ
, QAPISchemaArrayType
) and
236 typ
.element_type
.json_type() == 'int'):
237 typ
= self
._schema
.lookup_type('intList')
238 # Add type to work queue if new
239 if typ
not in self
._used
_types
:
240 self
._used
_types
.append(typ
)
241 # Clients should examine commands and events, not types. Hide
242 # type names as integers to reduce the temptation. Also, it
243 # saves a few characters on the wire.
244 if isinstance(typ
, QAPISchemaBuiltinType
):
246 if isinstance(typ
, QAPISchemaArrayType
):
247 return '[' + self
._use
_type
(typ
.element_type
) + ']'
248 return self
._name
(typ
.name
)
251 def _gen_features(features
: Sequence
[QAPISchemaFeature
]
252 ) -> List
[Annotated
[str]]:
253 return [Annotated(f
.name
, f
.ifcond
) for f
in features
]
255 def _gen_tree(self
, name
: str, mtype
: str, obj
: Dict
[str, object],
256 ifcond
: QAPISchemaIfCond
= QAPISchemaIfCond(),
257 features
: Sequence
[QAPISchemaFeature
] = ()) -> None:
259 Build and append a SchemaInfo object to self._trees.
261 :param name: The SchemaInfo's name.
262 :param mtype: The SchemaInfo's meta-type.
263 :param obj: Additional SchemaInfo members, as appropriate for
265 :param ifcond: Conditionals to apply to the SchemaInfo.
266 :param features: The SchemaInfo's features.
267 Will be omitted from the output if empty.
269 comment
: Optional
[str] = None
270 if mtype
not in ('command', 'event', 'builtin', 'array'):
272 # Output a comment to make it easy to map masked names
273 # back to the source when reading the generated output.
274 comment
= f
'"{self._name(name)}" = {name}'
275 name
= self
._name
(name
)
277 obj
['meta-type'] = mtype
279 obj
['features'] = self
._gen
_features
(features
)
280 self
._trees
.append(Annotated(obj
, ifcond
, comment
))
282 def _gen_member(self
, member
: QAPISchemaObjectTypeMember
283 ) -> Annotated
[SchemaInfoObjectMember
]:
284 obj
: SchemaInfoObjectMember
= {
286 'type': self
._use
_type
(member
.type)
289 obj
['default'] = None
291 obj
['features'] = self
._gen
_features
(member
.features
)
292 return Annotated(obj
, member
.ifcond
)
294 def _gen_variant(self
, variant
: QAPISchemaVariant
295 ) -> Annotated
[SchemaInfoObjectVariant
]:
296 obj
: SchemaInfoObjectVariant
= {
297 'case': variant
.name
,
298 'type': self
._use
_type
(variant
.type)
300 return Annotated(obj
, variant
.ifcond
)
302 def visit_builtin_type(self
, name
: str, info
: Optional
[QAPISourceInfo
],
303 json_type
: str) -> None:
304 self
._gen
_tree
(name
, 'builtin', {'json-type': json_type
})
306 def visit_enum_type(self
, name
: str, info
: Optional
[QAPISourceInfo
],
307 ifcond
: QAPISchemaIfCond
,
308 features
: List
[QAPISchemaFeature
],
309 members
: List
[QAPISchemaEnumMember
],
310 prefix
: Optional
[str]) -> None:
313 {'values': [Annotated(m
.name
, m
.ifcond
) for m
in members
]},
317 def visit_array_type(self
, name
: str, info
: Optional
[QAPISourceInfo
],
318 ifcond
: QAPISchemaIfCond
,
319 element_type
: QAPISchemaType
) -> None:
320 element
= self
._use
_type
(element_type
)
321 self
._gen
_tree
('[' + element
+ ']', 'array', {'element-type': element
},
324 def visit_object_type_flat(self
, name
: str, info
: Optional
[QAPISourceInfo
],
325 ifcond
: QAPISchemaIfCond
,
326 features
: List
[QAPISchemaFeature
],
327 members
: List
[QAPISchemaObjectTypeMember
],
328 variants
: Optional
[QAPISchemaVariants
]) -> None:
329 obj
: SchemaInfoObject
= {
330 'members': [self
._gen
_member
(m
) for m
in members
]
333 obj
['tag'] = variants
.tag_member
.name
334 obj
['variants'] = [self
._gen
_variant
(v
) for v
in variants
.variants
]
335 self
._gen
_tree
(name
, 'object', obj
, ifcond
, features
)
337 def visit_alternate_type(self
, name
: str, info
: Optional
[QAPISourceInfo
],
338 ifcond
: QAPISchemaIfCond
,
339 features
: List
[QAPISchemaFeature
],
340 variants
: QAPISchemaVariants
) -> None:
343 {'members': [Annotated({'type': self
._use
_type
(m
.type)},
345 for m
in variants
.variants
]},
349 def visit_command(self
, name
: str, info
: Optional
[QAPISourceInfo
],
350 ifcond
: QAPISchemaIfCond
,
351 features
: List
[QAPISchemaFeature
],
352 arg_type
: Optional
[QAPISchemaObjectType
],
353 ret_type
: Optional
[QAPISchemaType
], gen
: bool,
354 success_response
: bool, boxed
: bool, allow_oob
: bool,
355 allow_preconfig
: bool, coroutine
: bool) -> None:
356 assert self
._schema
is not None
358 arg_type
= arg_type
or self
._schema
.the_empty_object_type
359 ret_type
= ret_type
or self
._schema
.the_empty_object_type
360 obj
: SchemaInfoCommand
= {
361 'arg-type': self
._use
_type
(arg_type
),
362 'ret-type': self
._use
_type
(ret_type
)
365 obj
['allow-oob'] = allow_oob
366 self
._gen
_tree
(name
, 'command', obj
, ifcond
, features
)
368 def visit_event(self
, name
: str, info
: Optional
[QAPISourceInfo
],
369 ifcond
: QAPISchemaIfCond
,
370 features
: List
[QAPISchemaFeature
],
371 arg_type
: Optional
[QAPISchemaObjectType
],
372 boxed
: bool) -> None:
373 assert self
._schema
is not None
375 arg_type
= arg_type
or self
._schema
.the_empty_object_type
376 self
._gen
_tree
(name
, 'event', {'arg-type': self
._use
_type
(arg_type
)},
380 def gen_introspect(schema
: QAPISchema
, output_dir
: str, prefix
: str,
381 opt_unmask
: bool) -> None:
382 vis
= QAPISchemaGenIntrospectVisitor(prefix
, opt_unmask
)
384 vis
.write(output_dir
)