diff mbox series

[RFC,v2,3/8] qapi: golang: Generate qapi's struct types in Go

Message ID 20220617121932.249381-4-victortoso@redhat.com
State New
Headers show
Series qapi: add generator for Golang interface | expand

Commit Message

Victor Toso June 17, 2022, 12:19 p.m. UTC
This patch handles QAPI struct types and generates the equivalent
types in Go.

At the time of this writing, it generates 388 structures.

The highlights of this implementation are:

1. Generating an Go struct that requires a @base type, the @base type
   fields are copied over to the Go struct. The advantage of this
   approach is to not have embed structs in any of the QAPI types.
   The downside are some generated Types that are likely useless now,
   like InetSocketAddressBase from InetSocketAddress.

2. About the Go struct's fields:

  i) They can be either by Value or Reference.

  ii) Every field that is marked as optional in the QAPI specification
  are translated to Reference fields in its Go structure. This design
  decision is the most straightforward way to check if a given field
  was set or not.

  iii) Mandatory fields are always by Value with the exception of QAPI
  arrays, which are handled by Reference (to a block of memory) by Go.

  iv) All the fields are named with Uppercase due Golang's export
  convention.

  v) In order to avoid any kind of issues when encoding ordecoding, to
  or from JSON, we mark all fields with its @name and, when it is
  optional, member, with @omitempty

Example:

qapi:
  | { 'struct': 'BlockdevCreateOptionsFile',
  |   'data': { 'filename':             'str',
  |             'size':                 'size',
  |             '*preallocation':       'PreallocMode',
  |             '*nocow':               'bool',
  |             '*extent-size-hint':    'size'} }

go:
  | type BlockdevCreateOptionsFile struct {
  |         Filename       string        `json:"filename"`
  |         Size           uint64        `json:"size"`
  |         Preallocation  *PreallocMode `json:"preallocation,omitempty"`
  |         Nocow          *bool         `json:"nocow,omitempty"`
  |         ExtentSizeHint *uint64       `json:"extent-size-hint,omitempty"`
  | }

Signed-off-by: Victor Toso <victortoso@redhat.com>
---
 scripts/qapi/golang.py | 117 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 115 insertions(+), 2 deletions(-)

Comments

Daniel P. Berrangé June 17, 2022, 2:41 p.m. UTC | #1
On Fri, Jun 17, 2022 at 02:19:27PM +0200, Victor Toso wrote:
> This patch handles QAPI struct types and generates the equivalent
> types in Go.
> 
> At the time of this writing, it generates 388 structures.
> 
> The highlights of this implementation are:
> 
> 1. Generating an Go struct that requires a @base type, the @base type
>    fields are copied over to the Go struct. The advantage of this
>    approach is to not have embed structs in any of the QAPI types.
>    The downside are some generated Types that are likely useless now,
>    like InetSocketAddressBase from InetSocketAddress.
> 
> 2. About the Go struct's fields:
> 
>   i) They can be either by Value or Reference.
> 
>   ii) Every field that is marked as optional in the QAPI specification
>   are translated to Reference fields in its Go structure. This design
>   decision is the most straightforward way to check if a given field
>   was set or not.
> 
>   iii) Mandatory fields are always by Value with the exception of QAPI
>   arrays, which are handled by Reference (to a block of memory) by Go.
> 
>   iv) All the fields are named with Uppercase due Golang's export
>   convention.
> 
>   v) In order to avoid any kind of issues when encoding ordecoding, to
>   or from JSON, we mark all fields with its @name and, when it is
>   optional, member, with @omitempty
> 
> Example:
> 
> qapi:
>   | { 'struct': 'BlockdevCreateOptionsFile',
>   |   'data': { 'filename':             'str',
>   |             'size':                 'size',
>   |             '*preallocation':       'PreallocMode',
>   |             '*nocow':               'bool',
>   |             '*extent-size-hint':    'size'} }
> 
> go:
>   | type BlockdevCreateOptionsFile struct {
>   |         Filename       string        `json:"filename"`
>   |         Size           uint64        `json:"size"`
>   |         Preallocation  *PreallocMode `json:"preallocation,omitempty"`
>   |         Nocow          *bool         `json:"nocow,omitempty"`
>   |         ExtentSizeHint *uint64       `json:"extent-size-hint,omitempty"`
>   | }

One thing to bear in mind here

At the QAPI level, changing a field from mandatory to optional has
been considered a backwards compatible change by QEMU maintainers,
because any existing caller can happily continue passing the
optional field with no downside.

With this Go design, changing a field from mandatory to optional
will be an API breakage, because the developer will need to change
from passing a literal value, to a pointer to the value, when
initializing the struct.

IOW, this Go impl provides weaker compat guarantees than even
QAPI does, and QAPI compat guarantees were already weaker than
I would like as an app developer.

If we want to make ourselves future proof, we would have to make
all struct fields optional from the start, even if they are
mandatory at QAPI level. This would make the code less self-documenting
though, so that's not very appealing either.


If we want to avoid this, we would need the same approach I suggested
wrt support multiple versions of the API concurrently. Namely have
versioned structs, so every time there's a field change of any kind,
we introduce a new struct version.


With regards,
Daniel
Victor Toso June 17, 2022, 3:23 p.m. UTC | #2
Hi,

On Fri, Jun 17, 2022 at 03:41:10PM +0100, Daniel P. Berrangé wrote:
> On Fri, Jun 17, 2022 at 02:19:27PM +0200, Victor Toso wrote:
> > This patch handles QAPI struct types and generates the equivalent
> > types in Go.
> > 
> > At the time of this writing, it generates 388 structures.
> > 
> > The highlights of this implementation are:
> > 
> > 1. Generating an Go struct that requires a @base type, the @base type
> >    fields are copied over to the Go struct. The advantage of this
> >    approach is to not have embed structs in any of the QAPI types.
> >    The downside are some generated Types that are likely useless now,
> >    like InetSocketAddressBase from InetSocketAddress.
> > 
> > 2. About the Go struct's fields:
> > 
> >   i) They can be either by Value or Reference.
> > 
> >   ii) Every field that is marked as optional in the QAPI specification
> >   are translated to Reference fields in its Go structure. This design
> >   decision is the most straightforward way to check if a given field
> >   was set or not.
> > 
> >   iii) Mandatory fields are always by Value with the exception of QAPI
> >   arrays, which are handled by Reference (to a block of memory) by Go.
> > 
> >   iv) All the fields are named with Uppercase due Golang's export
> >   convention.
> > 
> >   v) In order to avoid any kind of issues when encoding ordecoding, to
> >   or from JSON, we mark all fields with its @name and, when it is
> >   optional, member, with @omitempty
> > 
> > Example:
> > 
> > qapi:
> >   | { 'struct': 'BlockdevCreateOptionsFile',
> >   |   'data': { 'filename':             'str',
> >   |             'size':                 'size',
> >   |             '*preallocation':       'PreallocMode',
> >   |             '*nocow':               'bool',
> >   |             '*extent-size-hint':    'size'} }
> > 
> > go:
> >   | type BlockdevCreateOptionsFile struct {
> >   |         Filename       string        `json:"filename"`
> >   |         Size           uint64        `json:"size"`
> >   |         Preallocation  *PreallocMode `json:"preallocation,omitempty"`
> >   |         Nocow          *bool         `json:"nocow,omitempty"`
> >   |         ExtentSizeHint *uint64       `json:"extent-size-hint,omitempty"`
> >   | }
> 
> One thing to bear in mind here
> 
> At the QAPI level, changing a field from mandatory to optional has
> been considered a backwards compatible change by QEMU maintainers,
> because any existing caller can happily continue passing the
> optional field with no downside.
> 
> With this Go design, changing a field from mandatory to optional
> will be an API breakage, because the developer will need to change
> from passing a literal value, to a pointer to the value, when
> initializing the struct.
> 
> IOW, this Go impl provides weaker compat guarantees than even
> QAPI does, and QAPI compat guarantees were already weaker than
> I would like as an app developer.

I think the current draft should be considered an interface that
can work with the QEMU version this was generated from. That is
the first thing we should get right.

> If we want to make ourselves future proof, we would have to
> make all struct fields optional from the start, even if they
> are mandatory at QAPI level. This would make the code less
> self-documenting though, so that's not very appealing either.
 
> If we want to avoid this, we would need the same approach I
> suggested wrt support multiple versions of the API
> concurrently. Namely have versioned structs, so every time
> there's a field change of any kind, we introduce a new struct
> version.

That's more or less what I had in mind. I mentioned it in the
item 8 of the cover-letter. I just did not want to address it at
before deciding what the structs should look like first, for the
version we are generating from.

Just to clarify, so far I plan to follow the suggestion:
    https://lists.gnu.org/archive/html/qemu-devel/2022-05/msg02147.html

Of course, If there are other ideas, we can discuss it too.

Cheers,
Victor
diff mbox series

Patch

diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
index 37d7c062c9..1ab0c0bb46 100644
--- a/scripts/qapi/golang.py
+++ b/scripts/qapi/golang.py
@@ -53,7 +53,7 @@  class QAPISchemaGenGolangVisitor(QAPISchemaVisitor):
 
     def __init__(self, prefix: str):
         super().__init__()
-        self.target = {name: "" for name in ["alternate", "enum", "helper"]}
+        self.target = {name: "" for name in ["alternate", "enum", "helper", "struct"]}
         self.objects_seen = {}
         self.schema = None
         self.golang_package_name = "qapi"
@@ -79,7 +79,37 @@  def visit_object_type(self: QAPISchemaGenGolangVisitor,
                           members: List[QAPISchemaObjectTypeMember],
                           variants: Optional[QAPISchemaVariants]
                           ) -> None:
-        pass
+        # Do not handle anything besides structs
+        if (name == self.schema.the_empty_object_type.name or
+                not isinstance(name, str) or
+                info.defn_meta not in ["struct"]):
+            return
+
+        # Safety checks.
+        assert name not in self.objects_seen
+        self.objects_seen[name] = True
+
+        # visit all inner objects as well, they are not going to be
+        # called by python's generator.
+        if variants:
+            for var in variants.variants:
+                assert isinstance(var.type, QAPISchemaObjectType)
+                self.visit_object_type(self,
+                                       var.type.name,
+                                       var.type.info,
+                                       var.type.ifcond,
+                                       var.type.base,
+                                       var.type.local_members,
+                                       var.type.variants)
+
+        # Save generated Go code to be written later
+        self.target[info.defn_meta] += qapi_to_golang_struct(name,
+                                                             info,
+                                                             ifcond,
+                                                             features,
+                                                             base,
+                                                             members,
+                                                             variants)
 
     def visit_alternate_type(self: QAPISchemaGenGolangVisitor,
                              name: str,
@@ -223,6 +253,72 @@  def generate_struct_type(type_name, args="") -> str:
 '''
 
 
+# Helper function that is used for most of QAPI types
+def qapi_to_golang_struct(name: str,
+                          info: Optional[QAPISourceInfo],
+                          ifcond: QAPISchemaIfCond,
+                          features: List[QAPISchemaFeature],
+                          base: Optional[QAPISchemaObjectType],
+                          members: List[QAPISchemaObjectTypeMember],
+                          variants: Optional[QAPISchemaVariants]) -> str:
+
+    type_name = qapi_to_go_type_name(name, info.defn_meta)
+
+    fields = ""
+
+    if base:
+        base_fields = ""
+        for lm in base.local_members:
+            # We do not include the Union's discriminator
+            # into the generated Go struct as the selection
+            # of what variant was set is enough on Go side.
+            if variants and variants.tag_member.name == lm.name:
+                continue
+
+            field = qapi_to_field_name(lm.name)
+            member_type = qapi_schema_type_to_go_type(lm.type.name)
+
+            isptr = "*" if lm.optional and member_type[0] not in "*[" else ""
+            optional = ",omitempty" if lm.optional else ""
+            fieldtag = f'`json:"{lm.name}{optional}"`'
+
+            base_fields += f"\t{field} {isptr}{member_type}{fieldtag}\n"
+
+        if len(base_fields) > 0:
+            fields += f"\t// Base fields\n\t{base_fields}\n"
+
+    if members:
+        for memb in members:
+            field = qapi_to_field_name(memb.name)
+            member_type = qapi_schema_type_to_go_type(memb.type.name)
+
+            isptr = "*" if memb.optional and member_type[0] not in "*[" else ""
+            optional = ",omitempty" if memb.optional else ""
+            fieldtag = f'`json:"{memb.name}{optional}"`'
+
+            fields += f"\t{field} {isptr}{member_type}{fieldtag}\n"
+
+        fields += "\n"
+
+    if variants:
+        fields += "\t// Variants fields\n"
+        for var in variants.variants:
+            if var.type.is_implicit():
+                continue
+
+            field = qapi_to_field_name(var.name)
+            member_type = qapi_schema_type_to_go_type(var.type.name)
+            # Variant's are handled in the Marshal/Unmarshal methods
+            fieldtag = '`json:"-"`'
+            fields += f"\t{field} *{member_type}{fieldtag}\n"
+            member_type = qapi_schema_type_to_go_type(var.type.name)
+            # Variant's are handled in the Marshal/Unmarshal methods
+            fieldtag = '`json:"-"`'
+            fields += f"\t{field} *{member_type}{fieldtag}\n"
+
+    return generate_struct_type(type_name, fields)
+
+
 def qapi_schema_type_to_go_type(type: str) -> str:
     schema_types_to_go = {
             'str': 'string', 'null': 'nil', 'bool': 'bool', 'number':
@@ -247,3 +343,20 @@  def qapi_to_field_name_enum(name: str) -> str:
 
 def qapi_to_field_name(name: str) -> str:
     return name.title().replace("_", "").replace("-", "")
+
+
+def qapi_to_go_type_name(name: str, meta: str) -> str:
+    if name.startswith("q_obj_"):
+        name = name[6:]
+
+    # We want to keep CamelCase for Golang types. We want to avoid removing
+    # already set CameCase names while fixing uppercase ones, eg:
+    # 1) q_obj_SocketAddress_base -> SocketAddressBase
+    # 2) q_obj_WATCHDOG-arg -> WatchdogArg
+    words = [word for word in name.replace("_", "-").split("-")]
+    name = words[0]
+    if name.islower() or name.isupper():
+        name = name.title()
+
+    name += ''.join(word.title() for word in words[1:])
+    return name