diff mbox series

[v1,3/9] qapi: golang: Generate qapi's struct types in Go

Message ID 20230927112544.85011-4-victortoso@redhat.com (mailing list archive)
State New, archived
Headers show
Series qapi-go: add generator for Golang interface | expand

Commit Message

Victor Toso Sept. 27, 2023, 11:25 a.m. UTC
This patch handles QAPI struct types and generates the equivalent
types in Go. The following patch adds extra logic when a member of the
struct has a Type that can take JSON Null value (e.g: StrOrNull in
QEMU)

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.
   Note that embedding a @base type is recursive, that is, if the
   @base type has a @base, all of those fields will be copied over.

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. Exception only for types that can
      take JSON Null value.

 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 or decoding,
      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 | 138 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 136 insertions(+), 2 deletions(-)

Comments

Daniel P. Berrangé Sept. 28, 2023, 2:06 p.m. UTC | #1
On Wed, Sep 27, 2023 at 01:25:38PM +0200, Victor Toso wrote:
> This patch handles QAPI struct types and generates the equivalent
> types in Go. The following patch adds extra logic when a member of the
> struct has a Type that can take JSON Null value (e.g: StrOrNull in
> QEMU)
> 
> 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.
>    Note that embedding a @base type is recursive, that is, if the
>    @base type has a @base, all of those fields will be copied over.
> 
> 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. Exception only for types that can
>       take JSON Null value.
> 
>  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 or decoding,
>       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"`
> | }

Note, 'omitempty' shouldn't be used on pointer fields, only scalar
fields. The pointer fields are always omitted when nil.


With regards,
Daniel
Victor Toso Sept. 29, 2023, 1:29 p.m. UTC | #2
Hi,

On Thu, Sep 28, 2023 at 03:06:23PM +0100, Daniel P. Berrangé wrote:
> On Wed, Sep 27, 2023 at 01:25:38PM +0200, Victor Toso wrote:
> > This patch handles QAPI struct types and generates the equivalent
> > types in Go. The following patch adds extra logic when a member of the
> > struct has a Type that can take JSON Null value (e.g: StrOrNull in
> > QEMU)
> > 
> > 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.
> >    Note that embedding a @base type is recursive, that is, if the
> >    @base type has a @base, all of those fields will be copied over.
> > 
> > 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. Exception only for types that can
> >       take JSON Null value.
> > 
> >  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 or decoding,
> >       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"`
> > | }
> 
> Note, 'omitempty' shouldn't be used on pointer fields, only
> scalar fields. The pointer fields are always omitted when nil.

'omitempty' should be used with pointer fields unless you want to
Marshal JSON Null, which is not the expected output.

'omitempty' is used when QAPI member is said to be optional. This
is true for all optional members with the exception of two
Alternates: StrOrNull and BlockdevRefOrNull, as we _do_ want to
express JSON Null with them.

```Go
    package main

    import (
        "encoding/json"
        "fmt"
    )

    type Test1 struct {
        Foo *bool `json:"foo"`
        Bar *bool `json:"bar,omitempty"`
        Esc bool  `json:"esc"`
        Lar bool  `json:"lar,omitempty"`
    }

    type Test2 struct {
        Foo *uint64 `json:"foo"`
        Bar *uint64 `json:"bar,omitempty"`
        Esc uint64  `json:"esc"`
        Lar uint64  `json:"lar,omitempty"`
    }

    func printIt(s any) {
        if b, err := json.Marshal(s); err != nil {
            panic(err)
        } else {
            fmt.Println(string(b))
        }
    }

    func main() {
        printIt(Test1{})
        printIt(Test2{})
    }
```

```console
    toso@tapioca /tmp> go run main.go
    {"foo":null,"esc":false}
    {"foo":null,"esc":0}
```

Cheers,
Victor
Daniel P. Berrangé Sept. 29, 2023, 1:33 p.m. UTC | #3
On Fri, Sep 29, 2023 at 03:29:34PM +0200, Victor Toso wrote:
> Hi,
> 
> On Thu, Sep 28, 2023 at 03:06:23PM +0100, Daniel P. Berrangé wrote:
> > On Wed, Sep 27, 2023 at 01:25:38PM +0200, Victor Toso wrote:
> > > This patch handles QAPI struct types and generates the equivalent
> > > types in Go. The following patch adds extra logic when a member of the
> > > struct has a Type that can take JSON Null value (e.g: StrOrNull in
> > > QEMU)
> > > 
> > > 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.
> > >    Note that embedding a @base type is recursive, that is, if the
> > >    @base type has a @base, all of those fields will be copied over.
> > > 
> > > 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. Exception only for types that can
> > >       take JSON Null value.
> > > 
> > >  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 or decoding,
> > >       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"`
> > > | }
> > 
> > Note, 'omitempty' shouldn't be used on pointer fields, only
> > scalar fields. The pointer fields are always omitted when nil.
> 
> 'omitempty' should be used with pointer fields unless you want to
> Marshal JSON Null, which is not the expected output.

Oh doh, did't notice that.


With regards,
Daniel
diff mbox series

Patch

diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
index 43dbdde14c..1b19e4b232 100644
--- a/scripts/qapi/golang.py
+++ b/scripts/qapi/golang.py
@@ -117,12 +117,35 @@  def gen_golang(schema: QAPISchema,
     schema.visit(vis)
     vis.write(output_dir)
 
+def qapi_name_is_base(name: str) -> bool:
+    return qapi_name_is_object(name) and name.endswith("-base")
+
+def qapi_name_is_object(name: str) -> bool:
+    return name.startswith("q_obj_")
+
 def qapi_to_field_name(name: str) -> str:
     return name.title().replace("_", "").replace("-", "")
 
 def qapi_to_field_name_enum(name: str) -> str:
     return name.title().replace("-", "")
 
+def qapi_to_go_type_name(name: str) -> str:
+    if qapi_name_is_object(name):
+        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 = list(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
+
 def qapi_schema_type_to_go_type(qapitype: str) -> str:
     schema_types_to_go = {
             'str': 'string', 'null': 'nil', 'bool': 'bool', 'number':
@@ -156,6 +179,82 @@  def generate_struct_type(type_name, args="") -> str:
     return f'''{with_type} struct {{{args}}}
 '''
 
+def get_struct_field(self: QAPISchemaGenGolangVisitor,
+                     qapi_name: str,
+                     qapi_type_name: str,
+                     is_optional: bool,
+                     is_variant: bool) -> str:
+
+    field = qapi_to_field_name(qapi_name)
+    member_type = qapi_schema_type_to_go_type(qapi_type_name)
+
+    optional = ""
+    if is_optional:
+        if member_type not in self.accept_null_types:
+            optional = ",omitempty"
+
+    # Use pointer to type when field is optional
+    isptr = "*" if is_optional and member_type[0] not in "*[" else ""
+
+    fieldtag = '`json:"-"`' if is_variant else f'`json:"{qapi_name}{optional}"`'
+    return f"\t{field} {isptr}{member_type}{fieldtag}\n"
+
+def recursive_base(self: QAPISchemaGenGolangVisitor,
+                   base: Optional[QAPISchemaObjectType]) -> str:
+    fields = ""
+
+    if not base:
+        return fields
+
+    if base.base is not None:
+        embed_base = self.schema.lookup_entity(base.base.name)
+        fields = recursive_base(self, embed_base)
+
+    for member in base.local_members:
+        if base.variants and base.variants.tag_member.name == member.name:
+            fields += '''// Discriminator\n'''
+
+        field = get_struct_field(self, member.name, member.type.name, member.optional, False)
+        fields += field
+
+    if len(fields) > 0:
+        fields += "\n"
+
+    return fields
+
+# Helper function that is used for most of QAPI types
+def qapi_to_golang_struct(self: QAPISchemaGenGolangVisitor,
+                          name: str,
+                          _: Optional[QAPISourceInfo],
+                          __: QAPISchemaIfCond,
+                          ___: List[QAPISchemaFeature],
+                          base: Optional[QAPISchemaObjectType],
+                          members: List[QAPISchemaObjectTypeMember],
+                          variants: Optional[QAPISchemaVariants]) -> str:
+
+
+    fields = recursive_base(self, base)
+
+    if members:
+        for member in members:
+            field = get_struct_field(self, member.name, member.type.name, member.optional, False)
+            fields += field
+
+        fields += "\n"
+
+    if variants:
+        fields += "\t// Variants fields\n"
+        for variant in variants.variants:
+            if variant.type.is_implicit():
+                continue
+
+            field = get_struct_field(self, variant.name, variant.type.name, True, True)
+            fields += field
+
+    type_name = qapi_to_go_type_name(name)
+    content = generate_struct_type(type_name, fields)
+    return content
+
 def generate_template_alternate(self: QAPISchemaGenGolangVisitor,
                                 name: str,
                                 variants: Optional[QAPISchemaVariants]) -> str:
@@ -218,7 +317,7 @@  class QAPISchemaGenGolangVisitor(QAPISchemaVisitor):
 
     def __init__(self, _: str):
         super().__init__()
-        types = ["alternate", "enum", "helper"]
+        types = ["alternate", "enum", "helper", "struct"]
         self.target = {name: "" for name in types}
         self.objects_seen = {}
         self.schema = None
@@ -258,7 +357,42 @@  def visit_object_type(self: QAPISchemaGenGolangVisitor,
                           members: List[QAPISchemaObjectTypeMember],
                           variants: Optional[QAPISchemaVariants]
                           ) -> None:
-        pass
+        # Do not handle anything besides struct.
+        if (name == self.schema.the_empty_object_type.name or
+                not isinstance(name, str) or
+                info.defn_meta not in ["struct"]):
+            return
+
+        # Base structs are embed
+        if qapi_name_is_base(name):
+            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(self,
+                                                             name,
+                                                             info,
+                                                             ifcond,
+                                                             features,
+                                                             base,
+                                                             members,
+                                                             variants)
 
     def visit_alternate_type(self: QAPISchemaGenGolangVisitor,
                              name: str,