From: Victor Toso <victortoso@redhat.com>
To: qemu-devel@nongnu.org
Cc: "Markus Armbruster" <armbru@redhat.com>,
"John Snow" <jsnow@redhat.com>,
"Daniel P . Berrangé" <berrange@redhat.com>,
"Andrea Bolognani" <abologna@redhat.com>
Subject: [PATCH v2 11/11] docs: add notes on Golang code generator
Date: Mon, 16 Oct 2023 17:27:04 +0200 [thread overview]
Message-ID: <20231016152704.221611-12-victortoso@redhat.com> (raw)
In-Reply-To: <20231016152704.221611-1-victortoso@redhat.com>
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
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
+==============================
+
+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"`
+ }
+
+
+Union
+-----
+
+Unions in QAPI are binded to a Enum type which provides all possible
+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
+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
+
+Go struct and also implement the Marshal interface.
+
+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.
+
+::
+
+ { 'union': 'ImageInfoSpecificQCow2Encryption',
+ 'base': 'ImageInfoSpecificQCow2EncryptionBase',
+ 'discriminator': 'format',
+ 'data': { 'luks': 'QCryptoBlockInfoLUKS' } }
+
+.. code-block:: go
+
+ type ImageInfoSpecificQCow2Encryption struct {
+ // Variants fields
+ Luks *QCryptoBlockInfoLUKS `json:"-"`
+ // Unbranched enum fields
+ Aes bool `json:"-"`
+ }
+
+ 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
+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
+ 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
+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
+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' }
+
+.. 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
--
2.41.0
next prev parent reply other threads:[~2023-10-16 15:28 UTC|newest]
Thread overview: 43+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-10-16 15:26 [PATCH v2 00/11] qapi-go: add generator for Golang interface Victor Toso
2023-10-16 15:26 ` [PATCH v2 01/11] qapi: re-establish linting baseline Victor Toso
2023-10-16 15:26 ` [PATCH v2 02/11] scripts: qapi: black format main.py Victor Toso
2023-10-18 11:00 ` Markus Armbruster
2023-10-18 11:13 ` Daniel P. Berrangé
2023-10-18 15:23 ` Victor Toso
2023-10-19 5:42 ` Markus Armbruster
2023-10-19 7:30 ` Daniel P. Berrangé
2023-10-16 15:26 ` [PATCH v2 03/11] qapi: golang: Generate qapi's enum types in Go Victor Toso
2023-10-16 15:26 ` [PATCH v2 04/11] qapi: golang: Generate qapi's alternate " Victor Toso
2023-11-06 15:28 ` Andrea Bolognani
2023-11-06 15:52 ` Victor Toso
2023-11-06 16:12 ` Andrea Bolognani
2023-11-09 17:34 ` Andrea Bolognani
2023-11-09 19:01 ` Victor Toso
2023-11-10 9:58 ` Andrea Bolognani
2023-11-10 15:43 ` Victor Toso
2023-10-16 15:26 ` [PATCH v2 05/11] qapi: golang: Generate qapi's struct " Victor Toso
2023-10-16 15:26 ` [PATCH v2 06/11] qapi: golang: structs: Address 'null' members Victor Toso
2023-10-16 15:27 ` [PATCH v2 07/11] qapi: golang: Generate qapi's union types in Go Victor Toso
2023-11-09 17:29 ` Andrea Bolognani
2023-11-09 18:35 ` Victor Toso
2023-11-10 9:54 ` Andrea Bolognani
2023-11-10 15:45 ` Victor Toso
2023-10-16 15:27 ` [PATCH v2 08/11] qapi: golang: Generate qapi's event " Victor Toso
2023-11-09 17:59 ` Andrea Bolognani
2023-11-09 19:13 ` Victor Toso
2023-11-10 9:52 ` Andrea Bolognani
2023-10-16 15:27 ` [PATCH v2 09/11] qapi: golang: Generate qapi's command " Victor Toso
2023-10-16 15:27 ` [PATCH v2 10/11] qapi: golang: Add CommandResult type to Go Victor Toso
2023-11-09 18:24 ` Andrea Bolognani
2023-11-09 19:31 ` Victor Toso
2023-11-10 9:46 ` Andrea Bolognani
2023-10-16 15:27 ` Victor Toso [this message]
2023-10-18 11:47 ` [PATCH v2 11/11] docs: add notes on Golang code generator Markus Armbruster
2023-10-18 16:21 ` Victor Toso
2023-10-19 6:56 ` Markus Armbruster
2023-10-27 17:33 ` [PATCH v2 00/11] qapi-go: add generator for Golang interface Victor Toso
2023-10-31 16:42 ` Andrea Bolognani
2023-11-03 18:34 ` Andrea Bolognani
2023-11-06 12:00 ` Victor Toso
2023-11-09 18:35 ` Andrea Bolognani
2023-11-09 19:03 ` Victor Toso
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20231016152704.221611-12-victortoso@redhat.com \
--to=victortoso@redhat.com \
--cc=abologna@redhat.com \
--cc=armbru@redhat.com \
--cc=berrange@redhat.com \
--cc=jsnow@redhat.com \
--cc=qemu-devel@nongnu.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).