Skip to content

Commit a9246c4

Browse files
committed
alt_core_api: Add protoc plugin for generating IPC definitions
Add `protoc-gen-arduinoif`, a protobuf plugin that generates IPC/interface headers from IDL `.proto` files. The plugin treats protobuf services as the IDL source and emits the current header surface used by `alt_core_api`. Runtime IPC/RPC transport and stub implementations are intentionally out of scope for now. Signed-off-by: TOKITA Hiroshi <tokita.hiroshi@gmail.com>
1 parent 19f3aa3 commit a9246c4

9 files changed

Lines changed: 687 additions & 0 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (c) 2026 TOKITA Hiroshi
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
import sys
8+
import pathlib
9+
10+
SCRIPT_DIR = pathlib.Path(__file__).resolve().parent
11+
sys.path.insert(0, str(SCRIPT_DIR))
12+
13+
from protoc_gen_arduinoif.core import main
14+
15+
if __name__ == "__main__":
16+
raise SystemExit(main())
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (c) 2026 TOKITA Hiroshi
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
from __future__ import annotations
8+
9+
import importlib.util
10+
import os
11+
import re
12+
import sys
13+
import types
14+
from functools import lru_cache
15+
from pathlib import Path
16+
from typing import List
17+
18+
__all__ = [
19+
"full_service_name",
20+
"snake_case",
21+
"uniq",
22+
"get_arduino_opts_pb2",
23+
]
24+
25+
26+
def full_service_name(pkgname: str, servicename: str) -> str:
27+
return f".{pkgname}.{servicename}" if pkgname else f".{servicename}"
28+
29+
30+
def snake_case(name: str) -> str:
31+
word_edges = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name)
32+
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", word_edges).lower()
33+
34+
35+
def uniq(values: List[str]) -> List[str]:
36+
return list(dict.fromkeys(values))
37+
38+
39+
@lru_cache(maxsize=1)
40+
def get_arduino_opts_pb2() -> types.ModuleType:
41+
module_name = "_protoc_gen_arduinoif_arduino_opts_pb2"
42+
pb2_path = os.environ.get("PROTOC_GEN_ARDUINOIF_PB2")
43+
if not pb2_path:
44+
raise RuntimeError(
45+
"PROTOC_GEN_ARDUINOIF_PB2 is not set; it must point to the "
46+
"generated arduino_opts_pb2.py file before running "
47+
"protoc-gen-arduinoif"
48+
)
49+
50+
source_path = Path(pb2_path)
51+
if not source_path.exists():
52+
raise RuntimeError(
53+
f"PROTOC_GEN_ARDUINOIF_PB2 points to a missing file: '{source_path}'"
54+
)
55+
56+
spec = importlib.util.spec_from_file_location(module_name, source_path)
57+
if spec is None or spec.loader is None:
58+
raise RuntimeError(
59+
"failed to load arduino_opts_pb2 from "
60+
f"PROTOC_GEN_ARDUINOIF_PB2='{source_path}'"
61+
)
62+
63+
module = importlib.util.module_from_spec(spec)
64+
sys.modules[module_name] = module
65+
spec.loader.exec_module(module)
66+
return module
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (c) 2026 TOKITA Hiroshi
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
from __future__ import annotations
8+
9+
import sys
10+
from typing import Iterator, Tuple
11+
12+
from google.protobuf.compiler import plugin_pb2 as pb2
13+
14+
from .common import get_arduino_opts_pb2
15+
from .enum_renderer import EnumRenderer
16+
from .service_renderer import ServiceRenderer
17+
from .service_model import ServiceModelBuilder
18+
from .request_context import RequestContext
19+
20+
__all__ = [
21+
"main",
22+
]
23+
24+
25+
def _iter_generated_files(
26+
request: pb2.CodeGeneratorRequest,
27+
) -> Iterator[Tuple[str, str]]:
28+
context = RequestContext.build(request)
29+
proto_names = {proto_file.name: proto_file for proto_file in request.proto_file}
30+
generated_files = [proto_names[name] for name in request.file_to_generate]
31+
32+
for proto_file in generated_files:
33+
if proto_file.enum_type and not proto_file.service:
34+
yield from EnumRenderer(proto_file.name, list(proto_file.enum_type))
35+
36+
for service in proto_file.service:
37+
service_model = ServiceModelBuilder.build(
38+
service,
39+
proto_file.package,
40+
list(proto_file.enum_type),
41+
context,
42+
)
43+
yield from ServiceRenderer(service_model)
44+
45+
46+
def main() -> int:
47+
# Extensions must be registered before parsing request payload.
48+
get_arduino_opts_pb2()
49+
req = pb2.CodeGeneratorRequest()
50+
req.ParseFromString(sys.stdin.buffer.read())
51+
resp = pb2.CodeGeneratorResponse()
52+
53+
try:
54+
for name, content in _iter_generated_files(req):
55+
output = resp.file.add()
56+
output.name = name
57+
output.content = content
58+
except Exception as error:
59+
resp.error = str(error)
60+
61+
if hasattr(pb2.CodeGeneratorResponse, "FEATURE_PROTO3_OPTIONAL"):
62+
resp.supported_features = pb2.CodeGeneratorResponse.FEATURE_PROTO3_OPTIONAL
63+
64+
sys.stdout.buffer.write(resp.SerializeToString())
65+
return 0
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (c) 2026 TOKITA Hiroshi
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
from __future__ import annotations
8+
9+
from pathlib import PurePosixPath
10+
from typing import Iterator, List, Tuple
11+
12+
from google.protobuf.descriptor_pb2 import EnumDescriptorProto
13+
14+
__all__ = [
15+
"EnumRenderer",
16+
]
17+
18+
19+
class EnumRenderer:
20+
"""Renders top-level protobuf enums as a C-compatible type header."""
21+
22+
def __init__(self, proto_name: str, enums: List[EnumDescriptorProto]) -> None:
23+
self._proto_name = proto_name
24+
self._enums = enums
25+
26+
def __iter__(self) -> Iterator[Tuple[str, str]]:
27+
yield self._header_name(), self._render_header_content()
28+
29+
def _header_name(self) -> str:
30+
return f"{PurePosixPath(self._proto_name).stem}_types.h"
31+
32+
def _render_header_content(self) -> str:
33+
lines = [
34+
"// Generated by protoc-gen-arduinoif. DO NOT EDIT.",
35+
"",
36+
"#pragma once",
37+
"",
38+
"#include <stdint.h>",
39+
"",
40+
]
41+
42+
for enum_desc in self._enums:
43+
lines.extend([f"typedef enum {enum_desc.name} {{"])
44+
lines.extend(f" {v.name} = {v.number}," for v in enum_desc.value)
45+
lines.extend([f"}} {enum_desc.name};", ""])
46+
47+
return "\n".join(lines)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (c) 2026 TOKITA Hiroshi
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
from __future__ import annotations
8+
9+
from typing import Dict, List, NamedTuple, Tuple
10+
11+
from google.protobuf.descriptor_pb2 import DescriptorProto, FieldDescriptorProto
12+
13+
__all__ = [
14+
"MethodSpec",
15+
]
16+
17+
_DEFAULT_FIELD_CPP_TYPES: Dict[int, str] = {
18+
FieldDescriptorProto.TYPE_BOOL: "bool",
19+
FieldDescriptorProto.TYPE_INT32: "int32_t",
20+
FieldDescriptorProto.TYPE_INT64: "int64_t",
21+
FieldDescriptorProto.TYPE_UINT32: "uint32_t",
22+
FieldDescriptorProto.TYPE_UINT64: "uint64_t",
23+
FieldDescriptorProto.TYPE_SINT32: "int32_t",
24+
FieldDescriptorProto.TYPE_SINT64: "int64_t",
25+
FieldDescriptorProto.TYPE_FIXED32: "uint32_t",
26+
FieldDescriptorProto.TYPE_FIXED64: "uint64_t",
27+
FieldDescriptorProto.TYPE_SFIXED32: "int32_t",
28+
FieldDescriptorProto.TYPE_SFIXED64: "int64_t",
29+
FieldDescriptorProto.TYPE_FLOAT: "float",
30+
FieldDescriptorProto.TYPE_DOUBLE: "double",
31+
FieldDescriptorProto.TYPE_STRING: "const char *",
32+
FieldDescriptorProto.TYPE_BYTES: "const uint8_t *",
33+
FieldDescriptorProto.TYPE_ENUM: "int32_t",
34+
}
35+
36+
37+
def _opt_str(options, extension) -> str:
38+
try:
39+
return str(options.Extensions[extension]).strip()
40+
except (AttributeError, KeyError):
41+
return ""
42+
43+
44+
class MethodSpec(NamedTuple):
45+
"""C++ method signature derived from one protobuf RPC method."""
46+
47+
decl: str
48+
overload_key: Tuple[str, Tuple[str, ...]]
49+
call_name: str
50+
arg_names: List[str]
51+
returns_void: bool
52+
visibility: str
53+
54+
@classmethod
55+
def build(
56+
cls,
57+
method,
58+
opts_pb2,
59+
message_map: Dict[str, DescriptorProto],
60+
) -> "MethodSpec":
61+
def _resolve_field(field: FieldDescriptorProto) -> Tuple[str, str]:
62+
field_opts = field.options
63+
explicit_cpp_type = _opt_str(field_opts, opts_pb2.cpp_type)
64+
if explicit_cpp_type:
65+
field_type = explicit_cpp_type
66+
else:
67+
if field.label == FieldDescriptorProto.LABEL_REPEATED:
68+
raise ValueError(
69+
f"{method.name}: field '{field.name}' is repeated; "
70+
"C++ header generation requires an explicit "
71+
"(arduino.cpp_type) override"
72+
)
73+
if field.type not in _DEFAULT_FIELD_CPP_TYPES:
74+
raise ValueError(
75+
f"{method.name}: field '{field.name}' has unsupported "
76+
f"protobuf type '{field.type}'; C++ header generation "
77+
"requires an explicit (arduino.cpp_type) override"
78+
)
79+
field_type = _DEFAULT_FIELD_CPP_TYPES[field.type]
80+
field_name = _opt_str(field_opts, opts_pb2.field_cpp_name) or field.name
81+
return field_type, field_name
82+
83+
input_msg = message_map.get(method.input_type)
84+
output_msg = message_map.get(method.output_type)
85+
86+
if input_msg is None:
87+
raise ValueError(
88+
f"{method.name}: unknown input type '{method.input_type}'; "
89+
"use google.protobuf.Empty for methods without parameters"
90+
)
91+
if output_msg is None:
92+
raise ValueError(
93+
f"{method.name}: unknown output type '{method.output_type}'; "
94+
"use google.protobuf.Empty for void methods"
95+
)
96+
if len(output_msg.field) > 1:
97+
raise ValueError(
98+
f"{method.name}: output type '{method.output_type}' has "
99+
f"{len(output_msg.field)} fields; C++ header generation supports "
100+
"void outputs or a single return-value field"
101+
)
102+
103+
visibility = _opt_str(method.options, opts_pb2.method_visibility).lower()
104+
if visibility not in {"public", "protected", "private", ""}:
105+
raise ValueError(
106+
f"{method.name}: unsupported method_visibility '{visibility}' "
107+
"(expected: public, protected, private)"
108+
)
109+
if not visibility:
110+
visibility = "public"
111+
112+
method_name = _opt_str(method.options, opts_pb2.cpp_name) or method.name
113+
return_type = _opt_str(method.options, opts_pb2.cpp_return)
114+
if not return_type:
115+
return_type = (
116+
"void"
117+
if len(output_msg.field) == 0
118+
else _resolve_field(output_msg.field[0])[0]
119+
)
120+
121+
params = [_resolve_field(f) for f in input_msg.field]
122+
arg_types = tuple(ftype for ftype, _ in params)
123+
arg_names = [name for _, name in params]
124+
params_blob = ", ".join(f"{ftype} {fname}" for ftype, fname in params)
125+
126+
decl = (
127+
f"{method_name}({params_blob})"
128+
if method_name.startswith("operator ")
129+
else f"{return_type} {method_name}({params_blob})"
130+
)
131+
132+
return cls(
133+
decl=decl,
134+
overload_key=(method_name, arg_types),
135+
call_name=method_name,
136+
arg_names=arg_names,
137+
returns_void=(return_type == "void"),
138+
visibility=visibility,
139+
)

0 commit comments

Comments
 (0)