This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new bfa42562b fix(grpc): fix rust/go grpc support (#3753)
bfa42562b is described below
commit bfa42562b295261c0cc4d178f20ccc0f67fdb809
Author: Shawn Yang <[email protected]>
AuthorDate: Fri Jun 12 15:46:24 2026 +0800
fix(grpc): fix rust/go grpc support (#3753)
## Why?
## What does this PR do?
## Related issues
#3698
#3738
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence of the final clean AI review results
from both fresh reviewers on the current PR diff or current HEAD after
the latest code changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
.github/workflows/ci.yml | 102 +++++++-
ci/release.py | 6 +
compiler/fory_compiler/generators/go.py | 4 -
compiler/fory_compiler/generators/services/base.py | 5 +-
compiler/fory_compiler/generators/services/go.py | 61 +++--
.../fory_compiler/tests/test_service_codegen.py | 62 ++++-
docs/compiler/compiler-guide.md | 58 ++---
docs/compiler/flatbuffers-idl.md | 13 +-
docs/compiler/generated-code.md | 86 +++++++
docs/compiler/index.md | 15 +-
docs/compiler/protobuf-idl.md | 27 ++-
docs/compiler/schema-idl.md | 10 +-
docs/guide/go/grpc-support.md | 241 +++++++++++++++++++
docs/guide/go/index.md | 1 +
docs/guide/go/troubleshooting.md | 2 +-
docs/guide/java/grpc-support.md | 236 +++++++++++++++++++
docs/guide/java/index.md | 1 +
docs/guide/java/troubleshooting.md | 2 +-
docs/guide/python/grpc-support.md | 210 +++++++++++++++++
docs/guide/python/index.md | 1 +
docs/guide/python/troubleshooting.md | 2 +-
docs/guide/rust/grpc-support.md | 229 ++++++++++++++++++
docs/guide/rust/index.md | 1 +
docs/guide/rust/troubleshooting.md | 2 +-
integration_tests/grpc_tests/go/main.go | 261 +++++++++++++++++----
.../apache/fory/grpc_tests/GoGrpcInteropTest.java | 43 ++++
.../{GrpcInteropTest.java => GrpcTestBase.java} | 207 +++++-----------
.../fory/grpc_tests/PythonGrpcInteropTest.java | 48 ++++
.../fory/grpc_tests/RustGrpcInteropTest.java | 44 ++++
integration_tests/grpc_tests/run_tests.sh | 3 +-
30 files changed, 1685 insertions(+), 298 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6506f5152..6dc1891c5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -49,6 +49,7 @@ jobs:
name: Detect Changed Paths
runs-on: ubuntu-latest
outputs:
+ compiler: ${{ steps.filter.outputs.compiler }}
cpp: ${{ steps.filter.outputs.cpp }}
cpp_code: ${{ steps.filter.outputs.cpp_code }}
java_code: ${{ steps.filter.outputs.java_code }}
@@ -68,6 +69,7 @@ jobs:
BASE_REF: ${{ github.base_ref }}
run: |
if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then
+ echo "compiler=true" >> "$GITHUB_OUTPUT"
echo "cpp=true" >> "$GITHUB_OUTPUT"
echo "cpp_code=true" >> "$GITHUB_OUTPUT"
echo "java_code=true" >> "$GITHUB_OUTPUT"
@@ -84,6 +86,12 @@ jobs:
git fetch --no-tags --depth=1 origin
"$BASE_REF:refs/remotes/origin/$BASE_REF"
changed_files="$(git diff --name-only "origin/$BASE_REF" HEAD)"
+ if grep -Eq '^(compiler/)' <<< "$changed_files"; then
+ echo "compiler=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "compiler=false" >> "$GITHUB_OUTPUT"
+ fi
+
if grep -Eq
'^(cpp/|examples/cpp/|benchmarks/cpp/|integration_tests/idl_tests/cpp/|bazel/|BUILD$|WORKSPACE$|MODULE\.bazel$|\.bazelrc$)'
<<< "$changed_files"; then
echo "cpp=true" >> "$GITHUB_OUTPUT"
else
@@ -687,8 +695,88 @@ jobs:
- name: Run CI
run: python ./ci/run_ci.py java --version integration_tests
- grpc_tests:
- name: Java/Python/Go/Rust gRPC Tests
+ grpc_java_python_tests:
+ name: Java/Python gRPC Tests
+ needs: changes
+ if: needs.changes.outputs.compiler == '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: 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: Install Python gRPC dependencies
+ run: |
+ python -m pip install "grpcio>=1.62.2,<1.71"
+ python -m pip install -v -e python
+ - name: Generate gRPC test sources
+ run: python integration_tests/grpc_tests/generate_grpc.py
+ - name: Run Java/Python gRPC Tests
+ run: |
+ cd integration_tests/grpc_tests/java
+ mvn -T16 --no-transfer-progress -Dtest=PythonGrpcInteropTest test
+
+ grpc_java_go_tests:
+ name: Java/Go gRPC Tests
+ needs: changes
+ if: needs.changes.outputs.compiler == '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: 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 Go gRPC peer
+ run: |
+ cd integration_tests/grpc_tests/go
+ go build -o grpc-interop .
+ - name: Run Java/Go gRPC Tests
+ run: |
+ cd integration_tests/grpc_tests/java
+ mvn -T16 --no-transfer-progress -Dtest=GoGrpcInteropTest test
+
+ grpc_java_rust_tests:
+ name: Java/Rust gRPC Tests
+ needs: changes
+ if: needs.changes.outputs.compiler == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
@@ -718,8 +806,14 @@ jobs:
run: |
cd java
mvn -T16 --no-transfer-progress clean install -DskipTests
-Dmaven.javadoc.skip=true -Dmaven.source.skip=true
- - name: Run Java/Python/Go/Rust gRPC Tests
- run: ./integration_tests/grpc_tests/run_tests.sh
+ - name: Generate gRPC test sources
+ run: python integration_tests/grpc_tests/generate_grpc.py
+ - name: Build Rust gRPC peer
+ run: cargo build --manifest-path
integration_tests/grpc_tests/rust/Cargo.toml --workspace --quiet
+ - name: Run Java/Rust gRPC Tests
+ run: |
+ cd integration_tests/grpc_tests/java
+ mvn -T16 --no-transfer-progress -Dtest=RustGrpcInteropTest test
javascript:
name: JavaScript CI
diff --git a/ci/release.py b/ci/release.py
index a6c810988..779d903a5 100644
--- a/ci/release.py
+++ b/ci/release.py
@@ -282,6 +282,12 @@ def bump_rust_version(new_version):
rust_version,
_update_cargo_package_version,
)
+ _bump_version(
+ "integration_tests/grpc_tests/rust",
+ "Cargo.toml",
+ rust_version,
+ _update_rust_version,
+ )
def bump_kotlin_version(new_version):
diff --git a/compiler/fory_compiler/generators/go.py
b/compiler/fory_compiler/generators/go.py
index c4cc57e73..383dd8a6e 100644
--- a/compiler/fory_compiler/generators/go.py
+++ b/compiler/fory_compiler/generators/go.py
@@ -207,10 +207,6 @@ class GoGenerator(GoServiceGeneratorMixin, BaseGenerator):
# Generate a single Go file with all types
files.append(self.generate_file())
- # Generate gRPC service stubs if requested
- if self.options.grpc:
- files.extend(self.generate_services())
-
return files
def get_package_name(self) -> str:
diff --git a/compiler/fory_compiler/generators/services/base.py
b/compiler/fory_compiler/generators/services/base.py
index 2bc2bcfb2..402d5b8f8 100644
--- a/compiler/fory_compiler/generators/services/base.py
+++ b/compiler/fory_compiler/generators/services/base.py
@@ -18,7 +18,7 @@
"Shared utilities for gRPC service stub generators."
from enum import Enum
-from typing import List, Dict
+from typing import Dict
from fory_compiler.ir.ast import RpcMethod
@@ -49,6 +49,3 @@ class ImportTracker:
def add(self, alias: str, import_path: str) -> None:
self._imports[alias] = import_path
-
- def go_imports(self) -> List[str]:
- return sorted(self._imports.values())
diff --git a/compiler/fory_compiler/generators/services/go.py
b/compiler/fory_compiler/generators/services/go.py
index 399dbe685..264c24aed 100644
--- a/compiler/fory_compiler/generators/services/go.py
+++ b/compiler/fory_compiler/generators/services/go.py
@@ -17,7 +17,7 @@
"""Go gRPC service code generator."""
-from typing import List
+from typing import Dict, List
from fory_compiler.generators.services.base import (
ImportTracker,
StreamingMode,
@@ -36,8 +36,22 @@ class GoServiceGeneratorMixin:
]
if not local_services:
return []
+ self._validate_method_name_collisions(local_services)
return [self._generate_grpc_file(local_services)]
+ def _validate_method_name_collisions(self, services: List[Service]) ->
None:
+ for service in services:
+ seen: Dict[str, str] = {}
+ for method in service.methods:
+ go_name = self.to_pascal_case(method.name)
+ prior = seen.get(go_name)
+ if prior is not None:
+ raise ValueError(
+ f"Go gRPC method name collision in service
{service.name}: "
+ f"{prior} and {method.name} both generate {go_name}"
+ )
+ seen[go_name] = method.name
+
def _generate_grpc_file(self, services: List[Service]) -> GeneratedFile:
"""Generate one _grpc.go file containing all services in the schema."""
lines: List[str] = []
@@ -85,7 +99,6 @@ class GoServiceGeneratorMixin:
'"google.golang.org/grpc/codes"',
'"google.golang.org/grpc/mem"',
'"google.golang.org/grpc/status"',
- '"github.com/apache/fory/go/fory"',
]
for alias, path in tracker._imports.items():
@@ -142,7 +155,6 @@ class GoServiceGeneratorMixin:
lines: List[str] = []
lines.append(f"type {self.to_camel_case(service.name)}Client struct
{{")
lines.append("\tcc grpc.ClientConnInterface")
- lines.append("\tfory *fory.Fory")
lines.append("}")
lines.append("")
return lines
@@ -150,11 +162,9 @@ class GoServiceGeneratorMixin:
def _generate_new_client(self, service: Service) -> List[str]:
lines: List[str] = []
lines.append(
- f"func New{service.name}Client(cc grpc.ClientConnInterface, f
*fory.Fory) {service.name}Client {{"
- )
- lines.append(
- f"\treturn &{self.to_camel_case(service.name)}Client{{cc: cc,
fory: f}}"
+ f"func New{service.name}Client(cc grpc.ClientConnInterface)
{service.name}Client {{"
)
+ lines.append(f"\treturn &{self.to_camel_case(service.name)}Client{{cc:
cc}}")
lines.append("}")
lines.append("")
return lines
@@ -164,31 +174,24 @@ class GoServiceGeneratorMixin:
lines.append(
"// CodecV2 implements grpc/encoding.CodecV2 using Fory
serialization."
)
- lines.append(
- "// Pass a configured *fory.Fory instance with all message types
registered."
- )
- lines.append("type CodecV2 struct {")
- lines.append("\tFory *fory.Fory")
- lines.append("}")
+ lines.append("// It uses the generated package-level thread-safe Fory
runtime.")
+ lines.append("type CodecV2 struct{}")
lines.append("")
lines.append(
- "// Marshal serializes v with Fory. The result is copied before
being handed"
+ "// Marshal serializes v with Fory. The generated thread-safe
runtime returns"
)
lines.append(
- "// to gRPC because Fory reuses its internal write buffer across
calls —"
+ "// a stable copy before releasing pooled Fory instances, so gRPC
never sees"
)
lines.append(
- "// streaming handlers may buffer multiple frames before sending,
and without"
+ "// the reusable internal buffers owned by plain fory.Fory
runtimes."
)
- lines.append("// a copy all frames would alias the last serialized
value.")
lines.append("func (c CodecV2) Marshal(v any) (mem.BufferSlice, error)
{")
- lines.append("\tb, err := c.Fory.Marshal(v)")
+ lines.append("\tb, err := getFory().Serialize(v)")
lines.append("\tif err != nil {")
lines.append("\t\treturn nil, err")
lines.append("\t}")
- lines.append("\tout := make([]byte, len(b))")
- lines.append("\tcopy(out, b)")
- lines.append("\treturn mem.BufferSlice{mem.NewBuffer(&out, nil)}, nil")
+ lines.append("\treturn mem.BufferSlice{mem.NewBuffer(&b, nil)}, nil")
lines.append("}")
lines.append("")
lines.append(
@@ -204,7 +207,7 @@ class GoServiceGeneratorMixin:
lines.append("\tfor _, buf := range data {")
lines.append("\t\tn += copy(b[n:], buf.ReadOnlyData())")
lines.append("\t}")
- lines.append("\treturn c.Fory.Unmarshal(b, v)")
+ lines.append("\treturn getFory().Deserialize(b, v)")
lines.append("}")
lines.append("")
lines.append(
@@ -229,7 +232,7 @@ class GoServiceGeneratorMixin:
)
lines.append(f"\tout := new({res_type[1:]})")
lines.append(
- "\tcallOpts :=
append([]grpc.CallOption{grpc.ForceCodecV2(CodecV2{Fory: c.fory})}, opts...)"
+ "\tcallOpts :=
append([]grpc.CallOption{grpc.ForceCodecV2(CodecV2{})}, opts...)"
)
lines.append(
f'\terr := c.cc.Invoke(ctx,
"{self.get_grpc_method_path(service, method)}", in, out, callOpts...)'
@@ -245,7 +248,7 @@ class GoServiceGeneratorMixin:
f"func (c *{self.to_camel_case(service.name)}Client)
{self.to_pascal_case(method.name)}(ctx context.Context, in {req_type}, opts
...grpc.CallOption) ({service.name}_{self.to_pascal_case(method.name)}Client,
error) {{"
)
lines.append(
- "\tcallOpts :=
append([]grpc.CallOption{grpc.ForceCodecV2(CodecV2{Fory: c.fory})}, opts...)"
+ "\tcallOpts :=
append([]grpc.CallOption{grpc.ForceCodecV2(CodecV2{})}, opts...)"
)
lines.append(
f'\tstream, err := c.cc.NewStream(ctx,
&_{service.name}_serviceDesc.Streams[{stream_index}],
"{self.get_grpc_method_path(service, method)}", callOpts...)'
@@ -271,7 +274,7 @@ class GoServiceGeneratorMixin:
f"func (c *{self.to_camel_case(service.name)}Client)
{self.to_pascal_case(method.name)}(ctx context.Context, opts
...grpc.CallOption) ({service.name}_{self.to_pascal_case(method.name)}Client,
error) {{"
)
lines.append(
- "\tcallOpts :=
append([]grpc.CallOption{grpc.ForceCodecV2(CodecV2{Fory: c.fory})}, opts...)"
+ "\tcallOpts :=
append([]grpc.CallOption{grpc.ForceCodecV2(CodecV2{})}, opts...)"
)
lines.append(
f'\tstream, err := c.cc.NewStream(ctx,
&_{service.name}_serviceDesc.Streams[{stream_index}],
"{self.get_grpc_method_path(service, method)}", callOpts...)'
@@ -683,9 +686,7 @@ class GoServiceGeneratorMixin:
mode = streaming_mode(method)
if mode is StreamingMode.UNARY:
lines.append("\t\t{")
- lines.append(
- f'\t\t\tMethodName:\t"{self.to_pascal_case(method.name)}",'
- )
+ lines.append(f'\t\t\tMethodName:\t"{method.name}",')
lines.append(
f"\t\t\tHandler:\t_{service.name}_{self.to_pascal_case(method.name)}_Handler,"
)
@@ -700,9 +701,7 @@ class GoServiceGeneratorMixin:
continue
else:
lines.append("\t\t{")
- lines.append(
- f'\t\t\tStreamName:\t"{self.to_pascal_case(method.name)}",'
- )
+ lines.append(f'\t\t\tStreamName:\t"{method.name}",')
lines.append(
f"\t\t\tHandler:\t_{service.name}_{self.to_pascal_case(method.name)}_Handler,"
)
diff --git a/compiler/fory_compiler/tests/test_service_codegen.py
b/compiler/fory_compiler/tests/test_service_codegen.py
index 8170b9988..2fb7ad62b 100644
--- a/compiler/fory_compiler/tests/test_service_codegen.py
+++ b/compiler/fory_compiler/tests/test_service_codegen.py
@@ -237,19 +237,59 @@ def test_go_grpc_service_codegen():
files = generate_service_files(schema, GoGenerator)
assert len(files) == 1
content = next(iter(files.values()))
- assert "func NewGreeterClient(" in content
+ assert "func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient"
in content
assert "func RegisterGreeterServer(" in content
assert "type GreeterClient interface" in content
assert "type GreeterServer interface" in content
assert "type UnimplementedGreeterServer struct" in content
- assert "CodecV2{Fory: c.fory}" in content
- assert "type CodecV2 struct" in content
+ assert "CodecV2{}" in content
+ assert "type CodecV2 struct{}" in content
assert "func (c CodecV2) Marshal(v any) (mem.BufferSlice, error)" in
content
assert "func (c CodecV2) Unmarshal(data mem.BufferSlice, v any) error" in
content
+ assert "getFory().Serialize(v)" in content
+ assert "getFory().Deserialize(b, v)" in content
+ assert "*fory.Fory" not in content
+ assert "c.fory" not in content
assert '"/demo.greeter.Greeter/SayHello"' in content
assert "mustEmbedUnimplementedGreeterServer()" in content
+def test_go_grpc_service_desc_uses_idl_method_names():
+ schema = parse_fdl(
+ dedent(
+ """
+ package demo.routes;
+
+ message Req {}
+ message Res {}
+
+ service Router {
+ rpc sayHello (Req) returns (Res);
+ rpc stream_replies (Req) returns (stream Res);
+ rpc clientTalk (stream Req) returns (Res);
+ rpc bidi_chat (stream Req) returns (stream Res);
+ }
+ """
+ )
+ )
+
+ content = next(iter(generate_service_files(schema, GoGenerator).values()))
+ assert "SayHello(ctx context.Context" in content
+ assert "StreamReplies(ctx context.Context" in content
+ assert "ClientTalk(ctx context.Context" in content
+ assert "BidiChat(ctx context.Context" in content
+ assert '"/demo.routes.Router/sayHello"' in content
+ assert '"/demo.routes.Router/stream_replies"' in content
+ assert '"/demo.routes.Router/clientTalk"' in content
+ assert '"/demo.routes.Router/bidi_chat"' in content
+ assert 'MethodName:\t"sayHello"' in content
+ assert 'StreamName:\t"stream_replies"' in content
+ assert 'StreamName:\t"clientTalk"' in content
+ assert 'StreamName:\t"bidi_chat"' in content
+ assert 'MethodName:\t"SayHello"' not in content
+ assert 'StreamName:\t"StreamReplies"' not in content
+
+
def test_java_outer_classname_service_references_nested_model_types():
schema = parse_fdl(
dedent(
@@ -626,6 +666,17 @@ def test_grpc_method_name_collisions_fail():
else:
raise AssertionError("Expected Rust gRPC method name collision")
+ go_generator = GoGenerator(
+ schema, GeneratorOptions(output_dir=Path("/tmp"), grpc=True)
+ )
+ try:
+ go_generator.generate_services()
+ except ValueError as e:
+ assert "Go gRPC method name collision" in str(e)
+ assert "Foo and foo" in str(e)
+ else:
+ raise AssertionError("Expected Go gRPC method name collision")
+
def test_java_python_grpc_method_keywords_are_safe_names():
schema = parse_fdl(
@@ -778,18 +829,21 @@ def
test_service_schema_produces_one_file_per_message_per_language():
)
-def test_compile_service_schema_with_grpc_flag(tmp_path: Path):
+def test_compile_service_schema_with_grpc_flag(tmp_path: Path, capsys):
example_path = Path(__file__).resolve().parents[2] / "examples" /
"service.fdl"
lang_dirs = {}
for lang in ("java", "python", "rust", "go", "cpp", "csharp", "swift"):
lang_dirs[lang] = tmp_path / lang
ok = compile_file(example_path, lang_dirs, grpc=True, generated_outputs={})
+ output = capsys.readouterr().out
assert ok is True
for lang, lang_dir in lang_dirs.items():
files = [p for p in lang_dir.rglob("*") if p.is_file()]
assert len(files) >= 1, f"{lang}: expected at least one file with
grpc=True"
assert (lang_dirs["java"] / "demo" / "greeter" /
"GreeterGrpc.java").exists()
assert (lang_dirs["python"] / "demo_greeter_grpc.py").exists()
+ assert (lang_dirs["go"] / "demo_greeter_grpc.go").exists()
+ assert output.count("demo_greeter_grpc.go") == 1
assert (lang_dirs["rust"] / "demo_greeter_service.rs").exists()
assert (lang_dirs["rust"] / "demo_greeter_service_grpc.rs").exists()
diff --git a/docs/compiler/compiler-guide.md b/docs/compiler/compiler-guide.md
index 7204116f1..dbad7568f 100644
--- a/docs/compiler/compiler-guide.md
+++ b/docs/compiler/compiler-guide.md
@@ -52,27 +52,27 @@ foryc --scan-generated [OPTIONS]
Compile options:
-| Option | Description
| Default |
-| ------------------------------------- |
----------------------------------------------------- | ------------- |
-| `--lang` | Comma-separated target languages
| `all` |
-| `--output`, `-o` | Output directory
| `./generated` |
-| `-I`, `--proto_path`, `--import_path` | Add directory to import search path
(can be repeated) | (none) |
-| `--java_out=DST_DIR` | Generate Java code in DST_DIR
| (none) |
-| `--python_out=DST_DIR` | Generate Python code in DST_DIR
| (none) |
-| `--cpp_out=DST_DIR` | Generate C++ code in DST_DIR
| (none) |
-| `--go_out=DST_DIR` | Generate Go code in DST_DIR
| (none) |
-| `--rust_out=DST_DIR` | Generate Rust code in DST_DIR
| (none) |
-| `--csharp_out=DST_DIR` | Generate C# code in DST_DIR
| (none) |
-| `--javascript_out=DST_DIR` | Generate JavaScript/TypeScript code
in DST_DIR | (none) |
-| `--swift_out=DST_DIR` | Generate Swift code in DST_DIR
| (none) |
-| `--dart_out=DST_DIR` | Generate Dart code in DST_DIR
| (none) |
-| `--scala_out=DST_DIR` | Generate Scala 3 code in DST_DIR
| (none) |
-| `--kotlin_out=DST_DIR` | Generate Kotlin code in DST_DIR
| (none) |
-| `--go_nested_type_style` | Go nested type naming: `camelcase`
or `underscore` | `underscore` |
-| `--swift_namespace_style` | Swift namespace style: `enum` or
`flatten` | `enum` |
-| `--emit-fdl` | Emit translated FDL (for non-FDL
inputs) | `false` |
-| `--emit-fdl-path` | Write translated FDL to this path
(file or directory) | (stdout) |
-| `--grpc` | Generate gRPC service companions for
Java and Python | `false` |
+| Option | Description
| Default |
+| ------------------------------------- |
------------------------------------------------------ | ------------- |
+| `--lang` | Comma-separated target languages
| `all` |
+| `--output`, `-o` | Output directory
| `./generated` |
+| `-I`, `--proto_path`, `--import_path` | Add directory to import search path
(can be repeated) | (none) |
+| `--java_out=DST_DIR` | Generate Java code in DST_DIR
| (none) |
+| `--python_out=DST_DIR` | Generate Python code in DST_DIR
| (none) |
+| `--cpp_out=DST_DIR` | Generate C++ code in DST_DIR
| (none) |
+| `--go_out=DST_DIR` | Generate Go code in DST_DIR
| (none) |
+| `--rust_out=DST_DIR` | Generate Rust code in DST_DIR
| (none) |
+| `--csharp_out=DST_DIR` | Generate C# code in DST_DIR
| (none) |
+| `--javascript_out=DST_DIR` | Generate JavaScript/TypeScript code
in DST_DIR | (none) |
+| `--swift_out=DST_DIR` | Generate Swift code in DST_DIR
| (none) |
+| `--dart_out=DST_DIR` | Generate Dart code in DST_DIR
| (none) |
+| `--scala_out=DST_DIR` | Generate Scala 3 code in DST_DIR
| (none) |
+| `--kotlin_out=DST_DIR` | Generate Kotlin code in DST_DIR
| (none) |
+| `--go_nested_type_style` | Go nested type naming: `camelcase`
or `underscore` | `underscore` |
+| `--swift_namespace_style` | Swift namespace style: `enum` or
`flatten` | `enum` |
+| `--emit-fdl` | Emit translated FDL (for non-FDL
inputs) | `false` |
+| `--emit-fdl-path` | Write translated FDL to this path
(file or directory) | (stdout) |
+| `--grpc` | Generate gRPC service companions for
supported outputs | `false` |
Schema-level file options are supported for language-specific generation
choices.
For `go_nested_type_style` and `swift_namespace_style`, the CLI flag overrides
@@ -141,23 +141,23 @@ foryc schema.fdl --output ./src/generated
foryc user.fdl order.fdl product.fdl --output ./generated
```
-**Compile a simple schema containing service definitions (Java + Python
models):**
+**Compile a simple schema containing service definitions (Java + Python + Rust
models):**
```bash
-foryc compiler/examples/service.fdl --java_out=./generated/java
--python_out=./generated/python
+foryc compiler/examples/service.fdl --java_out=./generated/java
--python_out=./generated/python --rust_out=./generated/rust
```
-**Generate Java and Python gRPC service companions:**
+**Generate Java, Python, and Rust gRPC service companions:**
```bash
-foryc compiler/examples/service.fdl --java_out=./generated/java
--python_out=./generated/python --grpc
+foryc compiler/examples/service.fdl --java_out=./generated/java
--python_out=./generated/python --rust_out=./generated/rust --grpc
```
The generated gRPC service code uses Fory to serialize request and response
-payloads. Java output imports grpc-java APIs and Python output imports `grpc`;
-applications that compile or run those generated service files must provide
-their own gRPC dependencies. Fory's Java and Python packages do not add a hard
-gRPC dependency for this feature.
+payloads. Java output imports grpc-java APIs, Python output imports `grpc`, and
+Rust output imports `tonic` and `bytes`; applications that compile or run those
+generated service files must provide their own gRPC dependencies. Fory packages
+do not add a hard gRPC dependency for this feature.
**Use import search paths:**
diff --git a/docs/compiler/flatbuffers-idl.md b/docs/compiler/flatbuffers-idl.md
index 2744ba6ae..45e88e10a 100644
--- a/docs/compiler/flatbuffers-idl.md
+++ b/docs/compiler/flatbuffers-idl.md
@@ -125,8 +125,9 @@ message Container {
### Services
FlatBuffers `rpc_service` definitions are translated to Fory services. With
-`--grpc`, the compiler emits Java and Python gRPC service companions that use
-Fory serialization for request and response payloads.
+`--grpc`, the compiler emits gRPC service companions for supported outputs such
+as Java, Python, Go, and Rust. These companions use Fory serialization for
+request and response payloads.
```fbs
rpc_service SearchService {
@@ -136,12 +137,12 @@ rpc_service SearchService {
```
```bash
-foryc api.fbs --java_out=./generated/java --python_out=./generated/python
--grpc
+foryc api.fbs --java_out=./generated/java --python_out=./generated/python
--rust_out=./generated/rust --grpc
```
-Generated service code imports grpc APIs, so applications must provide
grpc-java
-or `grpcio` dependencies when they compile or run those files. Fory packages do
-not add gRPC as a hard dependency.
+Generated service code imports grpc APIs, so applications must provide
grpc-java,
+`grpcio`, grpc-go, or Rust `tonic` and `bytes` dependencies when they compile
or
+run those files. Fory packages do not add gRPC as a hard dependency.
### Defaults and Metadata
diff --git a/docs/compiler/generated-code.md b/docs/compiler/generated-code.md
index 567b9c57f..1764c47a1 100644
--- a/docs/compiler/generated-code.md
+++ b/docs/compiler/generated-code.md
@@ -404,6 +404,11 @@ Rust output is one module file per schema, for example:
- `<rust_out>/addressbook.rs`
+When `--grpc` is used and the schema contains services, Rust also emits:
+
+- `<rust_out>/addressbook_service.rs`
+- `<rust_out>/addressbook_service_grpc.rs`
+
### Type Generation
Unions map to Rust enums with `#[fory(id = ...)]` schema case attributes.
@@ -516,6 +521,51 @@ let bytes = person.to_bytes()?;
let restored = Person::from_bytes(&bytes)?;
```
+### gRPC Service Companions
+
+When a schema contains services and the compiler is run with `--grpc`, Rust
+generation emits a service API module and a tonic binding module. For a schema
+module named `addressbook`, those files are `addressbook_service.rs` and
+`addressbook_service_grpc.rs`.
+
+The service API module contains the async trait and gRPC path constants:
+
+```rust
+#[::tonic::async_trait]
+pub trait AddressBookService: ::std::marker::Send + ::std::marker::Sync +
'static {
+ async fn lookup(
+ &self,
+ request: ::tonic::Request<crate::addressbook::Person>,
+ ) -> ::std::result::Result<
+ ::tonic::Response<crate::addressbook::AddressBook>,
+ ::tonic::Status,
+ >;
+}
+
+pub const ADDRESS_BOOK_SERVICE_SERVICE_NAME: &str =
"addressbook.AddressBookService";
+pub const ADDRESS_BOOK_SERVICE_LOOKUP_PATH: &str =
"/addressbook.AddressBookService/Lookup";
+```
+
+The tonic binding module contains Fory-backed codecs, payload implementations,
+and client/server wrappers. It serializes each request or response with the
+generated model type's `to_bytes` and `from_bytes` helpers:
+
+```rust
+impl codec::ForyGrpcPayload for crate::addressbook::Person {
+ fn encode_fory_payload(&self) ->
::std::result::Result<::std::vec::Vec<u8>, ::fory::Error> {
+ self.to_bytes()
+ }
+
+ fn decode_fory_payload(payload: &[u8]) -> ::std::result::Result<Self,
::fory::Error> {
+ Self::from_bytes(payload)
+ }
+}
+```
+
+Applications compiling the generated Rust service files must provide `tonic`
and
+`bytes` dependencies; Fory's Rust crate does not add those gRPC dependencies as
+hard dependencies.
+
## C++
### Output Layout
@@ -776,6 +826,42 @@ if err := restored.FromBytes(data); err != nil {
}
```
+### gRPC Service Companions
+
+When a schema contains services and the compiler is run with `--grpc`, Go
+generation emits one `<module>_grpc.go` file next to the model file. The
+companion contains grpc-go client and server interfaces plus a Fory-backed
+`CodecV2`.
+
+```go
+type AddressBookServiceClient interface {
+ Lookup(ctx context.Context, in *Person, opts ...grpc.CallOption)
(*AddressBook, error)
+}
+
+func NewAddressBookServiceClient(cc grpc.ClientConnInterface)
AddressBookServiceClient { ... }
+
+type CodecV2 struct{}
+```
+
+The generated codec uses the same package-level thread-safe Fory runtime as the
+generated `ToBytes` and `FromBytes` helpers. Applications should pass
+`CodecV2{}` to grpc-go server options, and generated clients force the same
+codec on each call:
+
+```go
+server := grpc.NewServer(grpc.ForceServerCodecV2(addressbook.CodecV2{}))
+addressbook.RegisterAddressBookServiceServer(server, service)
+
+client := addressbook.NewAddressBookServiceClient(conn)
+```
+
+Go method names are exported as PascalCase identifiers, while the gRPC method
+path keeps the exact service and method names from the schema. Regenerate both
+peers after changing service or method names.
+
+Applications compiling these files must provide grpc-go dependencies; Fory Go
+packages do not add gRPC as a hard dependency.
+
## C\#
### Output Layout
diff --git a/docs/compiler/index.md b/docs/compiler/index.md
index be9990e01..0bcc5ba6f 100644
--- a/docs/compiler/index.md
+++ b/docs/compiler/index.md
@@ -23,9 +23,9 @@ Fory IDL is a schema definition language for Apache Fory that
enables type-safe
cross-language serialization. Define your data structures once and generate
native data structure code for Java, Python, C++, Go, Rust,
JavaScript/TypeScript, C#, Swift, Dart, Scala, and Kotlin. Fory IDL can also
-describe RPC services; for Java and Python, the compiler can generate gRPC
-service companions that use Fory serialization for request and response
-payloads.
+describe RPC services; for Java, Python, Go, and Rust, the compiler can
+generate gRPC service companions that use Fory serialization for request and
+response payloads.
## Example Schema
@@ -88,15 +88,16 @@ service AnimalService {
}
```
-Generate Java and Python models plus gRPC service companions with:
+Generate Java, Python, and Rust models plus gRPC service companions with:
```bash
-foryc animals.fdl --java_out=./generated/java --python_out=./generated/python
--grpc
+foryc animals.fdl --java_out=./generated/java --python_out=./generated/python
--rust_out=./generated/rust --grpc
```
The generated service code uses normal gRPC APIs, but request and response
-objects are serialized with Fory. Applications provide their own grpc-java or
-`grpcio` dependencies; Fory packages do not add gRPC as a hard dependency.
+objects are serialized with Fory. Applications provide their own grpc-java,
+`grpcio`, grpc-go, or Rust `tonic` and `bytes` dependencies; Fory packages do
+not add gRPC as a hard dependency.
## Why Fory IDL?
diff --git a/docs/compiler/protobuf-idl.md b/docs/compiler/protobuf-idl.md
index a683f8cd2..66e0e4bc1 100644
--- a/docs/compiler/protobuf-idl.md
+++ b/docs/compiler/protobuf-idl.md
@@ -49,13 +49,13 @@ how protobuf concepts map to Fory, and how to use
protobuf-only Fory extension o
| Circular refs | Not supported | Supported
|
| Unknown fields | Preserved | Not preserved
|
| Generated types | Protobuf-specific model types | Native language
constructs |
-| gRPC ecosystem | Native | Java/Python service
codegen |
+| gRPC ecosystem | Native | Java/Python/Go/Rust
service codegen |
-Fory can generate Java and Python gRPC service companions with `--grpc`. Those
-services use normal gRPC transports but serialize request and response payloads
-with Fory rather than protobuf. For broad gRPC ecosystem tooling, schema
-reflection, and protobuf-native interceptors, protobuf remains the
mature/default
-choice.
+Fory can generate Java, Python, Go, and Rust gRPC service companions with
+`--grpc`. Those services use normal gRPC transports but serialize request and
+response payloads with Fory rather than protobuf. For broad gRPC ecosystem
+tooling, schema reflection, and protobuf-native interceptors, protobuf remains
+the mature/default choice.
## Why Use Apache Fory
@@ -311,17 +311,18 @@ modifiers (and optional `ref(weak=true)` where needed).
Replace protobuf generation steps with the Fory compiler invocation for target
languages.
-For Java and Python services, add `--grpc` to emit gRPC companion code:
+For supported service outputs, add `--grpc` to emit gRPC companion code:
```bash
-foryc api.proto --java_out=./generated/java --python_out=./generated/python
--grpc
+foryc api.proto --java_out=./generated/java --python_out=./generated/python
--rust_out=./generated/rust --grpc
```
-Generated Java service files compile against grpc-java, and generated Python
-service modules import `grpc`. Add those dependencies in your application
build;
-Fory packages do not add gRPC as a hard dependency. Protobuf `oneof` fields are
-translated to Fory union fields inside request and response messages.
-Direct union RPC request or response types are not part of normal protobuf RPC
+Generated Java service files compile against grpc-java, generated Python
service
+modules import `grpc`, and generated Rust service files import `tonic` and
+`bytes`. Add those dependencies in your application build; Fory packages do not
+add gRPC as a hard dependency. Protobuf `oneof` fields are translated to Fory
+union fields inside request and response messages. Direct union RPC request or
+response types are not part of normal protobuf RPC
syntax.
### Step 5: Run Compatibility Checks
diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md
index c3d951b69..c59bbe4c6 100644
--- a/docs/compiler/schema-idl.md
+++ b/docs/compiler/schema-idl.md
@@ -907,7 +907,8 @@ union_field := ['repeated'] field_type IDENTIFIER '='
INTEGER [field_options] ';
Services define RPC method contracts in Fory IDL. They are optional: schemas
with services still generate the normal data model types, and gRPC service code
-is generated only when the compiler is run with `--grpc` for Java or Python.
+is generated only when the compiler is run with `--grpc` for supported language
+outputs such as Java, Python, Go, and Rust.
```protobuf
message GetPetRequest [id=200] {
@@ -947,9 +948,10 @@ service PetDirectory {
- Enum, primitive, collection, map, and array types are not valid direct RPC
request or response types. Wrap those values in a message when they are part
of a service contract.
-- The generated Java and Python gRPC companions use Fory serialization for each
- RPC payload. Applications that compile or run those companions provide their
- own grpc-java or `grpcio` dependency.
+- The generated gRPC companions use Fory serialization for each RPC payload.
+ Applications that compile or run those companions provide their own gRPC
+ dependency, such as grpc-java, `grpcio`, grpc-go, or Rust `tonic` and
+ `bytes`.
**Grammar:**
diff --git a/docs/guide/go/grpc-support.md b/docs/guide/go/grpc-support.md
new file mode 100644
index 000000000..02418eba1
--- /dev/null
+++ b/docs/guide/go/grpc-support.md
@@ -0,0 +1,241 @@
+---
+title: gRPC Support
+sidebar_position: 13
+id: grpc_support
+license: |
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+---
+
+Fory can generate Go gRPC service companions for schemas that define services.
+The generated code uses grpc-go for transport and a Fory-backed `CodecV2` for
+request and response payloads.
+
+Use this mode when every RPC peer is generated from the same Fory IDL, protobuf
+IDL, or FlatBuffers IDL and you want gRPC transport semantics with Fory payload
+encoding. Use standard protobuf gRPC code generation when clients or tools must
+consume protobuf message bytes directly.
+
+## Add Dependencies
+
+Add grpc-go to your module. Fory Go packages do not add gRPC as a hard
+dependency.
+
+```bash
+go get google.golang.org/grpc
+```
+
+Your generated code also imports the Fory Go module:
+
+```bash
+go get github.com/apache/fory/go/fory
+```
+
+## Define a Service
+
+Service definitions can come from Fory IDL, protobuf IDL, or FlatBuffers
+`rpc_service` definitions. A Fory IDL service looks like this:
+
+```protobuf
+package demo.greeter;
+
+message HelloRequest {
+ string name = 1;
+}
+
+message HelloReply {
+ string reply = 1;
+}
+
+service Greeter {
+ rpc SayHello (HelloRequest) returns (HelloReply);
+}
+```
+
+Generate Go model and gRPC companion code with `--grpc`:
+
+```bash
+foryc service.fdl --go_out=./generated/go --grpc
+```
+
+For this schema, the Go generator emits:
+
+| File | Purpose
|
+| ------------------------------ |
-------------------------------------------- |
+| `greeter/demo_greeter.go` | Fory model types and registration helpers
|
+| `greeter/demo_greeter_grpc.go` | grpc-go client, server interfaces, and
codec |
+
+Generated Go methods use exported PascalCase names such as `SayHello`. The
+underlying gRPC method path keeps the exact schema method name, so names such
as
+`sayHello` or `say_hello` continue to route by their schema spelling.
+
+## Implement a Server
+
+Implement the generated `GreeterServer` interface, create a grpc-go server with
+the generated Fory codec, and register the service.
+
+```go
+package main
+
+import (
+ "context"
+ "log"
+ "net"
+
+ "google.golang.org/grpc"
+
+ "example.com/app/generated/go/greeter"
+)
+
+type greeterService struct {
+ greeter.UnimplementedGreeterServer
+}
+
+func (greeterService) SayHello(
+ ctx context.Context,
+ request *greeter.HelloRequest,
+) (*greeter.HelloReply, error) {
+ return &greeter.HelloReply{Reply: "Hello, " + request.Name}, nil
+}
+
+func main() {
+ listener, err := net.Listen("tcp", ":50051")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ server := grpc.NewServer(
+ grpc.ForceServerCodecV2(greeter.CodecV2{}),
+ )
+ greeter.RegisterGreeterServer(server, greeterService{})
+
+ if err := server.Serve(listener); err != nil {
+ log.Fatal(err)
+ }
+}
+```
+
+`grpc.ForceServerCodecV2(...)` is required so the server decodes incoming
frames
+with the generated Fory codec instead of the default protobuf codec.
+
+Use the zero-value generated `CodecV2{}` for the service schema. The generated
+client methods force the same codec for outgoing calls.
+
+## Create a Client
+
+The generated client constructor accepts a grpc-go connection. Generated client
+methods force the generated Fory codec for each call.
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "time"
+
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+
+ "example.com/app/generated/go/greeter"
+)
+
+func main() {
+ conn, err := grpc.NewClient(
+ "localhost:50051",
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
+ )
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer conn.Close()
+
+ client := greeter.NewGreeterClient(conn)
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ reply, err := client.SayHello(ctx, &greeter.HelloRequest{Name: "Fory"})
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Println(reply.Reply)
+}
+```
+
+## Streaming RPCs
+
+Fory service definitions can use unary, server-streaming, client-streaming, and
+bidirectional streaming RPC shapes:
+
+```protobuf
+service Greeter {
+ rpc SayHello (HelloRequest) returns (HelloReply);
+ rpc LotsOfReplies (HelloRequest) returns (stream HelloReply);
+ rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply);
+ rpc Chat (stream HelloRequest) returns (stream HelloReply);
+}
+```
+
+Generated Go code follows grpc-go conventions:
+
+- Unary methods take `context.Context`, a request pointer, and return a
response
+ pointer plus `error`.
+- Server-streaming client methods return a generated stream client.
+- Client-streaming server methods receive a generated stream server.
+- Bidirectional streaming methods use generated stream client and server
+ interfaces.
+- The generated codec is used for every message frame, including streaming
+ frames.
+
+## Operations
+
+The generated service companion only supplies Fory serialization. Operational
+behavior remains standard grpc-go behavior:
+
+- Deadlines and cancellations
+- TLS and credentials
+- Unary and stream interceptors
+- Status codes and metadata
+- Name resolution and load balancing
+- Connection lifecycle and backoff
+
+## Troubleshooting
+
+### Missing `google.golang.org/grpc` Packages
+
+Add grpc-go to your module:
+
+```bash
+go get google.golang.org/grpc
+```
+
+### `grpc: error while marshaling`
+
+Confirm that both the client and server use the generated `CodecV2{}` and that
+the generated model file is compiled into the same package as the gRPC
companion.
+
+### `UNIMPLEMENTED`
+
+Confirm that the generated service was registered with
+`RegisterGreeterServer(...)`, and that the client and server were generated
from
+the same package, service, and method names.
+
+### Protobuf Clients Cannot Decode the Service
+
+Fory gRPC companions do not use protobuf wire encoding for messages. Use a
+Fory-generated client for Fory-generated services, or provide a separate
+protobuf service endpoint for generic protobuf clients.
diff --git a/docs/guide/go/index.md b/docs/guide/go/index.md
index 52f162fcd..2cf4ebaf4 100644
--- a/docs/guide/go/index.md
+++ b/docs/guide/go/index.md
@@ -150,6 +150,7 @@ See [Xlang Serialization](xlang-serialization.md) for type
mapping and compatibi
| [Schema Evolution](schema-evolution.md) | Forward/backward
compatibility |
| [Custom Serializers](custom-serializers.md) | Extend serialization
behavior |
| [Thread Safety](thread-safety.md) | Concurrent usage patterns
|
+| [gRPC Support](grpc-support.md) | Fory payloads over grpc-go
|
| [Troubleshooting](troubleshooting.md) | Common issues and
solutions |
## Related Resources
diff --git a/docs/guide/go/troubleshooting.md b/docs/guide/go/troubleshooting.md
index 7bcfb2d9f..2c538385e 100644
--- a/docs/guide/go/troubleshooting.md
+++ b/docs/guide/go/troubleshooting.md
@@ -1,6 +1,6 @@
---
title: Troubleshooting
-sidebar_position: 13
+sidebar_position: 14
id: troubleshooting
license: |
Licensed to the Apache Software Foundation (ASF) under one or more
diff --git a/docs/guide/java/grpc-support.md b/docs/guide/java/grpc-support.md
new file mode 100644
index 000000000..bfa1781e3
--- /dev/null
+++ b/docs/guide/java/grpc-support.md
@@ -0,0 +1,236 @@
+---
+title: gRPC Support
+sidebar_position: 17
+id: grpc_support
+license: |
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+---
+
+Fory can generate Java gRPC service companions for schemas that define
+services. The generated service code uses normal grpc-java channels, servers,
+deadlines, status codes, interceptors, and transport security, while request
+and response objects are serialized with Fory instead of protobuf.
+
+Use this mode when both sides of the RPC are generated from the same Fory IDL,
+protobuf IDL, or FlatBuffers IDL and you want gRPC transport semantics with
+Fory payload encoding. Use standard protobuf gRPC code generation when your API
+must be consumed by generic protobuf clients, reflection tools, or components
+that expect protobuf message bytes.
+
+## Add Dependencies
+
+The generated Java service files compile against grpc-java. Fory Java artifacts
+do not add gRPC as a hard dependency, so add grpc-java dependencies in your
+application build and align the version with the rest of your service stack.
+
+Maven:
+
+```xml
+<dependencies>
+ <dependency>
+ <groupId>org.apache.fory</groupId>
+ <artifactId>fory-core</artifactId>
+ <version>${fory.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-api</artifactId>
+ <version>${grpc.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-stub</artifactId>
+ <version>${grpc.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-netty-shaded</artifactId>
+ <version>${grpc.version}</version>
+ </dependency>
+</dependencies>
+```
+
+Gradle:
+
+```kotlin
+dependencies {
+ implementation("org.apache.fory:fory-core:$foryVersion")
+ implementation("io.grpc:grpc-api:$grpcVersion")
+ implementation("io.grpc:grpc-stub:$grpcVersion")
+ implementation("io.grpc:grpc-netty-shaded:$grpcVersion")
+}
+```
+
+## Define a Service
+
+Service definitions can come from Fory IDL, protobuf IDL, or FlatBuffers
+`rpc_service` definitions. A Fory IDL service looks like this:
+
+```protobuf
+package demo.greeter;
+
+message HelloRequest {
+ string name = 1;
+}
+
+message HelloReply {
+ string reply = 1;
+}
+
+service Greeter {
+ rpc SayHello (HelloRequest) returns (HelloReply);
+}
+```
+
+Generate Java model and gRPC companion code with `--grpc`:
+
+```bash
+foryc service.fdl --java_out=./generated/java --grpc
+```
+
+For this schema, the Java generator emits:
+
+| File | Purpose |
+| ------------------------ | -------------------------------------------- |
+| `HelloRequest.java` | Fory model type for the request |
+| `HelloReply.java` | Fory model type for the response |
+| `GreeterForyModule.java` | Fory registration module for generated types |
+| `GreeterGrpc.java` | grpc-java service base class, stubs, codecs |
+
+## Implement a Server
+
+Extend the generated `GreeterGrpc.GreeterImplBase` class and register it with a
+standard grpc-java `Server`.
+
+```java
+package demo.greeter;
+
+import io.grpc.Server;
+import io.grpc.ServerBuilder;
+import io.grpc.stub.StreamObserver;
+
+final class GreeterService extends GreeterGrpc.GreeterImplBase {
+ @Override
+ public void sayHello(
+ HelloRequest request, StreamObserver<HelloReply> responseObserver) {
+ HelloReply reply = new HelloReply();
+ reply.setReply("Hello, " + request.getName());
+ responseObserver.onNext(reply);
+ responseObserver.onCompleted();
+ }
+}
+
+public final class GreeterServer {
+ public static void main(String[] args) throws Exception {
+ Server server =
+ ServerBuilder.forPort(50051)
+ .addService(new GreeterService())
+ .build()
+ .start();
+ server.awaitTermination();
+ }
+}
+```
+
+Generated request and response types are registered by the generated code, so
+service implementations do not perform manual serializer registration.
+
+## Create a Client
+
+Use the generated stubs with an ordinary grpc-java channel:
+
+```java
+package demo.greeter;
+
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+
+public final class GreeterClient {
+ public static void main(String[] args) {
+ ManagedChannel channel =
+ ManagedChannelBuilder.forAddress("localhost", 50051)
+ .usePlaintext()
+ .build();
+ try {
+ GreeterGrpc.GreeterBlockingStub stub =
+ GreeterGrpc.newBlockingStub(channel);
+
+ HelloRequest request = new HelloRequest();
+ request.setName("Fory");
+ HelloReply reply = stub.sayHello(request);
+ System.out.println(reply.getReply());
+ } finally {
+ channel.shutdownNow();
+ }
+ }
+}
+```
+
+For asynchronous calls, use `GreeterGrpc.newStub(channel)`. For future-based
+unary calls, use `GreeterGrpc.newFutureStub(channel)`.
+
+## Streaming RPCs
+
+Fory service definitions can use the same gRPC streaming shapes:
+
+```protobuf
+service Greeter {
+ rpc SayHello (HelloRequest) returns (HelloReply);
+ rpc LotsOfReplies (HelloRequest) returns (stream HelloReply);
+ rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply);
+ rpc Chat (stream HelloRequest) returns (stream HelloReply);
+}
+```
+
+Generated Java service methods follow grpc-java conventions:
+
+- Unary and server-streaming methods receive a request object and a
+ `StreamObserver` for responses.
+- Client-streaming and bidirectional methods return a `StreamObserver` for
+ incoming requests and receive a `StreamObserver` for outgoing responses.
+- Blocking stubs expose the grpc-java blocking APIs for supported streaming
+ shapes.
+
+## Operations
+
+The generated service code only replaces request and response serialization.
+All normal gRPC operational features still belong to grpc-java:
+
+- Deadlines and cancellations
+- TLS and authentication
+- Name resolution and load balancing
+- Client and server interceptors
+- Status codes and metadata
+- Channel pooling and lifecycle management
+
+## Troubleshooting
+
+### Missing `io.grpc` or Guava Classes
+
+Add the grpc-java dependencies shown above. Generated Fory service files import
+grpc-java APIs, but Fory Java artifacts intentionally do not depend on gRPC.
+
+### `UNIMPLEMENTED`
+
+Confirm that the generated service implementation is registered with
+`ServerBuilder.addService(...)`, and that the client and server were generated
+from the same package, service, and method names.
+
+### Protobuf Clients Cannot Decode the Service
+
+Fory gRPC companions do not use protobuf wire encoding for messages. Use a
+Fory-generated client for Fory-generated services, or provide a separate
+protobuf service endpoint for generic protobuf clients.
diff --git a/docs/guide/java/index.md b/docs/guide/java/index.md
index ac8ed7f46..580701f47 100644
--- a/docs/guide/java/index.md
+++ b/docs/guide/java/index.md
@@ -244,6 +244,7 @@ ThreadSafeFory threadLocalFory = Fory.builder()
- [Object Copy](object-copy.md) - Deep-copy Java object graphs in memory
- [Compression](compression.md) - Integer, long, and array compression options
- [Virtual Threads](virtual-threads.md) - Virtual-thread usage and pool sizing
guidance
+- [gRPC Support](grpc-support.md) - Fory payloads over grpc-java
- [Type Registration](type-registration.md) - Class registration and security
- [Custom Serializers](custom-serializers.md) - Implement custom serializers
- [Xlang Serialization](xlang-serialization.md) - Serialize data for other
languages
diff --git a/docs/guide/java/troubleshooting.md
b/docs/guide/java/troubleshooting.md
index 141f645b9..2795f8376 100644
--- a/docs/guide/java/troubleshooting.md
+++ b/docs/guide/java/troubleshooting.md
@@ -1,6 +1,6 @@
---
title: Troubleshooting
-sidebar_position: 17
+sidebar_position: 18
id: troubleshooting
license: |
Licensed to the Apache Software Foundation (ASF) under one or more
diff --git a/docs/guide/python/grpc-support.md
b/docs/guide/python/grpc-support.md
new file mode 100644
index 000000000..c5106f91b
--- /dev/null
+++ b/docs/guide/python/grpc-support.md
@@ -0,0 +1,210 @@
+---
+title: gRPC Support
+sidebar_position: 13
+id: grpc_support
+license: |
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+---
+
+Fory can generate Python gRPC service companions for schemas that define
+services. The generated modules use `grpcio` for transport and use Fory to
+serialize request and response objects.
+
+Use this mode when every RPC peer is generated from the same Fory IDL, protobuf
+IDL, or FlatBuffers IDL and you want gRPC transport semantics with Fory payload
+encoding. Use standard protobuf gRPC code generation when clients or tools must
+consume protobuf message bytes directly.
+
+## Install Dependencies
+
+Install `grpcio` alongside `pyfory`. The generated companion imports `grpc`,
but
+`pyfory` does not add gRPC as a hard dependency.
+
+```bash
+pip install pyfory grpcio
+```
+
+## Define a Service
+
+Service definitions can come from Fory IDL, protobuf IDL, or FlatBuffers
+`rpc_service` definitions. A Fory IDL service looks like this:
+
+```protobuf
+package demo.greeter;
+
+message HelloRequest {
+ string name = 1;
+}
+
+message HelloReply {
+ string reply = 1;
+}
+
+service Greeter {
+ rpc SayHello (HelloRequest) returns (HelloReply);
+}
+```
+
+Generate Python model and gRPC companion code with `--grpc`:
+
+```bash
+foryc service.fdl --python_out=./generated/python --grpc
+```
+
+For this schema, the Python generator emits:
+
+| File | Purpose |
+| ---------------------- | ------------------------------------------- |
+| `demo_greeter.py` | Fory dataclasses and registration helpers |
+| `demo_greeter_grpc.py` | `grpcio` stub, servicer base, and registrar |
+
+The module name is derived from the Fory package by replacing dots with
+underscores. A schema with no package uses `generated.py` and
+`generated_grpc.py`.
+
+## Implement a Server
+
+Subclass the generated servicer and register it with a normal `grpcio` server.
+Generated Python method names use snake_case, while the gRPC wire path keeps
the
+original IDL method name.
+
+```python
+from concurrent import futures
+
+import grpc
+
+import demo_greeter
+import demo_greeter_grpc
+
+
+class Greeter(demo_greeter_grpc.GreeterServicer):
+ def say_hello(self, request, context):
+ return demo_greeter.HelloReply(reply=f"Hello, {request.name}")
+
+
+def serve():
+ server = grpc.server(futures.ThreadPoolExecutor(max_workers=8))
+ demo_greeter_grpc.add_servicer(Greeter(), server)
+ server.add_insecure_port("[::]:50051")
+ server.start()
+ server.wait_for_termination()
+
+
+if __name__ == "__main__":
+ serve()
+```
+
+Generated request and response types are serialized by the generated companion,
+so service implementations do not perform manual Fory registration.
+
+## Create a Client
+
+Use the generated stub with a normal `grpcio` channel:
+
+```python
+import grpc
+
+import demo_greeter
+import demo_greeter_grpc
+
+
+def main():
+ with grpc.insecure_channel("localhost:50051") as channel:
+ stub = demo_greeter_grpc.GreeterStub(channel)
+ reply = stub.say_hello(demo_greeter.HelloRequest(name="Fory"))
+ print(reply.reply)
+
+
+if __name__ == "__main__":
+ main()
+```
+
+`grpcio` still owns channel options, credentials, deadlines, metadata, retries,
+and interceptors.
+
+## Streaming RPCs
+
+Fory service definitions can use unary, server-streaming, client-streaming, and
+bidirectional streaming RPC shapes:
+
+```protobuf
+service Greeter {
+ rpc SayHello (HelloRequest) returns (HelloReply);
+ rpc LotsOfReplies (HelloRequest) returns (stream HelloReply);
+ rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply);
+ rpc Chat (stream HelloRequest) returns (stream HelloReply);
+}
+```
+
+Generated Python code follows `grpcio` conventions:
+
+- Unary stubs call `channel.unary_unary(...)`.
+- Server-streaming stubs return an iterator over response objects.
+- Client-streaming stubs accept an iterator of request objects.
+- Bidirectional stubs accept a request iterator and return a response iterator.
+- Servicer methods use snake_case names and return either a single response or
+ an iterator, depending on the streaming shape.
+
+Example server-streaming implementation:
+
+```python
+class Greeter(demo_greeter_grpc.GreeterServicer):
+ def lots_of_replies(self, request, context):
+ for index in range(3):
+ yield demo_greeter.HelloReply(
+ reply=f"Hello {request.name}, response {index}"
+ )
+```
+
+## Operations
+
+The generated service companion only supplies Fory serialization callbacks.
+Operational behavior remains standard `grpcio` behavior:
+
+- Deadlines and cancellations
+- TLS and authentication credentials
+- Client and server interceptors
+- Status codes, details, and metadata
+- Channel and server lifecycle
+- Thread pool sizing for synchronous servers
+
+## Troubleshooting
+
+### `ModuleNotFoundError: No module named 'grpc'`
+
+Install `grpcio` in the environment that runs the generated service module:
+
+```bash
+pip install grpcio
+```
+
+### `TypeError: Unsupported gRPC servicer type`
+
+Pass an instance of the generated servicer subclass to
+`demo_greeter_grpc.add_servicer(...)`. If the schema contains multiple
services,
+the generated registrar accepts only the matching generated servicer types.
+
+### `UNIMPLEMENTED`
+
+Confirm that the generated servicer was registered with the server, and that
the
+client and server were generated from the same package, service, and method
+names.
+
+### Protobuf Clients Cannot Decode the Service
+
+Fory gRPC companions do not use protobuf wire encoding for messages. Use a
+Fory-generated client for Fory-generated services, or provide a separate
+protobuf service endpoint for generic protobuf clients.
diff --git a/docs/guide/python/index.md b/docs/guide/python/index.md
index 36c0cb9ba..5b74d7de6 100644
--- a/docs/guide/python/index.md
+++ b/docs/guide/python/index.md
@@ -159,6 +159,7 @@ See [Native Serialization](native-serialization.md) for
Python-only serializatio
- [Type Registration](type-registration.md) - User-defined type registration
- [Custom Serializers](custom-serializers.md) - Extend serialization behavior
- [Row Format](row-format.md) - Zero-copy row format
+- [gRPC Support](grpc-support.md) - Fory payloads over grpcio
## Links
diff --git a/docs/guide/python/troubleshooting.md
b/docs/guide/python/troubleshooting.md
index bd4124d49..8dc56c160 100644
--- a/docs/guide/python/troubleshooting.md
+++ b/docs/guide/python/troubleshooting.md
@@ -1,6 +1,6 @@
---
title: Troubleshooting
-sidebar_position: 13
+sidebar_position: 14
id: troubleshooting
license: |
Licensed to the Apache Software Foundation (ASF) under one or more
diff --git a/docs/guide/rust/grpc-support.md b/docs/guide/rust/grpc-support.md
new file mode 100644
index 000000000..3cb6f3011
--- /dev/null
+++ b/docs/guide/rust/grpc-support.md
@@ -0,0 +1,229 @@
+---
+title: gRPC Support
+sidebar_position: 12
+id: grpc_support
+license: |
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+---
+
+Fory can generate Rust gRPC service companions for schemas that define
+services. The generated code uses `tonic` for transport and Fory for request
and
+response payload serialization.
+
+Use this mode when every RPC peer is generated from the same Fory IDL, protobuf
+IDL, or FlatBuffers IDL and you want gRPC transport semantics with Fory payload
+encoding. Use standard protobuf gRPC code generation when clients or tools must
+consume protobuf message bytes directly.
+
+## Add Dependencies
+
+Add `tonic` and `bytes` to the crate that compiles the generated service files.
+Fory Rust crates do not add gRPC as a hard dependency. Add `tokio` for async
+servers and clients, and `tokio-stream` when your service implementation needs
+to build streaming responses.
+
+```toml
+[dependencies]
+fory = "1.1.0"
+bytes = "1"
+tonic = { version = "0.14", features = ["transport"] }
+tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
+tokio-stream = "0.1"
+```
+
+Use dependency versions that are compatible with the rest of your service
+stack.
+
+## Define a Service
+
+Service definitions can come from Fory IDL, protobuf IDL, or FlatBuffers
+`rpc_service` definitions. A Fory IDL service looks like this:
+
+```protobuf
+package demo.greeter;
+
+message HelloRequest {
+ string name = 1;
+}
+
+message HelloReply {
+ string reply = 1;
+}
+
+service Greeter {
+ rpc SayHello (HelloRequest) returns (HelloReply);
+}
+```
+
+Generate Rust model and gRPC companion code with `--grpc`:
+
+```bash
+foryc service.fdl --rust_out=./generated/rust --grpc
+```
+
+For this schema, the Rust generator emits:
+
+| File | Purpose
|
+| ------------------------------ |
-------------------------------------------- |
+| `demo_greeter.rs` | Fory model types and registration helpers
|
+| `demo_greeter_service.rs` | Async service trait and gRPC path constants
|
+| `demo_greeter_service_grpc.rs` | tonic client, server wrapper, and Fory
codec |
+
+Add the generated files to your crate root:
+
+```rust
+pub mod demo_greeter;
+pub mod demo_greeter_service;
+pub mod demo_greeter_service_grpc;
+```
+
+## Implement a Server
+
+Implement the generated async trait and add the generated server wrapper to a
+normal `tonic` server.
+
+```rust
+use demo_greeter::{HelloReply, HelloRequest};
+use demo_greeter_service::Greeter;
+use demo_greeter_service_grpc::greeter_server::GreeterServer;
+use tonic::{Request, Response, Status};
+
+#[derive(Default)]
+struct MyGreeter;
+
+#[tonic::async_trait]
+impl Greeter for MyGreeter {
+ async fn say_hello(
+ &self,
+ request: Request<HelloRequest>,
+ ) -> Result<Response<HelloReply>, Status> {
+ let request = request.into_inner();
+ Ok(Response::new(HelloReply {
+ reply: format!("Hello, {}", request.name),
+ }))
+ }
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let addr = "[::1]:50051".parse()?;
+ tonic::transport::Server::builder()
+ .add_service(GreeterServer::new(MyGreeter::default()))
+ .serve(addr)
+ .await?;
+ Ok(())
+}
+```
+
+Generated request and response types are serialized by the generated service
+code, so service implementations do not perform manual Fory registration.
+
+## Create a Client
+
+Use the generated tonic client:
+
+```rust
+use demo_greeter::HelloRequest;
+use demo_greeter_service_grpc::greeter_client::GreeterClient;
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let mut client = GreeterClient::connect("http://[::1]:50051").await?;
+ let response = client
+ .say_hello(HelloRequest {
+ name: "Fory".to_string(),
+ })
+ .await?;
+ println!("{}", response.into_inner().reply);
+ Ok(())
+}
+```
+
+`tonic` still owns channel configuration, TLS, deadlines, metadata,
+interceptors, and transport lifecycle.
+
+## Streaming RPCs
+
+Fory service definitions can use unary, server-streaming, client-streaming, and
+bidirectional streaming RPC shapes:
+
+```protobuf
+service Greeter {
+ rpc SayHello (HelloRequest) returns (HelloReply);
+ rpc LotsOfReplies (HelloRequest) returns (stream HelloReply);
+ rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply);
+ rpc Chat (stream HelloRequest) returns (stream HelloReply);
+}
+```
+
+Generated Rust code follows tonic conventions:
+
+- Unary methods use `tonic::Request<T>` and return `tonic::Response<U>`.
+- Server-streaming methods return a response whose inner value is a stream of
+ `Result<U, tonic::Status>`.
+- Client-streaming and bidirectional methods receive `tonic::Streaming<T>`.
+- The generated client module exposes matching async methods for each service
+ method.
+- The generated codec is used for every message frame, including streaming
+ frames.
+
+Use the generated trait signatures as the source of truth for the concrete
+associated stream types in your service implementation.
+
+## Thread Safety and Payload Types
+
+Generated Rust gRPC payloads must be `Send + 'static` so tonic can move request
+and response values across async tasks. If a schema uses non-thread-safe
+reference metadata for a request or response type, Rust gRPC generation rejects
+that service. Use thread-safe reference shapes for gRPC payloads, or keep the
+non-thread-safe type out of the RPC boundary.
+
+## Operations
+
+The generated service companion only supplies Fory serialization and tonic
+bindings. Operational behavior remains standard tonic behavior:
+
+- Deadlines and cancellations
+- TLS and authentication
+- Tower middleware and interceptors
+- Status codes and metadata
+- Channel and server lifecycle
+- Backpressure through async streams
+
+## Troubleshooting
+
+### Missing `tonic` or `bytes` Crates
+
+Add the dependencies shown above to the crate that compiles the generated
+service files.
+
+### `UNIMPLEMENTED`
+
+Confirm that the generated server wrapper was added with
+`Server::builder().add_service(...)`, and that the client and server were
+generated from the same package, service, and method names.
+
+### Non-Thread-Safe Reference Errors During Code Generation
+
+Rust gRPC payloads must be `Send + 'static`. Change the request or response
+schema to use thread-safe reference shapes, or wrap the non-thread-safe data
in a
+type that is not part of the gRPC payload.
+
+### Protobuf Clients Cannot Decode the Service
+
+Fory gRPC companions do not use protobuf wire encoding for messages. Use a
+Fory-generated client for Fory-generated services, or provide a separate
+protobuf service endpoint for generic protobuf clients.
diff --git a/docs/guide/rust/index.md b/docs/guide/rust/index.md
index 93c3537ed..c2212a1f7 100644
--- a/docs/guide/rust/index.md
+++ b/docs/guide/rust/index.md
@@ -193,3 +193,4 @@ fory-derive/ # Procedural macros
- [Polymorphism](polymorphism.md) - Trait object serialization
- [Custom Serializers](custom-serializers.md) - Extend serialization behavior
- [Row Format](row-format.md) - Zero-copy row-based format
+- [gRPC Support](grpc-support.md) - Fory payloads over tonic
diff --git a/docs/guide/rust/troubleshooting.md
b/docs/guide/rust/troubleshooting.md
index 6b46d1b6d..285c99a8d 100644
--- a/docs/guide/rust/troubleshooting.md
+++ b/docs/guide/rust/troubleshooting.md
@@ -1,6 +1,6 @@
---
title: Troubleshooting
-sidebar_position: 12
+sidebar_position: 13
id: troubleshooting
license: |
Licensed to the Apache Software Foundation (ASF) under one or more
diff --git a/integration_tests/grpc_tests/go/main.go
b/integration_tests/grpc_tests/go/main.go
index caa9e3909..0d414d2b5 100644
--- a/integration_tests/grpc_tests/go/main.go
+++ b/integration_tests/grpc_tests/go/main.go
@@ -16,7 +16,7 @@
// under the License.
// Binary grpc-interop is the Go peer for Java-driven gRPC integration tests.
-// It is invoked as a subprocess by GrpcInteropTest.java and supports two
modes:
+// It is invoked as a subprocess by Java gRPC integration tests and supports
two modes:
//
// server --port-file <path> start a gRPC server and write the bound port
to the file
// client --target <addr> connect to addr and exercise all four
streaming modes
@@ -32,23 +32,13 @@ import (
"os"
"strings"
- "github.com/apache/fory/go/fory"
grpc_fdl
"github.com/apache/fory/integration_tests/grpc_tests/go/generated/grpc_fdl"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
- "google.golang.org/grpc/encoding"
)
// --- helpers ----------------------------------------------------------------
-func newFory() *fory.Fory {
- f := fory.New(fory.WithXlang(true), fory.WithRefTracking(true),
fory.WithCompatible(true))
- if err := grpc_fdl.RegisterTypes(f); err != nil {
- log.Fatalf("RegisterTypes: %v", err)
- }
- return f
-}
-
func fdlResponse(req *grpc_fdl.GrpcFdlRequest, tag string, offset int)
*grpc_fdl.GrpcFdlResponse {
return &grpc_fdl.GrpcFdlResponse{
Id: fmt.Sprintf("%s:%s", tag, req.Id),
@@ -73,25 +63,59 @@ func fdlAggregate(requests []*grpc_fdl.GrpcFdlRequest)
*grpc_fdl.GrpcFdlResponse
}
}
-// protoFallbackCodec overrides the built-in "proto" codec (v1 registry) with
-// Fory so the server can decode requests from Java clients, which send with
the
-// default content-type (application/grpc) rather than application/grpc+fory.
-type protoFallbackCodec struct{ grpc_fdl.CodecV2 }
+func fdlRequestUnion(req *grpc_fdl.GrpcFdlRequest) *grpc_fdl.GrpcFdlUnion {
+ union := grpc_fdl.RequestGrpcFdlUnion(req)
+ return &union
+}
-func (protoFallbackCodec) Name() string { return "proto" }
+func fdlResponseUnion(response *grpc_fdl.GrpcFdlResponse)
*grpc_fdl.GrpcFdlUnion {
+ union := grpc_fdl.ResponseGrpcFdlUnion(response)
+ return &union
+}
-func (c protoFallbackCodec) Marshal(v interface{}) ([]byte, error) {
- b, err := c.Fory.Marshal(v)
- if err != nil {
- return nil, err
+func fdlUnionResponse(req *grpc_fdl.GrpcFdlRequest, tag string, offset int)
*grpc_fdl.GrpcFdlUnion {
+ return fdlResponseUnion(fdlResponse(req, tag, offset))
+}
+
+func fdlUnionAggregate(unions []*grpc_fdl.GrpcFdlUnion)
(*grpc_fdl.GrpcFdlUnion, error) {
+ requests := make([]*grpc_fdl.GrpcFdlRequest, len(unions))
+ for i, union := range unions {
+ req, err := fdlRequestFromUnion(union)
+ if err != nil {
+ return nil, err
+ }
+ requests[i] = req
+ }
+ return fdlResponseUnion(fdlAggregate(requests)), nil
+}
+
+func fdlRequestFromUnion(union *grpc_fdl.GrpcFdlUnion)
(*grpc_fdl.GrpcFdlRequest, error) {
+ if union == nil {
+ return nil, fmt.Errorf("expected request union, got nil")
}
- out := make([]byte, len(b))
- copy(out, b)
- return out, nil
+ req, ok := union.AsRequest()
+ if !ok {
+ return nil, fmt.Errorf("expected request union, got case %d",
union.Case())
+ }
+ return req, nil
}
-func (c protoFallbackCodec) Unmarshal(data []byte, v interface{}) error {
- return c.Fory.Unmarshal(data, v)
+func expectUnionResponse(name string, got *grpc_fdl.GrpcFdlUnion, want
*grpc_fdl.GrpcFdlUnion) error {
+ if got == nil || want == nil {
+ return fmt.Errorf("%s: got %+v, want %+v", name, got, want)
+ }
+ gotResponse, ok := got.AsResponse()
+ if !ok {
+ return fmt.Errorf("%s: got non-response union case %d", name,
got.Case())
+ }
+ wantResponse, ok := want.AsResponse()
+ if !ok {
+ return fmt.Errorf("%s: want non-response union case %d", name,
want.Case())
+ }
+ if *gotResponse != *wantResponse {
+ return fmt.Errorf("%s: got %+v, want %+v", name, gotResponse,
wantResponse)
+ }
+ return nil
}
// --- server -----------------------------------------------------------------
@@ -144,19 +168,72 @@ func (s *fdlService) BidiStreamMessage(stream
grpc_fdl.FdlGrpcService_BidiStream
}
}
-func runServer(portFile string, f *fory.Fory) error {
+func (s *fdlService) UnaryUnion(_ context.Context, req *grpc_fdl.GrpcFdlUnion)
(*grpc_fdl.GrpcFdlUnion, error) {
+ message, err := fdlRequestFromUnion(req)
+ if err != nil {
+ return nil, err
+ }
+ return fdlUnionResponse(message, "unary", 10), nil
+}
+
+func (s *fdlService) ServerStreamUnion(req *grpc_fdl.GrpcFdlUnion, stream
grpc_fdl.FdlGrpcService_ServerStreamUnionServer) error {
+ message, err := fdlRequestFromUnion(req)
+ if err != nil {
+ return err
+ }
+ for i := 0; i < 3; i++ {
+ if err := stream.Send(fdlUnionResponse(message,
fmt.Sprintf("server-%d", i), i)); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (s *fdlService) ClientStreamUnion(stream
grpc_fdl.FdlGrpcService_ClientStreamUnionServer) error {
+ var requests []*grpc_fdl.GrpcFdlUnion
+ for {
+ req, err := stream.Recv()
+ if err == io.EOF {
+ response, err := fdlUnionAggregate(requests)
+ if err != nil {
+ return err
+ }
+ return stream.SendAndClose(response)
+ }
+ if err != nil {
+ return err
+ }
+ requests = append(requests, req)
+ }
+}
+
+func (s *fdlService) BidiStreamUnion(stream
grpc_fdl.FdlGrpcService_BidiStreamUnionServer) error {
+ index := 0
+ for {
+ req, err := stream.Recv()
+ if err == io.EOF {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ message, err := fdlRequestFromUnion(req)
+ if err != nil {
+ return err
+ }
+ if err := stream.Send(fdlUnionResponse(message,
fmt.Sprintf("bidi-%d", index), index)); err != nil {
+ return err
+ }
+ index++
+ }
+}
+
+func runServer(portFile string) error {
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return fmt.Errorf("listen: %w", err)
}
- codec := grpc_fdl.CodecV2{Fory: f}
- // "fory" (v2): used by Go clients via grpc.ForceCodecV2.
- encoding.RegisterCodecV2(codec)
- // "proto" (v1): overrides the built-in proto codec so Java clients are
- // also handled by Fory. RegisterCodecV2 writes to a separate registry
and
- // does not replace the v1 "proto" entry; RegisterCodec must be used.
- encoding.RegisterCodec(protoFallbackCodec{codec})
- s := grpc.NewServer()
+ s := grpc.NewServer(grpc.ForceServerCodecV2(grpc_fdl.CodecV2{}))
grpc_fdl.RegisterFdlGrpcServiceServer(s, &fdlService{})
// Write the bound port so the Java test harness knows where to connect.
@@ -246,19 +323,123 @@ func exerciseMessageStub(stub
grpc_fdl.FdlGrpcServiceClient, requests []*grpc_fd
return nil
}
-func runClient(target string, f *fory.Fory) error {
+func exerciseUnionStub(stub grpc_fdl.FdlGrpcServiceClient, requests
[]*grpc_fdl.GrpcFdlUnion) error {
+ ctx := context.Background()
+ first := requests[0]
+ firstMessage, err := fdlRequestFromUnion(first)
+ if err != nil {
+ return err
+ }
+
+ // unary
+ got, err := stub.UnaryUnion(ctx, first)
+ if err != nil {
+ return fmt.Errorf("UnaryUnion: %w", err)
+ }
+ if err := expectUnionResponse("UnaryUnion", got,
fdlUnionResponse(firstMessage, "unary", 10)); err != nil {
+ return err
+ }
+
+ // server streaming
+ ss, err := stub.ServerStreamUnion(ctx, first)
+ if err != nil {
+ return fmt.Errorf("ServerStreamUnion: %w", err)
+ }
+ for i := 0; i < 3; i++ {
+ got, err := ss.Recv()
+ if err != nil {
+ return fmt.Errorf("ServerStreamUnion Recv[%d]: %w", i,
err)
+ }
+ if err := expectUnionResponse(
+ fmt.Sprintf("ServerStreamUnion[%d]", i),
+ got,
+ fdlUnionResponse(firstMessage, fmt.Sprintf("server-%d",
i), i),
+ ); err != nil {
+ return err
+ }
+ }
+ if _, err := ss.Recv(); err != io.EOF {
+ return fmt.Errorf("ServerStreamUnion: expected EOF, got %v",
err)
+ }
+
+ // client streaming
+ cs, err := stub.ClientStreamUnion(ctx)
+ if err != nil {
+ return fmt.Errorf("ClientStreamUnion: %w", err)
+ }
+ for _, req := range requests {
+ if err := cs.Send(req); err != nil {
+ return fmt.Errorf("ClientStreamUnion Send: %w", err)
+ }
+ }
+ csResp, err := cs.CloseAndRecv()
+ if err != nil {
+ return fmt.Errorf("ClientStreamUnion CloseAndRecv: %w", err)
+ }
+ wantAggregate, err := fdlUnionAggregate(requests)
+ if err != nil {
+ return err
+ }
+ if err := expectUnionResponse("ClientStreamUnion", csResp,
wantAggregate); err != nil {
+ return err
+ }
+
+ // bidirectional streaming
+ bidi, err := stub.BidiStreamUnion(ctx)
+ if err != nil {
+ return fmt.Errorf("BidiStreamUnion: %w", err)
+ }
+ for i, req := range requests {
+ message, err := fdlRequestFromUnion(req)
+ if err != nil {
+ return err
+ }
+ if err := bidi.Send(req); err != nil {
+ return fmt.Errorf("BidiStreamUnion Send[%d]: %w", i,
err)
+ }
+ got, err := bidi.Recv()
+ if err != nil {
+ return fmt.Errorf("BidiStreamUnion Recv[%d]: %w", i,
err)
+ }
+ if err := expectUnionResponse(
+ fmt.Sprintf("BidiStreamUnion[%d]", i),
+ got,
+ fdlUnionResponse(message, fmt.Sprintf("bidi-%d", i), i),
+ ); err != nil {
+ return err
+ }
+ }
+ if err := bidi.CloseSend(); err != nil {
+ return fmt.Errorf("BidiStreamUnion CloseSend: %w", err)
+ }
+ if _, err := bidi.Recv(); err != io.EOF {
+ return fmt.Errorf("BidiStreamUnion: expected EOF after
CloseSend, got %v", err)
+ }
+
+ return nil
+}
+
+func runClient(target string) error {
conn, err := grpc.NewClient(target,
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return fmt.Errorf("dial %s: %w", target, err)
}
defer conn.Close()
- stub := grpc_fdl.NewFdlGrpcServiceClient(conn, f)
+ stub := grpc_fdl.NewFdlGrpcServiceClient(conn)
requests := []*grpc_fdl.GrpcFdlRequest{
{Id: "fdl-a", Count: 1, Payload: "alpha"},
{Id: "fdl-b", Count: 2, Payload: "beta"},
}
- return exerciseMessageStub(stub, requests)
+ if err := exerciseMessageStub(stub, requests); err != nil {
+ return err
+ }
+
+ unionRequests := []*grpc_fdl.GrpcFdlUnion{
+ fdlRequestUnion(&grpc_fdl.GrpcFdlRequest{Id: "fdl-u-a", Count:
3, Payload: "union-alpha"}),
+ fdlRequestUnion(&grpc_fdl.GrpcFdlRequest{Id: "fdl-u-b", Count:
4, Payload: "union-beta"}),
+ }
+ return exerciseUnionStub(stub, unionRequests)
}
// --- entry point ------------------------------------------------------------
@@ -274,15 +455,13 @@ func main() {
log.Fatal("usage: grpc-interop <server|client> [flags]")
}
- f := newFory()
-
switch os.Args[1] {
case "server":
serverCmd.Parse(os.Args[2:])
if *serverPortFile == "" {
log.Fatal("--port-file is required")
}
- if err := runServer(*serverPortFile, f); err != nil {
+ if err := runServer(*serverPortFile); err != nil {
log.Fatalf("server error: %v", err)
}
case "client":
@@ -290,7 +469,7 @@ func main() {
if *clientTarget == "" {
log.Fatal("--target is required")
}
- if err := runClient(*clientTarget, f); err != nil {
+ if err := runClient(*clientTarget); err != nil {
log.Fatalf("client error: %v", err)
}
default:
diff --git
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GoGrpcInteropTest.java
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GoGrpcInteropTest.java
new file mode 100644
index 000000000..44d9b9c03
--- /dev/null
+++
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GoGrpcInteropTest.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.grpc_tests;
+
+import io.grpc.Server;
+import java.util.concurrent.TimeUnit;
+import org.testng.annotations.Test;
+
+public class GoGrpcInteropTest extends GrpcTestBase {
+
+ @Test
+ public void testJavaServerGoClient() throws Exception {
+ Server server = startJavaFdlServer();
+ try {
+ runGo("go-grpc-client", "client", "--target", "127.0.0.1:" +
server.getPort());
+ } finally {
+ server.shutdownNow();
+ server.awaitTermination(10, TimeUnit.SECONDS);
+ }
+ }
+
+ @Test
+ public void testGoServerJavaClient() throws Exception {
+ exercisePeerServer("go-grpc", "Go", "fory-grpc-go-", goCommand("server"),
this::exerciseFdl);
+ }
+}
diff --git
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcInteropTest.java
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcTestBase.java
similarity index 85%
rename from
integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcInteropTest.java
rename to
integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcTestBase.java
index bbf6a8629..730bfbf95 100644
---
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcInteropTest.java
+++
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcTestBase.java
@@ -40,94 +40,52 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import org.testng.Assert;
-import org.testng.annotations.Test;
-
-public class GrpcInteropTest {
-
- @Test
- public void testJavaServerPythonClient() throws Exception {
- Server server =
- ServerBuilder.forPort(0)
- .addService(new FdlService())
- .addService(new FbsService())
- .addService(new PbService())
- .build()
- .start();
- try {
- runPython("python-grpc-client", "client", "--target", "127.0.0.1:" +
server.getPort());
- } finally {
- server.shutdownNow();
- server.awaitTermination(10, TimeUnit.SECONDS);
- }
+
+public abstract class GrpcTestBase {
+
+ @FunctionalInterface
+ protected interface ChannelExercise {
+ void run(ManagedChannel channel) throws Exception;
}
- @Test
- public void testJavaClientPythonServer() throws Exception {
- Path portFile = Files.createTempFile("fory-grpc-python-", ".port");
- Files.deleteIfExists(portFile);
- PeerCommand command = pythonCommand("server", "--port-file",
portFile.toString());
- Process process = startPeer(command);
- PeerOutputCollector outputCollector =
- new PeerOutputCollector(process.getInputStream(),
"python-grpc-server");
- outputCollector.start();
- try {
- int port = waitForPort(process, outputCollector, portFile, "Python");
- ManagedChannel channel =
- ManagedChannelBuilder.forAddress("127.0.0.1",
port).usePlaintext().build();
- try {
- exerciseFdl(channel);
- exerciseFbs(channel);
- exercisePb(channel);
- } finally {
- channel.shutdownNow();
- channel.awaitTermination(10, TimeUnit.SECONDS);
- }
- } finally {
- process.destroy();
- process.waitFor(10, TimeUnit.SECONDS);
- if (process.isAlive()) {
- process.destroyForcibly();
- process.waitFor(10, TimeUnit.SECONDS);
- }
- outputCollector.awaitOutput();
- Files.deleteIfExists(portFile);
- }
+ protected Server startJavaAllSchemasServer() throws IOException {
+ return ServerBuilder.forPort(0)
+ .addService(new FdlService())
+ .addService(new FbsService())
+ .addService(new PbService())
+ .build()
+ .start();
}
- @Test
- public void testJavaServerRustClient() throws Exception {
- Server server =
- ServerBuilder.forPort(0)
- .addService(new FdlService())
- .addService(new FbsService())
- .addService(new PbService())
- .build()
- .start();
- try {
- runRust("rust-grpc-client", "client", "--target", "127.0.0.1:" +
server.getPort());
- } finally {
- server.shutdownNow();
- server.awaitTermination(10, TimeUnit.SECONDS);
- }
+ protected Server startJavaFdlServer() throws IOException {
+ return ServerBuilder.forPort(0).addService(new
FdlService()).build().start();
}
- @Test
- public void testJavaClientRustServer() throws Exception {
- Path portFile = Files.createTempFile("fory-grpc-rust-", ".port");
+ protected void exerciseAllSchemas(ManagedChannel channel) throws
InterruptedException {
+ exerciseFdl(channel);
+ exerciseFbs(channel);
+ exercisePb(channel);
+ }
+
+ protected void exercisePeerServer(
+ String peer,
+ String language,
+ String portPrefix,
+ PeerCommand command,
+ ChannelExercise exercise)
+ throws Exception {
+ Path portFile = Files.createTempFile(portPrefix, ".port");
Files.deleteIfExists(portFile);
- PeerCommand command = rustCommand("server", "--port-file",
portFile.toString());
- Process process = startPeer(command);
+ Process process = startPeer(command.withPortFile(portFile));
PeerOutputCollector outputCollector =
- new PeerOutputCollector(process.getInputStream(), "rust-grpc-server");
+ new PeerOutputCollector(process.getInputStream(), peer + "-server");
outputCollector.start();
try {
- int port = waitForPort(process, outputCollector, portFile, "Rust");
+ int port = waitForPort(process, outputCollector, portFile, language);
ManagedChannel channel =
ManagedChannelBuilder.forAddress("127.0.0.1",
port).usePlaintext().build();
try {
- exerciseFdl(channel);
- exerciseFbs(channel);
- exercisePb(channel);
+ exercise.run(channel);
} finally {
channel.shutdownNow();
channel.awaitTermination(10, TimeUnit.SECONDS);
@@ -144,7 +102,7 @@ public class GrpcInteropTest {
}
}
- private void exerciseFdl(ManagedChannel channel) throws InterruptedException
{
+ protected void exerciseFdl(ManagedChannel channel) throws
InterruptedException {
grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceBlockingStub blocking =
grpc_fdl.FdlGrpcServiceGrpc.newBlockingStub(channel);
grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceStub async =
@@ -161,18 +119,6 @@ public class GrpcInteropTest {
assertFdlUnions(blocking, async, unions);
}
- // exerciseFdlMessages exercises the four FDL message streaming modes
without union methods.
- private void exerciseFdlMessages(ManagedChannel channel) throws
InterruptedException {
- grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceBlockingStub blocking =
- grpc_fdl.FdlGrpcServiceGrpc.newBlockingStub(channel);
- grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceStub async =
- grpc_fdl.FdlGrpcServiceGrpc.newStub(channel);
-
- List<grpc_fdl.GrpcFdlRequest> messages =
- Arrays.asList(fdlRequest("fdl-a", 1, "alpha"), fdlRequest("fdl-b", 2,
"beta"));
- assertFdlMessages(blocking, async, messages);
- }
-
private void assertFdlMessages(
grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceBlockingStub blocking,
grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceStub async,
@@ -227,7 +173,7 @@ public class GrpcInteropTest {
fdlUnionResponse(fdlRequestFromUnion(requests.get(1)), "bidi-1",
1)));
}
- private void exerciseFbs(ManagedChannel channel) throws InterruptedException
{
+ protected void exerciseFbs(ManagedChannel channel) throws
InterruptedException {
grpc_fbs.FbsGrpcServiceGrpc.FbsGrpcServiceBlockingStub blocking =
grpc_fbs.FbsGrpcServiceGrpc.newBlockingStub(channel);
grpc_fbs.FbsGrpcServiceGrpc.FbsGrpcServiceStub async =
@@ -298,7 +244,7 @@ public class GrpcInteropTest {
fbsUnionResponse(fbsRequestFromUnion(requests.get(1)), "bidi-1",
1)));
}
- private void exercisePb(ManagedChannel channel) throws InterruptedException {
+ protected void exercisePb(ManagedChannel channel) throws
InterruptedException {
grpc_pb.PbGrpcServiceGrpc.PbGrpcServiceBlockingStub blocking =
grpc_pb.PbGrpcServiceGrpc.newBlockingStub(channel);
grpc_pb.PbGrpcServiceGrpc.PbGrpcServiceStub async =
grpc_pb.PbGrpcServiceGrpc.newStub(channel);
@@ -328,49 +274,7 @@ public class GrpcInteropTest {
pbResponse(requests.get(0), "bidi-0", 0),
pbResponse(requests.get(1), "bidi-1", 1)));
}
- @Test
- public void testJavaServerGoClient() throws Exception {
- Server server = ServerBuilder.forPort(0).addService(new
FdlService()).build().start();
- try {
- runGo("go-grpc-client", "client", "--target", "127.0.0.1:" +
server.getPort());
- } finally {
- server.shutdownNow();
- server.awaitTermination(10, TimeUnit.SECONDS);
- }
- }
-
- @Test
- public void testGoServerJavaClient() throws Exception {
- Path portFile = Files.createTempFile("fory-grpc-go-", ".port");
- Files.deleteIfExists(portFile);
- PeerCommand command = goCommand("server", "--port-file",
portFile.toString());
- Process process = startPeer(command);
- PeerOutputCollector outputCollector =
- new PeerOutputCollector(process.getInputStream(), "go-grpc-server");
- outputCollector.start();
- try {
- int port = waitForPort(process, outputCollector, portFile, "Go");
- ManagedChannel channel =
- ManagedChannelBuilder.forAddress("127.0.0.1",
port).usePlaintext().build();
- try {
- exerciseFdlMessages(channel);
- } finally {
- channel.shutdownNow();
- channel.awaitTermination(10, TimeUnit.SECONDS);
- }
- } finally {
- process.destroy();
- process.waitFor(10, TimeUnit.SECONDS);
- if (process.isAlive()) {
- process.destroyForcibly();
- process.waitFor(10, TimeUnit.SECONDS);
- }
- outputCollector.awaitOutput();
- Files.deleteIfExists(portFile);
- }
- }
-
- private PeerCommand goCommand(String... args) {
+ protected PeerCommand goCommand(String... args) {
Path grpcRoot =
repoRoot().resolve("integration_tests").resolve("grpc_tests");
Path goRoot = grpcRoot.resolve("go");
List<String> command = new ArrayList<>();
@@ -384,7 +288,7 @@ public class GrpcInteropTest {
return peerCommand;
}
- private void runGo(String peer, String... args) throws IOException,
InterruptedException {
+ protected void runGo(String peer, String... args) throws IOException,
InterruptedException {
Process process = startPeer(goCommand(args));
PeerOutputCollector outputCollector = new
PeerOutputCollector(process.getInputStream(), peer);
outputCollector.start();
@@ -406,7 +310,7 @@ public class GrpcInteropTest {
outputCollector.awaitOutput();
}
- private PeerCommand pythonCommand(String... args) {
+ protected PeerCommand pythonCommand(String... args) {
Path repoRoot = repoRoot();
Path grpcRoot =
repoRoot.resolve("integration_tests").resolve("grpc_tests");
Path pythonRoot = grpcRoot.resolve("python");
@@ -443,7 +347,7 @@ public class GrpcInteropTest {
return peerCommand;
}
- private PeerCommand rustCommand(String... args) {
+ protected PeerCommand rustCommand(String... args) {
Path repoRoot = repoRoot();
Path grpcRoot =
repoRoot.resolve("integration_tests").resolve("grpc_tests");
Path rustRoot = grpcRoot.resolve("rust");
@@ -471,7 +375,7 @@ public class GrpcInteropTest {
return peerCommand;
}
- private void runPython(String peer, String... args) throws IOException,
InterruptedException {
+ protected void runPython(String peer, String... args) throws IOException,
InterruptedException {
Process process = startPeer(pythonCommand(args));
PeerOutputCollector outputCollector = new
PeerOutputCollector(process.getInputStream(), peer);
outputCollector.start();
@@ -493,7 +397,7 @@ public class GrpcInteropTest {
outputCollector.awaitOutput();
}
- private void runRust(String peer, String... args) throws IOException,
InterruptedException {
+ protected void runRust(String peer, String... args) throws IOException,
InterruptedException {
Process process = startPeer(rustCommand(args));
PeerOutputCollector outputCollector = new
PeerOutputCollector(process.getInputStream(), peer);
outputCollector.start();
@@ -589,7 +493,7 @@ public class GrpcInteropTest {
private static grpc_fdl.GrpcFdlUnion
fdlUnionAggregate(List<grpc_fdl.GrpcFdlUnion> unions) {
return grpc_fdl.GrpcFdlUnion.ofResponse(
- fdlAggregate(map(unions, GrpcInteropTest::fdlRequestFromUnion)));
+ fdlAggregate(map(unions, GrpcTestBase::fdlRequestFromUnion)));
}
private static grpc_fdl.GrpcFdlRequest
fdlRequestFromUnion(grpc_fdl.GrpcFdlUnion union) {
@@ -629,7 +533,7 @@ public class GrpcInteropTest {
private static grpc_fbs.GrpcFbsUnion
fbsUnionAggregate(List<grpc_fbs.GrpcFbsUnion> unions) {
return grpc_fbs.GrpcFbsUnion.ofGrpcFbsResponse(
- fbsAggregate(map(unions, GrpcInteropTest::fbsRequestFromUnion)));
+ fbsAggregate(map(unions, GrpcTestBase::fbsRequestFromUnion)));
}
private static grpc_fbs.GrpcFbsRequest
fbsRequestFromUnion(grpc_fbs.GrpcFbsUnion union) {
@@ -795,7 +699,7 @@ public class GrpcInteropTest {
@Override
public StreamObserver<grpc_fdl.GrpcFdlRequest> clientStreamMessage(
StreamObserver<grpc_fdl.GrpcFdlResponse> responseObserver) {
- return collectAndRespond(responseObserver,
GrpcInteropTest::fdlAggregate);
+ return collectAndRespond(responseObserver, GrpcTestBase::fdlAggregate);
}
@Override
@@ -843,7 +747,7 @@ public class GrpcInteropTest {
@Override
public StreamObserver<grpc_fdl.GrpcFdlUnion> clientStreamUnion(
StreamObserver<grpc_fdl.GrpcFdlUnion> responseObserver) {
- return collectAndRespond(responseObserver,
GrpcInteropTest::fdlUnionAggregate);
+ return collectAndRespond(responseObserver,
GrpcTestBase::fdlUnionAggregate);
}
@Override
@@ -895,7 +799,7 @@ public class GrpcInteropTest {
@Override
public StreamObserver<grpc_fbs.GrpcFbsRequest> clientStreamMessage(
StreamObserver<grpc_fbs.GrpcFbsResponse> responseObserver) {
- return collectAndRespond(responseObserver,
GrpcInteropTest::fbsAggregate);
+ return collectAndRespond(responseObserver, GrpcTestBase::fbsAggregate);
}
@Override
@@ -943,7 +847,7 @@ public class GrpcInteropTest {
@Override
public StreamObserver<grpc_fbs.GrpcFbsUnion> clientStreamUnion(
StreamObserver<grpc_fbs.GrpcFbsUnion> responseObserver) {
- return collectAndRespond(responseObserver,
GrpcInteropTest::fbsUnionAggregate);
+ return collectAndRespond(responseObserver,
GrpcTestBase::fbsUnionAggregate);
}
@Override
@@ -993,7 +897,7 @@ public class GrpcInteropTest {
@Override
public StreamObserver<grpc_pb.GrpcPbRequest> clientStreamMessage(
StreamObserver<grpc_pb.GrpcPbResponse> responseObserver) {
- return collectAndRespond(responseObserver, GrpcInteropTest::pbAggregate);
+ return collectAndRespond(responseObserver, GrpcTestBase::pbAggregate);
}
@Override
@@ -1053,10 +957,21 @@ public class GrpcInteropTest {
}
}
- private static final class PeerCommand {
+ protected static final class PeerCommand {
private List<String> command;
private Path workDir;
private final java.util.Map<String, String> environment = new
java.util.HashMap<>();
+
+ private PeerCommand withPortFile(Path portFile) {
+ List<String> serverCommand = new ArrayList<>(command);
+ serverCommand.add("--port-file");
+ serverCommand.add(portFile.toString());
+ PeerCommand peerCommand = new PeerCommand();
+ peerCommand.command = serverCommand;
+ peerCommand.workDir = workDir;
+ peerCommand.environment.putAll(environment);
+ return peerCommand;
+ }
}
private static final class PeerOutputCollector extends Thread {
diff --git
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonGrpcInteropTest.java
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonGrpcInteropTest.java
new file mode 100644
index 000000000..676defdaa
--- /dev/null
+++
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonGrpcInteropTest.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.grpc_tests;
+
+import io.grpc.Server;
+import java.util.concurrent.TimeUnit;
+import org.testng.annotations.Test;
+
+public class PythonGrpcInteropTest extends GrpcTestBase {
+
+ @Test
+ public void testJavaServerPythonClient() throws Exception {
+ Server server = startJavaAllSchemasServer();
+ try {
+ runPython("python-grpc-client", "client", "--target", "127.0.0.1:" +
server.getPort());
+ } finally {
+ server.shutdownNow();
+ server.awaitTermination(10, TimeUnit.SECONDS);
+ }
+ }
+
+ @Test
+ public void testJavaClientPythonServer() throws Exception {
+ exercisePeerServer(
+ "python-grpc",
+ "Python",
+ "fory-grpc-python-",
+ pythonCommand("server"),
+ this::exerciseAllSchemas);
+ }
+}
diff --git
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcInteropTest.java
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcInteropTest.java
new file mode 100644
index 000000000..6dae4a745
--- /dev/null
+++
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcInteropTest.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.grpc_tests;
+
+import io.grpc.Server;
+import java.util.concurrent.TimeUnit;
+import org.testng.annotations.Test;
+
+public class RustGrpcInteropTest extends GrpcTestBase {
+
+ @Test
+ public void testJavaServerRustClient() throws Exception {
+ Server server = startJavaAllSchemasServer();
+ try {
+ runRust("rust-grpc-client", "client", "--target", "127.0.0.1:" +
server.getPort());
+ } finally {
+ server.shutdownNow();
+ server.awaitTermination(10, TimeUnit.SECONDS);
+ }
+ }
+
+ @Test
+ public void testJavaClientRustServer() throws Exception {
+ exercisePeerServer(
+ "rust-grpc", "Rust", "fory-grpc-rust-", rustCommand("server"),
this::exerciseAllSchemas);
+ }
+}
diff --git a/integration_tests/grpc_tests/run_tests.sh
b/integration_tests/grpc_tests/run_tests.sh
index 22b9cd3aa..8028b3477 100755
--- a/integration_tests/grpc_tests/run_tests.sh
+++ b/integration_tests/grpc_tests/run_tests.sh
@@ -21,6 +21,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
+TEST_CLASSES="${1:-PythonGrpcInteropTest,RustGrpcInteropTest,GoGrpcInteropTest}"
python -m pip install "grpcio>=1.62.2,<1.71"
python -m pip install -v -e "${ROOT_DIR}/python"
@@ -32,5 +33,5 @@ go build -o grpc-interop .
cargo build --manifest-path "${SCRIPT_DIR}/rust/Cargo.toml" --workspace --quiet
cd "${ROOT_DIR}/integration_tests/grpc_tests/java"
mvn -T16 --no-transfer-progress \
- -Dtest=GrpcInteropTest \
+ -Dtest="${TEST_CLASSES}" \
test
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]