Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,50 @@ jobs:
cd integration_tests/grpc_tests/java
mvn -T16 --no-transfer-progress -Dtest=RustGrpcTest test

grpc_java_cpp_tests:
name: Java/C++ gRPC Tests
needs: changes
if: needs.changes.outputs.grpc_tests == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: 21
distribution: "temurin"
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.11
cache: "pip"
- name: Set up Bazel
uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
with:
bazelisk-cache: true
bazelisk-version: "1.x"
- name: Cache Maven local repository
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Install Java artifacts for gRPC tests
run: |
cd java
mvn -T16 --no-transfer-progress clean install -DskipTests -Dmaven.javadoc.skip=true -Dmaven.source.skip=true
- name: Generate gRPC test sources
run: python integration_tests/grpc_tests/generate_grpc.py
- name: Build C++ gRPC peer
run: |
cd cpp
bazel build //integration_tests/grpc_tests/cpp:grpc_interop --config=x86_64
- name: Run Java/C++ gRPC tests
run: |
cd integration_tests/grpc_tests/java
mvn -T16 --no-transfer-progress -Dtest=CppGrpcTest test

grpc_java_kotlin_tests:
name: Java/Kotlin gRPC Tests
needs: changes
Expand Down
7 changes: 6 additions & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ use_repo(
bazel_dep(name = "cython", version = "3.1.3")

# Google Test
bazel_dep(name = "googletest", version = "1.15.2")
bazel_dep(name = "googletest", version = "1.17.0")

# gRPC C++ integration tests
bazel_dep(name = "grpc", version = "1.75.0")
bazel_dep(name = "protobuf", version = "31.1")
single_version_override(module_name = "grpc-java", version = "1.75.0")

# Hedron's Compile Commands Extractor for Bazel
bazel_dep(name = "hedron_compile_commands", dev_dependency = True)
Expand Down
3 changes: 2 additions & 1 deletion compiler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The FDL compiler generates cross-language serialization code from schema definit
- **Type ID and namespace support**: Both numeric IDs and name-based type registration
- **Field modifiers**: Optional fields, reference tracking, list fields, scalar encoding modifiers
- **File imports**: Modular schemas with import support
- **gRPC service generation**: Native gRPC stubs and service bases for Java, Python, Go, Rust, C#, JavaScript, Dart, Kotlin, and Scala
- **gRPC service generation**: Native gRPC stubs and service bases for Java, Python, Go, Rust, C++, C#, JavaScript, Dart, Kotlin, and Scala

## Documentation

Expand Down Expand Up @@ -335,6 +335,7 @@ fory_compiler/
├── python.py # Python gRPC companion module (grpcio style)
├── go.py # Go gRPC stub generator (google.golang.org/grpc)
├── rust.py # Rust gRPC service module (tonic style)
├── cpp.py # C++ synchronous gRPC service companions
├── csharp.py # C# gRPC service companion (Grpc.Core style)
├── javascript.py # JavaScript Node.js and gRPC-Web client generators
├── dart.py # Dart gRPC service companion
Expand Down
44 changes: 34 additions & 10 deletions compiler/fory_compiler/generators/cpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@

"""C++ code generator."""

import re
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple
import typing

from fory_compiler.generators.base import BaseGenerator, GeneratedFile
from fory_compiler.generators.services.cpp import CppServiceGeneratorMixin
from fory_compiler.frontend.utils import parse_idl_file
from fory_compiler.ir.ast import (
Message,
Expand All @@ -39,7 +41,7 @@
from fory_compiler.ir.types import PrimitiveKind


class CppGenerator(BaseGenerator):
class CppGenerator(CppServiceGeneratorMixin, BaseGenerator):
"""Generates C++ classes with FORY_STRUCT macros."""

language_name = "cpp"
Expand Down Expand Up @@ -741,7 +743,7 @@ def _get_union_selector_base(self, union: Union) -> str:
def _ensure_name_caches(self, schema: Schema) -> None:
"""Construct the naming caches once for a schema file."""
if not hasattr(self, "_named_schema_ids"):
# Init everything.
# We don't initialize cache for gRPC code generation here.
self._named_schema_ids: Set[int] = set()
self._type_identifier_cache: Dict[Tuple[object, ...], str] = {}
self._field_identifier_cache: Dict[
Expand Down Expand Up @@ -883,12 +885,19 @@ def generate_bytes_methods(self, class_name: str, indent: str) -> List[str]:
lines.append(f"{indent} return {detail}::get_fory().serialize(*this);")
lines.append(f"{indent}}}")
lines.append("")
# gRPC payload deserialization would require this.
lines.append(
f"{indent}static ::fory::Result<{class_name}, ::fory::Error> from_bytes(const ::std::vector<::uint8_t>& data) {{"
f"{indent}static ::fory::Result<{class_name}, ::fory::Error> from_bytes(const ::uint8_t* data, ::std::size_t size) {{"
)
lines.append(
f"{indent} return {detail}::get_fory().deserialize<{class_name}>(data, size);"
)
lines.append(f"{indent}}}")
lines.append("")
lines.append(
f"{indent} return {detail}::get_fory().deserialize<{class_name}>(data);"
f"{indent}static ::fory::Result<{class_name}, ::fory::Error> from_bytes(const ::std::vector<::uint8_t>& data) {{"
)
lines.append(f"{indent} return from_bytes(data.data(), data.size());")
lines.append(f"{indent}}}")
return lines

Expand All @@ -911,7 +920,7 @@ def generate_header(self) -> GeneratedFile:
includes.add("<utility>")
includes.add('"fory/serialization/fory.h"')
if self.schema_has_unions():
includes.add("<cstddef>") # todo: what's this??
includes.add("<cstddef>")
includes.add("<utility>")
includes.add("<variant>")
includes.add("<memory>")
Expand All @@ -936,14 +945,11 @@ def generate_header(self) -> GeneratedFile:
self.collect_union_includes(union, includes)

# License header
lines.append("/*")
for line in self.get_license_header(" *").split("\n"):
lines.append(line)
lines.append(" */")
lines.extend(self._cpp_license_lines())
lines.append("")

# Header guard
guard_name = f"{self.get_header_name().upper()}_H_"
guard_name = self._cpp_header_guard(f"{self.get_header_name()}.h")
lines.append(f"#ifndef {guard_name}")
lines.append(f"#define {guard_name}")
lines.append("")
Expand Down Expand Up @@ -1024,6 +1030,24 @@ def generate_header(self) -> GeneratedFile:
content="\n".join(lines),
)

def _cpp_license_lines(self) -> List[str]:
"""Generate the Apache license block for generated C++ files."""
lines = ["/*"]
lines.extend(self.get_license_header(" *").split("\n"))
lines.append(" */")
return lines

def _cpp_header_guard(self, path: str) -> str:
"""Generate a header guard name for a path."""
# Header names can contain '.', '-', '/', or other characters outside
# the conservative ASCII macro identifier set used by generated headers.
return re.sub(r"[^A-Za-z0-9]", "_", path).upper() + "_"

def indent_lines(self, lines: List[str], level: int) -> List[str]:
"""Indent a list of lines by the given level."""
prefix = " " * level # C++ uses two-spaces style.
return [f"{prefix}{line}" if line else line for line in lines]

def collect_message_includes(self, message: Message, includes: Set[str]):
"""Collect includes for a message and its nested types recursively."""
for field in message.fields:
Expand Down
22 changes: 1 addition & 21 deletions compiler/fory_compiler/generators/rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ def _allocate_scoped_message_identifiers(self, message: Message) -> None:
def _ensure_name_caches(self, schema: Schema) -> None:
"""Construct the naming caches once for a schema file."""
if not hasattr(self, "_named_schema_ids"):
# Init everything.
# We don't initialize cache for gRPC code generation here.
self._named_schema_ids: Set[int] = set()
self._type_identifier_cache: Dict[Tuple[object, ...], str] = {}
self._module_identifier_cache: Dict[Tuple[object, ...], str] = {}
Expand All @@ -420,26 +420,6 @@ def _ensure_name_caches(self, schema: Schema) -> None:
self._union_case_identifier_cache: Dict[
Tuple[object, ...], Dict[Tuple[object, ...], str]
] = {}
self._named_service_schema_ids: Set[int] = set()
self._service_trait_identifier_cache: Dict[Tuple[object, ...], str] = {}
self._service_client_module_identifier_cache: Dict[
Tuple[object, ...], str
] = {}
self._service_server_module_identifier_cache: Dict[
Tuple[object, ...], str
] = {}
self._service_name_constant_identifier_cache: Dict[
Tuple[object, ...], str
] = {}
self._rpc_method_identifier_cache: Dict[
Tuple[object, ...], Dict[Tuple[object, ...], str]
] = {}
self._rpc_stream_type_identifier_cache: Dict[
Tuple[object, ...], Dict[Tuple[object, ...], str]
] = {}
self._rpc_path_constant_identifier_cache: Dict[
Tuple[object, ...], Dict[Tuple[object, ...], str]
] = {}
schema_id = id(schema)
if schema_id in self._named_schema_ids:
# Cache exists.
Expand Down
Loading
Loading