[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[RFC PATCH v1 1/8] qapi: golang: Generate qapi's enum types in Go
From: |
Victor Toso |
Subject: |
[RFC PATCH v1 1/8] qapi: golang: Generate qapi's enum types in Go |
Date: |
Sat, 2 Apr 2022 00:40:57 +0200 |
This patch handles QAPI enum types and generates its equivalent in Go.
The highlights of this implementation are:
1. For each QAPI enum, we will define an int32 type in Go to be the
assigned type of this specific enum
2. While in the Go codebase we can use the generated enum values, the
specification requires that, on the wire, the enumeration type's
value to be represented by its string name.
For this reason, each Go type that represent's a QAPI enum will be
implementing the Marshaler[0] and Unmarshaler[1] interfaces to
seamless handle QMP's string to Go int32 value and vice-versa.
3. Naming: CamelCase will be used in any identifier that we want to
export [2], which is everything in this patch.
[0] https://pkg.go.dev/encoding/json#Marshaler
[1] https://pkg.go.dev/encoding/json#Unmarshaler
[2] https://go.dev/ref/spec#Exported_identifiers
Signed-off-by: Victor Toso <victortoso@redhat.com>
---
qapi/meson.build | 1 +
scripts/qapi/golang.py | 225 +++++++++++++++++++++++++++++++++++++++++
scripts/qapi/main.py | 2 +
3 files changed, 228 insertions(+)
create mode 100644 scripts/qapi/golang.py
diff --git a/qapi/meson.build b/qapi/meson.build
index 656ef0e039..0951692332 100644
--- a/qapi/meson.build
+++ b/qapi/meson.build
@@ -90,6 +90,7 @@ qapi_nonmodule_outputs = [
'qapi-init-commands.h', 'qapi-init-commands.c',
'qapi-events.h', 'qapi-events.c',
'qapi-emit-events.c', 'qapi-emit-events.h',
+ 'qapibara.go',
]
# First build all sources
diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
new file mode 100644
index 0000000000..070d4cbbae
--- /dev/null
+++ b/scripts/qapi/golang.py
@@ -0,0 +1,225 @@
+"""
+Golang QAPI generator
+"""
+# Copyright (c) 2021 Red Hat Inc.
+#
+# Authors:
+# Victor Toso <victortoso@redhat.com>
+#
+# This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+
+# Just for type hint on self
+from __future__ import annotations
+
+import os
+from typing import Dict, List, Optional
+
+from .schema import (
+ QAPISchema,
+ QAPISchemaVisitor,
+ QAPISchemaEnumMember,
+ QAPISchemaFeature,
+ QAPISchemaIfCond,
+ QAPISchemaObjectType,
+ QAPISchemaObjectTypeMember,
+ QAPISchemaVariants,
+)
+from .source import QAPISourceInfo
+
+class QAPISchemaGenGolangVisitor(QAPISchemaVisitor):
+
+ def __init__(self, prefix: str):
+ super().__init__()
+ self.target = {name: "" for name in ["enum"]}
+ self.schema = None
+ self._docmap = {}
+ self.golang_package_name = "qapi"
+
+ def visit_begin(self, schema):
+ self.schema = schema
+
+ # Every Go file needs to reference its package name
+ for target in self.target:
+ self.target[target] = f"package {self.golang_package_name}\n"
+
+ # Iterate once in schema.docs to map doc objects to its name
+ for doc in schema.docs:
+ if doc.symbol is None:
+ continue
+ self._docmap[doc.symbol] = doc
+
+ def visit_end(self):
+ self.schema = None
+
+ def visit_object_type(self: QAPISchemaGenGolangVisitor,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ base: Optional[QAPISchemaObjectType],
+ members: List[QAPISchemaObjectTypeMember],
+ variants: Optional[QAPISchemaVariants]
+ ) -> None:
+ pass
+
+ def visit_alternate_type(self: QAPISchemaGenGolangVisitor,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ variants: QAPISchemaVariants
+ ) -> None:
+ pass
+
+ def visit_enum_type(self: QAPISchemaGenGolangVisitor,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ members: List[QAPISchemaEnumMember],
+ prefix: Optional[str]
+ ) -> None:
+ doc = self._docmap.get(name, None)
+ doc_struct, doc_fields = qapi_to_golang_struct_docs(doc)
+
+ value = qapi_to_field_name_enum(members[0].name)
+ fields = f"\t{name}{value} {name} = iota\n"
+ for member in members[1:]:
+ field_doc = " " + doc_fields.get(member.name, "") if doc_fields
else ""
+ value = qapi_to_field_name_enum(member.name)
+ fields += f"\t{name}{value}{field_doc}\n"
+
+ self.target["enum"] += f'''
+{doc_struct}
+type {name} int32
+const (
+{fields[:-1]}
+)
+{generate_marshal_methods_enum(members)}
+'''
+
+ def visit_array_type(self, name, info, ifcond, element_type):
+ pass
+
+ def visit_command(self,
+ name: str,
+ info: Optional[QAPISourceInfo],
+ ifcond: QAPISchemaIfCond,
+ features: List[QAPISchemaFeature],
+ arg_type: Optional[QAPISchemaObjectType],
+ ret_type: Optional[QAPISchemaType],
+ gen: bool,
+ success_response: bool,
+ boxed: bool,
+ allow_oob: bool,
+ allow_preconfig: bool,
+ coroutine: bool) -> None:
+ pass
+
+ def visit_event(self, name, info, ifcond, features, arg_type, boxed):
+ pass
+
+ def write(self, output_dir: str) -> None:
+ for module_name, content in self.target.items():
+ go_module = module_name + "s.go"
+ go_dir = "go"
+ pathname = os.path.join(output_dir, go_dir, go_module)
+ odir = os.path.dirname(pathname)
+ os.makedirs(odir, exist_ok=True)
+
+ with open(pathname, "w") as outfile:
+ outfile.write(content)
+
+
+def gen_golang(schema: QAPISchema,
+ output_dir: str,
+ prefix: str) -> None:
+ vis = QAPISchemaGenGolangVisitor(prefix)
+ schema.visit(vis)
+ vis.write(output_dir)
+
+def generate_marshal_methods_enum(members: List[QAPISchemaEnumMember]) -> str:
+ type = qapi_to_go_type_name(members[0].defined_in, "enum")
+
+ marshal_switch_cases = ""
+ unmarshal_switch_cases = ""
+ for i in range(len(members)):
+ go_type = type + qapi_to_field_name_enum(members[i].name)
+ name = members[i].name
+
+ marshal_switch_cases += f'''
+ case {go_type}:
+ return []byte(`"{name}"`), nil'''
+
+ unmarshal_switch_cases += f'''
+ case "{name}":
+ (*s) = {go_type}'''
+
+ return f'''
+func (s {type}) MarshalJSON() ([]byte, error) {{
+ switch s {{
+{marshal_switch_cases[1:]}
+ default:
+ fmt.Println("Failed to decode {type}", s)
+ }}
+ return nil, errors.New("Failed")
+}}
+
+func (s *{type}) UnmarshalJSON(data []byte) error {{
+ var name string
+
+ if err := json.Unmarshal(data, &name); err != nil {{
+ return err
+ }}
+
+ switch name {{
+{unmarshal_switch_cases[1:]}
+ default:
+ fmt.Println("Failed to decode {type}", *s)
+ }}
+ return nil
+}}
+'''
+
+# Takes the documentation object of a specific type and returns
+# that type's documentation followed by its member's docs.
+def qapi_to_golang_struct_docs(doc: QAPIDoc) -> (str, Dict[str, str]):
+ if doc is None:
+ return "// No documentation available", None
+
+ main = ""
+ if len(doc.body.text) > 0:
+ main = f"// {doc.body.text}".replace("\n", "\n// ")
+
+ for section in doc.sections:
+ # Skip sections that are not useful for Golang consumers
+ if section.name and "TODO" in section.name:
+ continue
+
+ # Small hack to only add // when doc.body.text was empty
+ prefix = "// " if len(main) == 0 else "\n\n"
+ main += f"{prefix}{section.name}: {section.text}".replace("\n", "\n//
")
+
+ fields = {}
+ for key, value in doc.args.items():
+ if len(value.text) > 0:
+ fields[key] = " // " + ' '.join(value.text.replace("\n", "
").split())
+
+ return main, fields
+
+def qapi_to_field_name_enum(name: str) -> str:
+ return name.title().replace("-", "")
+
+def qapi_to_go_type_name(name: str, meta: str) -> str:
+ if name.startswith("q_obj_"):
+ name = name[6:]
+
+ # We want to keep CamelCase for Golang types. We want to avoid removing
+ # already set CameCase names while fixing uppercase ones, eg:
+ # 1) q_obj_SocketAddress_base -> SocketAddressBase
+ # 2) q_obj_WATCHDOG-arg -> WatchdogArg
+ words = [word for word in name.replace("_", "-").split("-")]
+ name = words[0].title() if words[0].islower() or words[0].isupper() else
words[0]
+ name += ''.join(word.title() for word in words[1:])
+ return name
diff --git a/scripts/qapi/main.py b/scripts/qapi/main.py
index fc216a53d3..661fb1e091 100644
--- a/scripts/qapi/main.py
+++ b/scripts/qapi/main.py
@@ -15,6 +15,7 @@
from .common import must_match
from .error import QAPIError
from .events import gen_events
+from .golang import gen_golang
from .introspect import gen_introspect
from .schema import QAPISchema
from .types import gen_types
@@ -54,6 +55,7 @@ def generate(schema_file: str,
gen_events(schema, output_dir, prefix)
gen_introspect(schema, output_dir, prefix, unmask)
+ gen_golang(schema, output_dir, prefix)
def main() -> int:
"""
--
2.35.1
- [RFC PATCH v1 0/8] qapi: add generator for Golang interface, Victor Toso, 2022/04/01
- [RFC PATCH v1 1/8] qapi: golang: Generate qapi's enum types in Go,
Victor Toso <=
- [RFC PATCH v1 2/8] qapi: golang: Generate qapi's alternate types in Go, Victor Toso, 2022/04/01
- [RFC PATCH v1 3/8] qapi: golang: Generate qapi's struct types in Go, Victor Toso, 2022/04/01
- [RFC PATCH v1 4/8] qapi: golang: Generate qapi's union types in Go, Victor Toso, 2022/04/01
- [RFC PATCH v1 5/8] qapi: golang: Generate qapi's event types in Go, Victor Toso, 2022/04/01
- [RFC PATCH v1 7/8] qapi: golang: Add CommandResult type to Go, Victor Toso, 2022/04/01
- [RFC PATCH v1 8/8] qapi: golang: document skip function visit_array_types, Victor Toso, 2022/04/01
- [RFC PATCH v1 6/8] qapi: golang: Generate qapi's command types in Go, Victor Toso, 2022/04/01
- Re: [RFC PATCH v1 0/8] qapi: add generator for Golang interface, Andrea Bolognani, 2022/04/19