[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: [PATCH v2 11/11] docs: add notes on Golang code generator
|
From: |
Victor Toso |
|
Subject: |
Re: [PATCH v2 11/11] docs: add notes on Golang code generator |
|
Date: |
Wed, 18 Oct 2023 18:21:42 +0200 |
Hi,
On Wed, Oct 18, 2023 at 01:47:56PM +0200, Markus Armbruster wrote:
> Victor Toso <victortoso@redhat.com> writes:
>
> > The goal of this patch is converge discussions into a documentation,
> > to make it easy and explicit design decisions, known issues and what
> > else might help a person interested in how the Go module is generated.
> >
> > Signed-off-by: Victor Toso <victortoso@redhat.com>
> > ---
> > docs/devel/index-build.rst | 1 +
> > docs/devel/qapi-golang-code-gen.rst | 376 ++++++++++++++++++++++++++++
> > 2 files changed, 377 insertions(+)
> > create mode 100644 docs/devel/qapi-golang-code-gen.rst
> >
> > diff --git a/docs/devel/index-build.rst b/docs/devel/index-build.rst
> > index 57e8d39d98..8f7c6f5dc7 100644
> > --- a/docs/devel/index-build.rst
> > +++ b/docs/devel/index-build.rst
> > @@ -15,5 +15,6 @@ the basics if you are adding new files and targets to the
> > build.
> > qtest
> > ci
> > qapi-code-gen
> > + qapi-golang-code-gen
> > fuzzing
> > control-flow-integrity
>
> Let's not worry whether and how this should be integrated with
> qapi-code-gen.rst for now.
>
> I'm a Go ignoramus. I hope my comments are at least somewhat
> helpful all the same.
They always are.
> > diff --git a/docs/devel/qapi-golang-code-gen.rst
> > b/docs/devel/qapi-golang-code-gen.rst
> > new file mode 100644
> > index 0000000000..b62daf3bad
> > --- /dev/null
> > +++ b/docs/devel/qapi-golang-code-gen.rst
> > @@ -0,0 +1,376 @@
> > +==========================
> > +QAPI Golang code generator
> > +==========================
> > +
> > +..
> > + Copyright (C) 2023 Red Hat, Inc.
> > +
> > + This work is licensed under the terms of the GNU GPL, version 2 or
> > + later. See the COPYING file in the top-level directory.
> > +
> > +
> > +Introduction
> > +============
> > +
> > +This document provides information of how the generated Go code maps
> > +with the QAPI specification, clarifying design decisions when needed.
> > +
> > +
> > +Scope of the generated Go code
> > +==============================
>
> What do you mean by "scope"?
To build an application to talk with QEMU over QMP, this
generated code is not enough. What I mean is that, this is just
the first layer. We still need a library on top to do the work of
connecting, sending/receiving messages, etc.
Any recommendations on how to word this better?
> > +
> > +The scope is limited to data structures that can interpret and be used
> > +to generate valid QMP messages. These data structures are generated
> > +from a QAPI schema and should be able to handle QMP messages from the
> > +same schema.
> > +
> > +The generated Go code is a Go module with data structs that uses Go
> > +standard library ``encoding/json``, implementing its field tags and
> > +Marshal interface whenever needed.
> > +
> > +
> > +QAPI types to Go structs
> > +========================
> > +
> > +Enum
> > +----
> > +
> > +Enums are mapped as strings in Go, using a specified string type per
> > +Enum to help with type safety in the Go application.
> > +
> > +::
> > +
> > + { 'enum': 'HostMemPolicy',
> > + 'data': [ 'default', 'preferred', 'bind', 'interleave' ] }
> > +
> > +.. code-block:: go
> > +
> > + type HostMemPolicy string
> > +
> > + const (
> > + HostMemPolicyDefault HostMemPolicy = "default"
> > + HostMemPolicyPreferred HostMemPolicy = "preferred"
> > + HostMemPolicyBind HostMemPolicy = "bind"
> > + HostMemPolicyInterleave HostMemPolicy = "interleave"
> > + )
> > +
> > +
> > +Struct
> > +------
> > +
> > +The mapping between a QAPI struct in Go struct is very straightforward.
> > + - Each member of the QAPI struct has its own field in a Go struct.
> > + - Optional members are pointers type with 'omitempty' field tag set
> > +
> > +One important design decision was to _not_ embed base struct, copying
> > +the base members to the original struct. This reduces the complexity
> > +for the Go application.
> > +
> > +::
> > +
> > + { 'struct': 'BlockExportOptionsNbdBase',
> > + 'data': { '*name': 'str', '*description': 'str' } }
> > +
> > + { 'struct': 'BlockExportOptionsNbd',
> > + 'base': 'BlockExportOptionsNbdBase',
> > + 'data': { '*bitmaps': ['BlockDirtyBitmapOrStr'],
> > + '*allocation-depth': 'bool' } }
> > +
> > +.. code-block:: go
> > +
> > + type BlockExportOptionsNbd struct {
> > + Name *string `json:"name,omitempty"`
> > + Description *string `json:"description,omitempty"`
> > +
> > + Bitmaps []BlockDirtyBitmapOrStr `json:"bitmaps,omitempty"`
> > + AllocationDepth *bool
> > `json:"allocation-depth,omitempty"`
> > + }
>
> Is there a need to convert from type to base type?
Do you mean why we don't embed a base struct and do a copy of its
fields instead? If yes, the main reason is aesthetic. See this
issue: https://github.com/golang/go/issues/29438
So, we could have a situation where we have a embed type, inside
a embed type, inside of embed type making it horrible to write
the code for using it.
> In C, we get away with (BlockExportOptionsNbdBase *)obj. We hide it in
> an inline function, though.
>
> > +
> > +
> > +Union
> > +-----
> > +
> > +Unions in QAPI are binded to a Enum type which provides all possible
>
> bound to an Enum
Fixed.
> > +branches of the union. The most important caveat here is that the Union
> > +does not need to have a complex type implemented for all possible
> > +branches of the Enum. Receiving a enum value of a unimplemented branch
>
> I'd call that branch empty, not unimplemented.
Ok, changed them all to empty branch.
> > +is valid.
> > +
> > +For this reason, the generated Go struct will define a field for each
> > +Enum value. The Go type defined for unbranched Enum values is bool
>
> Important design decision: since Go sucks at sum types, you elect to add
> all the variant members to the struct. Only one of them may be used at
> any time. Please spell that out more clearly.
What about:
The generated Go struct will then define a field for each
Enum value. The type for Enum values of empty branch is bool.
Only one field can be set at time.
> > +
> > +Go struct and also implement the Marshal interface.
>
> Blank line in the middle of a sentence? Or two sentences? Can't make
> sense of them.
Leftover when rewriting it. Removed it.
> What do you mean by "unbranched" Enum value? Enum value with an
> implicit (empty) branch?
Yes, changing it to empty branch or empty branched.
> > +
> > +As each Union Go struct type has both the discriminator field and
> > +optional fields, it is important to note that when converting Go struct
> > +to JSON, we only consider the discriminator field if no optional field
> > +member was set. In practice, the user should use the optional fields if
> > +the QAPI Union type has defined them, otherwise the user can set the
> > +discriminator field for the unbranched enum value.
>
> I don't think I get this paragraph.
Sorry, leftover again. I've removed it entirely.
This bit was when we had a Discriminator field, we don't have it
anymore.
> > +
> > +::
> > +
> > + { 'union': 'ImageInfoSpecificQCow2Encryption',
> > + 'base': 'ImageInfoSpecificQCow2EncryptionBase',
> > + 'discriminator': 'format',
> > + 'data': { 'luks': 'QCryptoBlockInfoLUKS' } }
>
> The example is hard to understand without additional context, namely:
>
> { 'struct': 'ImageInfoSpecificQCow2EncryptionBase',
> 'data': { 'format': 'BlockdevQcow2EncryptionFormat'}}
>
> { 'enum': 'BlockdevQcow2EncryptionFormat',
> 'data': [ 'aes', 'luks' ] }
Added.
> > +
> > +.. code-block:: go
> > +
> > + type ImageInfoSpecificQCow2Encryption struct {
> > + // Variants fields
> > + Luks *QCryptoBlockInfoLUKS `json:"-"`
> > + // Unbranched enum fields
> > + Aes bool `json:"-"`
> > + }
>
> The members of the base type are the common members, and the members of
> the branch's type are the variant members.
>
> Your example shows the variant member: 'luks'.
>
> The common member @format isn't there. I guess you're eliding it
> because you can derive its value from other members.
Correct. We can define @format value based on user's setting Aes
or Luks.
> If there were other common members, where would they go?
They should all be at the top of the struct, for example:
{ 'union': 'ExpirePasswordOptions',
'base': { 'protocol': 'DisplayProtocol',
'time': 'str' },
'discriminator': 'protocol',
'data': { 'vnc': 'ExpirePasswordOptionsVnc' } }
{ 'enum': 'DisplayProtocol',
'data': [ 'vnc', 'spice' ] }
generates:
type ExpirePasswordOptions struct {
Time string `json:"time"`
// Variants fields
Vnc *ExpirePasswordOptionsVnc `json:"-"`
// Unbranched enum fields
Spice bool `json:"-"`
}
if you want to navigate over it:
https://gitlab.com/victortoso/qapi-go/-/blob/qapi-golang-v2-by-tags/pkg/qapi/unions.go?ref_type=heads#L4516
> > +
> > + func (s ImageInfoSpecificQCow2Encryption) MarshalJSON() ([]byte,
> > error) {
> > + // ...
> > + // Logic for branched Enum
> > + if s.Luks != nil && err == nil {
> > + if len(bytes) != 0 {
> > + err = errors.New(`multiple variant fields set`)
> > + } else if err = unwrapToMap(m, s.Luks); err == nil {
> > + m["format"] = BlockdevQcow2EncryptionFormatLuks
> > + bytes, err = json.Marshal(m)
> > + }
> > + }
> > +
> > + // Logic for unbranched Enum
> > + if s.Aes && err == nil {
> > + if len(bytes) != 0 {
> > + err = errors.New(`multiple variant fields set`)
> > + } else {
> > + m["format"] = BlockdevQcow2EncryptionFormatAes
> > + bytes, err = json.Marshal(m)
> > + }
> > + }
> > +
> > + // ...
> > + // Handle errors
> > + }
> > +
> > +
> > + func (s *ImageInfoSpecificQCow2Encryption) UnmarshalJSON(data []byte)
> > error {
> > + // ...
> > +
> > + switch tmp.Format {
> > + case BlockdevQcow2EncryptionFormatLuks:
> > + s.Luks = new(QCryptoBlockInfoLUKS)
> > + if err := json.Unmarshal(data, s.Luks); err != nil {
> > + s.Luks = nil
> > + return err
> > + }
> > + case BlockdevQcow2EncryptionFormatAes:
> > + s.Aes = true
> > +
> > + default:
> > + return fmt.Errorf("error: unmarshal:
> > ImageInfoSpecificQCow2Encryption: received unrecognized value: '%s'",
> > + tmp.Format)
> > + }
> > + return nil
> > + }
> > +
> > +
> > +Alternate
> > +---------
> > +
> > +Like Unions, alternates can have a few branches. Unlike Unions, they
>
> Scratch "a few".
Fixed
> > +don't have a discriminator field and each branch should be a different
> > +class of Type entirely (e.g: You can't have two branches of type int in
> > +one Alternate).
> > +
> > +While the marshalling is similar to Unions, the unmarshalling uses a
> > +try-and-error approach, trying to fit the data payload in one of the
> > +Alternate fields.
> > +
> > +The biggest caveat is handling Alternates that can take JSON Null as
> > +value. The issue lies on ``encoding/json`` library limitation where
> > +unmarshalling JSON Null data to a Go struct which has the 'omitempty'
> > +field that, it bypass the Marshal interface. The same happens when
> > +marshalling, if the field tag 'omitempty' is used, a nil pointer would
> > +never be translated to null JSON value.
> > +
> > +The problem being, we use pointer to type plus ``omitempty`` field to
> > +express a QAPI optional member.
> > +
> > +In order to handle JSON Null, the generator needs to do the following:
> > + - Read the QAPI schema prior to generate any code and cache
> > + all alternate types that can take JSON Null
> > + - For all Go structs that should be considered optional and they type
> > + are one of those alternates, do not set ``omitempty`` and implement
> > + Marshal interface for this Go struct, to properly handle JSON Null
> > + - In the Alternate, uses a boolean 'IsNull' to express a JSON Null
> > + and implement the AbsentAlternate interface, to help sturcts know
>
> Typo: to help structs
Thanks
> > + if a given Alternate type should be considered Absent (not set) or
> > + any other possible Value, including JSON Null.
> > +
> > +::
> > +
> > + { 'alternate': 'BlockdevRefOrNull',
> > + 'data': { 'definition': 'BlockdevOptions',
> > + 'reference': 'str',
> > + 'null': 'null' } }
> > +
> > +.. code-block:: go
> > +
> > + type BlockdevRefOrNull struct {
> > + Definition *BlockdevOptions
> > + Reference *string
> > + IsNull bool
> > + }
> > +
> > + func (s *BlockdevRefOrNull) ToAnyOrAbsent() (any, bool) {
> > + if s != nil {
> > + if s.IsNull {
> > + return nil, false
> > + } else if s.Definition != nil {
> > + return *s.Definition, false
> > + } else if s.Reference != nil {
> > + return *s.Reference, false
> > + }
> > + }
> > +
> > + return nil, true
> > + }
> > +
> > + func (s BlockdevRefOrNull) MarshalJSON() ([]byte, error) {
> > + if s.IsNull {
> > + return []byte("null"), nil
> > + } else if s.Definition != nil {
> > + return json.Marshal(s.Definition)
> > + } else if s.Reference != nil {
> > + return json.Marshal(s.Reference)
> > + }
> > + return []byte("{}"), nil
> > + }
> > +
> > + func (s *BlockdevRefOrNull) UnmarshalJSON(data []byte) error {
> > + // Check for json-null first
> > + if string(data) == "null" {
> > + s.IsNull = true
> > + return nil
> > + }
> > + // Check for BlockdevOptions
> > + {
> > + s.Definition = new(BlockdevOptions)
> > + if err := StrictDecode(s.Definition, data); err == nil {
> > + return nil
> > + }
> > + s.Definition = nil
> > + }
> > + // Check for string
> > + {
> > + s.Reference = new(string)
> > + if err := StrictDecode(s.Reference, data); err == nil {
> > + return nil
> > + }
> > + s.Reference = nil
> > + }
> > +
> > + return fmt.Errorf("Can't convert to BlockdevRefOrNull: %s",
> > string(data))
> > + }
> > +
> > +
> > +Event
> > +-----
> > +
> > +All events are mapped to its own struct with the additional
>
> Each event is mapped to its own
Fixed
> > +MessageTimestamp field, for the over-the-wire 'timestamp' value.
> > +
> > +Marshaling and Unmarshaling happens over the Event interface, so users
> > +should use the MarshalEvent() and UnmarshalEvent() methods.
> > +
> > +::
> > +
> > + { 'event': 'SHUTDOWN',
> > + 'data': { 'guest': 'bool',
> > + 'reason': 'ShutdownCause' } }
> > +
> > +.. code-block:: go
> > +
> > + type Event interface {
> > + GetName() string
> > + GetTimestamp() Timestamp
> > + }
> > +
> > + type ShutdownEvent struct {
> > + MessageTimestamp Timestamp `json:"-"`
> > + Guest bool `json:"guest"`
> > + Reason ShutdownCause `json:"reason"`
> > + }
> > +
> > + func (s *ShutdownEvent) GetName() string {
> > + return "SHUTDOWN"
> > + }
> > +
> > + func (s *ShutdownEvent) GetTimestamp() Timestamp {
> > + return s.MessageTimestamp
> > + }
> > +
> > +
> > +Command
> > +-------
> > +
> > +All commands are mapped to its own struct with the additional MessageId
>
> Each command is mapped to its own
Fixed
> > +field for the optional 'id'. If the command has a boxed data struct,
> > +the option struct will be embed in the command struct.
> > +
> > +As commands do require a return value, every command has its own return
> > +type. The Command interface has a GetReturnType() method that returns a
> > +CommandReturn interface, to help Go application handling the data.
> > +
> > +Marshaling and Unmarshaling happens over the Command interface, so
> > +users should use the MarshalCommand() and UnmarshalCommand() methods.
> > +
> > +::
> > +
> > + { 'command': 'set_password',
> > + 'boxed': true,
> > + 'data': 'SetPasswordOptions' }
>
> Since you show the Go type generated for QAPI type SetPasswordOptions,
> you should show the QAPI type here.
Added.
> > +
> > +.. code-block:: go
> > +
> > + type Command interface {
> > + GetId() string
> > + GetName() string
> > + GetReturnType() CommandReturn
> > + }
> > +
> > + // SetPasswordOptions is embed
> > + type SetPasswordCommand struct {
> > + SetPasswordOptions
> > + MessageId string `json:"-"`
> > + }
> > +
> > + // This is an union
> > + type SetPasswordOptions struct {
> > + Protocol DisplayProtocol `json:"protocol"`
> > + Password string `json:"password"`
> > + Connected *SetPasswordAction `json:"connected,omitempty"`
> > +
> > + // Variants fields
> > + Vnc *SetPasswordOptionsVnc `json:"-"`
> > + }
> > +
> > +Now an example of a command without boxed type.
> > +
> > +::
> > +
> > + { 'command': 'set_link',
> > + 'data': {'name': 'str', 'up': 'bool'} }
> > +
> > +.. code-block:: go
> > +
> > + type SetLinkCommand struct {
> > + MessageId string `json:"-"`
> > + Name string `json:"name"`
> > + Up bool `json:"up"`
> > + }
> > +
> > +Known issues
> > +============
> > +
> > +- Type names might not follow proper Go convention. Andrea suggested an
> > + annotation to the QAPI schema that could solve it.
> > + https://lists.gnu.org/archive/html/qemu-devel/2022-05/msg00127.html
Many thanks for the review,
Victor
signature.asc
Description: PGP signature
- Re: [PATCH v2 02/11] scripts: qapi: black format main.py, (continued)
[PATCH v2 08/11] qapi: golang: Generate qapi's event types in Go, Victor Toso, 2023/10/16
[PATCH v2 07/11] qapi: golang: Generate qapi's union types in Go, Victor Toso, 2023/10/16
[PATCH v2 09/11] qapi: golang: Generate qapi's command types in Go, Victor Toso, 2023/10/16
[PATCH v2 10/11] qapi: golang: Add CommandResult type to Go, Victor Toso, 2023/10/16
[PATCH v2 11/11] docs: add notes on Golang code generator, Victor Toso, 2023/10/16
[PATCH v2 06/11] qapi: golang: structs: Address 'null' members, Victor Toso, 2023/10/16
Re: [PATCH v2 00/11] qapi-go: add generator for Golang interface, Victor Toso, 2023/10/27