1""" 2QAPI introspection generator 3 4Copyright (C) 2015-2018 Red Hat, Inc. 5 6Authors: 7 Markus Armbruster <armbru@redhat.com> 8 9This work is licensed under the terms of the GNU GPL, version 2. 10See the COPYING file in the top-level directory. 11""" 12 13from typing import ( 14 Any, 15 Dict, 16 List, 17 Optional, 18 Union, 19) 20 21from .common import ( 22 c_name, 23 gen_endif, 24 gen_if, 25 mcgen, 26) 27from .gen import QAPISchemaMonolithicCVisitor 28from .schema import ( 29 QAPISchemaArrayType, 30 QAPISchemaBuiltinType, 31 QAPISchemaType, 32) 33 34 35# This module constructs a tree data structure that is used to 36# generate the introspection information for QEMU. It is shaped 37# like a JSON value. 38# 39# A complexity over JSON is that our values may or may not be annotated. 40# 41# Un-annotated values may be: 42# Scalar: str, bool, None. 43# Non-scalar: List, Dict 44# _value = Union[str, bool, None, Dict[str, JSONValue], List[JSONValue]] 45# 46# With optional annotations, the type of all values is: 47# JSONValue = Union[_Value, Annotated[_Value]] 48# 49# Sadly, mypy does not support recursive types; so the _Stub alias is used to 50# mark the imprecision in the type model where we'd otherwise use JSONValue. 51_Stub = Any 52_Scalar = Union[str, bool, None] 53_NonScalar = Union[Dict[str, _Stub], List[_Stub]] 54_Value = Union[_Scalar, _NonScalar] 55# JSONValue = TODO, in a forthcoming commit. 56 57 58def _make_tree(obj, ifcond, comment=None): 59 extra = { 60 'if': ifcond, 61 'comment': comment 62 } 63 return (obj, extra) 64 65 66def _tree_to_qlit(obj, level=0, dict_value=False): 67 68 def indent(level): 69 return level * 4 * ' ' 70 71 if isinstance(obj, tuple): 72 ifobj, extra = obj 73 ifcond = extra.get('if') 74 comment = extra.get('comment') 75 76 # NB: _tree_to_qlit is called recursively on the values of a 77 # key:value pair; those values can't be decorated with 78 # comments or conditionals. 79 msg = "dict values cannot have attached comments or if-conditionals." 80 assert not dict_value, msg 81 82 ret = '' 83 if comment: 84 ret += indent(level) + '/* %s */\n' % comment 85 if ifcond: 86 ret += gen_if(ifcond) 87 ret += _tree_to_qlit(ifobj, level) 88 if ifcond: 89 ret += '\n' + gen_endif(ifcond) 90 return ret 91 92 ret = '' 93 if not dict_value: 94 ret += indent(level) 95 if obj is None: 96 ret += 'QLIT_QNULL' 97 elif isinstance(obj, str): 98 ret += 'QLIT_QSTR(' + to_c_string(obj) + ')' 99 elif isinstance(obj, list): 100 elts = [_tree_to_qlit(elt, level + 1).strip('\n') 101 for elt in obj] 102 elts.append(indent(level + 1) + "{}") 103 ret += 'QLIT_QLIST(((QLitObject[]) {\n' 104 ret += '\n'.join(elts) + '\n' 105 ret += indent(level) + '}))' 106 elif isinstance(obj, dict): 107 elts = [] 108 for key, value in sorted(obj.items()): 109 elts.append(indent(level + 1) + '{ %s, %s }' % 110 (to_c_string(key), 111 _tree_to_qlit(value, level + 1, True))) 112 elts.append(indent(level + 1) + '{}') 113 ret += 'QLIT_QDICT(((QLitDictEntry[]) {\n' 114 ret += ',\n'.join(elts) + '\n' 115 ret += indent(level) + '}))' 116 elif isinstance(obj, bool): 117 ret += 'QLIT_QBOOL(%s)' % ('true' if obj else 'false') 118 else: 119 assert False # not implemented 120 if level > 0: 121 ret += ',' 122 return ret 123 124 125def to_c_string(string): 126 return '"' + string.replace('\\', r'\\').replace('"', r'\"') + '"' 127 128 129class QAPISchemaGenIntrospectVisitor(QAPISchemaMonolithicCVisitor): 130 131 def __init__(self, prefix, unmask): 132 super().__init__( 133 prefix, 'qapi-introspect', 134 ' * QAPI/QMP schema introspection', __doc__) 135 self._unmask = unmask 136 self._schema = None 137 self._trees = [] 138 self._used_types = [] 139 self._name_map = {} 140 self._genc.add(mcgen(''' 141#include "qemu/osdep.h" 142#include "%(prefix)sqapi-introspect.h" 143 144''', 145 prefix=prefix)) 146 147 def visit_begin(self, schema): 148 self._schema = schema 149 150 def visit_end(self): 151 # visit the types that are actually used 152 for typ in self._used_types: 153 typ.visit(self) 154 # generate C 155 name = c_name(self._prefix, protect=False) + 'qmp_schema_qlit' 156 self._genh.add(mcgen(''' 157#include "qapi/qmp/qlit.h" 158 159extern const QLitObject %(c_name)s; 160''', 161 c_name=c_name(name))) 162 self._genc.add(mcgen(''' 163const QLitObject %(c_name)s = %(c_string)s; 164''', 165 c_name=c_name(name), 166 c_string=_tree_to_qlit(self._trees))) 167 self._schema = None 168 self._trees = [] 169 self._used_types = [] 170 self._name_map = {} 171 172 def visit_needed(self, entity): 173 # Ignore types on first pass; visit_end() will pick up used types 174 return not isinstance(entity, QAPISchemaType) 175 176 def _name(self, name): 177 if self._unmask: 178 return name 179 if name not in self._name_map: 180 self._name_map[name] = '%d' % len(self._name_map) 181 return self._name_map[name] 182 183 def _use_type(self, typ): 184 assert self._schema is not None 185 186 # Map the various integer types to plain int 187 if typ.json_type() == 'int': 188 typ = self._schema.lookup_type('int') 189 elif (isinstance(typ, QAPISchemaArrayType) and 190 typ.element_type.json_type() == 'int'): 191 typ = self._schema.lookup_type('intList') 192 # Add type to work queue if new 193 if typ not in self._used_types: 194 self._used_types.append(typ) 195 # Clients should examine commands and events, not types. Hide 196 # type names as integers to reduce the temptation. Also, it 197 # saves a few characters on the wire. 198 if isinstance(typ, QAPISchemaBuiltinType): 199 return typ.name 200 if isinstance(typ, QAPISchemaArrayType): 201 return '[' + self._use_type(typ.element_type) + ']' 202 return self._name(typ.name) 203 204 @staticmethod 205 def _gen_features(features): 206 return [_make_tree(f.name, f.ifcond) for f in features] 207 208 def _gen_tree(self, name, mtype, obj, ifcond, features): 209 comment: Optional[str] = None 210 if mtype not in ('command', 'event', 'builtin', 'array'): 211 if not self._unmask: 212 # Output a comment to make it easy to map masked names 213 # back to the source when reading the generated output. 214 comment = f'"{self._name(name)}" = {name}' 215 name = self._name(name) 216 obj['name'] = name 217 obj['meta-type'] = mtype 218 if features: 219 obj['features'] = self._gen_features(features) 220 self._trees.append(_make_tree(obj, ifcond, comment)) 221 222 def _gen_member(self, member): 223 obj = {'name': member.name, 'type': self._use_type(member.type)} 224 if member.optional: 225 obj['default'] = None 226 if member.features: 227 obj['features'] = self._gen_features(member.features) 228 return _make_tree(obj, member.ifcond) 229 230 def _gen_variants(self, tag_name, variants): 231 return {'tag': tag_name, 232 'variants': [self._gen_variant(v) for v in variants]} 233 234 def _gen_variant(self, variant): 235 obj = {'case': variant.name, 'type': self._use_type(variant.type)} 236 return _make_tree(obj, variant.ifcond) 237 238 def visit_builtin_type(self, name, info, json_type): 239 self._gen_tree(name, 'builtin', {'json-type': json_type}, [], None) 240 241 def visit_enum_type(self, name, info, ifcond, features, members, prefix): 242 self._gen_tree(name, 'enum', 243 {'values': [_make_tree(m.name, m.ifcond, None) 244 for m in members]}, 245 ifcond, features) 246 247 def visit_array_type(self, name, info, ifcond, element_type): 248 element = self._use_type(element_type) 249 self._gen_tree('[' + element + ']', 'array', {'element-type': element}, 250 ifcond, None) 251 252 def visit_object_type_flat(self, name, info, ifcond, features, 253 members, variants): 254 obj = {'members': [self._gen_member(m) for m in members]} 255 if variants: 256 obj.update(self._gen_variants(variants.tag_member.name, 257 variants.variants)) 258 259 self._gen_tree(name, 'object', obj, ifcond, features) 260 261 def visit_alternate_type(self, name, info, ifcond, features, variants): 262 self._gen_tree(name, 'alternate', 263 {'members': [ 264 _make_tree({'type': self._use_type(m.type)}, 265 m.ifcond, None) 266 for m in variants.variants]}, 267 ifcond, features) 268 269 def visit_command(self, name, info, ifcond, features, 270 arg_type, ret_type, gen, success_response, boxed, 271 allow_oob, allow_preconfig, coroutine): 272 assert self._schema is not None 273 274 arg_type = arg_type or self._schema.the_empty_object_type 275 ret_type = ret_type or self._schema.the_empty_object_type 276 obj = {'arg-type': self._use_type(arg_type), 277 'ret-type': self._use_type(ret_type)} 278 if allow_oob: 279 obj['allow-oob'] = allow_oob 280 self._gen_tree(name, 'command', obj, ifcond, features) 281 282 def visit_event(self, name, info, ifcond, features, arg_type, boxed): 283 assert self._schema is not None 284 arg_type = arg_type or self._schema.the_empty_object_type 285 self._gen_tree(name, 'event', {'arg-type': self._use_type(arg_type)}, 286 ifcond, features) 287 288 289def gen_introspect(schema, output_dir, prefix, opt_unmask): 290 vis = QAPISchemaGenIntrospectVisitor(prefix, opt_unmask) 291 schema.visit(vis) 292 vis.write(output_dir) 293