1
Hello!
1
Hi again,
2
2
3
This patch gives the ability to build TCG builtin even if
3
This patch series intent is to introduce a generator that produces a Go
4
--enable-modules is selected. This is useful to have a base
4
module for Go applications to interact over QMP with QEMU.
5
QEMU with TCG native product but still using the benefits of
6
modules.
7
5
8
Thank you!
6
Previous version (10 Jan 2025)
7
https://lists.gnu.org/archive/html/qemu-devel/2025-01/msg01530.html
9
8
10
Jose R. Ziviani (1):
9
The generated code was mostly tested using existing examples in the QAPI
11
modules: Option to build native TCG with --enable-modules
10
documentation, 192 instances that might have multiple QMP messages each.
12
11
13
configure | 12 ++++++++++--
12
You can find the the tests and the generated code in my personal repo,
14
meson.build | 11 ++++++++++-
13
main branch:
15
meson_options.txt | 2 ++
14
16
3 files changed, 22 insertions(+), 3 deletions(-)
15
https://gitlab.com/victortoso/qapi-go
16
17
If you want to see the generated code from QEMU's master but per patch:
18
19
https://gitlab.com/victortoso/qapi-go/-/commits/qapi-golang-v4-by-patch
20
21
If you rather see the diff between v9.1.0, v9.2.0 and latest:
22
23
https://gitlab.com/victortoso/qapi-go/-/commits/qapi-golang-v4-by-tags
24
25
26
#################
27
# Changes in v4 #
28
#################
29
30
1. Daniel wrote a demo on top of v3 and proposed changes that would
31
result in more interesting module to build on top:
32
https://lists.gnu.org/archive/html/qemu-devel/2025-01/msg03052.html
33
34
I've implemented all the suggestions that are relevant for this
35
introductory series, they are:
36
37
a. New struct type Message, that shall be used for a 1st level
38
unmarshalling of the JSON message.
39
b. Removal of Marshal/Unmarshal code in both Events and Comands,
40
together with utility code that is not relevant anymore.
41
c. Declaration of 3 new interfaces:
42
i. Events
43
ii. Commands
44
iii. CommandsAsync
45
46
2. I've moved the code to a new folder: scripts/qapi/golang. This
47
allowed me to move templates out of golang.py, keeping go related
48
code self-contained in the new directory.
49
50
3. As mentioned in (2), created protocol.go and utils.go that are 100%
51
hand generated Go code. Message mentioned in (1a) is under
52
protocol.go
53
54
4. Defined license using SPDX-License-Identifier.
55
a. Every Go source code written by hand is 100% MIT-0
56
b. Every Go source code generated is dual licensed as MIT-0 and
57
GPL-2.0-or-later
58
c. The binary code is expected to be MIT-0 only but not really
59
relevant for this series.
60
61
If you want more information, please check the thread:
62
https://lists.gnu.org/archive/html/qemu-devel/2024-11/msg01621.html
63
64
5. I've renamed the generated files.
65
a. Any type related file is now prefixed with "gen_type_"
66
b. Any interface related file is prefixed as "gen_iface_"
67
68
6. Relevant changes were made to the doc but it is not complete. I plan
69
that follow-up proposals would add to the documentation.
70
71
7. Improvements to the generator were made to.
72
73
8. Also worth to mention that resulting generated code does not have any
74
diff with gofmt and goimport tools, as requested in the past.
75
76
################
77
# Expectations #
78
################
79
80
As is, this still is a PoC that works. I'd like to have the generated
81
code included in QEMU's gitlab [0] in order to write library and tools
82
on top. Initial version should be considered alpha. Moving to
83
beta/stable would require functional libraries and tools, but this work
84
needs to be merged before one commit to that.
85
86
[0] https://lists.gnu.org/archive/html/qemu-devel/2023-09/msg07024.html
87
88
89
##################
90
# Follow-up work #
91
##################
92
93
When this is merged we would need to:
94
1. Create gitlab's repo
95
2. Add unit test and CI to new repos
96
3. Have first alpha relase/tag
97
4. Start working on top :)
98
99
100
Thanks for the time looking at this. I appreciate it.
101
102
Victor Toso (11):
103
qapi: golang: first level unmarshalling type
104
qapi: golang: Generate enum type
105
qapi: golang: Generate alternate types
106
qapi: golang: Generate struct types
107
qapi: golang: structs: Address nullable members
108
qapi: golang: Generate union type
109
qapi: golang: Generate event type
110
qapi: golang: Generate Event interface
111
qapi: golang: Generate command type
112
qapi: golang: Generate Command sync/async interfaces
113
docs: add notes on Golang code generator
114
115
docs/devel/index-build.rst | 1 +
116
docs/devel/qapi-golang-code-gen.rst | 420 ++++++++
117
scripts/qapi/golang/__init__.py | 0
118
scripts/qapi/golang/golang.py | 1444 +++++++++++++++++++++++++++
119
scripts/qapi/golang/protocol.go | 48 +
120
scripts/qapi/golang/utils.go | 38 +
121
scripts/qapi/main.py | 2 +
122
7 files changed, 1953 insertions(+)
123
create mode 100644 docs/devel/qapi-golang-code-gen.rst
124
create mode 100644 scripts/qapi/golang/__init__.py
125
create mode 100644 scripts/qapi/golang/golang.py
126
create mode 100644 scripts/qapi/golang/protocol.go
127
create mode 100644 scripts/qapi/golang/utils.go
17
128
18
--
129
--
19
2.32.0
130
2.48.1
20
21
diff view generated by jsdifflib
New patch
1
1
This first patch introduces protocol.go. It introduces the Message Go
2
struct type that can unmarshall any QMP message.
3
4
It does not handle deeper than 1st layer of the JSON object, that is,
5
with:
6
7
1. {
8
"execute": "query-machines",
9
"arguments": { "compat-props": true }
10
}
11
12
2. {
13
"event": "BALLOON_CHANGE",
14
"data": { "actual": 944766976 },
15
"timestamp": {
16
"seconds": 1267020223,
17
"microseconds": 435656
18
}
19
}
20
21
We will be able to know it is a query-machine command or a
22
balloon-change event. Specific data type to handle arguments/data will
23
be introduced further in the series.
24
25
This patch also introduces the Visitor skeleton with a proper write()
26
function to copy-over the protocol.go to the target destination.
27
28
Note, you can execute any patch of this series with:
29
30
python3 ./scripts/qapi-gen.py -o /tmp/out qapi/qapi-schema.json
31
32
Signed-off-by: Victor Toso <victortoso@redhat.com>
33
---
34
scripts/qapi/golang/__init__.py | 0
35
scripts/qapi/golang/golang.py | 135 ++++++++++++++++++++++++++++++++
36
scripts/qapi/golang/protocol.go | 48 ++++++++++++
37
scripts/qapi/main.py | 2 +
38
4 files changed, 185 insertions(+)
39
create mode 100644 scripts/qapi/golang/__init__.py
40
create mode 100644 scripts/qapi/golang/golang.py
41
create mode 100644 scripts/qapi/golang/protocol.go
42
43
diff --git a/scripts/qapi/golang/__init__.py b/scripts/qapi/golang/__init__.py
44
new file mode 100644
45
index XXXXXXX..XXXXXXX
46
diff --git a/scripts/qapi/golang/golang.py b/scripts/qapi/golang/golang.py
47
new file mode 100644
48
index XXXXXXX..XXXXXXX
49
--- /dev/null
50
+++ b/scripts/qapi/golang/golang.py
51
@@ -XXX,XX +XXX,XX @@
52
+"""
53
+Golang QAPI generator
54
+"""
55
+
56
+# Copyright (c) 2025 Red Hat Inc.
57
+#
58
+# Authors:
59
+# Victor Toso <victortoso@redhat.com>
60
+#
61
+# This work is licensed under the terms of the GNU GPL, version 2.
62
+# See the COPYING file in the top-level directory.
63
+
64
+# Just for type hint on self
65
+from __future__ import annotations
66
+
67
+import os, shutil
68
+from typing import List, Optional
69
+
70
+from ..schema import (
71
+ QAPISchema,
72
+ QAPISchemaBranches,
73
+ QAPISchemaEnumMember,
74
+ QAPISchemaFeature,
75
+ QAPISchemaIfCond,
76
+ QAPISchemaObjectType,
77
+ QAPISchemaObjectTypeMember,
78
+ QAPISchemaType,
79
+ QAPISchemaVariants,
80
+ QAPISchemaVisitor,
81
+)
82
+from ..source import QAPISourceInfo
83
+
84
+
85
+def gen_golang(schema: QAPISchema, output_dir: str, prefix: str) -> None:
86
+ vis = QAPISchemaGenGolangVisitor(prefix)
87
+ schema.visit(vis)
88
+ vis.write(output_dir)
89
+
90
+
91
+class QAPISchemaGenGolangVisitor(QAPISchemaVisitor):
92
+ # pylint: disable=too-many-arguments
93
+ def __init__(self, _: str):
94
+ super().__init__()
95
+ gofiles = ("protocol.go",)
96
+ self.schema: QAPISchema
97
+ self.golang_package_name = "qapi"
98
+ self.duplicate = list(gofiles)
99
+
100
+ def visit_begin(self, schema: QAPISchema) -> None:
101
+ self.schema = schema
102
+
103
+ def visit_end(self) -> None:
104
+ del self.schema
105
+
106
+ def visit_object_type(
107
+ self,
108
+ name: str,
109
+ info: Optional[QAPISourceInfo],
110
+ ifcond: QAPISchemaIfCond,
111
+ features: List[QAPISchemaFeature],
112
+ base: Optional[QAPISchemaObjectType],
113
+ members: List[QAPISchemaObjectTypeMember],
114
+ branches: Optional[QAPISchemaBranches],
115
+ ) -> None:
116
+ pass
117
+
118
+ def visit_alternate_type(
119
+ self,
120
+ name: str,
121
+ info: Optional[QAPISourceInfo],
122
+ ifcond: QAPISchemaIfCond,
123
+ features: List[QAPISchemaFeature],
124
+ variants: QAPISchemaVariants,
125
+ ) -> None:
126
+ pass
127
+
128
+ def visit_enum_type(
129
+ self,
130
+ name: str,
131
+ info: Optional[QAPISourceInfo],
132
+ ifcond: QAPISchemaIfCond,
133
+ features: List[QAPISchemaFeature],
134
+ members: List[QAPISchemaEnumMember],
135
+ prefix: Optional[str],
136
+ ) -> None:
137
+ pass
138
+
139
+ def visit_array_type(
140
+ self,
141
+ name: str,
142
+ info: Optional[QAPISourceInfo],
143
+ ifcond: QAPISchemaIfCond,
144
+ element_type: QAPISchemaType,
145
+ ) -> None:
146
+ pass
147
+
148
+ def visit_command(
149
+ self,
150
+ name: str,
151
+ info: Optional[QAPISourceInfo],
152
+ ifcond: QAPISchemaIfCond,
153
+ features: List[QAPISchemaFeature],
154
+ arg_type: Optional[QAPISchemaObjectType],
155
+ ret_type: Optional[QAPISchemaType],
156
+ gen: bool,
157
+ success_response: bool,
158
+ boxed: bool,
159
+ allow_oob: bool,
160
+ allow_preconfig: bool,
161
+ coroutine: bool,
162
+ ) -> None:
163
+ pass
164
+
165
+ def visit_event(
166
+ self,
167
+ name: str,
168
+ info: Optional[QAPISourceInfo],
169
+ ifcond: QAPISchemaIfCond,
170
+ features: List[QAPISchemaFeature],
171
+ arg_type: Optional[QAPISchemaObjectType],
172
+ boxed: bool,
173
+ ) -> None:
174
+ pass
175
+
176
+ def write(self, outdir: str) -> None:
177
+ godir = "go"
178
+ targetpath = os.path.join(outdir, godir)
179
+ os.makedirs(targetpath, exist_ok=True)
180
+
181
+ # Content to be copied over
182
+ srcdir = os.path.dirname(os.path.realpath(__file__))
183
+ for filename in self.duplicate:
184
+ srcpath = os.path.join(srcdir, filename)
185
+ dstpath = os.path.join(targetpath, filename)
186
+ shutil.copyfile(srcpath, dstpath)
187
diff --git a/scripts/qapi/golang/protocol.go b/scripts/qapi/golang/protocol.go
188
new file mode 100644
189
index XXXXXXX..XXXXXXX
190
--- /dev/null
191
+++ b/scripts/qapi/golang/protocol.go
192
@@ -XXX,XX +XXX,XX @@
193
+/*
194
+ * Copyright 2025 Red Hat, Inc.
195
+ * SPDX-License-Identifier: MIT-0
196
+ *
197
+ * Authors:
198
+ * Victor Toso <victortoso@redhat.com>
199
+ * Daniel P. Berrange <berrange@redhat.com>
200
+ */
201
+package qapi
202
+
203
+import (
204
+    "encoding/json"
205
+    "time"
206
+)
207
+
208
+/* Union of data for command, response, error, or event,
209
+ * since when receiving we don't know upfront which we
210
+ * must deserialize */
211
+type Message struct {
212
+    QMP *json.RawMessage `json:"QMP,omitempty"`
213
+    Execute string `json:"execute,omitempty"`
214
+    ExecOOB string `json:"exec-oob,omitempty"`
215
+    Event string `json:"event,omitempty"`
216
+    Error *json.RawMessage `json:"error,omitempty"`
217
+    Return *json.RawMessage `json:"return,omitempty"`
218
+    ID string `json:"id,omitempty"`
219
+    Timestamp *Timestamp `json:"timestamp,omitempty"`
220
+    Data *json.RawMessage `json:"data,omitempty"`
221
+    Arguments *json.RawMessage `json:"arguments,omitempty"`
222
+}
223
+
224
+type QAPIError struct {
225
+    Class string `json:"class"`
226
+    Description string `json:"desc"`
227
+}
228
+
229
+func (err *QAPIError) Error() string {
230
+    return err.Description
231
+}
232
+
233
+type Timestamp struct {
234
+    Seconds int `json:"seconds"`
235
+    MicroSeconds int `json:"microseconds"`
236
+}
237
+
238
+func (t *Timestamp) AsTime() time.Time {
239
+    return time.Unix(int64(t.Seconds), int64(t.MicroSeconds)*1000)
240
+}
241
diff --git a/scripts/qapi/main.py b/scripts/qapi/main.py
242
index XXXXXXX..XXXXXXX 100644
243
--- a/scripts/qapi/main.py
244
+++ b/scripts/qapi/main.py
245
@@ -XXX,XX +XXX,XX @@
246
from .error import QAPIError
247
from .events import gen_events
248
from .features import gen_features
249
+from .golang import golang
250
from .introspect import gen_introspect
251
from .schema import QAPISchema
252
from .types import gen_types
253
@@ -XXX,XX +XXX,XX @@ def generate(schema_file: str,
254
gen_commands(schema, output_dir, prefix, gen_tracing)
255
gen_events(schema, output_dir, prefix)
256
gen_introspect(schema, output_dir, prefix, unmask)
257
+ golang.gen_golang(schema, output_dir, prefix)
258
259
260
def main() -> int:
261
--
262
2.48.1
diff view generated by jsdifflib
New patch
1
1
This patch handles QAPI enum types and generates its equivalent in Go.
2
We sort the output based on enum's type name.
3
4
Enums are being handled as strings in Golang.
5
6
1. For each QAPI enum, we will define a string type in Go to be the
7
assigned type of this specific enum.
8
9
2. Naming: CamelCase will be used in any identifier that we want to
10
export, which is everything.
11
12
Example:
13
14
qapi:
15
| ##
16
| # @DisplayProtocol:
17
| #
18
| # Display protocols which support changing password options.
19
| #
20
| # Since: 7.0
21
| ##
22
| { 'enum': 'DisplayProtocol',
23
| 'data': [ 'vnc', 'spice' ] }
24
25
go:
26
| // Display protocols which support changing password options.
27
| //
28
| // Since: 7.0
29
| type DisplayProtocol string
30
|
31
| const (
32
|     DisplayProtocolVnc DisplayProtocol = "vnc"
33
|     DisplayProtocolSpice DisplayProtocol = "spice"
34
| )
35
36
Signed-off-by: Victor Toso <victortoso@redhat.com>
37
---
38
scripts/qapi/golang/golang.py | 185 +++++++++++++++++++++++++++++++++-
39
1 file changed, 183 insertions(+), 2 deletions(-)
40
41
diff --git a/scripts/qapi/golang/golang.py b/scripts/qapi/golang/golang.py
42
index XXXXXXX..XXXXXXX 100644
43
--- a/scripts/qapi/golang/golang.py
44
+++ b/scripts/qapi/golang/golang.py
45
@@ -XXX,XX +XXX,XX @@
46
# Just for type hint on self
47
from __future__ import annotations
48
49
-import os, shutil
50
+import os, shutil, textwrap
51
from typing import List, Optional
52
53
from ..schema import (
54
@@ -XXX,XX +XXX,XX @@
55
)
56
from ..source import QAPISourceInfo
57
58
+TEMPLATE_GENERATED_HEADER = """
59
+/*
60
+ * Copyright 2025 Red Hat, Inc.
61
+ * SPDX-License-Identifier: (MIT-0 and GPL-2.0-or-later)
62
+ */
63
+
64
+/****************************************************************************
65
+ * THIS CODE HAS BEEN GENERATED. DO NOT CHANGE IT DIRECTLY *
66
+ ****************************************************************************/
67
+package {package_name}
68
+"""
69
+
70
+TEMPLATE_GO_IMPORTS = """
71
+import (
72
+{imports}
73
+)
74
+"""
75
+
76
+TEMPLATE_ENUM = """
77
+type {name} string
78
+
79
+const (
80
+{fields}
81
+)
82
+"""
83
+
84
+
85
+# Takes the documentation object of a specific type and returns
86
+# that type's documentation and its member's docs.
87
+def qapi_to_golang_struct_docs(doc: QAPIDoc) -> (str, Dict[str, str]):
88
+ if doc is None:
89
+ return "", {}
90
+
91
+ cmt = "// "
92
+ fmt = textwrap.TextWrapper(
93
+ width=70, initial_indent=cmt, subsequent_indent=cmt
94
+ )
95
+ main = fmt.fill(doc.body.text)
96
+
97
+ for section in doc.sections:
98
+ # TODO is not a relevant section to Go applications
99
+ if section.tag in ["TODO"]:
100
+ continue
101
+
102
+ if main != "":
103
+ # Give empty line as space for the tag.
104
+ main += "\n//\n"
105
+
106
+ tag = "" if section.tag is None else f"{section.tag}: "
107
+ text = section.text.replace(" ", " ")
108
+ main += fmt.fill(f"{tag}{text}")
109
+
110
+ fields = {}
111
+ for key, value in doc.args.items():
112
+ if len(value.text) > 0:
113
+ fields[key] = " ".join(value.text.replace("\n", " ").split())
114
+
115
+ return main, fields
116
+
117
118
def gen_golang(schema: QAPISchema, output_dir: str, prefix: str) -> None:
119
vis = QAPISchemaGenGolangVisitor(prefix)
120
@@ -XXX,XX +XXX,XX @@ def gen_golang(schema: QAPISchema, output_dir: str, prefix: str) -> None:
121
vis.write(output_dir)
122
123
124
+def qapi_to_field_name_enum(name: str) -> str:
125
+ return name.title().replace("-", "")
126
+
127
+
128
+def fetch_indent_blocks_over_enum_with_docs(
129
+ name: str, members: List[QAPISchemaEnumMember], docfields: Dict[str, str]
130
+) -> Tuple[int]:
131
+ maxname = 0
132
+ blocks: List[int] = [0]
133
+ for member in members:
134
+ # For simplicity, every time we have doc, we add a new indent block
135
+ hasdoc = member.name is not None and member.name in docfields
136
+
137
+ enum_name = f"{name}{qapi_to_field_name_enum(member.name)}"
138
+ maxname = (
139
+ max(maxname, len(enum_name)) if not hasdoc else len(enum_name)
140
+ )
141
+
142
+ if hasdoc:
143
+ blocks.append(maxname)
144
+ else:
145
+ blocks[-1] = maxname
146
+
147
+ return blocks
148
+
149
+
150
+def generate_content_from_dict(data: dict[str, str]) -> str:
151
+ content = ""
152
+
153
+ for name in sorted(data):
154
+ content += data[name]
155
+
156
+ return content.replace("\n\n\n", "\n\n")
157
+
158
+
159
+def generate_template_imports(words: List[str]) -> str:
160
+ if len(words) == 0:
161
+ return ""
162
+
163
+ if len(words) == 1:
164
+ return '\nimport "{words[0]}"\n'
165
+
166
+ return TEMPLATE_GO_IMPORTS.format(
167
+ imports="\n".join(f'\t"{w}"' for w in words)
168
+ )
169
+
170
+
171
class QAPISchemaGenGolangVisitor(QAPISchemaVisitor):
172
# pylint: disable=too-many-arguments
173
def __init__(self, _: str):
174
super().__init__()
175
gofiles = ("protocol.go",)
176
+ # Map each qapi type to the necessary Go imports
177
+ types = {
178
+ "enum": [],
179
+ }
180
+
181
self.schema: QAPISchema
182
self.golang_package_name = "qapi"
183
self.duplicate = list(gofiles)
184
+ self.enums: dict[str, str] = {}
185
+ self.docmap = {}
186
+
187
+ self.types = dict.fromkeys(types, "")
188
+ self.types_import = types
189
190
def visit_begin(self, schema: QAPISchema) -> None:
191
self.schema = schema
192
193
+ # iterate once in schema.docs to map doc objects to its name
194
+ for doc in schema.docs:
195
+ if doc.symbol is None:
196
+ continue
197
+ self.docmap[doc.symbol] = doc
198
+
199
+ for qapitype, imports in self.types_import.items():
200
+ self.types[qapitype] = TEMPLATE_GENERATED_HEADER[1:].format(
201
+ package_name=self.golang_package_name
202
+ )
203
+ self.types[qapitype] += generate_template_imports(imports)
204
+
205
def visit_end(self) -> None:
206
del self.schema
207
+ self.types["enum"] += generate_content_from_dict(self.enums)
208
209
def visit_object_type(
210
self,
211
@@ -XXX,XX +XXX,XX @@ def visit_enum_type(
212
members: List[QAPISchemaEnumMember],
213
prefix: Optional[str],
214
) -> None:
215
- pass
216
+ assert name not in self.enums
217
+ doc = self.docmap.get(name, None)
218
+ maindoc, docfields = qapi_to_golang_struct_docs(doc)
219
+
220
+ # The logic below is to generate QAPI enums as blocks of Go consts
221
+ # each with its own type for type safety inside Go applications.
222
+ #
223
+ # Block of const() blocks are vertically indented so we have to
224
+ # first iterate over all names to calculate space between
225
+ # $var_name and $var_type. This is achieved by helper function
226
+ # @fetch_indent_blocks_over_enum_with_docs()
227
+ #
228
+ # A new indentation block is defined by empty line or a comment.
229
+
230
+ indent_block = iter(
231
+ fetch_indent_blocks_over_enum_with_docs(name, members, docfields)
232
+ )
233
+ maxname = next(indent_block)
234
+ fields = ""
235
+ for index, member in enumerate(members):
236
+ # For simplicity, every time we have doc, we go to next indent block
237
+ hasdoc = member.name is not None and member.name in docfields
238
+
239
+ if hasdoc:
240
+ maxname = next(indent_block)
241
+
242
+ enum_name = f"{name}{qapi_to_field_name_enum(member.name)}"
243
+ name2type = " " * (maxname - len(enum_name) + 1)
244
+
245
+ if hasdoc:
246
+ docstr = (
247
+ textwrap.TextWrapper(width=80)
248
+ .fill(docfields[member.name])
249
+ .replace("\n", "\n\t// ")
250
+ )
251
+ fields += f"""\t// {docstr}\n"""
252
+
253
+ fields += f"""\t{enum_name}{name2type}{name} = "{member.name}"\n"""
254
+
255
+ if maindoc != "":
256
+ maindoc = f"\n{maindoc}"
257
+
258
+ self.enums[name] = maindoc + TEMPLATE_ENUM.format(
259
+ name=name, fields=fields[:-1]
260
+ )
261
262
def visit_array_type(
263
self,
264
@@ -XXX,XX +XXX,XX @@ def write(self, outdir: str) -> None:
265
srcpath = os.path.join(srcdir, filename)
266
dstpath = os.path.join(targetpath, filename)
267
shutil.copyfile(srcpath, dstpath)
268
+
269
+ # Types to be generated
270
+ for qapitype, content in self.types.items():
271
+ gofile = f"gen_type_{qapitype}.go"
272
+ pathname = os.path.join(targetpath, gofile)
273
+
274
+ with open(pathname, "w", encoding="utf8") as outfile:
275
+ outfile.write(content)
276
--
277
2.48.1
diff view generated by jsdifflib
New patch
1
1
This patch handles QAPI alternate types and generates data structures
2
in Go that handles it.
3
4
Alternate types are similar to Union but without a discriminator that
5
can be used to identify the underlying value on the wire.
6
7
1. Over the wire, we need to infer underlying value by its type
8
9
2. Pointer to types are mapped as optional. Absent value can be a
10
valid value.
11
12
3. We use Go's standard 'encoding/json' library with its Marshal
13
and Unmarshal interfaces.
14
15
4. As an exceptional but valid case, there are types that accept
16
JSON NULL as value. Due to limitations with Go's standard library
17
(point 3) combined with Absent being a possibility (point 2), we
18
translante NULL values to a boolean field called 'IsNull'. See the
19
second example and docs/devel/qapi-golang-code-gen.rst under
20
Alternate section.
21
22
* First example:
23
24
qapi:
25
| ##
26
| # @BlockdevRef:
27
| #
28
| # Reference to a block device.
29
| #
30
| # @definition: defines a new block device inline
31
| #
32
| # @reference: references the ID of an existing block device
33
| #
34
| # Since: 2.9
35
| ##
36
| { 'alternate': 'BlockdevRef',
37
| 'data': { 'definition': 'BlockdevOptions',
38
| 'reference': 'str' } }
39
40
go:
41
| // Reference to a block device.
42
| //
43
| // Since: 2.9
44
| type BlockdevRef struct {
45
|     // defines a new block device inline
46
|     Definition *BlockdevOptions
47
|     // references the ID of an existing block device
48
|     Reference *string
49
| }
50
|
51
| func (s BlockdevRef) MarshalJSON() ([]byte, error) {
52
| ...
53
| }
54
|
55
| func (s *BlockdevRef) UnmarshalJSON(data []byte) error {
56
| ...
57
| }
58
59
usage:
60
| input := `{"driver":"qcow2","data-file":"/some/place/my-image"}`
61
| k := BlockdevRef{}
62
| err := json.Unmarshal([]byte(input), &k)
63
| if err != nil {
64
| panic(err)
65
| }
66
| // *k.Definition.Qcow2.DataFile.Reference == "/some/place/my-image"
67
68
* Second example:
69
70
qapi:
71
| { 'alternate': 'StrOrNull',
72
| 'data': { 's': 'str',
73
| 'n': 'null' } }
74
75
| // This is a string value or the explicit lack of a string (null
76
| // pointer in C). Intended for cases when 'optional absent' already
77
| // has a different meaning.
78
| //
79
| // Since: 2.10
80
| type StrOrNull struct {
81
|     // the string value
82
|     S *string
83
|     // no string value
84
|     IsNull bool
85
| }
86
|
87
| // Helper function to get its underlying Go value or absent of value
88
| func (s *StrOrNull) ToAnyOrAbsent() (any, bool) {
89
| ...
90
| }
91
|
92
| func (s StrOrNull) MarshalJSON() ([]byte, error) {
93
| ...
94
| }
95
|
96
| func (s *StrOrNull) UnmarshalJSON(data []byte) error {
97
| ...
98
| }
99
100
Signed-off-by: Victor Toso <victortoso@redhat.com>
101
---
102
scripts/qapi/golang/golang.py | 306 +++++++++++++++++++++++++++++++++-
103
scripts/qapi/golang/utils.go | 26 +++
104
2 files changed, 329 insertions(+), 3 deletions(-)
105
create mode 100644 scripts/qapi/golang/utils.go
106
107
diff --git a/scripts/qapi/golang/golang.py b/scripts/qapi/golang/golang.py
108
index XXXXXXX..XXXXXXX 100644
109
--- a/scripts/qapi/golang/golang.py
110
+++ b/scripts/qapi/golang/golang.py
111
@@ -XXX,XX +XXX,XX @@
112
from __future__ import annotations
113
114
import os, shutil, textwrap
115
-from typing import List, Optional
116
+from typing import List, Optional, Tuple
117
118
from ..schema import (
119
QAPISchema,
120
+ QAPISchemaAlternateType,
121
QAPISchemaBranches,
122
QAPISchemaEnumMember,
123
QAPISchemaFeature,
124
@@ -XXX,XX +XXX,XX @@
125
)
126
from ..source import QAPISourceInfo
127
128
+FOUR_SPACES = " "
129
+
130
TEMPLATE_GENERATED_HEADER = """
131
/*
132
* Copyright 2025 Red Hat, Inc.
133
@@ -XXX,XX +XXX,XX @@
134
)
135
"""
136
137
+TEMPLATE_ALTERNATE_CHECK_INVALID_JSON_NULL = """
138
+ // Check for json-null first
139
+ if string(data) == "null" {{
140
+ return errors.New(`null not supported for {name}`)
141
+ }}"""
142
+
143
+TEMPLATE_ALTERNATE_NULLABLE_CHECK = """
144
+ }} else if s.{var_name} != nil {{
145
+ return *s.{var_name}, false"""
146
+
147
+TEMPLATE_ALTERNATE_MARSHAL_CHECK = """
148
+ if s.{var_name} != nil {{
149
+ return json.Marshal(s.{var_name})
150
+ }} else """
151
+
152
+TEMPLATE_ALTERNATE_UNMARSHAL_CHECK = """
153
+ // Check for {var_type}
154
+ {{
155
+ s.{var_name} = new({var_type})
156
+ if err := strictDecode(s.{var_name}, data); err == nil {{
157
+ return nil
158
+ }}
159
+ s.{var_name} = nil
160
+ }}
161
+
162
+"""
163
+
164
+TEMPLATE_ALTERNATE_NULLABLE_MARSHAL_CHECK = """
165
+ if s.IsNull {
166
+ return []byte("null"), nil
167
+ } else """
168
+
169
+TEMPLATE_ALTERNATE_NULLABLE_UNMARSHAL_CHECK = """
170
+ // Check for json-null first
171
+ if string(data) == "null" {
172
+ s.IsNull = true
173
+ return nil
174
+ }"""
175
+
176
+TEMPLATE_ALTERNATE_METHODS = """
177
+func (s {name}) MarshalJSON() ([]byte, error) {{
178
+{marshal_check_fields}
179
+ return {marshal_return_default}
180
+}}
181
+
182
+func (s *{name}) UnmarshalJSON(data []byte) error {{
183
+{unmarshal_check_fields}
184
+ return fmt.Errorf("Can't convert to {name}: %s", string(data))
185
+}}
186
+"""
187
+
188
189
# Takes the documentation object of a specific type and returns
190
# that type's documentation and its member's docs.
191
@@ -XXX,XX +XXX,XX @@ def gen_golang(schema: QAPISchema, output_dir: str, prefix: str) -> None:
192
vis.write(output_dir)
193
194
195
+def qapi_to_field_name(name: str) -> str:
196
+ return name.title().replace("_", "").replace("-", "")
197
+
198
+
199
def qapi_to_field_name_enum(name: str) -> str:
200
return name.title().replace("-", "")
201
202
203
+def qapi_schema_type_to_go_type(qapitype: str) -> str:
204
+ schema_types_to_go = {
205
+ "str": "string",
206
+ "null": "nil",
207
+ "bool": "bool",
208
+ "number": "float64",
209
+ "size": "uint64",
210
+ "int": "int64",
211
+ "int8": "int8",
212
+ "int16": "int16",
213
+ "int32": "int32",
214
+ "int64": "int64",
215
+ "uint8": "uint8",
216
+ "uint16": "uint16",
217
+ "uint32": "uint32",
218
+ "uint64": "uint64",
219
+ "any": "any",
220
+ "QType": "QType",
221
+ }
222
+
223
+ prefix = ""
224
+ if qapitype.endswith("List"):
225
+ prefix = "[]"
226
+ qapitype = qapitype[:-4]
227
+
228
+ qapitype = schema_types_to_go.get(qapitype, qapitype)
229
+ return prefix + qapitype
230
+
231
+
232
+# Helper for Alternate generation
233
+def qapi_field_to_alternate_go_field(
234
+ member_name: str, type_name: str
235
+) -> Tuple[str, str, str]:
236
+ # Nothing to generate on null types. We update some
237
+ # variables to handle json-null on marshalling methods.
238
+ if type_name == "null":
239
+ return "IsNull", "bool", ""
240
+
241
+ # On Alternates, fields are optional represented in Go as pointer
242
+ return (
243
+ qapi_to_field_name(member_name),
244
+ qapi_schema_type_to_go_type(type_name),
245
+ "*",
246
+ )
247
+
248
+
249
+def fetch_indent_blocks_over_args(
250
+ args: List[dict[str:str]],
251
+) -> Tuple[int, int]:
252
+ maxname, maxtype = 0, 0
253
+ blocks: tuple(int, int) = []
254
+ for arg in args:
255
+ if "comment" in arg or "doc" in arg:
256
+ blocks.append((maxname, maxtype))
257
+ maxname, maxtype = 0, 0
258
+
259
+ if "comment" in arg:
260
+ # They are single blocks
261
+ continue
262
+
263
+ if "type" not in arg:
264
+ # Embed type are on top of the struct and the following
265
+ # fields do not consider it for formatting
266
+ blocks.append((maxname, maxtype))
267
+ maxname, maxtype = 0, 0
268
+ continue
269
+
270
+ maxname = max(maxname, len(arg.get("name", "")))
271
+ maxtype = max(maxtype, len(arg.get("type", "")))
272
+
273
+ blocks.append((maxname, maxtype))
274
+ return blocks
275
+
276
+
277
def fetch_indent_blocks_over_enum_with_docs(
278
name: str, members: List[QAPISchemaEnumMember], docfields: Dict[str, str]
279
) -> Tuple[int]:
280
@@ -XXX,XX +XXX,XX @@ def fetch_indent_blocks_over_enum_with_docs(
281
return blocks
282
283
284
+# Helper function for boxed or self contained structures.
285
+def generate_struct_type(
286
+ type_name,
287
+ type_doc: str = "",
288
+ args: List[dict[str:str]] = None,
289
+ indent: int = 0,
290
+) -> str:
291
+ base_indent = FOUR_SPACES * indent
292
+
293
+ with_type = ""
294
+ if type_name != "":
295
+ with_type = f"\n{base_indent}type {type_name}"
296
+
297
+ if type_doc != "":
298
+ # Append line jump only if type_doc exists
299
+ type_doc = f"\n{type_doc}"
300
+
301
+ if args is None:
302
+ # No args, early return
303
+ return f"""{type_doc}{with_type} struct{{}}"""
304
+
305
+ # The logic below is to generate fields of the struct.
306
+ # We have to be mindful of the different indentation possibilities between
307
+ # $var_name $var_type $var_tag that are vertically indented with gofmt.
308
+ #
309
+ # So, we first have to iterate over all args and find all indent blocks
310
+ # by calculating the spaces between (1) member and type and between (2)
311
+ # the type and tag. (1) and (2) is the tuple present in List returned
312
+ # by the helper function fetch_indent_blocks_over_args.
313
+ inner_indent = base_indent + FOUR_SPACES
314
+ doc_indent = inner_indent + "// "
315
+ fmt = textwrap.TextWrapper(
316
+ width=70, initial_indent=doc_indent, subsequent_indent=doc_indent
317
+ )
318
+
319
+ indent_block = iter(fetch_indent_blocks_over_args(args))
320
+ maxname, maxtype = next(indent_block)
321
+ members = " {\n"
322
+ for index, arg in enumerate(args):
323
+ if "comment" in arg:
324
+ maxname, maxtype = next(indent_block)
325
+ members += f""" // {arg["comment"]}\n"""
326
+ # comments are single blocks, so we can skip to next arg
327
+ continue
328
+
329
+ name2type = ""
330
+ if "doc" in arg:
331
+ maxname, maxtype = next(indent_block)
332
+ members += fmt.fill(arg["doc"])
333
+ members += "\n"
334
+
335
+ name = arg["name"]
336
+ if "type" in arg:
337
+ namelen = len(name)
338
+ name2type = " " * max(1, (maxname - namelen + 1))
339
+
340
+ type2tag = ""
341
+ if "tag" in arg:
342
+ typelen = len(arg["type"])
343
+ type2tag = " " * max(1, (maxtype - typelen + 1))
344
+
345
+ gotype = arg.get("type", "")
346
+ tag = arg.get("tag", "")
347
+ members += (
348
+ f"""{inner_indent}{name}{name2type}{gotype}{type2tag}{tag}\n"""
349
+ )
350
+
351
+ members += f"{base_indent}}}\n"
352
+ return f"""{type_doc}{with_type} struct{members}"""
353
+
354
+
355
+def generate_template_alternate(
356
+ self: QAPISchemaGenGolangVisitor,
357
+ name: str,
358
+ variants: Optional[QAPISchemaVariants],
359
+) -> str:
360
+ args: List[dict[str:str]] = []
361
+ nullable = name in self.accept_null_types
362
+ if nullable:
363
+ # Examples in QEMU QAPI schema: StrOrNull and BlockdevRefOrNull
364
+ marshal_return_default = """[]byte("{}"), nil"""
365
+ marshal_check_fields = TEMPLATE_ALTERNATE_NULLABLE_MARSHAL_CHECK[1:]
366
+ unmarshal_check_fields = TEMPLATE_ALTERNATE_NULLABLE_UNMARSHAL_CHECK
367
+ else:
368
+ marshal_return_default = f'nil, errors.New("{name} has empty fields")'
369
+ marshal_check_fields = ""
370
+ unmarshal_check_fields = (
371
+ TEMPLATE_ALTERNATE_CHECK_INVALID_JSON_NULL.format(name=name)
372
+ )
373
+
374
+ doc = self.docmap.get(name, None)
375
+ content, docfields = qapi_to_golang_struct_docs(doc)
376
+ if variants:
377
+ for var in variants.variants:
378
+ var_name, var_type, isptr = qapi_field_to_alternate_go_field(
379
+ var.name, var.type.name
380
+ )
381
+ args.append(
382
+ {
383
+ "name": f"{var_name}",
384
+ "type": f"{isptr}{var_type}",
385
+ "doc": docfields.get(var.name, ""),
386
+ }
387
+ )
388
+ # Null is special, handled first
389
+ if var.type.name == "null":
390
+ assert nullable
391
+ continue
392
+
393
+ skip_indent = 1 + len(FOUR_SPACES)
394
+ if marshal_check_fields == "":
395
+ skip_indent = 1
396
+ marshal_check_fields += TEMPLATE_ALTERNATE_MARSHAL_CHECK[
397
+ skip_indent:
398
+ ].format(var_name=var_name)
399
+ unmarshal_check_fields += TEMPLATE_ALTERNATE_UNMARSHAL_CHECK[
400
+ :-1
401
+ ].format(var_name=var_name, var_type=var_type)
402
+
403
+ content += string_to_code(generate_struct_type(name, args=args))
404
+ content += string_to_code(
405
+ TEMPLATE_ALTERNATE_METHODS.format(
406
+ name=name,
407
+ marshal_check_fields=marshal_check_fields[:-6],
408
+ marshal_return_default=marshal_return_default,
409
+ unmarshal_check_fields=unmarshal_check_fields[1:],
410
+ )
411
+ )
412
+ return "\n" + content
413
+
414
+
415
def generate_content_from_dict(data: dict[str, str]) -> str:
416
content = ""
417
418
@@ -XXX,XX +XXX,XX @@ def generate_content_from_dict(data: dict[str, str]) -> str:
419
return content.replace("\n\n\n", "\n\n")
420
421
422
+def string_to_code(text: str) -> str:
423
+ DOUBLE_BACKTICK = "``"
424
+ result = ""
425
+ for line in text.splitlines():
426
+ # replace left four spaces with tabs
427
+ limit = len(line) - len(line.lstrip())
428
+ result += line[:limit].replace(FOUR_SPACES, "\t")
429
+
430
+ # work with the rest of the line
431
+ if line[limit : limit + 2] == "//":
432
+ # gofmt tool does not like comments with backticks.
433
+ result += line[limit:].replace(DOUBLE_BACKTICK, '"')
434
+ else:
435
+ result += line[limit:]
436
+ result += "\n"
437
+
438
+ return result
439
+
440
+
441
def generate_template_imports(words: List[str]) -> str:
442
if len(words) == 0:
443
return ""
444
@@ -XXX,XX +XXX,XX @@ class QAPISchemaGenGolangVisitor(QAPISchemaVisitor):
445
# pylint: disable=too-many-arguments
446
def __init__(self, _: str):
447
super().__init__()
448
- gofiles = ("protocol.go",)
449
+ gofiles = ("protocol.go", "utils.go")
450
# Map each qapi type to the necessary Go imports
451
types = {
452
+ "alternate": ["encoding/json", "errors", "fmt"],
453
"enum": [],
454
}
455
456
@@ -XXX,XX +XXX,XX @@ def __init__(self, _: str):
457
self.golang_package_name = "qapi"
458
self.duplicate = list(gofiles)
459
self.enums: dict[str, str] = {}
460
+ self.alternates: dict[str, str] = {}
461
+ self.accept_null_types = []
462
self.docmap = {}
463
464
self.types = dict.fromkeys(types, "")
465
@@ -XXX,XX +XXX,XX @@ def __init__(self, _: str):
466
def visit_begin(self, schema: QAPISchema) -> None:
467
self.schema = schema
468
469
+ # We need to be aware of any types that accept JSON NULL
470
+ for name, entity in self.schema._entity_dict.items():
471
+ if not isinstance(entity, QAPISchemaAlternateType):
472
+ # Assume that only Alternate types accept JSON NULL
473
+ continue
474
+
475
+ for var in entity.alternatives.variants:
476
+ if var.type.name == "null":
477
+ self.accept_null_types.append(name)
478
+ break
479
+
480
# iterate once in schema.docs to map doc objects to its name
481
for doc in schema.docs:
482
if doc.symbol is None:
483
@@ -XXX,XX +XXX,XX @@ def visit_begin(self, schema: QAPISchema) -> None:
484
def visit_end(self) -> None:
485
del self.schema
486
self.types["enum"] += generate_content_from_dict(self.enums)
487
+ self.types["alternate"] += generate_content_from_dict(self.alternates)
488
489
def visit_object_type(
490
self,
491
@@ -XXX,XX +XXX,XX @@ def visit_alternate_type(
492
features: List[QAPISchemaFeature],
493
variants: QAPISchemaVariants,
494
) -> None:
495
- pass
496
+ assert name not in self.alternates
497
+ self.alternates[name] = generate_template_alternate(
498
+ self, name, variants
499
+ )
500
501
def visit_enum_type(
502
self,
503
diff --git a/scripts/qapi/golang/utils.go b/scripts/qapi/golang/utils.go
504
new file mode 100644
505
index XXXXXXX..XXXXXXX
506
--- /dev/null
507
+++ b/scripts/qapi/golang/utils.go
508
@@ -XXX,XX +XXX,XX @@
509
+/*
510
+ * Copyright 2025 Red Hat, Inc.
511
+ * SPDX-License-Identifier: MIT-0
512
+ *
513
+ * Authors:
514
+ * Victor Toso <victortoso@redhat.com>
515
+ */
516
+package qapi
517
+
518
+import (
519
+    "encoding/json"
520
+    "strings"
521
+)
522
+
523
+// Creates a decoder that errors on unknown Fields
524
+// Returns nil if successfully decoded @from payload to @into type
525
+// Returns error if failed to decode @from payload to @into type
526
+func strictDecode(into interface{}, from []byte) error {
527
+    dec := json.NewDecoder(strings.NewReader(string(from)))
528
+    dec.DisallowUnknownFields()
529
+
530
+    if err := dec.Decode(into); err != nil {
531
+        return err
532
+    }
533
+    return nil
534
+}
535
--
536
2.48.1
diff view generated by jsdifflib
New patch
1
1
This patch handles QAPI struct types and generates the equivalent
2
types in Go. The following patch adds extra logic when a member of the
3
struct has a Type that can take JSON Null value (e.g: StrOrNull in
4
QEMU)
5
6
The highlights of this implementation are:
7
8
1. Generating a Go struct that requires a @base type, the @base type
9
fields are copied over to the Go struct. The advantage of this
10
approach is to not have embed structs in any of the QAPI types.
11
Note that embedding a @base type is recursive, that is, if the
12
@base type has a @base, all of those fields will be copied over.
13
14
2. About the Go struct's fields:
15
16
i) They can be either by Value or Reference.
17
18
ii) Every field that is marked as optional in the QAPI specification
19
are translated to Reference fields in its Go structure. This
20
design decision is the most straightforward way to check if a
21
given field was set or not. Exception only for types that can
22
take JSON Null value.
23
24
iii) Mandatory fields are always by Value with the exception of QAPI
25
arrays, which are handled by Reference (to a block of memory) by
26
Go.
27
28
iv) All the fields are named with Uppercase due Golang's export
29
convention.
30
31
Example:
32
33
qapi:
34
| ##
35
| # @BlockdevCreateOptionsFile:
36
| #
37
| # Driver specific image creation options for file.
38
| #
39
| # @filename: Filename for the new image file
40
| #
41
| # @size: Size of the virtual disk in bytes
42
| #
43
| # @preallocation: Preallocation mode for the new image (default: off;
44
| # allowed values: off, falloc (if CONFIG_POSIX_FALLOCATE), full
45
| # (if CONFIG_POSIX))
46
| #
47
| # @nocow: Turn off copy-on-write (valid only on btrfs; default: off)
48
| #
49
| # @extent-size-hint: Extent size hint to add to the image file; 0 for
50
| # not adding an extent size hint (default: 1 MB, since 5.1)
51
| #
52
| # Since: 2.12
53
| ##
54
| { 'struct': 'BlockdevCreateOptionsFile',
55
| 'data': { 'filename': 'str',
56
| 'size': 'size',
57
| '*preallocation': 'PreallocMode',
58
| '*nocow': 'bool',
59
| '*extent-size-hint': 'size'} }
60
61
go:
62
| // Driver specific image creation options for file.
63
| //
64
| // Since: 2.12
65
| type BlockdevCreateOptionsFile struct {
66
|     // Filename for the new image file
67
|     Filename string `json:"filename"`
68
|     // Size of the virtual disk in bytes
69
|     Size uint64 `json:"size"`
70
|     // Preallocation mode for the new image (default: off; allowed
71
|     // values: off, falloc (if CONFIG_POSIX_FALLOCATE), full (if
72
|     // CONFIG_POSIX))
73
|     Preallocation *PreallocMode `json:"preallocation,omitempty"`
74
|     // Turn off copy-on-write (valid only on btrfs; default: off)
75
|     Nocow *bool `json:"nocow,omitempty"`
76
|     // Extent size hint to add to the image file; 0 for not adding an
77
|     // extent size hint (default: 1 MB, since 5.1)
78
|     ExtentSizeHint *uint64 `json:"extent-size-hint,omitempty"`
79
| }
80
81
Signed-off-by: Victor Toso <victortoso@redhat.com>
82
---
83
scripts/qapi/golang/golang.py | 193 +++++++++++++++++++++++++++++++++-
84
1 file changed, 192 insertions(+), 1 deletion(-)
85
86
diff --git a/scripts/qapi/golang/golang.py b/scripts/qapi/golang/golang.py
87
index XXXXXXX..XXXXXXX 100644
88
--- a/scripts/qapi/golang/golang.py
89
+++ b/scripts/qapi/golang/golang.py
90
@@ -XXX,XX +XXX,XX @@ def gen_golang(schema: QAPISchema, output_dir: str, prefix: str) -> None:
91
vis.write(output_dir)
92
93
94
+def qapi_name_is_base(name: str) -> bool:
95
+ return qapi_name_is_object(name) and name.endswith("-base")
96
+
97
+
98
+def qapi_name_is_object(name: str) -> bool:
99
+ return name.startswith("q_obj_")
100
+
101
+
102
def qapi_to_field_name(name: str) -> str:
103
return name.title().replace("_", "").replace("-", "")
104
105
@@ -XXX,XX +XXX,XX @@ def qapi_to_field_name_enum(name: str) -> str:
106
return name.title().replace("-", "")
107
108
109
+def qapi_to_go_type_name(name: str) -> str:
110
+ # We want to keep CamelCase for Golang types. We want to avoid removing
111
+ # already set CameCase names while fixing uppercase ones, eg:
112
+ # 1) q_obj_SocketAddress_base -> SocketAddressBase
113
+ # 2) q_obj_WATCHDOG-arg -> WatchdogArg
114
+
115
+ if qapi_name_is_object(name):
116
+ # Remove q_obj_ prefix
117
+ name = name[6:]
118
+
119
+ # Handle CamelCase
120
+ words = list(name.replace("_", "-").split("-"))
121
+ name = words[0]
122
+ if name.islower() or name.isupper():
123
+ name = name.title()
124
+
125
+ name += "".join(word.title() for word in words[1:])
126
+
127
+ return name
128
+
129
+
130
def qapi_schema_type_to_go_type(qapitype: str) -> str:
131
schema_types_to_go = {
132
"str": "string",
133
@@ -XXX,XX +XXX,XX @@ def generate_struct_type(
134
return f"""{type_doc}{with_type} struct{members}"""
135
136
137
+def get_struct_field(
138
+ self: QAPISchemaGenGolangVisitor,
139
+ qapi_name: str,
140
+ qapi_type_name: str,
141
+ field_doc: str,
142
+ is_optional: bool,
143
+ is_variant: bool,
144
+) -> dict[str:str]:
145
+ field = qapi_to_field_name(qapi_name)
146
+ member_type = qapi_schema_type_to_go_type(qapi_type_name)
147
+
148
+ optional = ""
149
+ if is_optional:
150
+ if member_type not in self.accept_null_types:
151
+ optional = ",omitempty"
152
+
153
+ # Use pointer to type when field is optional
154
+ isptr = "*" if is_optional and member_type[0] not in "*[" else ""
155
+
156
+ fieldtag = (
157
+ '`json:"-"`' if is_variant else f'`json:"{qapi_name}{optional}"`'
158
+ )
159
+ arg = {
160
+ "name": f"{field}",
161
+ "type": f"{isptr}{member_type}",
162
+ "tag": f"{fieldtag}",
163
+ }
164
+ if field_doc != "":
165
+ arg["doc"] = field_doc
166
+
167
+ return arg
168
+
169
+
170
+def recursive_base(
171
+ self: QAPISchemaGenGolangVisitor,
172
+ base: Optional[QAPISchemaObjectType],
173
+) -> List[dict[str:str]]:
174
+ fields: List[dict[str:str]] = []
175
+
176
+ if not base:
177
+ return fields
178
+
179
+ if base.base is not None:
180
+ embed_base = self.schema.lookup_entity(base.base.name)
181
+ fields = recursive_base(self, embed_base)
182
+
183
+ doc = self.docmap.get(base.name, None)
184
+ _, docfields = qapi_to_golang_struct_docs(doc)
185
+
186
+ for member in base.local_members:
187
+ field_doc = docfields.get(member.name, "")
188
+ field = get_struct_field(
189
+ self,
190
+ member.name,
191
+ member.type.name,
192
+ field_doc,
193
+ member.optional,
194
+ False,
195
+ )
196
+ fields.append(field)
197
+
198
+ return fields
199
+
200
+
201
+# Helper function that is used for most of QAPI types
202
+def qapi_to_golang_struct(
203
+ self: QAPISchemaGenGolangVisitor,
204
+ name: str,
205
+ info: Optional[QAPISourceInfo],
206
+ __: QAPISchemaIfCond,
207
+ ___: List[QAPISchemaFeature],
208
+ base: Optional[QAPISchemaObjectType],
209
+ members: List[QAPISchemaObjectTypeMember],
210
+ variants: Optional[QAPISchemaVariants],
211
+ indent: int = 0,
212
+ doc_enabled: bool = True,
213
+) -> str:
214
+ fields = recursive_base(self, base)
215
+
216
+ doc = self.docmap.get(name, None)
217
+ type_doc, docfields = qapi_to_golang_struct_docs(doc)
218
+ if not doc_enabled:
219
+ type_doc = ""
220
+
221
+ if members:
222
+ for member in members:
223
+ field_doc = docfields.get(member.name, "") if doc_enabled else ""
224
+ field = get_struct_field(
225
+ self,
226
+ member.name,
227
+ member.type.name,
228
+ field_doc,
229
+ member.optional,
230
+ False,
231
+ )
232
+ fields.append(field)
233
+
234
+ exists = {}
235
+ if variants:
236
+ fields.append({"comment": "Variants fields"})
237
+ for variant in variants.variants:
238
+ if variant.type.is_implicit():
239
+ continue
240
+
241
+ exists[variant.name] = True
242
+ field_doc = docfields.get(variant.name, "") if doc_enabled else ""
243
+ field = get_struct_field(
244
+ self,
245
+ variant.name,
246
+ variant.type.name,
247
+ field_doc,
248
+ True,
249
+ True,
250
+ )
251
+ fields.append(field)
252
+
253
+ type_name = qapi_to_go_type_name(name)
254
+ content = string_to_code(
255
+ generate_struct_type(
256
+ type_name, type_doc=type_doc, args=fields, indent=indent
257
+ )
258
+ )
259
+ return content
260
+
261
+
262
def generate_template_alternate(
263
self: QAPISchemaGenGolangVisitor,
264
name: str,
265
@@ -XXX,XX +XXX,XX @@ def __init__(self, _: str):
266
types = {
267
"alternate": ["encoding/json", "errors", "fmt"],
268
"enum": [],
269
+ "struct": ["encoding/json"],
270
}
271
272
self.schema: QAPISchema
273
@@ -XXX,XX +XXX,XX @@ def __init__(self, _: str):
274
self.duplicate = list(gofiles)
275
self.enums: dict[str, str] = {}
276
self.alternates: dict[str, str] = {}
277
+ self.structs: dict[str, str] = {}
278
self.accept_null_types = []
279
self.docmap = {}
280
281
@@ -XXX,XX +XXX,XX @@ def visit_end(self) -> None:
282
del self.schema
283
self.types["enum"] += generate_content_from_dict(self.enums)
284
self.types["alternate"] += generate_content_from_dict(self.alternates)
285
+ self.types["struct"] += generate_content_from_dict(self.structs)
286
287
def visit_object_type(
288
self,
289
@@ -XXX,XX +XXX,XX @@ def visit_object_type(
290
members: List[QAPISchemaObjectTypeMember],
291
branches: Optional[QAPISchemaBranches],
292
) -> None:
293
- pass
294
+ # Do not handle anything besides struct.
295
+ if (
296
+ name == self.schema.the_empty_object_type.name
297
+ or not isinstance(name, str)
298
+ or info.defn_meta not in ["struct"]
299
+ ):
300
+ return
301
+
302
+ # Base structs are embed
303
+ if qapi_name_is_base(name):
304
+ return
305
+
306
+ # visit all inner objects as well, they are not going to be
307
+ # called by python's generator.
308
+ if branches:
309
+ for branch in branches.variants:
310
+ assert isinstance(branch.type, QAPISchemaObjectType)
311
+ self.visit_object_type(
312
+ self,
313
+ branch.type.name,
314
+ branch.type.info,
315
+ branch.type.ifcond,
316
+ branch.type.base,
317
+ branch.type.local_members,
318
+ branch.type.branches,
319
+ )
320
+
321
+ # Save generated Go code to be written later
322
+ if info.defn_meta == "struct":
323
+ assert name not in self.structs
324
+ self.structs[name] = string_to_code(
325
+ qapi_to_golang_struct(
326
+ self, name, info, ifcond, features, base, members, branches
327
+ )
328
+ )
329
330
def visit_alternate_type(
331
self,
332
--
333
2.48.1
diff view generated by jsdifflib
New patch
1
1
Explaining why this is needed needs some context, so taking the
2
example of StrOrNull alternate type and considering a simplified
3
struct that has two fields:
4
5
qapi:
6
| { 'struct': 'MigrationExample',
7
| 'data': { '*label': 'StrOrNull',
8
| 'target': 'StrOrNull' } }
9
10
We have an optional member 'label' which can have three JSON values:
11
1. A string: { "target": "a.host.com", "label": "happy" }
12
2. A null : { "target": "a.host.com", "label": null }
13
3. Absent : { "target": null}
14
15
The member 'target' is not optional, hence it can't be absent.
16
17
A Go struct that contains an optional type that can be JSON Null like
18
'label' in the example above, will need extra care when Marshaling and
19
Unmarshaling from JSON.
20
21
This patch handles this very specific case:
22
- It implements the Marshaler interface for these structs to properly
23
handle these values.
24
- It adds the interface AbsentAlternate() and implement it for any
25
Alternate that can be JSON Null. See its uses in map_and_set()
26
27
Signed-off-by: Victor Toso <victortoso@redhat.com>
28
---
29
scripts/qapi/golang/golang.py | 290 ++++++++++++++++++++++++++++++++--
30
1 file changed, 279 insertions(+), 11 deletions(-)
31
32
diff --git a/scripts/qapi/golang/golang.py b/scripts/qapi/golang/golang.py
33
index XXXXXXX..XXXXXXX 100644
34
--- a/scripts/qapi/golang/golang.py
35
+++ b/scripts/qapi/golang/golang.py
36
@@ -XXX,XX +XXX,XX @@
37
)
38
"""
39
40
+TEMPLATE_ALTERNATE = """
41
+// Only implemented on Alternate types that can take JSON NULL as value.
42
+//
43
+// This is a helper for the marshalling code. It should return true only when
44
+// the Alternate is empty (no members are set), otherwise it returns false and
45
+// the member set to be Marshalled.
46
+type AbsentAlternate interface {
47
+ ToAnyOrAbsent() (any, bool)
48
+}
49
+"""
50
+
51
TEMPLATE_ALTERNATE_CHECK_INVALID_JSON_NULL = """
52
// Check for json-null first
53
if string(data) == "null" {{
54
@@ -XXX,XX +XXX,XX @@
55
return nil
56
}"""
57
58
+TEMPLATE_ALTERNATE_NULLABLE = """
59
+func (s *{name}) ToAnyOrAbsent() (any, bool) {{
60
+ if s != nil {{
61
+ if s.IsNull {{
62
+ return nil, false
63
+{absent_check_fields}
64
+ }}
65
+ }}
66
+
67
+ return nil, true
68
+}}
69
+"""
70
+
71
TEMPLATE_ALTERNATE_METHODS = """
72
func (s {name}) MarshalJSON() ([]byte, error) {{
73
{marshal_check_fields}
74
@@ -XXX,XX +XXX,XX @@
75
"""
76
77
78
+TEMPLATE_STRUCT_WITH_NULLABLE_MARSHAL = """
79
+func (s {type_name}) MarshalJSON() ([]byte, error) {{
80
+ m := make(map[string]any)
81
+{map_members}{map_special}
82
+ return json.Marshal(&m)
83
+}}
84
+
85
+func (s *{type_name}) UnmarshalJSON(data []byte) error {{
86
+ tmp := {struct}{{}}
87
+
88
+ if err := json.Unmarshal(data, &tmp); err != nil {{
89
+ return err
90
+ }}
91
+
92
+{set_members}{set_special}
93
+ return nil
94
+}}
95
+"""
96
+
97
+
98
# Takes the documentation object of a specific type and returns
99
# that type's documentation and its member's docs.
100
def qapi_to_golang_struct_docs(doc: QAPIDoc) -> (str, Dict[str, str]):
101
@@ -XXX,XX +XXX,XX @@ def get_struct_field(
102
qapi_name: str,
103
qapi_type_name: str,
104
field_doc: str,
105
+ within_nullable_struct: bool,
106
is_optional: bool,
107
is_variant: bool,
108
-) -> dict[str:str]:
109
+) -> Tuple[dict[str:str], bool]:
110
field = qapi_to_field_name(qapi_name)
111
member_type = qapi_schema_type_to_go_type(qapi_type_name)
112
+ is_nullable = False
113
114
optional = ""
115
if is_optional:
116
- if member_type not in self.accept_null_types:
117
+ if member_type in self.accept_null_types:
118
+ is_nullable = True
119
+ else:
120
optional = ",omitempty"
121
122
# Use pointer to type when field is optional
123
isptr = "*" if is_optional and member_type[0] not in "*[" else ""
124
125
+ if within_nullable_struct:
126
+ # Within a struct which has a field of type that can hold JSON NULL,
127
+ # we have to _not_ use a pointer, otherwise the Marshal methods are
128
+ # not called.
129
+ isptr = "" if member_type in self.accept_null_types else isptr
130
+
131
fieldtag = (
132
'`json:"-"`' if is_variant else f'`json:"{qapi_name}{optional}"`'
133
)
134
@@ -XXX,XX +XXX,XX @@ def get_struct_field(
135
if field_doc != "":
136
arg["doc"] = field_doc
137
138
- return arg
139
+ return arg, is_nullable
140
+
141
+
142
+# This helper is used whithin a struct that has members that accept JSON NULL.
143
+def map_and_set(
144
+ is_nullable: bool, field: str, field_is_optional: bool, name: str
145
+) -> Tuple[str, str]:
146
+ mapstr = ""
147
+ setstr = ""
148
+ if is_nullable:
149
+ mapstr = f"""
150
+ if val, absent := s.{field}.ToAnyOrAbsent(); !absent {{
151
+ m["{name}"] = val
152
+ }}
153
+"""
154
+ setstr += f"""
155
+ if _, absent := (&tmp.{field}).ToAnyOrAbsent(); !absent {{
156
+ s.{field} = &tmp.{field}
157
+ }}
158
+"""
159
+ elif field_is_optional:
160
+ mapstr = f"""
161
+ if s.{field} != nil {{
162
+ m["{name}"] = s.{field}
163
+ }}
164
+"""
165
+ setstr = f""" s.{field} = tmp.{field}\n"""
166
+ else:
167
+ mapstr = f""" m["{name}"] = s.{field}\n"""
168
+ setstr = f""" s.{field} = tmp.{field}\n"""
169
+
170
+ return mapstr, setstr
171
+
172
+
173
+def recursive_base_nullable(
174
+ self: QAPISchemaGenGolangVisitor, base: Optional[QAPISchemaObjectType]
175
+) -> Tuple[List[dict[str:str]], str, str, str, str]:
176
+ fields: List[dict[str:str]] = []
177
+ map_members = ""
178
+ set_members = ""
179
+ map_special = ""
180
+ set_special = ""
181
+
182
+ if not base:
183
+ return fields, map_members, set_members, map_special, set_special
184
+
185
+ doc = self.docmap.get(base.name, None)
186
+ _, docfields = qapi_to_golang_struct_docs(doc)
187
+
188
+ if base.base is not None:
189
+ embed_base = self.schema.lookup_entity(base.base.name)
190
+ (
191
+ fields,
192
+ map_members,
193
+ set_members,
194
+ map_special,
195
+ set_special,
196
+ ) = recursive_base_nullable(self, embed_base)
197
+
198
+ for member in base.local_members:
199
+ field_doc = docfields.get(member.name, "")
200
+ field, _ = get_struct_field(
201
+ self,
202
+ member.name,
203
+ member.type.name,
204
+ field_doc,
205
+ True,
206
+ member.optional,
207
+ False,
208
+ )
209
+ fields.append(field)
210
+
211
+ member_type = qapi_schema_type_to_go_type(member.type.name)
212
+ nullable = member_type in self.accept_null_types
213
+ field_name = qapi_to_field_name(member.name)
214
+ tomap, toset = map_and_set(
215
+ nullable, field_name, member.optional, member.name
216
+ )
217
+ if nullable:
218
+ map_special += tomap
219
+ set_special += toset
220
+ else:
221
+ map_members += tomap
222
+ set_members += toset
223
+
224
+ return fields, map_members, set_members, map_special, set_special
225
+
226
+
227
+# Helper function. This is executed when the QAPI schema has members
228
+# that could accept JSON NULL (e.g: StrOrNull in QEMU"s QAPI schema).
229
+# This struct will need to be extended with Marshal/Unmarshal methods to
230
+# properly handle such atypical members.
231
+#
232
+# Only the Marshallaing methods are generated but we do need to iterate over
233
+# all the members to properly set/check them in those methods.
234
+def struct_with_nullable_generate_marshal(
235
+ self: QAPISchemaGenGolangVisitor,
236
+ name: str,
237
+ base: Optional[QAPISchemaObjectType],
238
+ members: List[QAPISchemaObjectTypeMember],
239
+ variants: Optional[QAPISchemaVariants],
240
+) -> str:
241
+ (
242
+ fields,
243
+ map_members,
244
+ set_members,
245
+ map_special,
246
+ set_special,
247
+ ) = recursive_base_nullable(self, base)
248
+
249
+ doc = self.docmap.get(name, None)
250
+ _, docfields = qapi_to_golang_struct_docs(doc)
251
+
252
+ if members:
253
+ for member in members:
254
+ field_doc = docfields.get(member.name, "")
255
+ field, _ = get_struct_field(
256
+ self,
257
+ member.name,
258
+ member.type.name,
259
+ field_doc,
260
+ True,
261
+ member.optional,
262
+ False,
263
+ )
264
+ fields.append(field)
265
+
266
+ member_type = qapi_schema_type_to_go_type(member.type.name)
267
+ nullable = member_type in self.accept_null_types
268
+ tomap, toset = map_and_set(
269
+ nullable,
270
+ qapi_to_field_name(member.name),
271
+ member.optional,
272
+ member.name,
273
+ )
274
+ if nullable:
275
+ map_special += tomap
276
+ set_special += toset
277
+ else:
278
+ map_members += tomap
279
+ set_members += toset
280
+
281
+ if variants:
282
+ for variant in variants.variants:
283
+ if variant.type.is_implicit():
284
+ continue
285
+
286
+ field, _ = get_struct_field(
287
+ self,
288
+ variant.name,
289
+ variant.type.name,
290
+ True,
291
+ variant.optional,
292
+ True,
293
+ )
294
+ fields.append(field)
295
+
296
+ member_type = qapi_schema_type_to_go_type(variant.type.name)
297
+ nullable = member_type in self.accept_null_types
298
+ tomap, toset = map_and_set(
299
+ nullable,
300
+ qapi_to_field_name(variant.name),
301
+ variant.optional,
302
+ variant.name,
303
+ )
304
+ if nullable:
305
+ map_special += tomap
306
+ set_special += toset
307
+ else:
308
+ map_members += tomap
309
+ set_members += toset
310
+
311
+ type_name = qapi_to_go_type_name(name)
312
+ struct = generate_struct_type("", args=fields, indent=1)
313
+ return string_to_code(
314
+ TEMPLATE_STRUCT_WITH_NULLABLE_MARSHAL.format(
315
+ struct=struct[1:-1],
316
+ type_name=type_name,
317
+ map_members=map_members,
318
+ map_special=map_special,
319
+ set_members=set_members,
320
+ set_special=set_special,
321
+ )
322
+ )
323
324
325
def recursive_base(
326
self: QAPISchemaGenGolangVisitor,
327
base: Optional[QAPISchemaObjectType],
328
-) -> List[dict[str:str]]:
329
+ discriminator: Optional[str] = None,
330
+) -> Tuple[List[dict[str:str]], bool]:
331
fields: List[dict[str:str]] = []
332
+ with_nullable = False
333
334
if not base:
335
- return fields
336
+ return fields, with_nullable
337
338
if base.base is not None:
339
embed_base = self.schema.lookup_entity(base.base.name)
340
- fields = recursive_base(self, embed_base)
341
+ fields, with_nullable = recursive_base(self, embed_base, discriminator)
342
343
doc = self.docmap.get(base.name, None)
344
_, docfields = qapi_to_golang_struct_docs(doc)
345
346
for member in base.local_members:
347
+ if discriminator and member.name == discriminator:
348
+ continue
349
+
350
field_doc = docfields.get(member.name, "")
351
- field = get_struct_field(
352
+ field, nullable = get_struct_field(
353
self,
354
member.name,
355
member.type.name,
356
field_doc,
357
+ False,
358
member.optional,
359
False,
360
)
361
fields.append(field)
362
+ with_nullable = True if nullable else with_nullable
363
364
- return fields
365
+ return fields, with_nullable
366
367
368
# Helper function that is used for most of QAPI types
369
@@ -XXX,XX +XXX,XX @@ def qapi_to_golang_struct(
370
indent: int = 0,
371
doc_enabled: bool = True,
372
) -> str:
373
- fields = recursive_base(self, base)
374
+ discriminator = None if not variants else variants.tag_member.name
375
+ fields, with_nullable = recursive_base(self, base, discriminator)
376
377
doc = self.docmap.get(name, None)
378
type_doc, docfields = qapi_to_golang_struct_docs(doc)
379
@@ -XXX,XX +XXX,XX @@ def qapi_to_golang_struct(
380
if members:
381
for member in members:
382
field_doc = docfields.get(member.name, "") if doc_enabled else ""
383
- field = get_struct_field(
384
+ field, nullable = get_struct_field(
385
self,
386
member.name,
387
member.type.name,
388
field_doc,
389
+ False,
390
member.optional,
391
False,
392
)
393
fields.append(field)
394
+ with_nullable = True if nullable else with_nullable
395
396
exists = {}
397
if variants:
398
@@ -XXX,XX +XXX,XX @@ def qapi_to_golang_struct(
399
400
exists[variant.name] = True
401
field_doc = docfields.get(variant.name, "") if doc_enabled else ""
402
- field = get_struct_field(
403
+ field, nullable = get_struct_field(
404
self,
405
variant.name,
406
variant.type.name,
407
field_doc,
408
+ False,
409
True,
410
True,
411
)
412
fields.append(field)
413
+ with_nullable = True if nullable else with_nullable
414
415
type_name = qapi_to_go_type_name(name)
416
content = string_to_code(
417
@@ -XXX,XX +XXX,XX @@ def qapi_to_golang_struct(
418
type_name, type_doc=type_doc, args=fields, indent=indent
419
)
420
)
421
+ if with_nullable:
422
+ content += struct_with_nullable_generate_marshal(
423
+ self, name, base, members, variants
424
+ )
425
return content
426
427
428
@@ -XXX,XX +XXX,XX @@ def generate_template_alternate(
429
name: str,
430
variants: Optional[QAPISchemaVariants],
431
) -> str:
432
+ absent_check_fields = ""
433
args: List[dict[str:str]] = []
434
nullable = name in self.accept_null_types
435
if nullable:
436
@@ -XXX,XX +XXX,XX @@ def generate_template_alternate(
437
assert nullable
438
continue
439
440
+ if nullable:
441
+ absent_check_fields += string_to_code(
442
+ TEMPLATE_ALTERNATE_NULLABLE_CHECK[1:].format(
443
+ var_name=var_name
444
+ )
445
+ )
446
skip_indent = 1 + len(FOUR_SPACES)
447
if marshal_check_fields == "":
448
skip_indent = 1
449
@@ -XXX,XX +XXX,XX @@ def generate_template_alternate(
450
].format(var_name=var_name, var_type=var_type)
451
452
content += string_to_code(generate_struct_type(name, args=args))
453
+ if nullable:
454
+ content += string_to_code(
455
+ TEMPLATE_ALTERNATE_NULLABLE.format(
456
+ name=name, absent_check_fields=absent_check_fields[:-1]
457
+ )
458
+ )
459
content += string_to_code(
460
TEMPLATE_ALTERNATE_METHODS.format(
461
name=name,
462
@@ -XXX,XX +XXX,XX @@ def visit_begin(self, schema: QAPISchema) -> None:
463
)
464
self.types[qapitype] += generate_template_imports(imports)
465
466
+ self.types["alternate"] += string_to_code(TEMPLATE_ALTERNATE)
467
+
468
def visit_end(self) -> None:
469
del self.schema
470
self.types["enum"] += generate_content_from_dict(self.enums)
471
--
472
2.48.1
diff view generated by jsdifflib
New patch
1
1
This patch handles QAPI union types and generates the equivalent data
2
structures and methods in Go to handle it.
3
4
The QAPI union type has two types of fields: The @base and the
5
@Variants members. The @base fields can be considered common members
6
for the union while only one field maximum is set for the @Variants.
7
8
In the QAPI specification, it defines a @discriminator field, which is
9
an Enum type. The purpose of the @discriminator is to identify which
10
@variant type is being used.
11
12
For the @discriminator's enum that are not handled by the QAPI Union,
13
we add in the Go struct a separate block as "Unbranched enum fields".
14
The rationale for this extra block is to allow the user to pass that
15
enum value under the discriminator, without extra payload.
16
17
The union types implement the Marshaler and Unmarshaler interfaces to
18
seamless decode from JSON objects to Golang structs and vice versa.
19
20
qapi:
21
| ##
22
| # @SetPasswordOptions:
23
| #
24
| # Options for set_password.
25
| #
26
| # @protocol:
27
| # - 'vnc' to modify the VNC server password
28
| # - 'spice' to modify the Spice server password
29
| #
30
| # @password: the new password
31
| #
32
| # @connected: How to handle existing clients when changing the
33
| # password. If nothing is specified, defaults to 'keep'. For
34
| # VNC, only 'keep' is currently implemented.
35
| #
36
| # Since: 7.0
37
| ##
38
| { 'union': 'SetPasswordOptions',
39
| 'base': { 'protocol': 'DisplayProtocol',
40
| 'password': 'str',
41
| '*connected': 'SetPasswordAction' },
42
| 'discriminator': 'protocol',
43
| 'data': { 'vnc': 'SetPasswordOptionsVnc' } }
44
45
go:
46
| // Options for set_password.
47
| //
48
| // Since: 7.0
49
| type SetPasswordOptions struct {
50
|     // the new password
51
|     Password string `json:"password"`
52
|     // How to handle existing clients when changing the password. If
53
|     // nothing is specified, defaults to 'keep'. For VNC, only 'keep'
54
|     // is currently implemented.
55
|     Connected *SetPasswordAction `json:"connected,omitempty"`
56
|     // Variants fields
57
|     Vnc *SetPasswordOptionsVnc `json:"-"`
58
|     // Unbranched enum fields
59
|     Spice bool `json:"-"`
60
| }
61
|
62
| func (s SetPasswordOptions) MarshalJSON() ([]byte, error) {
63
| ...
64
| }
65
66
|
67
| func (s *SetPasswordOptions) UnmarshalJSON(data []byte) error {
68
| ...
69
| }
70
71
Signed-off-by: Victor Toso <victortoso@redhat.com>
72
---
73
scripts/qapi/golang/golang.py | 208 +++++++++++++++++++++++++++++++++-
74
scripts/qapi/golang/utils.go | 12 ++
75
2 files changed, 217 insertions(+), 3 deletions(-)
76
77
diff --git a/scripts/qapi/golang/golang.py b/scripts/qapi/golang/golang.py
78
index XXXXXXX..XXXXXXX 100644
79
--- a/scripts/qapi/golang/golang.py
80
+++ b/scripts/qapi/golang/golang.py
81
@@ -XXX,XX +XXX,XX @@
82
"""
83
84
85
+TEMPLATE_UNION_CHECK_VARIANT_FIELD = """
86
+ if s.{field} != nil && err == nil {{
87
+ if len(bytes) != 0 {{
88
+ err = errors.New(`multiple variant fields set`)
89
+ }} else if err = unwrapToMap(m, s.{field}); err == nil {{
90
+ m["{discriminator}"] = {go_enum_value}
91
+ bytes, err = json.Marshal(m)
92
+ }}
93
+ }}
94
+"""
95
+
96
+TEMPLATE_UNION_CHECK_UNBRANCHED_FIELD = """
97
+ if s.{field} && err == nil {{
98
+ if len(bytes) != 0 {{
99
+ err = errors.New(`multiple variant fields set`)
100
+ }} else {{
101
+ m["{discriminator}"] = {go_enum_value}
102
+ bytes, err = json.Marshal(m)
103
+ }}
104
+ }}
105
+"""
106
+
107
+TEMPLATE_UNION_DRIVER_VARIANT_CASE = """
108
+ case {go_enum_value}:
109
+ s.{field} = new({member_type})
110
+ if err := json.Unmarshal(data, s.{field}); err != nil {{
111
+ s.{field} = nil
112
+ return err
113
+ }}"""
114
+
115
+TEMPLATE_UNION_DRIVER_UNBRANCHED_CASE = """
116
+ case {go_enum_value}:
117
+ s.{field} = true
118
+"""
119
+
120
+TEMPLATE_UNION_METHODS = """
121
+func (s {type_name}) MarshalJSON() ([]byte, error) {{
122
+ var bytes []byte
123
+ var err error
124
+ m := make(map[string]any)
125
+ {{
126
+ type Alias {type_name}
127
+ v := Alias(s)
128
+ unwrapToMap(m, &v)
129
+ }}
130
+{check_fields}
131
+ if err != nil {{
132
+ return nil, fmt.Errorf("marshal {type_name} due:'%s' struct='%+v'", err, s)
133
+ }} else if len(bytes) == 0 {{
134
+ return nil, fmt.Errorf("marshal {type_name} unsupported, struct='%+v'", s)
135
+ }}
136
+ return bytes, nil
137
+}}
138
+
139
+func (s *{type_name}) UnmarshalJSON(data []byte) error {{
140
+{base_type_def}
141
+ tmp := struct {{
142
+ {base_type_name}
143
+ }}{{}}
144
+
145
+ if err := json.Unmarshal(data, &tmp); err != nil {{
146
+ return err
147
+ }}
148
+{base_type_assign_unmarshal}
149
+ switch tmp.{discriminator} {{
150
+{driver_cases}
151
+ default:
152
+ return fmt.Errorf("unmarshal {type_name} received unrecognized value '%s'",
153
+ tmp.{discriminator})
154
+ }}
155
+ return nil
156
+}}
157
+"""
158
+
159
+
160
# Takes the documentation object of a specific type and returns
161
# that type's documentation and its member's docs.
162
def qapi_to_golang_struct_docs(doc: QAPIDoc) -> (str, Dict[str, str]):
163
@@ -XXX,XX +XXX,XX @@ def qapi_name_is_object(name: str) -> bool:
164
return name.startswith("q_obj_")
165
166
167
+def qapi_base_name_to_parent(name: str) -> str:
168
+ if qapi_name_is_base(name):
169
+ name = name[6:-5]
170
+ return name
171
+
172
+
173
def qapi_to_field_name(name: str) -> str:
174
return name.title().replace("_", "").replace("-", "")
175
176
@@ -XXX,XX +XXX,XX @@ def recursive_base(
177
embed_base = self.schema.lookup_entity(base.base.name)
178
fields, with_nullable = recursive_base(self, embed_base, discriminator)
179
180
- doc = self.docmap.get(base.name, None)
181
+ doc = self.docmap.get(qapi_base_name_to_parent(base.name), None)
182
_, docfields = qapi_to_golang_struct_docs(doc)
183
184
for member in base.local_members:
185
@@ -XXX,XX +XXX,XX @@ def qapi_to_golang_struct(
186
fields.append(field)
187
with_nullable = True if nullable else with_nullable
188
189
+ if info.defn_meta == "union" and variants:
190
+ enum_name = variants.tag_member.type.name
191
+ enum_obj = self.schema.lookup_entity(enum_name)
192
+ if len(exists) != len(enum_obj.members):
193
+ fields.append({"comment": "Unbranched enum fields"})
194
+ for member in enum_obj.members:
195
+ if member.name in exists:
196
+ continue
197
+
198
+ field_doc = (
199
+ docfields.get(member.name, "") if doc_enabled else ""
200
+ )
201
+ field, nullable = get_struct_field(
202
+ self, member.name, "bool", field_doc, False, False, True
203
+ )
204
+ fields.append(field)
205
+ with_nullable = True if nullable else with_nullable
206
+
207
type_name = qapi_to_go_type_name(name)
208
content = string_to_code(
209
generate_struct_type(
210
@@ -XXX,XX +XXX,XX @@ def qapi_to_golang_struct(
211
return content
212
213
214
+def qapi_to_golang_methods_union(
215
+ self: QAPISchemaGenGolangVisitor,
216
+ name: str,
217
+ base: Optional[QAPISchemaObjectType],
218
+ variants: Optional[QAPISchemaVariants],
219
+) -> str:
220
+ type_name = qapi_to_go_type_name(name)
221
+
222
+ assert base
223
+ base_type_assign_unmarshal = ""
224
+ base_type_name = qapi_to_go_type_name(base.name)
225
+ base_type_def = qapi_to_golang_struct(
226
+ self,
227
+ base.name,
228
+ base.info,
229
+ base.ifcond,
230
+ base.features,
231
+ base.base,
232
+ base.members,
233
+ base.branches,
234
+ indent=1,
235
+ doc_enabled=False,
236
+ )
237
+
238
+ discriminator = qapi_to_field_name(variants.tag_member.name)
239
+ for member in base.local_members:
240
+ field = qapi_to_field_name(member.name)
241
+ if field == discriminator:
242
+ continue
243
+ base_type_assign_unmarshal += f"""
244
+ s.{field} = tmp.{field}"""
245
+
246
+ driver_cases = ""
247
+ check_fields = ""
248
+ exists = {}
249
+ enum_name = variants.tag_member.type.name
250
+ if variants:
251
+ for var in variants.variants:
252
+ if var.type.is_implicit():
253
+ continue
254
+
255
+ field = qapi_to_field_name(var.name)
256
+ enum_value = qapi_to_field_name_enum(var.name)
257
+ member_type = qapi_schema_type_to_go_type(var.type.name)
258
+ go_enum_value = f"""{enum_name}{enum_value}"""
259
+ exists[go_enum_value] = True
260
+
261
+ check_fields += TEMPLATE_UNION_CHECK_VARIANT_FIELD.format(
262
+ field=field,
263
+ discriminator=variants.tag_member.name,
264
+ go_enum_value=go_enum_value,
265
+ )
266
+ driver_cases += TEMPLATE_UNION_DRIVER_VARIANT_CASE.format(
267
+ go_enum_value=go_enum_value,
268
+ field=field,
269
+ member_type=member_type,
270
+ )
271
+
272
+ enum_obj = self.schema.lookup_entity(enum_name)
273
+ if len(exists) != len(enum_obj.members):
274
+ for member in enum_obj.members:
275
+ value = qapi_to_field_name_enum(member.name)
276
+ go_enum_value = f"""{enum_name}{value}"""
277
+
278
+ if go_enum_value in exists:
279
+ continue
280
+
281
+ field = qapi_to_field_name(member.name)
282
+
283
+ check_fields += TEMPLATE_UNION_CHECK_UNBRANCHED_FIELD.format(
284
+ field=field,
285
+ discriminator=variants.tag_member.name,
286
+ go_enum_value=go_enum_value,
287
+ )
288
+ driver_cases += TEMPLATE_UNION_DRIVER_UNBRANCHED_CASE.format(
289
+ go_enum_value=go_enum_value,
290
+ field=field,
291
+ )
292
+
293
+ return string_to_code(
294
+ TEMPLATE_UNION_METHODS.format(
295
+ type_name=type_name,
296
+ check_fields=check_fields[1:],
297
+ base_type_def=base_type_def[1:],
298
+ base_type_name=base_type_name,
299
+ base_type_assign_unmarshal=base_type_assign_unmarshal,
300
+ discriminator=discriminator,
301
+ driver_cases=driver_cases[1:],
302
+ )
303
+ )
304
+
305
+
306
def generate_template_alternate(
307
self: QAPISchemaGenGolangVisitor,
308
name: str,
309
@@ -XXX,XX +XXX,XX @@ def __init__(self, _: str):
310
"alternate": ["encoding/json", "errors", "fmt"],
311
"enum": [],
312
"struct": ["encoding/json"],
313
+ "union": ["encoding/json", "errors", "fmt"],
314
}
315
316
self.schema: QAPISchema
317
@@ -XXX,XX +XXX,XX @@ def __init__(self, _: str):
318
self.enums: dict[str, str] = {}
319
self.alternates: dict[str, str] = {}
320
self.structs: dict[str, str] = {}
321
+ self.unions: dict[str, str] = {}
322
self.accept_null_types = []
323
self.docmap = {}
324
325
@@ -XXX,XX +XXX,XX @@ def visit_end(self) -> None:
326
self.types["enum"] += generate_content_from_dict(self.enums)
327
self.types["alternate"] += generate_content_from_dict(self.alternates)
328
self.types["struct"] += generate_content_from_dict(self.structs)
329
+ self.types["union"] += generate_content_from_dict(self.unions)
330
331
def visit_object_type(
332
self,
333
@@ -XXX,XX +XXX,XX @@ def visit_object_type(
334
members: List[QAPISchemaObjectTypeMember],
335
branches: Optional[QAPISchemaBranches],
336
) -> None:
337
- # Do not handle anything besides struct.
338
+ # Do not handle anything besides struct and unions.
339
if (
340
name == self.schema.the_empty_object_type.name
341
or not isinstance(name, str)
342
- or info.defn_meta not in ["struct"]
343
+ or info.defn_meta not in ["struct", "union"]
344
):
345
return
346
347
@@ -XXX,XX +XXX,XX @@ def visit_object_type(
348
self, name, info, ifcond, features, base, members, branches
349
)
350
)
351
+ else:
352
+ assert name not in self.unions
353
+ self.unions[name] = qapi_to_golang_struct(
354
+ self, name, info, ifcond, features, base, members, branches
355
+ )
356
+ self.unions[name] += qapi_to_golang_methods_union(
357
+ self, name, base, branches
358
+ )
359
360
def visit_alternate_type(
361
self,
362
diff --git a/scripts/qapi/golang/utils.go b/scripts/qapi/golang/utils.go
363
index XXXXXXX..XXXXXXX 100644
364
--- a/scripts/qapi/golang/utils.go
365
+++ b/scripts/qapi/golang/utils.go
366
@@ -XXX,XX +XXX,XX @@ package qapi
367
368
import (
369
    "encoding/json"
370
+    "fmt"
371
    "strings"
372
)
373
374
@@ -XXX,XX +XXX,XX @@ func strictDecode(into interface{}, from []byte) error {
375
    }
376
    return nil
377
}
378
+
379
+// This helper is used to move struct's fields into a map.
380
+// This function is useful to merge JSON objects.
381
+func unwrapToMap(m map[string]any, data any) error {
382
+    if bytes, err := json.Marshal(&data); err != nil {
383
+        return fmt.Errorf("unwrapToMap: %s", err)
384
+    } else if err := json.Unmarshal(bytes, &m); err != nil {
385
+        return fmt.Errorf("unwrapToMap: %s, data=%s", err, string(bytes))
386
+    }
387
+    return nil
388
+}
389
--
390
2.48.1
diff view generated by jsdifflib
New patch
1
This patch handles QAPI event types and generates data structures in
2
Go that handles it.
1
3
4
Note that the timestamp is part of the first layer of unmarshal, so it
5
s a member of protocol.go's Message type.
6
7
Example:
8
qapi:
9
| ##
10
| # @MEMORY_DEVICE_SIZE_CHANGE:
11
| #
12
| # Emitted when the size of a memory device changes. Only emitted for
13
| # memory devices that can actually change the size (e.g., virtio-mem
14
| # due to guest action).
15
| #
16
| # @id: device's ID
17
| #
18
| # @size: the new size of memory that the device provides
19
| #
20
| # @qom-path: path to the device object in the QOM tree (since 6.2)
21
| #
22
| # .. note:: This event is rate-limited.
23
| #
24
| # Since: 5.1
25
| #
26
| # .. qmp-example::
27
| #
28
| # <- { "event": "MEMORY_DEVICE_SIZE_CHANGE",
29
| # "data": { "id": "vm0", "size": 1073741824,
30
| # "qom-path": "/machine/unattached/device[2]" },
31
| # "timestamp": { "seconds": 1588168529, "microseconds": 201316 } }
32
| ##
33
| { 'event': 'MEMORY_DEVICE_SIZE_CHANGE',
34
| 'data': { '*id': 'str', 'size': 'size', 'qom-path' : 'str'} }
35
36
go:
37
| // Emitted when the size of a memory device changes. Only emitted for
38
| // memory devices that can actually change the size (e.g., virtio-mem
39
| // due to guest action).
40
| //
41
| // .. note:: This event is rate-limited.
42
| //
43
| // Since: 5.1
44
| //
45
| // .. qmp-example:: <- { "event": "MEMORY_DEVICE_SIZE_CHANGE",
46
| // "data": { "id": "vm0", "size": 1073741824, "qom-path":
47
| // "/machine/unattached/device[2]" }, "timestamp": { "seconds":
48
| // 1588168529, "microseconds": 201316 } }
49
| type MemoryDeviceSizeChangeEvent struct {
50
|     // device's ID
51
|     Id *string `json:"id,omitempty"`
52
|     // the new size of memory that the device provides
53
|     Size uint64 `json:"size"`
54
|     // path to the device object in the QOM tree (since 6.2)
55
|     QomPath string `json:"qom-path"`
56
| }
57
58
Signed-off-by: Victor Toso <victortoso@redhat.com>
59
---
60
scripts/qapi/golang/golang.py | 49 ++++++++++++++++++++++++++++++++---
61
1 file changed, 46 insertions(+), 3 deletions(-)
62
63
diff --git a/scripts/qapi/golang/golang.py b/scripts/qapi/golang/golang.py
64
index XXXXXXX..XXXXXXX 100644
65
--- a/scripts/qapi/golang/golang.py
66
+++ b/scripts/qapi/golang/golang.py
67
@@ -XXX,XX +XXX,XX @@ def qapi_to_field_name_enum(name: str) -> str:
68
return name.title().replace("-", "")
69
70
71
-def qapi_to_go_type_name(name: str) -> str:
72
+def qapi_to_go_type_name(name: str, meta: Optional[str] = None) -> str:
73
# We want to keep CamelCase for Golang types. We want to avoid removing
74
# already set CameCase names while fixing uppercase ones, eg:
75
# 1) q_obj_SocketAddress_base -> SocketAddressBase
76
@@ -XXX,XX +XXX,XX @@ def qapi_to_go_type_name(name: str) -> str:
77
78
name += "".join(word.title() for word in words[1:])
79
80
+ # Handle specific meta suffix
81
+ types = ["event"]
82
+ if meta in types:
83
+ name = name[:-3] if name.endswith("Arg") else name
84
+ name += meta.title().replace(" ", "")
85
+
86
return name
87
88
89
@@ -XXX,XX +XXX,XX @@ def qapi_to_golang_struct(
90
fields.append(field)
91
with_nullable = True if nullable else with_nullable
92
93
- type_name = qapi_to_go_type_name(name)
94
+ type_name = qapi_to_go_type_name(name, info.defn_meta)
95
+
96
content = string_to_code(
97
generate_struct_type(
98
type_name, type_doc=type_doc, args=fields, indent=indent
99
@@ -XXX,XX +XXX,XX @@ def generate_template_alternate(
100
return "\n" + content
101
102
103
+def generate_template_event(events: dict[str, Tuple[str, str]]) -> str:
104
+ content = ""
105
+ for name in sorted(events):
106
+ type_name, gocode = events[name]
107
+ content += gocode
108
+
109
+ return content
110
+
111
+
112
def generate_content_from_dict(data: dict[str, str]) -> str:
113
content = ""
114
115
@@ -XXX,XX +XXX,XX @@ def __init__(self, _: str):
116
types = {
117
"alternate": ["encoding/json", "errors", "fmt"],
118
"enum": [],
119
+ "event": [],
120
"struct": ["encoding/json"],
121
"union": ["encoding/json", "errors", "fmt"],
122
}
123
124
self.schema: QAPISchema
125
+ self.events: dict[str, Tuple[str, str]] = {}
126
self.golang_package_name = "qapi"
127
self.duplicate = list(gofiles)
128
self.enums: dict[str, str] = {}
129
@@ -XXX,XX +XXX,XX @@ def visit_end(self) -> None:
130
self.types["alternate"] += generate_content_from_dict(self.alternates)
131
self.types["struct"] += generate_content_from_dict(self.structs)
132
self.types["union"] += generate_content_from_dict(self.unions)
133
+ self.types["event"] += generate_template_event(self.events)
134
135
def visit_object_type(
136
self,
137
@@ -XXX,XX +XXX,XX @@ def visit_event(
138
arg_type: Optional[QAPISchemaObjectType],
139
boxed: bool,
140
) -> None:
141
- pass
142
+ assert name == info.defn_name
143
+ assert name not in self.events
144
+ type_name = qapi_to_go_type_name(name, info.defn_meta)
145
+
146
+ if isinstance(arg_type, QAPISchemaObjectType):
147
+ content = string_to_code(
148
+ qapi_to_golang_struct(
149
+ self,
150
+ name,
151
+ info,
152
+ arg_type.ifcond,
153
+ arg_type.features,
154
+ arg_type.base,
155
+ arg_type.members,
156
+ arg_type.branches,
157
+ )
158
+ )
159
+ else:
160
+ doc = self.docmap.get(name, None)
161
+ type_doc, _ = qapi_to_golang_struct_docs(doc)
162
+ content = string_to_code(
163
+ generate_struct_type(type_name, type_doc=type_doc)
164
+ )
165
+
166
+ self.events[name] = (type_name, content)
167
168
def write(self, outdir: str) -> None:
169
godir = "go"
170
--
171
2.48.1
diff view generated by jsdifflib
New patch
1
The Event interface is an abstraction that can be used by client and
2
server to the manager the Event types albeit with a different
3
implementation for sending and receiving.
1
4
5
The implementation of client/server is not part of this series.
6
7
Signed-off-by: Victor Toso <victortoso@redhat.com>
8
---
9
scripts/qapi/golang/golang.py | 38 ++++++++++++++++++++++++++++++++---
10
1 file changed, 35 insertions(+), 3 deletions(-)
11
12
diff --git a/scripts/qapi/golang/golang.py b/scripts/qapi/golang/golang.py
13
index XXXXXXX..XXXXXXX 100644
14
--- a/scripts/qapi/golang/golang.py
15
+++ b/scripts/qapi/golang/golang.py
16
@@ -XXX,XX +XXX,XX @@
17
}}
18
"""
19
20
+TEMPLATE_EVENT = """
21
+type Event interface {{
22
+{methods}
23
+}}
24
+"""
25
+
26
27
# Takes the documentation object of a specific type and returns
28
# that type's documentation and its member's docs.
29
@@ -XXX,XX +XXX,XX @@ def generate_template_alternate(
30
return "\n" + content
31
32
33
-def generate_template_event(events: dict[str, Tuple[str, str]]) -> str:
34
+def generate_template_event(events: dict[str, Tuple[str, str]]) -> (str, str):
35
content = ""
36
+ methods = ""
37
for name in sorted(events):
38
type_name, gocode = events[name]
39
+ methods += f"\t{type_name}({type_name}, time.Time) error\n"
40
content += gocode
41
42
- return content
43
+ iface = string_to_code(TEMPLATE_EVENT.format(methods=methods[:-1]))
44
+ return content, iface
45
46
47
def generate_content_from_dict(data: dict[str, str]) -> str:
48
@@ -XXX,XX +XXX,XX @@ def __init__(self, _: str):
49
"struct": ["encoding/json"],
50
"union": ["encoding/json", "errors", "fmt"],
51
}
52
+ interfaces = {
53
+ "event": ["time"],
54
+ }
55
56
self.schema: QAPISchema
57
self.events: dict[str, Tuple[str, str]] = {}
58
@@ -XXX,XX +XXX,XX @@ def __init__(self, _: str):
59
self.types = dict.fromkeys(types, "")
60
self.types_import = types
61
62
+ self.interfaces = dict.fromkeys(interfaces, "")
63
+ self.interface_imports = interfaces
64
+
65
def visit_begin(self, schema: QAPISchema) -> None:
66
self.schema = schema
67
68
@@ -XXX,XX +XXX,XX @@ def visit_begin(self, schema: QAPISchema) -> None:
69
continue
70
self.docmap[doc.symbol] = doc
71
72
+ for qapitype, imports in self.interface_imports.items():
73
+ self.interfaces[qapitype] = TEMPLATE_GENERATED_HEADER[1:].format(
74
+ package_name=self.golang_package_name
75
+ )
76
+ self.interfaces[qapitype] += generate_template_imports(imports)
77
+
78
for qapitype, imports in self.types_import.items():
79
self.types[qapitype] = TEMPLATE_GENERATED_HEADER[1:].format(
80
package_name=self.golang_package_name
81
@@ -XXX,XX +XXX,XX @@ def visit_end(self) -> None:
82
self.types["alternate"] += generate_content_from_dict(self.alternates)
83
self.types["struct"] += generate_content_from_dict(self.structs)
84
self.types["union"] += generate_content_from_dict(self.unions)
85
- self.types["event"] += generate_template_event(self.events)
86
+
87
+ evtype, eviface = generate_template_event(self.events)
88
+ self.types["event"] += evtype
89
+ self.interfaces["event"] += eviface
90
91
def visit_object_type(
92
self,
93
@@ -XXX,XX +XXX,XX @@ def write(self, outdir: str) -> None:
94
95
with open(pathname, "w", encoding="utf8") as outfile:
96
outfile.write(content)
97
+
98
+ # Interfaces to be generated
99
+ for qapitype, content in self.interfaces.items():
100
+ gofile = f"gen_iface_{qapitype}.go"
101
+ pathname = os.path.join(targetpath, gofile)
102
+
103
+ with open(pathname, "w", encoding="utf8") as outfile:
104
+ outfile.write(content)
105
--
106
2.48.1
diff view generated by jsdifflib
New patch
1
This patch handles QAPI command types and generates data structures in
2
Go that handles it.
1
3
4
Note that command's id is part of the first layer of unmarshal, so it
5
is a member of protocol.go's Message type.
6
7
qapi:
8
| ##
9
| # @add-fd:
10
| #
11
| # Add a file descriptor, that was passed via SCM rights, to an fd set.
12
| #
13
| # @fdset-id: The ID of the fd set to add the file descriptor to.
14
| #
15
| # @opaque: A free-form string that can be used to describe the fd.
16
| #
17
| # Returns:
18
| # @AddfdInfo
19
| #
20
| # Errors:
21
| # - If file descriptor was not received, GenericError
22
| # - If @fdset-id is a negative value, GenericError
23
| #
24
| # .. note:: The list of fd sets is shared by all monitor connections.
25
| #
26
| # .. note:: If @fdset-id is not specified, a new fd set will be
27
| # created.
28
| #
29
| # Since: 1.2
30
| #
31
| # .. qmp-example::
32
| #
33
| # -> { "execute": "add-fd", "arguments": { "fdset-id": 1 } }
34
| # <- { "return": { "fdset-id": 1, "fd": 3 } }
35
| ##
36
| { 'command': 'add-fd',
37
| 'data': { '*fdset-id': 'int',
38
| '*opaque': 'str' },
39
| 'returns': 'AddfdInfo' }
40
41
go:
42
| // Add a file descriptor, that was passed via SCM rights, to an fd
43
| // set.
44
| //
45
| // Returns: @AddfdInfo
46
| //
47
| // Errors: - If file descriptor was not received, GenericError -
48
| // If @fdset-id is a negative value, GenericError
49
| //
50
| // .. note:: The list of fd sets is shared by all monitor connections.
51
| // .. note:: If @fdset-id is not specified, a new fd set will be
52
| // created.
53
| //
54
| // Since: 1.2
55
| //
56
| // .. qmp-example:: -> { "execute": "add-fd", "arguments": {
57
| // "fdset-id": 1 } } <- { "return": { "fdset-id": 1, "fd": 3 } }
58
| type AddFdCommand struct {
59
|     // The ID of the fd set to add the file descriptor to.
60
|     FdsetId *int64 `json:"fdset-id,omitempty"`
61
|     // A free-form string that can be used to describe the fd.
62
|     Opaque *string `json:"opaque,omitempty"`
63
| }
64
65
Signed-off-by: Victor Toso <victortoso@redhat.com>
66
---
67
scripts/qapi/golang/golang.py | 52 +++++++++++++++++++++++++++++++++--
68
1 file changed, 50 insertions(+), 2 deletions(-)
69
70
diff --git a/scripts/qapi/golang/golang.py b/scripts/qapi/golang/golang.py
71
index XXXXXXX..XXXXXXX 100644
72
--- a/scripts/qapi/golang/golang.py
73
+++ b/scripts/qapi/golang/golang.py
74
@@ -XXX,XX +XXX,XX @@ def qapi_to_go_type_name(name: str, meta: Optional[str] = None) -> str:
75
name += "".join(word.title() for word in words[1:])
76
77
# Handle specific meta suffix
78
- types = ["event"]
79
+ types = ["event", "command"]
80
if meta in types:
81
name = name[:-3] if name.endswith("Arg") else name
82
name += meta.title().replace(" ", "")
83
@@ -XXX,XX +XXX,XX @@ def generate_template_alternate(
84
return "\n" + content
85
86
87
+def generate_template_command(commands: dict[str, Tuple[str, str]]) -> str:
88
+ content = ""
89
+ for name in sorted(commands):
90
+ type_name, gocode = commands[name]
91
+ content += gocode
92
+
93
+ return content
94
+
95
+
96
def generate_template_event(events: dict[str, Tuple[str, str]]) -> (str, str):
97
content = ""
98
methods = ""
99
@@ -XXX,XX +XXX,XX @@ def __init__(self, _: str):
100
# Map each qapi type to the necessary Go imports
101
types = {
102
"alternate": ["encoding/json", "errors", "fmt"],
103
+ "command": [],
104
"enum": [],
105
"event": [],
106
"struct": ["encoding/json"],
107
@@ -XXX,XX +XXX,XX @@ def __init__(self, _: str):
108
109
self.schema: QAPISchema
110
self.events: dict[str, Tuple[str, str]] = {}
111
+ self.commands: dict[str, Tuple[str, str]] = {}
112
self.golang_package_name = "qapi"
113
self.duplicate = list(gofiles)
114
self.enums: dict[str, str] = {}
115
@@ -XXX,XX +XXX,XX @@ def visit_end(self) -> None:
116
self.types["event"] += evtype
117
self.interfaces["event"] += eviface
118
119
+ self.types["command"] += generate_template_command(self.commands)
120
+
121
def visit_object_type(
122
self,
123
name: str,
124
@@ -XXX,XX +XXX,XX @@ def visit_command(
125
allow_preconfig: bool,
126
coroutine: bool,
127
) -> None:
128
- pass
129
+ assert name == info.defn_name
130
+ assert name not in self.commands
131
+
132
+ type_name = qapi_to_go_type_name(name, info.defn_meta)
133
+
134
+ doc = self.docmap.get(name, None)
135
+ type_doc, _ = qapi_to_golang_struct_docs(doc)
136
+
137
+ content = ""
138
+ if boxed or not arg_type or not qapi_name_is_object(arg_type.name):
139
+ args: List[dict[str:str]] = []
140
+ if arg_type:
141
+ args.append(
142
+ {
143
+ "name": f"{arg_type.name}",
144
+ }
145
+ )
146
+ content += string_to_code(
147
+ generate_struct_type(type_name, type_doc=type_doc, args=args)
148
+ )
149
+ else:
150
+ assert isinstance(arg_type, QAPISchemaObjectType)
151
+ content += string_to_code(
152
+ qapi_to_golang_struct(
153
+ self,
154
+ name,
155
+ arg_type.info,
156
+ arg_type.ifcond,
157
+ arg_type.features,
158
+ arg_type.base,
159
+ arg_type.members,
160
+ arg_type.branches,
161
+ )
162
+ )
163
+
164
+ self.commands[name] = (type_name, content)
165
166
def visit_event(
167
self,
168
--
169
2.48.1
diff view generated by jsdifflib
New patch
1
The Command interface is an abstraction that can be used by client and
2
server to the manager the Command types albeit with a different
3
implementation for sending and receiving.
1
4
5
The proposal for Sync is defined as Command while the Async is named
6
CommandAsync.
7
8
The implementation of client/server is not part of this series.
9
10
Signed-off-by: Victor Toso <victortoso@redhat.com>
11
---
12
scripts/qapi/golang/golang.py | 56 +++++++++++++++++++++++++++++++----
13
1 file changed, 50 insertions(+), 6 deletions(-)
14
15
diff --git a/scripts/qapi/golang/golang.py b/scripts/qapi/golang/golang.py
16
index XXXXXXX..XXXXXXX 100644
17
--- a/scripts/qapi/golang/golang.py
18
+++ b/scripts/qapi/golang/golang.py
19
@@ -XXX,XX +XXX,XX @@
20
}}
21
"""
22
23
+TEMPLATE_COMMAND = """
24
+// Synchronous interface of all available commands
25
+type Commands interface {{
26
+{methods}
27
+}}
28
+
29
+{callbacks}
30
+
31
+// Asynchronous interface of all available commands
32
+type CommandsAsync interface {{
33
+{async_methods}
34
+}}
35
+"""
36
+
37
38
# Takes the documentation object of a specific type and returns
39
# that type's documentation and its member's docs.
40
@@ -XXX,XX +XXX,XX @@ def generate_template_alternate(
41
return "\n" + content
42
43
44
-def generate_template_command(commands: dict[str, Tuple[str, str]]) -> str:
45
+def generate_template_command(
46
+ commands: dict[str, Tuple[str, str, str]]
47
+) -> (str, str):
48
content = ""
49
+ methods = ""
50
+ async_methods = ""
51
+ callbacks = ""
52
+
53
for name in sorted(commands):
54
- type_name, gocode = commands[name]
55
+ type_name, gocode, retarg = commands[name]
56
content += gocode
57
58
- return content
59
+ name = type_name[:-7]
60
+ cbname = f"{name}Complete"
61
+ syncret = "error"
62
+ cbarg = "error"
63
+ if retarg != "":
64
+ cbarg = f"{retarg}, error"
65
+ syncret = f"({retarg}, error)"
66
+ methods += f"\t{name}({type_name}) {syncret}\n"
67
+ async_methods += f"\t{name}({type_name}, {cbname}) error\n"
68
+ callbacks += f"type {cbname} func({cbarg})\n"
69
+
70
+ iface = string_to_code(
71
+ TEMPLATE_COMMAND.format(
72
+ methods=methods[:-1],
73
+ callbacks=callbacks[:-1],
74
+ async_methods=async_methods[:-1],
75
+ )
76
+ )
77
+ return content, iface
78
79
80
def generate_template_event(events: dict[str, Tuple[str, str]]) -> (str, str):
81
@@ -XXX,XX +XXX,XX @@ def __init__(self, _: str):
82
"union": ["encoding/json", "errors", "fmt"],
83
}
84
interfaces = {
85
+ "command": [],
86
"event": ["time"],
87
}
88
89
self.schema: QAPISchema
90
self.events: dict[str, Tuple[str, str]] = {}
91
- self.commands: dict[str, Tuple[str, str]] = {}
92
+ self.commands: dict[str, Tuple[str, str, str]] = {}
93
self.golang_package_name = "qapi"
94
self.duplicate = list(gofiles)
95
self.enums: dict[str, str] = {}
96
@@ -XXX,XX +XXX,XX @@ def visit_end(self) -> None:
97
self.types["event"] += evtype
98
self.interfaces["event"] += eviface
99
100
- self.types["command"] += generate_template_command(self.commands)
101
+ cmdtype, cmdiface = generate_template_command(self.commands)
102
+ self.types["command"] += cmdtype
103
+ self.interfaces["command"] += cmdiface
104
105
def visit_object_type(
106
self,
107
@@ -XXX,XX +XXX,XX @@ def visit_command(
108
)
109
)
110
111
- self.commands[name] = (type_name, content)
112
+ retarg = ""
113
+ if ret_type:
114
+ retarg = qapi_schema_type_to_go_type(ret_type.name)
115
+ self.commands[name] = (type_name, content, retarg)
116
117
def visit_event(
118
self,
119
--
120
2.48.1
diff view generated by jsdifflib
1
Adds an option (--enable-tcg-builtin) to build TCG natively when
1
The goal of this patch is converge discussions into a documentation,
2
--enable-modules argument is passed to the build system. It gives
2
to make it easy and explicit design decisions, known issues and what
3
the opportunity to have this important accelerator built-in and
3
else might help a person interested in how the Go module is generated.
4
still take advantage of the new modular system.
5
4
6
Signed-off-by: Jose R. Ziviani <jziviani@suse.de>
5
Signed-off-by: Victor Toso <victortoso@redhat.com>
7
---
6
---
8
configure | 12 ++++++++++--
7
docs/devel/index-build.rst | 1 +
9
meson.build | 11 ++++++++++-
8
docs/devel/qapi-golang-code-gen.rst | 420 ++++++++++++++++++++++++++++
10
meson_options.txt | 2 ++
9
2 files changed, 421 insertions(+)
11
3 files changed, 22 insertions(+), 3 deletions(-)
10
create mode 100644 docs/devel/qapi-golang-code-gen.rst
12
11
13
diff --git a/configure b/configure
12
diff --git a/docs/devel/index-build.rst b/docs/devel/index-build.rst
14
index XXXXXXX..XXXXXXX 100755
15
--- a/configure
16
+++ b/configure
17
@@ -XXX,XX +XXX,XX @@ tsan="no"
18
fortify_source="$default_feature"
19
strip_opt="yes"
20
tcg_interpreter="false"
21
+tcg_builtin="false"
22
bigendian="no"
23
mingw32="no"
24
gcov="no"
25
@@ -XXX,XX +XXX,XX @@ for opt do
26
;;
27
--enable-tcg) tcg="enabled"
28
;;
29
+ --enable-tcg-builtin) tcg_builtin="true"
30
+ ;;
31
--disable-malloc-trim) malloc_trim="disabled"
32
;;
33
--enable-malloc-trim) malloc_trim="enabled"
34
@@ -XXX,XX +XXX,XX @@ Advanced options (experts only):
35
Default:trace-<pid>
36
--disable-slirp disable SLIRP userspace network connectivity
37
--enable-tcg-interpreter enable TCI (TCG with bytecode interpreter, experimental and slow)
38
+ --enable-tcg-builtin force TCG builtin even with --enable-modules
39
--enable-malloc-trim enable libc malloc_trim() for memory optimization
40
--oss-lib path to OSS library
41
--cpu=CPU Build for host CPU [$cpu]
42
@@ -XXX,XX +XXX,XX @@ if test "$solaris" = "yes" ; then
43
fi
44
fi
45
46
-if test "$tcg" = "enabled"; then
47
+if test "$tcg" = "disabled"; then
48
+ debug_tcg="no"
49
+ tcg_interpreter="false"
50
+ tcg_builtin="false"
51
+else
52
git_submodules="$git_submodules tests/fp/berkeley-testfloat-3"
53
git_submodules="$git_submodules tests/fp/berkeley-softfloat-3"
54
fi
55
@@ -XXX,XX +XXX,XX @@ if test "$skip_meson" = no; then
56
-Dvhost_user_blk_server=$vhost_user_blk_server -Dmultiprocess=$multiprocess \
57
-Dfuse=$fuse -Dfuse_lseek=$fuse_lseek -Dguest_agent_msi=$guest_agent_msi -Dbpf=$bpf\
58
$(if test "$default_features" = no; then echo "-Dauto_features=disabled"; fi) \
59
-    -Dtcg_interpreter=$tcg_interpreter \
60
+ -Dtcg_interpreter=$tcg_interpreter -Dtcg_builtin=$tcg_builtin \
61
$cross_arg \
62
"$PWD" "$source_path"
63
64
diff --git a/meson.build b/meson.build
65
index XXXXXXX..XXXXXXX 100644
13
index XXXXXXX..XXXXXXX 100644
66
--- a/meson.build
14
--- a/docs/devel/index-build.rst
67
+++ b/meson.build
15
+++ b/docs/devel/index-build.rst
68
@@ -XXX,XX +XXX,XX @@ if cpu in ['x86', 'x86_64']
16
@@ -XXX,XX +XXX,XX @@ some of the basics if you are adding new files and targets to the build.
69
endif
17
kconfig
70
18
docs
71
modular_tcg = []
19
qapi-code-gen
72
+is_tcg_modular = false
20
+ qapi-golang-code-gen
73
# Darwin does not support references to thread-local variables in modules
21
control-flow-integrity
74
if targetos != 'darwin'
22
diff --git a/docs/devel/qapi-golang-code-gen.rst b/docs/devel/qapi-golang-code-gen.rst
75
modular_tcg = ['i386-softmmu', 'x86_64-softmmu']
23
new file mode 100644
76
+ is_tcg_modular = config_host.has_key('CONFIG_MODULES') \
24
index XXXXXXX..XXXXXXX
77
+ and get_option('tcg').enabled() \
25
--- /dev/null
78
+ and not get_option('tcg_builtin')
26
+++ b/docs/devel/qapi-golang-code-gen.rst
79
endif
27
@@ -XXX,XX +XXX,XX @@
80
28
+==========================
81
edk2_targets = [ 'arm-softmmu', 'aarch64-softmmu', 'i386-softmmu', 'x86_64-softmmu' ]
29
+QAPI Golang code generator
82
@@ -XXX,XX +XXX,XX @@ if not get_option('tcg').disabled()
30
+==========================
83
31
+
84
accelerators += 'CONFIG_TCG'
32
+..
85
config_host += { 'CONFIG_TCG': 'y' }
33
+ Copyright (C) 2025 Red Hat, Inc.
86
+ if is_tcg_modular
34
+
87
+ config_host += { 'CONFIG_TCG_MODULAR': 'y' }
35
+ This work is licensed under the terms of the GNU GPL, version 2 or
88
+ endif
36
+ later. See the COPYING file in the top-level directory.
89
endif
37
+
90
38
+
91
if 'CONFIG_KVM' not in accelerators and get_option('kvm').enabled()
39
+Introduction
92
@@ -XXX,XX +XXX,XX @@ foreach target : target_dirs
40
+============
93
elif sym == 'CONFIG_XEN' and have_xen_pci_passthrough
41
+
94
config_target += { 'CONFIG_XEN_PCI_PASSTHROUGH': 'y' }
42
+This document provides information of how the generated Go code maps
95
endif
43
+with the QAPI specification, clarifying design decisions when needed.
96
- if target in modular_tcg
44
+
97
+ if target in modular_tcg and is_tcg_modular
45
+
98
config_target += { 'CONFIG_TCG_MODULAR': 'y' }
46
+Scope of the generated Go code
99
else
47
+==============================
100
config_target += { 'CONFIG_TCG_BUILTIN': 'y' }
48
+
101
@@ -XXX,XX +XXX,XX @@ summary_info += {'TCG support': config_all.has_key('CONFIG_TCG')}
49
+The scope is to provide data structures that can interpret and be used
102
if config_all.has_key('CONFIG_TCG')
50
+to generate valid QMP messages. These data structures are generated
103
if get_option('tcg_interpreter')
51
+from a QAPI schema and should be able to handle QMP messages from the
104
summary_info += {'TCG backend': 'TCI (TCG with bytecode interpreter, experimental and slow)'}
52
+same schema.
105
+ elif is_tcg_modular
53
+
106
+ summary_info += {'TCG backend': 'module (@0@)'.format(cpu)}
54
+We also provide interfaces for Commands and Events which allows an
107
else
55
+abstraction for client and server applications with the possibility of
108
summary_info += {'TCG backend': 'native (@0@)'.format(cpu)}
56
+custom back end implantations.
109
endif
57
+
110
diff --git a/meson_options.txt b/meson_options.txt
58
+The generated Go code is a Go module with data structs that uses Go
111
index XXXXXXX..XXXXXXX 100644
59
+standard library ``encoding/json``, implementing its field tags and
112
--- a/meson_options.txt
60
+Marshal interface whenever needed.
113
+++ b/meson_options.txt
61
+
114
@@ -XXX,XX +XXX,XX @@ option('tcg', type: 'feature', value: 'auto',
62
+
115
description: 'TCG support')
63
+QAPI Documentation
116
option('tcg_interpreter', type: 'boolean', value: false,
64
+==================
117
description: 'TCG with bytecode interpreter (experimental and slow)')
65
+
118
+option('tcg_builtin', type: 'boolean', value: 'false',
66
+The documentation included in QAPI schema such as type and type's
119
+ description: 'Force TCG builtin')
67
+fields information, comments, examples and more, they are converted
120
option('cfi', type: 'boolean', value: 'false',
68
+and embed in the Go generated source code. Metadata information that
121
description: 'Control-Flow Integrity (CFI)')
69
+might not be relevant to developers are excluded (e.g: TODOs)
122
option('cfi_debug', type: 'boolean', value: 'false',
70
+
71
+
72
+QAPI types to Go structs
73
+========================
74
+
75
+Enum
76
+----
77
+
78
+Enums are mapped as strings in Go, using a specified string type per
79
+Enum to help with type safety in the Go application.
80
+
81
+::
82
+
83
+ { 'enum': 'HostMemPolicy',
84
+ 'data': [ 'default', 'preferred', 'bind', 'interleave' ] }
85
+
86
+.. code-block:: go
87
+
88
+ // Host memory policy types
89
+ //
90
+ // Since: 2.1
91
+ type HostMemPolicy string
92
+
93
+ const (
94
+ // restore default policy, remove any nondefault policy
95
+ HostMemPolicyDefault HostMemPolicy = "default"
96
+ // set the preferred host nodes for allocation
97
+ HostMemPolicyPreferred HostMemPolicy = "preferred"
98
+ // a strict policy that restricts memory allocation to the host nodes specified
99
+ HostMemPolicyBind HostMemPolicy = "bind"
100
+ // memory allocations are interleaved across the set of host nodes specified
101
+ HostMemPolicyInterleave HostMemPolicy = "interleave"
102
+ )
103
+
104
+
105
+Struct
106
+------
107
+
108
+The mapping between a QAPI struct in Go struct is very straightforward.
109
+ - Each member of the QAPI struct has its own field in a Go struct.
110
+ - Optional members are pointers type with 'omitempty' field tag set
111
+
112
+One important design decision was to _not_ embed base struct, copying
113
+the base members to the original struct. This reduces the complexity
114
+for the Go application.
115
+
116
+::
117
+
118
+ { 'struct': 'BlockExportOptionsNbdBase',
119
+ 'data': { '*name': 'str', '*description': 'str' } }
120
+
121
+ { 'struct': 'BlockExportOptionsNbd',
122
+ 'base': 'BlockExportOptionsNbdBase',
123
+ 'data': { '*bitmaps': ['BlockDirtyBitmapOrStr'],
124
+ '*allocation-depth': 'bool' } }
125
+
126
+.. code-block:: go
127
+
128
+ // An NBD block export (distinct options used in the NBD branch of
129
+ // block-export-add).
130
+ //
131
+ // Since: 5.2
132
+ type BlockExportOptionsNbd struct {
133
+ // Export name. If unspecified, the @device parameter is used as
134
+ // the export name. (Since 2.12)
135
+ Name *string `json:"name,omitempty"`
136
+ // Free-form description of the export, up to 4096 bytes. (Since
137
+ // 5.0)
138
+ Description *string `json:"description,omitempty"`
139
+ // Also export each of the named dirty bitmaps reachable from
140
+ // @device, so the NBD client can use NBD_OPT_SET_META_CONTEXT
141
+ // with the metadata context name "qemu:dirty-bitmap:BITMAP" to
142
+ // inspect each bitmap. Since 7.1 bitmap may be specified by
143
+ // node/name pair.
144
+ Bitmaps []BlockDirtyBitmapOrStr `json:"bitmaps,omitempty"`
145
+ // Also export the allocation depth map for @device, so the NBD
146
+ // client can use NBD_OPT_SET_META_CONTEXT with the metadata
147
+ // context name "qemu:allocation-depth" to inspect allocation
148
+ // details. (since 5.2)
149
+ AllocationDepth *bool `json:"allocation-depth,omitempty"`
150
+ }
151
+
152
+
153
+Union
154
+-----
155
+
156
+Unions in QAPI are bounded to a Enum type which provides all possible
157
+branches of the union. The most important caveat here is that the Union
158
+does not need to have a complex type implemented for all possible
159
+branches of the Enum. Receiving a enum value of a empty branch is valid.
160
+
161
+The generated Go struct will then define a field for each
162
+Enum value. The type for Enum values of empty branch is bool. Only one
163
+field can be set at time.
164
+
165
+::
166
+
167
+ { 'union': 'ImageInfoSpecificQCow2Encryption',
168
+ 'base': 'ImageInfoSpecificQCow2EncryptionBase',
169
+ 'discriminator': 'format',
170
+ 'data': { 'luks': 'QCryptoBlockInfoLUKS' } }
171
+
172
+ { 'struct': 'ImageInfoSpecificQCow2EncryptionBase',
173
+ 'data': { 'format': 'BlockdevQcow2EncryptionFormat'}}
174
+
175
+ { 'enum': 'BlockdevQcow2EncryptionFormat',
176
+ 'data': [ 'aes', 'luks' ] }
177
+
178
+.. code-block:: go
179
+
180
+ type ImageInfoSpecificQCow2Encryption struct {
181
+ // Variants fields
182
+ Luks *QCryptoBlockInfoLUKS `json:"-"`
183
+ // Empty branched enum fields
184
+ Aes bool `json:"-"`
185
+ }
186
+
187
+ func (s ImageInfoSpecificQCow2Encryption) MarshalJSON() ([]byte, error) {
188
+ // ...
189
+ // Logic for branched Enum
190
+ if s.Luks != nil && err == nil {
191
+ if len(bytes) != 0 {
192
+ err = errors.New(`multiple variant fields set`)
193
+ } else if err = unwrapToMap(m, s.Luks); err == nil {
194
+ m["format"] = BlockdevQcow2EncryptionFormatLuks
195
+ bytes, err = json.Marshal(m)
196
+ }
197
+ }
198
+
199
+ // Logic for unbranched Enum
200
+ if s.Aes && err == nil {
201
+ if len(bytes) != 0 {
202
+ err = errors.New(`multiple variant fields set`)
203
+ } else {
204
+ m["format"] = BlockdevQcow2EncryptionFormatAes
205
+ bytes, err = json.Marshal(m)
206
+ }
207
+ }
208
+
209
+ // ...
210
+ // Handle errors
211
+ }
212
+
213
+
214
+ func (s *ImageInfoSpecificQCow2Encryption) UnmarshalJSON(data []byte) error {
215
+ // ...
216
+
217
+ switch tmp.Format {
218
+ case BlockdevQcow2EncryptionFormatLuks:
219
+ s.Luks = new(QCryptoBlockInfoLUKS)
220
+ if err := json.Unmarshal(data, s.Luks); err != nil {
221
+ s.Luks = nil
222
+ return err
223
+ }
224
+ case BlockdevQcow2EncryptionFormatAes:
225
+ s.Aes = true
226
+
227
+ default:
228
+ return fmt.Errorf("error: unmarshal: ImageInfoSpecificQCow2Encryption: received unrecognized value: '%s'",
229
+ tmp.Format)
230
+ }
231
+ return nil
232
+ }
233
+
234
+
235
+Alternate
236
+---------
237
+
238
+Like Unions, alternates can have branches. Unlike Unions, they don't
239
+have a discriminator field and each branch should be a different class
240
+of Type entirely (e.g: You can't have two branches of type int in one
241
+Alternate).
242
+
243
+While the marshalling is similar to Unions, the unmarshalling uses a
244
+try-and-error approach, trying to fit the data payload in one of the
245
+Alternate fields.
246
+
247
+The biggest caveat is handling Alternates that can take JSON Null as
248
+value. The issue lies on ``encoding/json`` library limitation where
249
+unmarshalling JSON Null data to a Go struct which has the 'omitempty'
250
+field as it will bypass the Marshal interface. The same happens when
251
+marshalling, if the field tag 'omitempty' is used, a nil pointer would
252
+never be translated to null JSON value. The problem here is that we do
253
+use pointer to type plus ``omitempty`` field to express a QAPI
254
+optional member.
255
+
256
+In order to handle JSON Null, the generator needs to do the following:
257
+ - Read the QAPI schema prior to generate any code and cache
258
+ all alternate types that can take JSON Null
259
+ - For all Go structs that should be considered optional and they type
260
+ are one of those alternates, do not set ``omitempty`` and implement
261
+ Marshal interface for this Go struct, to properly handle JSON Null
262
+ - In the Alternate, uses a boolean 'IsNull' to express a JSON Null
263
+ and implement the AbsentAlternate interface, to help structs know
264
+ if a given Alternate type should be considered Absent (not set) or
265
+ any other possible Value, including JSON Null.
266
+
267
+::
268
+
269
+ { 'alternate': 'BlockdevRefOrNull',
270
+ 'data': { 'definition': 'BlockdevOptions',
271
+ 'reference': 'str',
272
+ 'null': 'null' } }
273
+
274
+.. code-block:: go
275
+
276
+ // Reference to a block device.
277
+ //
278
+ // Since: 2.9
279
+ type BlockdevRefOrNull struct {
280
+ // defines a new block device inline
281
+ Definition *BlockdevOptions
282
+ // references the ID of an existing block device. An empty string
283
+ // means that no block device should be referenced. Deprecated;
284
+ // use null instead.
285
+ Reference *string
286
+ // No block device should be referenced (since 2.10)
287
+ IsNull bool
288
+ }
289
+
290
+ func (s *BlockdevRefOrNull) ToAnyOrAbsent() (any, bool) {
291
+ if s != nil {
292
+ if s.IsNull {
293
+ return nil, false
294
+ } else if s.Definition != nil {
295
+ return *s.Definition, false
296
+ } else if s.Reference != nil {
297
+ return *s.Reference, false
298
+ }
299
+ }
300
+
301
+ return nil, true
302
+ }
303
+
304
+ func (s BlockdevRefOrNull) MarshalJSON() ([]byte, error) {
305
+ if s.IsNull {
306
+ return []byte("null"), nil
307
+ } else if s.Definition != nil {
308
+ return json.Marshal(s.Definition)
309
+ } else if s.Reference != nil {
310
+ return json.Marshal(s.Reference)
311
+ }
312
+ return []byte("{}"), nil
313
+ }
314
+
315
+ func (s *BlockdevRefOrNull) UnmarshalJSON(data []byte) error {
316
+ // Check for json-null first
317
+ if string(data) == "null" {
318
+ s.IsNull = true
319
+ return nil
320
+ }
321
+ // Check for BlockdevOptions
322
+ {
323
+ s.Definition = new(BlockdevOptions)
324
+ if err := StrictDecode(s.Definition, data); err == nil {
325
+ return nil
326
+ }
327
+ s.Definition = nil
328
+ }
329
+
330
+ // Check for string
331
+ {
332
+ s.Reference = new(string)
333
+ if err := StrictDecode(s.Reference, data); err == nil {
334
+ return nil
335
+ }
336
+ s.Reference = nil
337
+ }
338
+
339
+ return fmt.Errorf("Can't convert to BlockdevRefOrNull: %s", string(data))
340
+ }
341
+
342
+
343
+Event
344
+-----
345
+
346
+Each event is mapped to its own struct with.
347
+
348
+::
349
+
350
+ { 'event': 'SHUTDOWN',
351
+ 'data': { 'guest': 'bool',
352
+ 'reason': 'ShutdownCause' } }
353
+
354
+.. code-block:: go
355
+
356
+ // Emitted when the virtual machine has shut down, indicating that
357
+ // qemu is about to exit.
358
+ //
359
+ // .. note:: If the command-line option "-no-shutdown" has been
360
+ // specified, qemu will not exit, and a STOP event will eventually
361
+ // follow the SHUTDOWN event.
362
+ //
363
+ // Since: 0.12
364
+ //
365
+ // .. qmp-example:: <- { "event": "SHUTDOWN", "data": {
366
+ // "guest": true, "reason": "guest-shutdown" }, "timestamp": {
367
+ // "seconds": 1267040730, "microseconds": 682951 } }
368
+ type ShutdownEvent struct {
369
+ // If true, the shutdown was triggered by a guest request (such as
370
+ // a guest-initiated ACPI shutdown request or other hardware-
371
+ // specific action) rather than a host request (such as sending
372
+ // qemu a SIGINT). (since 2.10)
373
+ Guest bool `json:"guest"`
374
+ // The @ShutdownCause which resulted in the SHUTDOWN. (since 4.0)
375
+ Reason ShutdownCause `json:"reason"`
376
+ }
377
+
378
+
379
+Command
380
+-------
381
+
382
+Each commands is mapped to its own struct. If the command has a boxed
383
+data struct, the option struct will be embed in the command struct.
384
+
385
+The return value is always a well defined type and it is part of first
386
+layer unmarshalling type, Message.
387
+
388
+::
389
+
390
+ { 'command': 'set_password',
391
+ 'boxed': true,
392
+ 'data': 'SetPasswordOptions' }
393
+
394
+ { 'union': 'SetPasswordOptions',
395
+ 'base': { 'protocol': 'DisplayProtocol',
396
+ 'password': 'str',
397
+ '*connected': 'SetPasswordAction' },
398
+ 'discriminator': 'protocol',
399
+ 'data': { 'vnc': 'SetPasswordOptionsVnc' } }
400
+
401
+.. code-block:: go
402
+
403
+ // Set the password of a remote display server.
404
+ // Errors: - If Spice is not enabled, DeviceNotFound
405
+ //
406
+ // Since: 0.14
407
+ //
408
+ // .. qmp-example:: -> { "execute": "set_password", "arguments": {
409
+ // "protocol": "vnc", "password": "secret" }
410
+ // } <- { "return": {} }
411
+ type SetPasswordCommand struct {
412
+ SetPasswordOptions
413
+ }
414
+
415
+Now an example of a command without boxed type.
416
+
417
+::
418
+
419
+ { 'command': 'set_link',
420
+ 'data': {'name': 'str', 'up': 'bool'} }
421
+
422
+.. code-block:: go
423
+
424
+ // Sets the link status of a virtual network adapter.
425
+ //
426
+ // Errors: - If @name is not a valid network device, DeviceNotFound
427
+ //
428
+ // Since: 0.14
429
+ //
430
+ // .. note:: Not all network adapters support setting link status.
431
+ // This command will succeed even if the network adapter does not
432
+ // support link status notification. .. qmp-example:: -> {
433
+ // "execute": "set_link", "arguments": { "name": "e1000.0", "up":
434
+ // false } } <- { "return": {} }
435
+ type SetLinkCommand struct {
436
+ // the device name of the virtual network adapter
437
+ Name string `json:"name"`
438
+ // true to set the link status to be up
439
+ Up bool `json:"up"`
440
+ }
441
+
442
+Known issues
443
+============
444
+
445
+- Type names might not follow proper Go convention. Andrea suggested an
446
+ annotation to the QAPI schema that could solve it.
447
+ https://lists.gnu.org/archive/html/qemu-devel/2022-05/msg00127.html
123
--
448
--
124
2.32.0
449
2.48.1
125
126
diff view generated by jsdifflib