diff mbox series

[RFC,01/15] scripts/qapi: support type-based unions

Message ID 20240313150907.623462-2-vsementsov@yandex-team.ru
State New
Headers show
Series block job API | expand

Commit Message

Vladimir Sementsov-Ogievskiy March 13, 2024, 3:08 p.m. UTC
Look at block-job-change command: we have to specify both 'id' to chose
the job to operate on and 'type' for QAPI union be parsed. But for user
this looks redundant: when we specify 'id', QEMU should be able to get
corresponding job's type.

This commit brings such a possibility: just specify some Enum type as
'discriminator', and define a function somewhere with prototype

bool YourUnionType_mapper(YourUnionType *, EnumType *out, Error **errp)

Further commits will use this functionality to upgrade block-job-change
interface and introduce some new interfaces.

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
---
 scripts/qapi/introspect.py |  5 +++-
 scripts/qapi/schema.py     | 50 +++++++++++++++++++++++---------------
 scripts/qapi/types.py      |  3 ++-
 scripts/qapi/visit.py      | 43 ++++++++++++++++++++++++++------
 4 files changed, 73 insertions(+), 28 deletions(-)

Comments

Markus Armbruster March 28, 2024, 9:15 a.m. UTC | #1
Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru> writes:

> Look at block-job-change command: we have to specify both 'id' to chose
> the job to operate on and 'type' for QAPI union be parsed. But for user
> this looks redundant: when we specify 'id', QEMU should be able to get
> corresponding job's type.
>
> This commit brings such a possibility: just specify some Enum type as
> 'discriminator', and define a function somewhere with prototype
>
> bool YourUnionType_mapper(YourUnionType *, EnumType *out, Error **errp)
>
> Further commits will use this functionality to upgrade block-job-change
> interface and introduce some new interfaces.
>
> Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
> ---
>  scripts/qapi/introspect.py |  5 +++-
>  scripts/qapi/schema.py     | 50 +++++++++++++++++++++++---------------
>  scripts/qapi/types.py      |  3 ++-
>  scripts/qapi/visit.py      | 43 ++++++++++++++++++++++++++------
>  4 files changed, 73 insertions(+), 28 deletions(-)

I believe you need to update docs/devel/qapi-code-gen.rst.

Current text:

    Union types
    -----------

    Syntax::

        UNION = { 'union': STRING,
                  'base': ( MEMBERS | STRING ),
                  'discriminator': STRING,
                  'data': BRANCHES,
                  '*if': COND,
                  '*features': FEATURES }
        BRANCHES = { BRANCH, ... }
        BRANCH = STRING : TYPE-REF
               | STRING : { 'type': TYPE-REF, '*if': COND }

    Member 'union' names the union type.

    The 'base' member defines the common members.  If it is a MEMBERS_
    object, it defines common members just like a struct type's 'data'
    member defines struct type members.  If it is a STRING, it names a
    struct type whose members are the common members.

    Member 'discriminator' must name a non-optional enum-typed member of
    the base struct.  That member's value selects a branch by its name.
    If no such branch exists, an empty branch is assumed.

If I understand your commit message correctly, this paragraph is no
longer true.

    Each BRANCH of the 'data' object defines a branch of the union.  A
    union must have at least one branch.

    The BRANCH's STRING name is the branch name.  It must be a value of
    the discriminator enum type.

    The BRANCH's value defines the branch's properties, in particular its
    type.  The type must a struct type.  The form TYPE-REF_ is shorthand
    for :code:`{ 'type': TYPE-REF }`.

    In the Client JSON Protocol, a union is represented by an object with
    the common members (from the base type) and the selected branch's
    members.  The two sets of member names must be disjoint.

    Example::

     { 'enum': 'BlockdevDriver', 'data': [ 'file', 'qcow2' ] }
     { 'union': 'BlockdevOptions',
       'base': { 'driver': 'BlockdevDriver', '*read-only': 'bool' },
       'discriminator': 'driver',
       'data': { 'file': 'BlockdevOptionsFile',
                 'qcow2': 'BlockdevOptionsQcow2' } }

    Resulting in these JSON objects::

     { "driver": "file", "read-only": true,
       "filename": "/some/place/my-image" }
     { "driver": "qcow2", "read-only": false,
       "backing": "/some/place/my-image", "lazy-refcounts": true }

    The order of branches need not match the order of the enum values.
    The branches need not cover all possible enum values.  In the
    resulting generated C data types, a union is represented as a struct
    with the base members in QAPI schema order, and then a union of
    structures for each branch of the struct.

    The optional 'if' member specifies a conditional.  See `Configuring
    the schema`_ below for more on this.

    The optional 'features' member specifies features.  See Features_
    below for more on this.
Markus Armbruster March 28, 2024, 9:40 a.m. UTC | #2
Subject: all unions are type-based.  Perhaps "support implicit union
tags on the wire"?

Do you need this schema language feature for folding block jobs into the
jobs abstraction, or is it just for making the wire protocol nicer in
places?
Vladimir Sementsov-Ogievskiy March 28, 2024, 10:18 a.m. UTC | #3
On 28.03.24 12:40, Markus Armbruster wrote:
> Subject: all unions are type-based.  Perhaps "support implicit union
> tags on the wire"?

Yes, sounds good.

> 
> Do you need this schema language feature for folding block jobs into the
> jobs abstraction, or is it just for making the wire protocol nicer in
> places?
> 

It's not necessary, we can proceed with job-* API, specifying both type and id.

But I think, as we are not in a hurry, better to make new job-* API more effective from the beginning.
Vladimir Sementsov-Ogievskiy March 28, 2024, 10:44 a.m. UTC | #4
On 28.03.24 12:15, Markus Armbruster wrote:
> Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru> writes:
> 
>> Look at block-job-change command: we have to specify both 'id' to chose
>> the job to operate on and 'type' for QAPI union be parsed. But for user
>> this looks redundant: when we specify 'id', QEMU should be able to get
>> corresponding job's type.
>>
>> This commit brings such a possibility: just specify some Enum type as
>> 'discriminator', and define a function somewhere with prototype
>>
>> bool YourUnionType_mapper(YourUnionType *, EnumType *out, Error **errp)
>>
>> Further commits will use this functionality to upgrade block-job-change
>> interface and introduce some new interfaces.
>>
>> Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
>> ---
>>   scripts/qapi/introspect.py |  5 +++-
>>   scripts/qapi/schema.py     | 50 +++++++++++++++++++++++---------------
>>   scripts/qapi/types.py      |  3 ++-
>>   scripts/qapi/visit.py      | 43 ++++++++++++++++++++++++++------
>>   4 files changed, 73 insertions(+), 28 deletions(-)
> 
> I believe you need to update docs/devel/qapi-code-gen.rst.
> 
> Current text:
> 
>      Union types
>      -----------
> 
>      Syntax::
> 
>          UNION = { 'union': STRING,
>                    'base': ( MEMBERS | STRING ),
>                    'discriminator': STRING,
>                    'data': BRANCHES,
>                    '*if': COND,
>                    '*features': FEATURES }
>          BRANCHES = { BRANCH, ... }
>          BRANCH = STRING : TYPE-REF
>                 | STRING : { 'type': TYPE-REF, '*if': COND }
> 
>      Member 'union' names the union type.
> 
>      The 'base' member defines the common members.  If it is a MEMBERS_
>      object, it defines common members just like a struct type's 'data'
>      member defines struct type members.  If it is a STRING, it names a
>      struct type whose members are the common members.
> 
>      Member 'discriminator' must name a non-optional enum-typed member of
>      the base struct.  That member's value selects a branch by its name.
>      If no such branch exists, an empty branch is assumed.
> 
> If I understand your commit message correctly, this paragraph is no
> longer true.


Right. Like this:

Member 'discriminator' must name either a non-optional enum-typed member, or an enum type name. (and more description follow, about user defined function and so on).


Do you think that mixing member name and type name here is OK? Or should I instead add another field 'discriminator-type', so that exactly one of 'discriminator' and 'discriminator-type' should be in union definition?


> 
>      Each BRANCH of the 'data' object defines a branch of the union.  A
>      union must have at least one branch.
> 
>      The BRANCH's STRING name is the branch name.  It must be a value of
>      the discriminator enum type.
> 
>      The BRANCH's value defines the branch's properties, in particular its
>      type.  The type must a struct type.  The form TYPE-REF_ is shorthand
>      for :code:`{ 'type': TYPE-REF }`.
> 
>      In the Client JSON Protocol, a union is represented by an object with
>      the common members (from the base type) and the selected branch's
>      members.  The two sets of member names must be disjoint.
> 
>      Example::
> 
>       { 'enum': 'BlockdevDriver', 'data': [ 'file', 'qcow2' ] }
>       { 'union': 'BlockdevOptions',
>         'base': { 'driver': 'BlockdevDriver', '*read-only': 'bool' },
>         'discriminator': 'driver',
>         'data': { 'file': 'BlockdevOptionsFile',
>                   'qcow2': 'BlockdevOptionsQcow2' } }
> 
>      Resulting in these JSON objects::
> 
>       { "driver": "file", "read-only": true,
>         "filename": "/some/place/my-image" }
>       { "driver": "qcow2", "read-only": false,
>         "backing": "/some/place/my-image", "lazy-refcounts": true }
> 
>      The order of branches need not match the order of the enum values.
>      The branches need not cover all possible enum values.  In the
>      resulting generated C data types, a union is represented as a struct
>      with the base members in QAPI schema order, and then a union of
>      structures for each branch of the struct.
> 
>      The optional 'if' member specifies a conditional.  See `Configuring
>      the schema`_ below for more on this.
> 
>      The optional 'features' member specifies features.  See Features_
>      below for more on this.
>
diff mbox series

Patch

diff --git a/scripts/qapi/introspect.py b/scripts/qapi/introspect.py
index 67c7d89aae..04d8d424f5 100644
--- a/scripts/qapi/introspect.py
+++ b/scripts/qapi/introspect.py
@@ -336,7 +336,10 @@  def visit_object_type_flat(self, name: str, info: Optional[QAPISourceInfo],
             'members': [self._gen_object_member(m) for m in members]
         }
         if variants:
-            obj['tag'] = variants.tag_member.name
+            if variants.tag_member:
+                obj['tag'] = variants.tag_member.name
+            else:
+                obj['discriminator-type'] = variants.tag_type.name
             obj['variants'] = [self._gen_variant(v) for v in variants.variants]
         self._gen_tree(name, 'object', obj, ifcond, features)
 
diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 8ba5665bc6..0efe8d815c 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -585,16 +585,16 @@  def visit(self, visitor):
 
 
 class QAPISchemaVariants:
-    def __init__(self, tag_name, info, tag_member, variants):
+    def __init__(self, discriminator, info, tag_member, variants):
         # Unions pass tag_name but not tag_member.
         # Alternates pass tag_member but not tag_name.
         # After check(), tag_member is always set.
-        assert bool(tag_member) != bool(tag_name)
-        assert (isinstance(tag_name, str) or
+        assert bool(tag_member) != bool(discriminator)
+        assert (isinstance(discriminator, str) or
                 isinstance(tag_member, QAPISchemaObjectTypeMember))
         for v in variants:
             assert isinstance(v, QAPISchemaVariant)
-        self._tag_name = tag_name
+        self.discriminator = discriminator
         self.info = info
         self.tag_member = tag_member
         self.variants = variants
@@ -604,16 +604,24 @@  def set_defined_in(self, name):
             v.set_defined_in(name)
 
     def check(self, schema, seen):
-        if self._tag_name:      # union
-            self.tag_member = seen.get(c_name(self._tag_name))
+        self.tag_type = None
+
+        if self.discriminator:      # assume union with type discriminator
+            self.tag_type = schema.lookup_type(self.discriminator)
+
+        # union with discriminator field
+        if self.discriminator and not self.tag_type:
+            tag_name = self.discriminator
+            self.tag_member = seen.get(c_name(tag_name))
+            self.tag_type = self.tag_member.type
             base = "'base'"
             # Pointing to the base type when not implicit would be
             # nice, but we don't know it here
-            if not self.tag_member or self._tag_name != self.tag_member.name:
+            if not self.tag_member or tag_name != self.tag_member.name:
                 raise QAPISemError(
                     self.info,
                     "discriminator '%s' is not a member of %s"
-                    % (self._tag_name, base))
+                    % (tag_name, base))
             # Here we do:
             base_type = schema.lookup_type(self.tag_member.defined_in)
             assert base_type
@@ -623,29 +631,33 @@  def check(self, schema, seen):
                 raise QAPISemError(
                     self.info,
                     "discriminator member '%s' of %s must be of enum type"
-                    % (self._tag_name, base))
+                    % (tag_name, base))
             if self.tag_member.optional:
                 raise QAPISemError(
                     self.info,
                     "discriminator member '%s' of %s must not be optional"
-                    % (self._tag_name, base))
+                    % (tag_name, base))
             if self.tag_member.ifcond.is_present():
                 raise QAPISemError(
                     self.info,
                     "discriminator member '%s' of %s must not be conditional"
-                    % (self._tag_name, base))
-        else:                   # alternate
+                    % (tag_name, base))
+        elif not self.tag_type:  # alternate
             assert isinstance(self.tag_member.type, QAPISchemaEnumType)
             assert not self.tag_member.optional
             assert not self.tag_member.ifcond.is_present()
-        if self._tag_name:      # union
+
+        if self.tag_type:      # union
             # branches that are not explicitly covered get an empty type
             cases = {v.name for v in self.variants}
-            for m in self.tag_member.type.members:
+            for m in self.tag_type.members:
                 if m.name not in cases:
                     v = QAPISchemaVariant(m.name, self.info,
                                           'q_empty', m.ifcond)
-                    v.set_defined_in(self.tag_member.defined_in)
+                    if self.tag_member:
+                        v.set_defined_in(self.tag_member.defined_in)
+                    else:
+                        v.set_defined_in(self.tag_type.name)
                     self.variants.append(v)
         if not self.variants:
             raise QAPISemError(self.info, "union has no branches")
@@ -654,11 +666,11 @@  def check(self, schema, seen):
             # Union names must match enum values; alternate names are
             # checked separately. Use 'seen' to tell the two apart.
             if seen:
-                if v.name not in self.tag_member.type.member_names():
+                if v.name not in self.tag_type.member_names():
                     raise QAPISemError(
                         self.info,
                         "branch '%s' is not a value of %s"
-                        % (v.name, self.tag_member.type.describe()))
+                        % (v.name, self.tag_type.describe()))
                 if not isinstance(v.type, QAPISchemaObjectType):
                     raise QAPISemError(
                         self.info,
@@ -1116,7 +1128,7 @@  def _make_variant(self, case, typ, ifcond, info):
     def _def_union_type(self, expr: QAPIExpression):
         name = expr['union']
         base = expr['base']
-        tag_name = expr['discriminator']
+        discriminator = expr['discriminator']
         data = expr['data']
         assert isinstance(data, dict)
         info = expr.info
@@ -1136,7 +1148,7 @@  def _def_union_type(self, expr: QAPIExpression):
             QAPISchemaObjectType(name, info, expr.doc, ifcond, features,
                                  base, members,
                                  QAPISchemaVariants(
-                                     tag_name, info, None, variants)))
+                                     discriminator, info, None, variants)))
 
     def _def_alternate_type(self, expr: QAPIExpression):
         name = expr['alternate']
diff --git a/scripts/qapi/types.py b/scripts/qapi/types.py
index c39d054d2c..13021e3f82 100644
--- a/scripts/qapi/types.py
+++ b/scripts/qapi/types.py
@@ -230,7 +230,8 @@  def gen_variants(variants: QAPISchemaVariants) -> str:
     ret = mcgen('''
     union { /* union tag is @%(c_name)s */
 ''',
-                c_name=c_name(variants.tag_member.name))
+                c_name=c_name(variants.tag_member.name if variants.tag_member
+                              else variants.tag_type.name))
 
     for var in variants.variants:
         if var.type.name == 'q_empty':
diff --git a/scripts/qapi/visit.py b/scripts/qapi/visit.py
index c56ea4d724..fbe7ae071d 100644
--- a/scripts/qapi/visit.py
+++ b/scripts/qapi/visit.py
@@ -60,6 +60,13 @@  def gen_visit_members_decl(name: str) -> str:
                  c_name=c_name(name))
 
 
+def gen_visit_members_tag_mapper_decl(name: str, tag_type: str) -> str:
+    return mcgen('''
+
+bool %(c_name)s_mapper(%(c_name)s *obj, %(tag_type)s *tag, Error **errp);
+                 ''', c_name=c_name(name), tag_type=c_name(tag_type))
+
+
 def gen_visit_object_members(name: str,
                              base: Optional[QAPISchemaObjectType],
                              members: List[QAPISchemaObjectTypeMember],
@@ -83,6 +90,12 @@  def gen_visit_object_members(name: str,
             ret += memb.ifcond.gen_endif()
     ret += sep
 
+    if variants:
+        ret += mcgen('''
+    %(c_name)s tag;
+''',
+                     c_name=c_name(variants.tag_type.name))
+
     if base:
         ret += mcgen('''
     if (!visit_type_%(c_type)s_members(v, (%(c_type)s *)obj, errp)) {
@@ -132,17 +145,29 @@  def gen_visit_object_members(name: str,
         ret += memb.ifcond.gen_endif()
 
     if variants:
-        tag_member = variants.tag_member
-        assert isinstance(tag_member.type, QAPISchemaEnumType)
+        tag_type = variants.tag_type
+        assert isinstance(tag_type, QAPISchemaEnumType)
 
-        ret += mcgen('''
-    switch (obj->%(c_name)s) {
+        if variants.tag_member:
+            ret += mcgen('''
+    tag = obj->%(c_name)s;
 ''',
-                     c_name=c_name(tag_member.name))
+                         c_name=c_name(variants.tag_member.name))
+        else:
+            ret += mcgen('''
+    if (!%(c_name)s_mapper(obj, &tag, errp)) {
+        return false;
+    }
+''',
+                         c_name=c_name(name))
+
+        ret += mcgen('''
+    switch (tag) {
+''')
 
         for var in variants.variants:
-            case_str = c_enum_const(tag_member.type.name, var.name,
-                                    tag_member.type.prefix)
+            case_str = c_enum_const(tag_type.name, var.name,
+                                    tag_type.prefix)
             ret += var.ifcond.gen_if()
             if var.type.name == 'q_empty':
                 # valid variant and nothing to do
@@ -400,6 +425,10 @@  def visit_object_type(self,
             return
         with ifcontext(ifcond, self._genh, self._genc):
             self._genh.add(gen_visit_members_decl(name))
+            if variants and variants.tag_type:
+                self._genh.add(
+                    gen_visit_members_tag_mapper_decl(name,
+                                                      variants.tag_type.name))
             self._genc.add(gen_visit_object_members(name, base,
                                                     members, variants))
             # TODO Worth changing the visitor signature, so we could