Browse Source

qapi: Add feature flags to struct types

Sometimes, the behaviour of QEMU changes without a change in the QMP
syntax (usually by allowing values or operations that previously
resulted in an error). QMP clients may still need to know whether
they can rely on the changed behavior.

Let's add feature flags to the QAPI schema language, so that we can make
such changes visible with schema introspection.

An example for a schema definition using feature flags looks like this:

    { 'struct': 'TestType',
      'data': { 'number': 'int' },
      'features': [ 'allow-negative-numbers' ] }

Introspection information then looks like this:

    { "name": "TestType", "meta-type": "object",
      "members": [
          { "name": "number", "type": "int" } ],
      "features": [ "allow-negative-numbers" ] }

This patch implements feature flags only for struct types. We'll
implement them more widely as needed.

Signed-off-by: Kevin Wolf <kwolf@redhat.com>
Message-Id: <20190606153803.5278-2-armbru@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
Kevin Wolf 6 years ago
parent
commit
6a8c0b5102

+ 38 - 0
docs/devel/qapi-code-gen.txt

@@ -719,6 +719,34 @@ any non-empty complex type (struct, union, or alternate), and a
 pointer to that QAPI type is passed as a single argument.
 pointer to that QAPI type is passed as a single argument.
 
 
 
 
+=== Features ===
+
+Sometimes, the behaviour of QEMU changes compatibly, but without a
+change in the QMP syntax (usually by allowing values or operations that
+previously resulted in an error). QMP clients may still need to know
+whether the extension is available.
+
+For this purpose, a list of features can be specified for a struct type.
+This is exposed to the client as a list of string, where each string
+signals that this build of QEMU shows a certain behaviour.
+
+In the schema, features can be specified as simple strings, for example:
+
+{ 'struct': 'TestType',
+  'data': { 'number': 'int' },
+  'features': [ 'allow-negative-numbers' ] }
+
+Another option is to specify features as dictionaries, where the key
+'name' specifies the feature string to be exposed to clients:
+
+{ 'struct': 'TestType',
+  'data': { 'number': 'int' },
+  'features': [ { 'name': 'allow-negative-numbers' } ] }
+
+This expanded form is necessary if you want to make the feature
+conditional (see below in "Configuring the schema").
+
+
 === Downstream extensions ===
 === Downstream extensions ===
 
 
 QAPI schema names that are externally visible, say in the Client JSON
 QAPI schema names that are externally visible, say in the Client JSON
@@ -771,6 +799,16 @@ Example: a conditional 'bar' enum member.
   [ 'foo',
   [ 'foo',
     { 'name' : 'bar', 'if': 'defined(IFCOND)' } ] }
     { 'name' : 'bar', 'if': 'defined(IFCOND)' } ] }
 
 
+Similarly, features can be specified as a dictionary with a 'name' and
+an 'if' key.
+
+Example: a conditional 'allow-negative-numbers' feature
+
+{ 'struct': 'TestType',
+  'data': { 'number': 'int' },
+  'features': [ { 'name': 'allow-negative-numbers',
+                  'if' 'defined(IFCOND)' } ] }
+
 Please note that you are responsible to ensure that the C code will
 Please note that you are responsible to ensure that the C code will
 compile with an arbitrary combination of conditions, since the
 compile with an arbitrary combination of conditions, since the
 generators are unable to check it at this point.
 generators are unable to check it at this point.

+ 5 - 1
qapi/introspect.json

@@ -174,6 +174,9 @@
 #            and may even differ from the order of the values of the
 #            and may even differ from the order of the values of the
 #            enum type of the @tag.
 #            enum type of the @tag.
 #
 #
+# @features: names of features associated with the type, in no particular
+#            order. (since: 4.1)
+#
 # Values of this type are JSON object on the wire.
 # Values of this type are JSON object on the wire.
 #
 #
 # Since: 2.5
 # Since: 2.5
@@ -181,7 +184,8 @@
 { 'struct': 'SchemaInfoObject',
 { 'struct': 'SchemaInfoObject',
   'data': { 'members': [ 'SchemaInfoObjectMember' ],
   'data': { 'members': [ 'SchemaInfoObjectMember' ],
             '*tag': 'str',
             '*tag': 'str',
-            '*variants': [ 'SchemaInfoObjectVariant' ] } }
+            '*variants': [ 'SchemaInfoObjectVariant' ],
+            '*features': [ 'str' ] } }
 
 
 ##
 ##
 # @SchemaInfoObjectMember:
 # @SchemaInfoObjectMember:

+ 56 - 10
scripts/qapi/common.py

@@ -886,12 +886,26 @@ def check_enum(expr, info):
 def check_struct(expr, info):
 def check_struct(expr, info):
     name = expr['struct']
     name = expr['struct']
     members = expr['data']
     members = expr['data']
+    features = expr.get('features')
 
 
     check_type(info, "'data' for struct '%s'" % name, members,
     check_type(info, "'data' for struct '%s'" % name, members,
                allow_dict=True, allow_optional=True)
                allow_dict=True, allow_optional=True)
     check_type(info, "'base' for struct '%s'" % name, expr.get('base'),
     check_type(info, "'base' for struct '%s'" % name, expr.get('base'),
                allow_metas=['struct'])
                allow_metas=['struct'])
 
 
+    if features:
+        if not isinstance(features, list):
+            raise QAPISemError(info,
+                               "Struct '%s' requires an array for 'features'" %
+                               name)
+        for f in features:
+            assert isinstance(f, dict)
+            check_known_keys(info, "feature of struct %s" % name, f,
+                             ['name'], ['if'])
+
+            check_if(f, info)
+            check_name(info, "Feature of struct %s" % name, f['name'])
+
 
 
 def check_known_keys(info, source, keys, required, optional):
 def check_known_keys(info, source, keys, required, optional):
 
 
@@ -948,6 +962,12 @@ def normalize_members(members):
             members[key] = {'type': arg}
             members[key] = {'type': arg}
 
 
 
 
+def normalize_features(features):
+    if isinstance(features, list):
+        features[:] = [f if isinstance(f, dict) else {'name': f}
+                       for f in features]
+
+
 def check_exprs(exprs):
 def check_exprs(exprs):
     global all_names
     global all_names
 
 
@@ -986,8 +1006,10 @@ def check_exprs(exprs):
             normalize_members(expr['data'])
             normalize_members(expr['data'])
         elif 'struct' in expr:
         elif 'struct' in expr:
             meta = 'struct'
             meta = 'struct'
-            check_keys(expr_elem, 'struct', ['data'], ['base', 'if'])
+            check_keys(expr_elem, 'struct', ['data'],
+                       ['base', 'if', 'features'])
             normalize_members(expr['data'])
             normalize_members(expr['data'])
+            normalize_features(expr.get('features'))
             struct_types[expr[meta]] = expr
             struct_types[expr[meta]] = expr
         elif 'command' in expr:
         elif 'command' in expr:
             meta = 'command'
             meta = 'command'
@@ -1126,10 +1148,12 @@ def visit_enum_type(self, name, info, ifcond, members, prefix):
     def visit_array_type(self, name, info, ifcond, element_type):
     def visit_array_type(self, name, info, ifcond, element_type):
         pass
         pass
 
 
-    def visit_object_type(self, name, info, ifcond, base, members, variants):
+    def visit_object_type(self, name, info, ifcond, base, members, variants,
+                          features):
         pass
         pass
 
 
-    def visit_object_type_flat(self, name, info, ifcond, members, variants):
+    def visit_object_type_flat(self, name, info, ifcond, members, variants,
+                               features):
         pass
         pass
 
 
     def visit_alternate_type(self, name, info, ifcond, variants):
     def visit_alternate_type(self, name, info, ifcond, variants):
@@ -1290,7 +1314,7 @@ def visit(self, visitor):
 
 
 class QAPISchemaObjectType(QAPISchemaType):
 class QAPISchemaObjectType(QAPISchemaType):
     def __init__(self, name, info, doc, ifcond,
     def __init__(self, name, info, doc, ifcond,
-                 base, local_members, variants):
+                 base, local_members, variants, features):
         # struct has local_members, optional base, and no variants
         # struct has local_members, optional base, and no variants
         # flat union has base, variants, and no local_members
         # flat union has base, variants, and no local_members
         # simple union has local_members, variants, and no base
         # simple union has local_members, variants, and no base
@@ -1302,11 +1326,15 @@ def __init__(self, name, info, doc, ifcond,
         if variants is not None:
         if variants is not None:
             assert isinstance(variants, QAPISchemaObjectTypeVariants)
             assert isinstance(variants, QAPISchemaObjectTypeVariants)
             variants.set_owner(name)
             variants.set_owner(name)
+        for f in features:
+            assert isinstance(f, QAPISchemaFeature)
+            f.set_owner(name)
         self._base_name = base
         self._base_name = base
         self.base = None
         self.base = None
         self.local_members = local_members
         self.local_members = local_members
         self.variants = variants
         self.variants = variants
         self.members = None
         self.members = None
+        self.features = features
 
 
     def check(self, schema):
     def check(self, schema):
         QAPISchemaType.check(self, schema)
         QAPISchemaType.check(self, schema)
@@ -1332,6 +1360,12 @@ def check(self, schema):
             self.variants.check(schema, seen)
             self.variants.check(schema, seen)
             assert self.variants.tag_member in self.members
             assert self.variants.tag_member in self.members
             self.variants.check_clash(self.info, seen)
             self.variants.check_clash(self.info, seen)
+
+        # Features are in a name space separate from members
+        seen = {}
+        for f in self.features:
+            f.check_clash(self.info, seen)
+
         if self.doc:
         if self.doc:
             self.doc.check()
             self.doc.check()
 
 
@@ -1368,12 +1402,15 @@ def json_type(self):
 
 
     def visit(self, visitor):
     def visit(self, visitor):
         visitor.visit_object_type(self.name, self.info, self.ifcond,
         visitor.visit_object_type(self.name, self.info, self.ifcond,
-                                  self.base, self.local_members, self.variants)
+                                  self.base, self.local_members, self.variants,
+                                  self.features)
         visitor.visit_object_type_flat(self.name, self.info, self.ifcond,
         visitor.visit_object_type_flat(self.name, self.info, self.ifcond,
-                                       self.members, self.variants)
+                                       self.members, self.variants,
+                                       self.features)
 
 
 
 
 class QAPISchemaMember(object):
 class QAPISchemaMember(object):
+    """ Represents object members, enum members and features """
     role = 'member'
     role = 'member'
 
 
     def __init__(self, name, ifcond=None):
     def __init__(self, name, ifcond=None):
@@ -1419,6 +1456,10 @@ def describe(self):
         return "'%s' %s" % (self.name, self._pretty_owner())
         return "'%s' %s" % (self.name, self._pretty_owner())
 
 
 
 
+class QAPISchemaFeature(QAPISchemaMember):
+    role = 'feature'
+
+
 class QAPISchemaObjectTypeMember(QAPISchemaMember):
 class QAPISchemaObjectTypeMember(QAPISchemaMember):
     def __init__(self, name, typ, optional, ifcond=None):
     def __init__(self, name, typ, optional, ifcond=None):
         QAPISchemaMember.__init__(self, name, ifcond)
         QAPISchemaMember.__init__(self, name, ifcond)
@@ -1675,7 +1716,7 @@ def _def_predefineds(self):
                   ('null',   'null',    'QNull' + pointer_suffix)]:
                   ('null',   'null',    'QNull' + pointer_suffix)]:
             self._def_builtin_type(*t)
             self._def_builtin_type(*t)
         self.the_empty_object_type = QAPISchemaObjectType(
         self.the_empty_object_type = QAPISchemaObjectType(
-            'q_empty', None, None, None, None, [], None)
+            'q_empty', None, None, None, None, [], None, [])
         self._def_entity(self.the_empty_object_type)
         self._def_entity(self.the_empty_object_type)
 
 
         qtypes = ['none', 'qnull', 'qnum', 'qstring', 'qdict', 'qlist',
         qtypes = ['none', 'qnull', 'qnum', 'qstring', 'qdict', 'qlist',
@@ -1685,6 +1726,9 @@ def _def_predefineds(self):
         self._def_entity(QAPISchemaEnumType('QType', None, None, None,
         self._def_entity(QAPISchemaEnumType('QType', None, None, None,
                                             qtype_values, 'QTYPE'))
                                             qtype_values, 'QTYPE'))
 
 
+    def _make_features(self, features):
+        return [QAPISchemaFeature(f['name'], f.get('if')) for f in features]
+
     def _make_enum_members(self, values):
     def _make_enum_members(self, values):
         return [QAPISchemaMember(v['name'], v.get('if')) for v in values]
         return [QAPISchemaMember(v['name'], v.get('if')) for v in values]
 
 
@@ -1721,7 +1765,7 @@ def _make_implicit_object_type(self, name, info, doc, ifcond,
             assert ifcond == typ._ifcond # pylint: disable=protected-access
             assert ifcond == typ._ifcond # pylint: disable=protected-access
         else:
         else:
             self._def_entity(QAPISchemaObjectType(name, info, doc, ifcond,
             self._def_entity(QAPISchemaObjectType(name, info, doc, ifcond,
-                                                  None, members, None))
+                                                  None, members, None, []))
         return name
         return name
 
 
     def _def_enum_type(self, expr, info, doc):
     def _def_enum_type(self, expr, info, doc):
@@ -1752,9 +1796,11 @@ def _def_struct_type(self, expr, info, doc):
         base = expr.get('base')
         base = expr.get('base')
         data = expr['data']
         data = expr['data']
         ifcond = expr.get('if')
         ifcond = expr.get('if')
+        features = expr.get('features', [])
         self._def_entity(QAPISchemaObjectType(name, info, doc, ifcond, base,
         self._def_entity(QAPISchemaObjectType(name, info, doc, ifcond, base,
                                               self._make_members(data, info),
                                               self._make_members(data, info),
-                                              None))
+                                              None,
+                                              self._make_features(features)))
 
 
     def _make_variant(self, case, typ, ifcond):
     def _make_variant(self, case, typ, ifcond):
         return QAPISchemaObjectTypeVariant(case, typ, ifcond)
         return QAPISchemaObjectTypeVariant(case, typ, ifcond)
@@ -1795,7 +1841,7 @@ def _def_union_type(self, expr, info, doc):
             QAPISchemaObjectType(name, info, doc, ifcond, base, members,
             QAPISchemaObjectType(name, info, doc, ifcond, base, members,
                                  QAPISchemaObjectTypeVariants(tag_name,
                                  QAPISchemaObjectTypeVariants(tag_name,
                                                               tag_member,
                                                               tag_member,
-                                                              variants)))
+                                                              variants), []))
 
 
     def _def_alternate_type(self, expr, info, doc):
     def _def_alternate_type(self, expr, info, doc):
         name = expr['alternate']
         name = expr['alternate']

+ 2 - 1
scripts/qapi/doc.py

@@ -220,7 +220,8 @@ def visit_enum_type(self, name, info, ifcond, members, prefix):
                                body=texi_entity(doc, 'Values', ifcond,
                                body=texi_entity(doc, 'Values', ifcond,
                                                 member_func=texi_enum_value)))
                                                 member_func=texi_enum_value)))
 
 
-    def visit_object_type(self, name, info, ifcond, base, members, variants):
+    def visit_object_type(self, name, info, ifcond, base, members, variants,
+                          features):
         doc = self.cur_doc
         doc = self.cur_doc
         if base and base.is_implicit():
         if base and base.is_implicit():
             base = None
             base = None

+ 5 - 1
scripts/qapi/introspect.py

@@ -188,11 +188,15 @@ def visit_array_type(self, name, info, ifcond, element_type):
         self._gen_qlit('[' + element + ']', 'array', {'element-type': element},
         self._gen_qlit('[' + element + ']', 'array', {'element-type': element},
                        ifcond)
                        ifcond)
 
 
-    def visit_object_type_flat(self, name, info, ifcond, members, variants):
+    def visit_object_type_flat(self, name, info, ifcond, members, variants,
+                               features):
         obj = {'members': [self._gen_member(m) for m in members]}
         obj = {'members': [self._gen_member(m) for m in members]}
         if variants:
         if variants:
             obj.update(self._gen_variants(variants.tag_member.name,
             obj.update(self._gen_variants(variants.tag_member.name,
                                           variants.variants))
                                           variants.variants))
+        if features:
+            obj['features'] = [(f.name, {'if': f.ifcond}) for f in features]
+
         self._gen_qlit(name, 'object', obj, ifcond)
         self._gen_qlit(name, 'object', obj, ifcond)
 
 
     def visit_alternate_type(self, name, info, ifcond, variants):
     def visit_alternate_type(self, name, info, ifcond, variants):

+ 2 - 1
scripts/qapi/types.py

@@ -227,7 +227,8 @@ def visit_array_type(self, name, info, ifcond, element_type):
             self._genh.add(gen_array(name, element_type))
             self._genh.add(gen_array(name, element_type))
             self._gen_type_cleanup(name)
             self._gen_type_cleanup(name)
 
 
-    def visit_object_type(self, name, info, ifcond, base, members, variants):
+    def visit_object_type(self, name, info, ifcond, base, members, variants,
+                          features):
         # Nothing to do for the special empty builtin
         # Nothing to do for the special empty builtin
         if name == 'q_empty':
         if name == 'q_empty':
             return
             return

+ 2 - 1
scripts/qapi/visit.py

@@ -324,7 +324,8 @@ def visit_array_type(self, name, info, ifcond, element_type):
             self._genh.add(gen_visit_decl(name))
             self._genh.add(gen_visit_decl(name))
             self._genc.add(gen_visit_list(name, element_type))
             self._genc.add(gen_visit_list(name, element_type))
 
 
-    def visit_object_type(self, name, info, ifcond, base, members, variants):
+    def visit_object_type(self, name, info, ifcond, base, members, variants,
+                          features):
         # Nothing to do for the special empty builtin
         # Nothing to do for the special empty builtin
         if name == 'q_empty':
         if name == 'q_empty':
             return
             return

+ 1 - 1
tests/qapi-schema/double-type.err

@@ -1,2 +1,2 @@
 tests/qapi-schema/double-type.json:2: Unknown key 'command' in struct 'bar'
 tests/qapi-schema/double-type.json:2: Unknown key 'command' in struct 'bar'
-Valid keys are 'base', 'data', 'if', 'struct'.
+Valid keys are 'base', 'data', 'features', 'if', 'struct'.

+ 2 - 1
tests/qapi-schema/test-qapi.py

@@ -38,7 +38,8 @@ def visit_array_type(self, name, info, ifcond, element_type):
         print('array %s %s' % (name, element_type.name))
         print('array %s %s' % (name, element_type.name))
         self._print_if(ifcond)
         self._print_if(ifcond)
 
 
-    def visit_object_type(self, name, info, ifcond, base, members, variants):
+    def visit_object_type(self, name, info, ifcond, base, members, variants,
+                          features):
         print('object %s' % name)
         print('object %s' % name)
         if base:
         if base:
             print('    base %s' % base.name)
             print('    base %s' % base.name)

+ 1 - 1
tests/qapi-schema/unknown-expr-key.err

@@ -1,2 +1,2 @@
 tests/qapi-schema/unknown-expr-key.json:2: Unknown keys 'bogus', 'phony' in struct 'bar'
 tests/qapi-schema/unknown-expr-key.json:2: Unknown keys 'bogus', 'phony' in struct 'bar'
-Valid keys are 'base', 'data', 'if', 'struct'.
+Valid keys are 'base', 'data', 'features', 'if', 'struct'.