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 164636475 feat(kotlin): add kotlin grpc support (#3757)
164636475 is described below
commit 1646364751d431612abd808ab5f92a1b3b871d68
Author: Shawn Yang <[email protected]>
AuthorDate: Sat Jun 13 02:35:40 2026 +0800
feat(kotlin): add kotlin grpc support (#3757)
## Why?
## What does this PR do?
## Related issues
## 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
---
.agents/README.md | 7 +-
.agents/languages/kotlin.md | 5 +-
.github/workflows/ci.yml | 57 ++-
compiler/fory_compiler/cli.py | 8 +-
compiler/fory_compiler/generators/kotlin.py | 34 +-
.../fory_compiler/generators/services/kotlin.py | 403 ++++++++++++++++++++
.../fory_compiler/tests/test_kotlin_generator.py | 42 +++
.../fory_compiler/tests/test_service_codegen.py | 117 +++++-
docs/compiler/compiler-guide.md | 17 +-
docs/compiler/flatbuffers-idl.md | 9 +-
docs/compiler/generated-code.md | 71 ++++
docs/compiler/index.md | 10 +-
docs/compiler/protobuf-idl.md | 36 +-
docs/compiler/schema-idl.md | 6 +-
docs/guide/java/grpc-support.md | 3 +
docs/guide/kotlin/android-support.md | 2 +-
docs/guide/kotlin/grpc-support.md | 179 +++++++++
docs/guide/kotlin/index.md | 2 +
integration_tests/README.md | 2 +
integration_tests/grpc_tests/generate_grpc.py | 2 +
.../org/apache/fory/grpc_tests/GrpcTestBase.java | 44 +++
.../fory/grpc_tests/KotlinGrpcInteropTest.java | 48 +++
integration_tests/grpc_tests/kotlin/pom.xml | 191 ++++++++++
.../apache/fory/grpc_tests/KotlinGrpcInterop.kt | 420 +++++++++++++++++++++
integration_tests/grpc_tests/run_tests.sh | 4 +-
25 files changed, 1669 insertions(+), 50 deletions(-)
diff --git a/.agents/README.md b/.agents/README.md
index eb201cd9f..ed0e89ae3 100644
--- a/.agents/README.md
+++ b/.agents/README.md
@@ -20,7 +20,8 @@ Load `AGENTS.md` first. Do not load every file under
`.agents/` by default; read
- `languages/swift.md`: Swift rules, lint/test workflow, and Java-driven xlang
validation.
- `languages/javascript.md`: JavaScript/TypeScript commands and
package-manager expectations.
- `languages/dart.md`: Dart codegen, test, and analysis commands.
-- `languages/kotlin.md`: Kotlin build/test commands and dependency on
installed Java artifacts.
+- `languages/kotlin.md`: Kotlin build/test commands, generated Kotlin source
rules, and dependency
+ on installed Java artifacts.
- `languages/scala.md`: Scala build/test/format commands and dependency on
installed Java artifacts.
## Common Loading Recipes
@@ -34,6 +35,10 @@ Load `AGENTS.md` first. Do not load every file under
`.agents/` by default; read
- `docs-and-formatting.md`
- `testing/integration-tests.md`
- every touched runtime file under `languages/`
+- Compiler/codegen work that emits runtime-specific source:
+ - `AGENTS.md`
+ - `repo-reference.md`
+ - relevant runtime file under `languages/`
- Performance work:
- `AGENTS.md`
- relevant runtime file under `languages/`
diff --git a/.agents/languages/kotlin.md b/.agents/languages/kotlin.md
index 2df307a96..49b244438 100644
--- a/.agents/languages/kotlin.md
+++ b/.agents/languages/kotlin.md
@@ -1,6 +1,6 @@
# Kotlin
-Load this file when changing `kotlin/`.
+Load this file when changing `kotlin/` or compiler code that generates Kotlin
source.
## Rules
@@ -14,6 +14,9 @@ Load this file when changing `kotlin/`.
Fory. Do not auto-install a new serializer for an existing type-registered
Kotlin class unless the
wire format matches the previous serializer family and
old-payload/new-runtime compatibility is
tested.
+- When adding Kotlin gRPC service companions, emit Kotlin source only. Reuse
the generated schema
+ module's `ThreadSafeFory` and KSP-generated schema serializers, and keep
grpc-java/grpc-kotlin
+ dependencies application-owned instead of adding them as hard `fory-kotlin`
dependencies.
## Commands
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6dc1891c5..2d1da52a1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -50,6 +50,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
compiler: ${{ steps.filter.outputs.compiler }}
+ grpc_tests: ${{ steps.filter.outputs.grpc_tests }}
cpp: ${{ steps.filter.outputs.cpp }}
cpp_code: ${{ steps.filter.outputs.cpp_code }}
java_code: ${{ steps.filter.outputs.java_code }}
@@ -70,6 +71,7 @@ jobs:
run: |
if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then
echo "compiler=true" >> "$GITHUB_OUTPUT"
+ echo "grpc_tests=true" >> "$GITHUB_OUTPUT"
echo "cpp=true" >> "$GITHUB_OUTPUT"
echo "cpp_code=true" >> "$GITHUB_OUTPUT"
echo "java_code=true" >> "$GITHUB_OUTPUT"
@@ -92,6 +94,12 @@ jobs:
echo "compiler=false" >> "$GITHUB_OUTPUT"
fi
+ if grep -Eq '^(compiler/|integration_tests/grpc_tests/)' <<<
"$changed_files"; then
+ echo "grpc_tests=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "grpc_tests=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
@@ -698,7 +706,7 @@ jobs:
grpc_java_python_tests:
name: Java/Python gRPC Tests
needs: changes
- if: needs.changes.outputs.compiler == 'true'
+ if: needs.changes.outputs.grpc_tests == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
@@ -737,7 +745,7 @@ jobs:
grpc_java_go_tests:
name: Java/Go gRPC Tests
needs: changes
- if: needs.changes.outputs.compiler == 'true'
+ if: needs.changes.outputs.grpc_tests == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
@@ -776,7 +784,7 @@ jobs:
grpc_java_rust_tests:
name: Java/Rust gRPC Tests
needs: changes
- if: needs.changes.outputs.compiler == 'true'
+ if: needs.changes.outputs.grpc_tests == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
@@ -815,6 +823,49 @@ jobs:
cd integration_tests/grpc_tests/java
mvn -T16 --no-transfer-progress -Dtest=RustGrpcInteropTest test
+ grpc_java_kotlin_tests:
+ name: Java/Kotlin gRPC Tests
+ needs: changes
+ if: needs.changes.outputs.grpc_tests == 'true' ||
needs.changes.outputs.kotlin == '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 Kotlin artifacts for gRPC tests
+ run: |
+ cd kotlin
+ mvn -T16 --batch-mode --no-transfer-progress -pl
fory-kotlin,fory-kotlin-ksp -am -DskipTests install
+ - name: Generate gRPC test sources
+ run: python integration_tests/grpc_tests/generate_grpc.py
+ - name: Build Kotlin gRPC peer
+ run: |
+ cd integration_tests/grpc_tests/kotlin
+ mvn --no-transfer-progress -DskipTests package
+ - name: Run Java/Kotlin gRPC Tests
+ run: |
+ cd integration_tests/grpc_tests/java
+ mvn -T16 --no-transfer-progress -Dtest=KotlinGrpcInteropTest test
+
javascript:
name: JavaScript CI
needs: changes
diff --git a/compiler/fory_compiler/cli.py b/compiler/fory_compiler/cli.py
index ed81ad1e1..a654f6a4e 100644
--- a/compiler/fory_compiler/cli.py
+++ b/compiler/fory_compiler/cli.py
@@ -275,11 +275,12 @@ def validate_kotlin_import_packages(graph:
List[Tuple[Path, Schema]]) -> bool:
def validate_kotlin_output_paths(
graph: List[Tuple[Path, Schema]],
+ grpc: bool = False,
) -> bool:
"""Check Kotlin output paths for the current generation run."""
outputs: Dict[str, List[str]] = {}
for path, schema in graph:
- for output_path, owner in kotlin_output_paths(schema):
+ for output_path, owner in kotlin_output_paths(schema,
include_services=grpc):
outputs.setdefault(output_path, []).append(f"{path} {owner}")
collisions = {
output_path: paths for output_path, paths in outputs.items() if
len(paths) > 1
@@ -301,6 +302,7 @@ def validate_kotlin_output_paths(
def validate_kotlin_generation(
files: List[Path],
import_paths: List[Path],
+ grpc: bool = False,
) -> bool:
"""Preflight Kotlin package and helper paths before writing output."""
cache: Dict[Path, Schema] = {}
@@ -312,7 +314,7 @@ def validate_kotlin_generation(
graph.extend(file_graph)
if not validate_kotlin_import_packages(graph):
return False
- return validate_kotlin_output_paths(graph)
+ return validate_kotlin_output_paths(graph, grpc=grpc)
def _find_go_module_root(base_go_out: Path) -> Optional[Path]:
@@ -895,7 +897,7 @@ def cmd_compile(args: argparse.Namespace) -> int:
import_paths.append(resolved)
if "kotlin" in lang_output_dirs:
- if not validate_kotlin_generation(args.files, import_paths):
+ if not validate_kotlin_generation(args.files, import_paths,
grpc=args.grpc):
return 1
# Create output directories
diff --git a/compiler/fory_compiler/generators/kotlin.py
b/compiler/fory_compiler/generators/kotlin.py
index 8fad9c4af..25d9461aa 100644
--- a/compiler/fory_compiler/generators/kotlin.py
+++ b/compiler/fory_compiler/generators/kotlin.py
@@ -23,6 +23,7 @@ from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple
from fory_compiler.generators.base import BaseGenerator, GeneratedFile
+from fory_compiler.generators.services.kotlin import
KotlinServiceGeneratorMixin
from fory_compiler.ir.ast import (
ArrayType,
Enum,
@@ -117,6 +118,7 @@ def _is_schema_local(type_def: object, schema: Schema) ->
bool:
def kotlin_output_paths(
schema: Schema,
local_only: bool = False,
+ include_services: bool = False,
) -> List[Tuple[str, str]]:
"""Return generated Kotlin output paths and their owning schema
elements."""
outputs: List[Tuple[str, str]] = []
@@ -144,10 +146,20 @@ def kotlin_output_paths(
for message in schema.messages:
add_message(message, [])
outputs.append((kotlin_module_file_path(schema), "schema module"))
+ if include_services:
+ for service in schema.services:
+ if local_only and not _is_schema_local(service, schema):
+ continue
+ outputs.append(
+ (
+ _kotlin_source_path(schema, f"{service.name}GrpcKt"),
+ f"service {service.name}",
+ )
+ )
return outputs
-class KotlinGenerator(BaseGenerator):
+class KotlinGenerator(KotlinServiceGeneratorMixin, BaseGenerator):
"""Generates Kotlin models for Fory Schema IDL."""
language_name = "kotlin"
@@ -1033,7 +1045,11 @@ class KotlinGenerator(BaseGenerator):
outputs: Dict[str, List[str]] = {}
for source, schema in schemas:
local_only = schema is self.schema
- for path, owner in kotlin_output_paths(schema,
local_only=local_only):
+ for path, owner in kotlin_output_paths(
+ schema,
+ local_only=local_only,
+ include_services=self.options.grpc,
+ ):
outputs.setdefault(path, []).append(f"{source} {owner}")
collisions = {
path: sources for path, sources in outputs.items() if len(sources)
> 1
@@ -1049,6 +1065,20 @@ class KotlinGenerator(BaseGenerator):
f"types, or use distinct Kotlin packages. Collisions: {details}"
)
+ def kotlin_generated_output_paths(
+ self, include_services: bool = False
+ ) -> List[Tuple[str, str]]:
+ outputs: List[Tuple[str, str]] = []
+ for source, schema in self._schema_graph():
+ local_only = schema is self.schema
+ for path, owner in kotlin_output_paths(
+ schema,
+ local_only=local_only,
+ include_services=include_services,
+ ):
+ outputs.append((path, f"{source} {owner}"))
+ return outputs
+
def _kotlin_package_for_schema(self, schema: Schema) -> Optional[str]:
return self._schema_kotlin_package(schema)
diff --git a/compiler/fory_compiler/generators/services/kotlin.py
b/compiler/fory_compiler/generators/services/kotlin.py
new file mode 100644
index 000000000..60d142406
--- /dev/null
+++ b/compiler/fory_compiler/generators/services/kotlin.py
@@ -0,0 +1,403 @@
+# 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.
+
+"""Kotlin gRPC service generator helpers."""
+
+from typing import Dict, List
+
+from fory_compiler.generators.base import GeneratedFile
+from fory_compiler.generators.services.base import StreamingMode,
streaming_mode
+from fory_compiler.ir.ast import NamedType, RpcMethod, Service
+
+
+class KotlinServiceGeneratorMixin:
+ """Generates grpc-kotlin coroutine service companions."""
+
+ def generate_services(self) -> List[GeneratedFile]:
+ """Generate Kotlin gRPC service companions for local service
definitions."""
+ local_services = [
+ service
+ for service in self.schema.services
+ if not self.is_imported_type(service)
+ ]
+ if not local_services:
+ return []
+ self.check_kotlin_grpc_service_collisions(local_services)
+ self.check_kotlin_grpc_method_collisions(local_services)
+ return [
+ self.generate_kotlin_grpc_service_file(service)
+ for service in local_services
+ ]
+
+ def check_kotlin_grpc_service_collisions(self, services: List[Service]) ->
None:
+ generated_paths: Dict[str, str] = {}
+ for path, owner in
self.kotlin_generated_output_paths(include_services=True):
+ prior = generated_paths.get(path)
+ if prior is not None:
+ raise ValueError(
+ "Kotlin generated file path collision; rename schema
files, schema "
+ f"types, or services, or use distinct Kotlin packages.
Collisions: "
+ f"{path}: {prior}, {owner}"
+ )
+ generated_paths[path] = owner
+
+ seen_service_classes: Dict[str, str] = {}
+ for service in services:
+ service_class = self.kotlin_grpc_owner_name(service)
+ prior = seen_service_classes.get(service_class)
+ if prior is not None:
+ raise ValueError(
+ f"Kotlin gRPC service class collision: {prior} and
{service.name} "
+ f"both generate {service_class}"
+ )
+ seen_service_classes[service_class] = service.name
+
+ def check_kotlin_grpc_method_collisions(self, services: List[Service]) ->
None:
+ server_reserved = {"bindService", "context"}
+ stub_reserved = {"build", "channel", "callOptions"}
+ for service in services:
+ seen_functions: Dict[str, str] = {}
+ seen_descriptors: Dict[str, str] = {}
+ seen_backing_fields: Dict[str, str] = {}
+ for method in service.methods:
+ function_name = self.kotlin_grpc_method_name(method)
+ descriptor = self.kotlin_grpc_method_property(method)
+ backing_field = self.kotlin_grpc_method_backing_field(method)
+ if function_name in server_reserved or function_name in
stub_reserved:
+ raise ValueError(
+ f"Kotlin gRPC method name collision in service
{service.name}: "
+ f"{method.name} generates reserved member
{function_name}"
+ )
+ for seen, key, label in (
+ (seen_functions, function_name, "method"),
+ (seen_descriptors, descriptor, "method descriptor"),
+ (
+ seen_backing_fields,
+ backing_field,
+ "method descriptor backing field",
+ ),
+ ):
+ prior = seen.get(key)
+ if prior is not None:
+ raise ValueError(
+ f"Kotlin gRPC {label} name collision in service
{service.name}: "
+ f"{prior} and {method.name} both generate {key}"
+ )
+ seen[key] = method.name
+
+ def generate_kotlin_grpc_service_file(self, service: Service) ->
GeneratedFile:
+ imports = set()
+ lines = self.source_header(imports, needs_unsigned_opt_in=False)
+ owner = self.kotlin_grpc_owner_name(service)
+ lines.append(f"public object {owner} {{")
+ lines.append(
+ f' public const val SERVICE_NAME: String =
"{self.get_grpc_service_name(service)}"'
+ )
+ lines.append("")
+ lines.append(
+ f" private val FORY: org.apache.fory.ThreadSafeFory =
{self.get_module_name()}.getFory()"
+ )
+ lines.append("")
+ lines.append(
+ " private val serviceDescriptorValue: io.grpc.ServiceDescriptor
by lazy {"
+ )
+ lines.append("
io.grpc.ServiceDescriptor.newBuilder(SERVICE_NAME)")
+ for method in service.methods:
+ lines.append(
+ f"
.addMethod({self.kotlin_grpc_method_property(method)})"
+ )
+ lines.append(" .build()")
+ lines.append(" }")
+ lines.append("")
+ lines.append(" @JvmStatic")
+ lines.append(" public val serviceDescriptor:
io.grpc.ServiceDescriptor")
+ lines.append(" get() = serviceDescriptorValue")
+ lines.append("")
+ for method in service.methods:
+ lines.extend(self.generate_kotlin_grpc_method_descriptor(method))
+ lines.extend(self.generate_kotlin_grpc_service_base(service))
+ lines.extend(self.generate_kotlin_grpc_client_stub(service))
+ lines.extend(self.generate_kotlin_grpc_marshaller())
+ lines.append("}")
+ return GeneratedFile(
+ path=self.kotlin_grpc_service_file_path(service),
+ content="\n".join(lines) + "\n",
+ )
+
+ def generate_kotlin_grpc_method_descriptor(self, method: RpcMethod) ->
List[str]:
+ request_type = self.kotlin_grpc_type(method.request_type)
+ response_type = self.kotlin_grpc_type(method.response_type)
+ property_name = self.kotlin_grpc_method_property(method)
+ backing_field = self.kotlin_grpc_method_backing_field(method)
+ method_type = self.kotlin_grpc_method_type(method)
+ lines = [
+ f" private val {backing_field}:
io.grpc.MethodDescriptor<{request_type}, {response_type}> by lazy {{",
+ f" io.grpc.MethodDescriptor.newBuilder<{request_type},
{response_type}>()",
+ f"
.setType(io.grpc.MethodDescriptor.MethodType.{method_type})",
+ " .setFullMethodName(",
+ " io.grpc.MethodDescriptor.generateFullMethodName(",
+ f' SERVICE_NAME, "{method.name}",',
+ " ),",
+ " )",
+ " .setSampledToLocalTracing(true)",
+ f"
.setRequestMarshaller(marshaller({request_type}::class.java))",
+ f"
.setResponseMarshaller(marshaller({response_type}::class.java))",
+ " .build()",
+ " }",
+ "",
+ " @JvmStatic",
+ f" public val {property_name}:
io.grpc.MethodDescriptor<{request_type}, {response_type}>",
+ f" get() = {backing_field}",
+ "",
+ ]
+ return lines
+
+ def generate_kotlin_grpc_service_base(self, service: Service) -> List[str]:
+ base_name = self.kotlin_grpc_service_base_name(service)
+ lines = [
+ f" public abstract class {base_name}(",
+ " coroutineContext: kotlin.coroutines.CoroutineContext =
kotlin.coroutines.EmptyCoroutineContext,",
+ " ) :
io.grpc.kotlin.AbstractCoroutineServerImpl(coroutineContext) {",
+ ]
+ for method in service.methods:
+ lines.extend(self.generate_kotlin_grpc_server_method(service,
method))
+ lines.extend(self.generate_kotlin_grpc_bind_service(service))
+ lines.append(" }")
+ lines.append("")
+ return lines
+
+ def generate_kotlin_grpc_server_method(
+ self, service: Service, method: RpcMethod
+ ) -> List[str]:
+ function_name =
self.safe_identifier(self.kotlin_grpc_method_name(method))
+ request_type = self.kotlin_grpc_type(method.request_type)
+ response_type = self.kotlin_grpc_type(method.response_type)
+ if method.client_streaming:
+ parameter = f"requests:
kotlinx.coroutines.flow.Flow<{request_type}>"
+ else:
+ parameter = f"request: {request_type}"
+ return_type = (
+ f"kotlinx.coroutines.flow.Flow<{response_type}>"
+ if method.server_streaming
+ else response_type
+ )
+ suspend_modifier = "" if method.server_streaming else "suspend "
+ return [
+ "",
+ f" public open {suspend_modifier}fun
{function_name}({parameter}): {return_type} =",
+ " throw io.grpc.StatusException(",
+ " io.grpc.Status.UNIMPLEMENTED.withDescription(",
+ f' "Method
{self.get_grpc_service_name(service)}/{method.name} is unimplemented",',
+ " ),",
+ " )",
+ ]
+
+ def generate_kotlin_grpc_bind_service(self, service: Service) -> List[str]:
+ lines = [
+ "",
+ " final override fun bindService():
io.grpc.ServerServiceDefinition =",
+ "
io.grpc.ServerServiceDefinition.builder(serviceDescriptor)",
+ ]
+ for method in service.methods:
+ call = self.kotlin_grpc_server_call(method)
+ function_name =
self.safe_identifier(self.kotlin_grpc_method_name(method))
+ descriptor = self.kotlin_grpc_method_property(method)
+ lines.extend(
+ [
+ " .addMethod(",
+ f" io.grpc.kotlin.ServerCalls.{call}(",
+ " context = this.context,",
+ f" descriptor = {descriptor},",
+ f" implementation =
::{function_name},",
+ " ),",
+ " )",
+ ]
+ )
+ lines.append(" .build()")
+ return lines
+
+ def generate_kotlin_grpc_client_stub(self, service: Service) -> List[str]:
+ stub_name = self.kotlin_grpc_client_stub_name(service)
+ lines = [
+ f" public class {stub_name} @JvmOverloads constructor(",
+ " channel: io.grpc.Channel,",
+ " callOptions: io.grpc.CallOptions =
io.grpc.CallOptions.DEFAULT,",
+ f" ) :
io.grpc.kotlin.AbstractCoroutineStub<{stub_name}>(channel, callOptions) {{",
+ f" override fun build(channel: io.grpc.Channel,
callOptions: io.grpc.CallOptions): {stub_name} =",
+ f" {stub_name}(channel, callOptions)",
+ ]
+ for method in service.methods:
+ lines.extend(self.generate_kotlin_grpc_client_method(method))
+ lines.append(" }")
+ lines.append("")
+ return lines
+
+ def generate_kotlin_grpc_client_method(self, method: RpcMethod) ->
List[str]:
+ function_name =
self.safe_identifier(self.kotlin_grpc_method_name(method))
+ request_type = self.kotlin_grpc_type(method.request_type)
+ response_type = self.kotlin_grpc_type(method.response_type)
+ if method.client_streaming:
+ parameter = f"requests:
kotlinx.coroutines.flow.Flow<{request_type}>"
+ argument = "requests"
+ else:
+ parameter = f"request: {request_type}"
+ argument = "request"
+ return_type = (
+ f"kotlinx.coroutines.flow.Flow<{response_type}>"
+ if method.server_streaming
+ else response_type
+ )
+ suspend_modifier = "" if method.server_streaming else "suspend "
+ call = self.kotlin_grpc_client_call(method)
+ descriptor = self.kotlin_grpc_method_property(method)
+ return [
+ "",
+ f" public {suspend_modifier}fun {function_name}(",
+ f" {parameter},",
+ " headers: io.grpc.Metadata = io.grpc.Metadata(),",
+ f" ): {return_type} =",
+ f" io.grpc.kotlin.ClientCalls.{call}(",
+ " channel,",
+ f" {descriptor},",
+ f" {argument},",
+ " callOptions,",
+ " headers,",
+ " )",
+ ]
+
+ def generate_kotlin_grpc_marshaller(self) -> List[str]:
+ return [
+ " private fun <T : Any> marshaller(type: Class<T>):
io.grpc.MethodDescriptor.Marshaller<T> =",
+ " ForyMarshaller(FORY, type)",
+ "",
+ " private class ForyMarshaller<T : Any>(",
+ " private val fory: org.apache.fory.ThreadSafeFory,",
+ " private val type: Class<T>,",
+ " ) : io.grpc.MethodDescriptor.Marshaller<T> {",
+ " override fun stream(value: T): java.io.InputStream {",
+ " try {",
+ " return
KnownLengthByteArrayInputStream(fory.serialize(value))",
+ " } catch (e: RuntimeException) {",
+ ' throw internalError("Fory serialization failed",
e)',
+ " }",
+ " }",
+ "",
+ " override fun parse(stream: java.io.InputStream): T {",
+ " try {",
+ " return fory.deserialize(readBytes(stream), type)",
+ " } catch (e: java.io.IOException) {",
+ ' throw internalError("Fory deserialization
failed", e)',
+ " } catch (e: RuntimeException) {",
+ ' throw internalError("Fory deserialization
failed", e)',
+ " }",
+ " }",
+ "",
+ " private fun internalError(description: String, cause:
Throwable): io.grpc.StatusRuntimeException =",
+ "
io.grpc.Status.INTERNAL.withDescription(description).withCause(cause).asRuntimeException()",
+ "",
+ " private fun readBytes(stream: java.io.InputStream):
ByteArray {",
+ " if (stream is io.grpc.KnownLength) {",
+ " val size = stream.available()",
+ " val bytes = ByteArray(size)",
+ " var offset = 0",
+ " while (offset < size) {",
+ " val read = stream.read(bytes, offset, size -
offset)",
+ " if (read == -1) {",
+ ' throw java.io.EOFException("Expected
$size bytes, got $offset")',
+ " }",
+ " offset += read",
+ " }",
+ " return bytes",
+ " }",
+ " return readUnknownLengthBytes(stream)",
+ " }",
+ "",
+ " private fun readUnknownLengthBytes(stream:
java.io.InputStream): ByteArray {",
+ " val out = java.io.ByteArrayOutputStream()",
+ " val buffer = ByteArray(8192)",
+ " while (true) {",
+ " val read = stream.read(buffer)",
+ " if (read == -1) {",
+ " break",
+ " }",
+ " out.write(buffer, 0, read)",
+ " }",
+ " return out.toByteArray()",
+ " }",
+ "",
+ " private class KnownLengthByteArrayInputStream(bytes:
ByteArray) :",
+ " java.io.ByteArrayInputStream(bytes),",
+ " io.grpc.KnownLength",
+ " }",
+ ]
+
+ def kotlin_grpc_type(self, named_type: NamedType) -> str:
+ type_name = self.schema.resolve_type_name(named_type.name)
+ return self.resolve_kotlin_type_name(type_name, None)
+
+ def kotlin_grpc_owner_name(self, service: Service) -> str:
+ return f"{service.name}GrpcKt"
+
+ def kotlin_grpc_service_base_name(self, service: Service) -> str:
+ return f"{service.name}CoroutineImplBase"
+
+ def kotlin_grpc_client_stub_name(self, service: Service) -> str:
+ return f"{service.name}CoroutineStub"
+
+ def kotlin_grpc_service_file_path(self, service: Service) -> str:
+ path = self.get_kotlin_package_path()
+ filename = f"{self.kotlin_grpc_owner_name(service)}.kt"
+ return f"{path}/{filename}" if path else filename
+
+ def kotlin_grpc_method_name(self, method: RpcMethod) -> str:
+ return self.to_camel_case(method.name)
+
+ def kotlin_grpc_method_property(self, method: RpcMethod) -> str:
+ return f"{self.kotlin_grpc_method_name(method)}Method"
+
+ def kotlin_grpc_method_backing_field(self, method: RpcMethod) -> str:
+ return f"{self.kotlin_grpc_method_name(method)}MethodValue"
+
+ def kotlin_grpc_method_type(self, method: RpcMethod) -> str:
+ mode = streaming_mode(method)
+ if mode is StreamingMode.BIDIRECTIONAL:
+ return "BIDI_STREAMING"
+ if mode is StreamingMode.CLIENT_STREAMING:
+ return "CLIENT_STREAMING"
+ if mode is StreamingMode.SERVER_STREAMING:
+ return "SERVER_STREAMING"
+ return "UNARY"
+
+ def kotlin_grpc_client_call(self, method: RpcMethod) -> str:
+ mode = streaming_mode(method)
+ if mode is StreamingMode.BIDIRECTIONAL:
+ return "bidiStreamingRpc"
+ if mode is StreamingMode.CLIENT_STREAMING:
+ return "clientStreamingRpc"
+ if mode is StreamingMode.SERVER_STREAMING:
+ return "serverStreamingRpc"
+ return "unaryRpc"
+
+ def kotlin_grpc_server_call(self, method: RpcMethod) -> str:
+ mode = streaming_mode(method)
+ if mode is StreamingMode.BIDIRECTIONAL:
+ return "bidiStreamingServerMethodDefinition"
+ if mode is StreamingMode.CLIENT_STREAMING:
+ return "clientStreamingServerMethodDefinition"
+ if mode is StreamingMode.SERVER_STREAMING:
+ return "serverStreamingServerMethodDefinition"
+ return "unaryServerMethodDefinition"
diff --git a/compiler/fory_compiler/tests/test_kotlin_generator.py
b/compiler/fory_compiler/tests/test_kotlin_generator.py
index a9f9d5665..034310da2 100644
--- a/compiler/fory_compiler/tests/test_kotlin_generator.py
+++ b/compiler/fory_compiler/tests/test_kotlin_generator.py
@@ -247,6 +247,48 @@ def
test_registration_type_path_collision_rejected(tmp_path, capsys):
assert not out.exists()
+def test_grpc_service_path_collision_rejected(tmp_path, capsys):
+ schema_file = tmp_path / "demo.fdl"
+ schema_file.write_text(
+ """
+ package app;
+
+ message GreeterGrpcKt [id=200] {
+ string name = 1;
+ }
+
+ message Req [id=201] {}
+ message Res [id=202] {}
+
+ service Greeter {
+ rpc Call (Req) returns (Res);
+ }
+ """
+ )
+ schema = resolve_imports(schema_file)
+ validator = SchemaValidator(schema)
+ assert validator.validate(), validator.errors
+ with pytest.raises(ValueError, match="generated file path collision"):
+ KotlinGenerator(schema, GeneratorOptions(output_dir=tmp_path,
grpc=True))
+
+ out = tmp_path / "out"
+ result = foryc_main(
+ [
+ "--lang",
+ "kotlin",
+ "--kotlin_out",
+ str(out),
+ "--grpc",
+ str(schema_file),
+ ]
+ )
+
+ captured = capsys.readouterr()
+ assert result == 1
+ assert "generated file path collision" in captured.err
+ assert not out.exists()
+
+
def test_module_name_sanitizes_source_stem(tmp_path):
schema = tmp_path / "123-my-schema.fdl"
schema.write_text(
diff --git a/compiler/fory_compiler/tests/test_service_codegen.py
b/compiler/fory_compiler/tests/test_service_codegen.py
index 2fb7ad62b..f2e794ba7 100644
--- a/compiler/fory_compiler/tests/test_service_codegen.py
+++ b/compiler/fory_compiler/tests/test_service_codegen.py
@@ -37,6 +37,7 @@ from fory_compiler.generators.cpp import CppGenerator
from fory_compiler.generators.csharp import CSharpGenerator
from fory_compiler.generators.go import GoGenerator
from fory_compiler.generators.java import JavaGenerator
+from fory_compiler.generators.kotlin import KotlinGenerator
from fory_compiler.generators.python import PythonGenerator
from fory_compiler.generators.rust import RustGenerator
from fory_compiler.generators.swift import SwiftGenerator
@@ -52,6 +53,7 @@ GENERATOR_CLASSES: Tuple[Type[BaseGenerator], ...] = (
GoGenerator,
CSharpGenerator,
SwiftGenerator,
+ KotlinGenerator,
)
_GREETER_WITH_SERVICE = dedent(
@@ -136,6 +138,7 @@ def
test_generate_services_returns_empty_list_for_unsupported_generators():
PythonGenerator,
GoGenerator,
RustGenerator,
+ KotlinGenerator,
):
continue
options = GeneratorOptions(output_dir=Path("/tmp"))
@@ -180,6 +183,42 @@ def test_python_grpc_service_codegen_uses_byte_callbacks():
assert "FromString" not in content
+def test_kotlin_grpc_service_codegen_contains_fory_marshaller():
+ schema = parse_fdl(_GREETER_WITH_SERVICE)
+ files = generate_service_files(schema, KotlinGenerator)
+ assert set(files) == {"demo/greeter/GreeterGrpcKt.kt"}
+ content = files["demo/greeter/GreeterGrpcKt.kt"]
+ assert "public object GreeterGrpcKt" in content
+ assert 'SERVICE_NAME: String = "demo.greeter.Greeter"' in content
+ assert "private val FORY: org.apache.fory.ThreadSafeFory" in content
+ assert "GreeterForyModule.getFory()" in content
+ assert (
+ "private val serviceDescriptorValue: io.grpc.ServiceDescriptor by lazy"
+ in content
+ )
+ assert "public val serviceDescriptor: io.grpc.ServiceDescriptor" in content
+ assert (
+ "private val sayHelloMethodValue:
io.grpc.MethodDescriptor<HelloRequest, HelloReply> by lazy"
+ in content
+ )
+ assert (
+ "public val sayHelloMethod: io.grpc.MethodDescriptor<HelloRequest,
HelloReply>"
+ in content
+ )
+ assert "public abstract class GreeterCoroutineImplBase" in content
+ assert "io.grpc.kotlin.ServerCalls.unaryServerMethodDefinition" in content
+ assert "public class GreeterCoroutineStub @JvmOverloads constructor" in
content
+ assert "io.grpc.kotlin.ClientCalls.unaryRpc" in content
+ assert "implements" not in content
+ assert "private class ForyMarshaller<T : Any>(" in content
+ assert "fory.serialize(value)" in content
+ assert "fory.deserialize(readBytes(stream), type)" in content
+ assert "stream is io.grpc.KnownLength" in content
+ assert "readUnknownLengthBytes(stream)" in content
+ assert "ProtoUtils" not in content
+ assert "MessageLite" not in content
+
+
def test_grpc_streaming_method_shapes():
schema = parse_fdl(
dedent(
@@ -231,6 +270,23 @@ def test_grpc_streaming_method_shapes():
assert "grpc.ClientStream" in go
assert "grpc.ServerStream" in go
+ kotlin = next(iter(generate_service_files(schema,
KotlinGenerator).values()))
+ assert "io.grpc.MethodDescriptor.MethodType.UNARY" in kotlin
+ assert "io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING" in kotlin
+ assert "io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING" in kotlin
+ assert "io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING" in kotlin
+ assert "public suspend fun unary(" in kotlin
+ assert "public fun server(" in kotlin
+ assert "public suspend fun client(" in kotlin
+ assert "public fun bidi(" in kotlin
+ assert "kotlinx.coroutines.flow.Flow<Payload>" in kotlin
+ assert "io.grpc.kotlin.ClientCalls.serverStreamingRpc" in kotlin
+ assert "io.grpc.kotlin.ClientCalls.clientStreamingRpc" in kotlin
+ assert "io.grpc.kotlin.ClientCalls.bidiStreamingRpc" in kotlin
+ assert "io.grpc.kotlin.ServerCalls.serverStreamingServerMethodDefinition"
in kotlin
+ assert "io.grpc.kotlin.ServerCalls.clientStreamingServerMethodDefinition"
in kotlin
+ assert "io.grpc.kotlin.ServerCalls.bidiStreamingServerMethodDefinition" in
kotlin
+
def test_go_grpc_service_codegen():
schema = parse_fdl(_GREETER_WITH_SERVICE)
@@ -369,6 +425,13 @@ def
test_grpc_services_use_imported_java_type_references(tmp_path: Path):
assert "class ApiServiceStub" in python
assert "ImportedCall" not in python
+ kotlin_files = generate_service_files(schema, KotlinGenerator)
+ assert set(kotlin_files) == {"api/ApiServiceGrpcKt.kt"}
+ kotlin = kotlin_files["api/ApiServiceGrpcKt.kt"]
+ assert "io.grpc.MethodDescriptor<common.Shared, Local>" in kotlin
+ assert "marshaller(common.Shared::class.java)" in kotlin
+ assert "public open suspend fun get(request: common.Shared): Local" in
kotlin
+
def test_proto_grpc_services_use_imported_qualified_type_references(tmp_path:
Path):
common = tmp_path / "common.proto"
@@ -422,6 +485,11 @@ def
test_proto_grpc_services_use_imported_qualified_type_references(tmp_path: Pa
assert "crate::common::common::Shared" not in rust_service
assert "crate::api::api::Local" not in rust_service
+ kotlin_files = generate_service_files(schema, KotlinGenerator)
+ kotlin = kotlin_files["api/ApiServiceGrpcKt.kt"]
+ assert "io.grpc.MethodDescriptor<common.Shared, Local>" in kotlin
+ assert "marshaller(common.Shared::class.java)" in kotlin
+
def test_proto_grpc_absolute_rpc_type_uses_package_type_not_nested_shadow():
schema = parse_proto(
@@ -457,6 +525,11 @@ def
test_proto_grpc_absolute_rpc_type_uses_package_type_not_nested_shadow():
assert "crate::demo::demo::Request" not in rust_service
assert "crate::demo::::demo::Request" not in rust_service
+ kotlin_files = generate_service_files(schema, KotlinGenerator)
+ kotlin = kotlin_files["demo/ApiServiceGrpcKt.kt"]
+ assert "io.grpc.MethodDescriptor<Request, Response>" in kotlin
+ assert "io.grpc.MethodDescriptor<demo.Request, Response>" not in kotlin
+
def test_proto_grpc_absolute_rpc_type_prefers_longest_package_prefix(tmp_path:
Path):
common = tmp_path / "common.proto"
@@ -508,6 +581,11 @@ def
test_proto_grpc_absolute_rpc_type_prefers_longest_package_prefix(tmp_path: P
assert "::tonic::Response<crate::alpha_beta::C>" in rust_service
assert "crate::alpha::beta::C" not in rust_service
+ kotlin_files = generate_service_files(schema, KotlinGenerator)
+ kotlin = kotlin_files["alpha/ApiServiceGrpcKt.kt"]
+ assert "io.grpc.MethodDescriptor<alpha.beta.C, alpha.beta.C>" in kotlin
+ assert "io.grpc.MethodDescriptor<beta.C, beta.C>" not in kotlin
+
def test_java_grpc_service_class_collision_fails():
schema = parse_fdl(
@@ -536,6 +614,26 @@ def test_java_grpc_service_class_collision_fails():
raise AssertionError("Expected Java gRPC service class collision")
+def test_kotlin_grpc_service_class_collision_fails():
+ schema = parse_fdl(
+ dedent(
+ """
+ package demo.collision;
+
+ message GreeterGrpcKt {}
+ message Req {}
+ message Res {}
+
+ service Greeter {
+ rpc Call (Req) returns (Res);
+ }
+ """
+ )
+ )
+ with pytest.raises(ValueError, match="Kotlin generated file path
collision"):
+ KotlinGenerator(schema, GeneratorOptions(output_dir=Path("/tmp"),
grpc=True))
+
+
def test_java_grpc_service_class_collision_with_imported_type_fails(tmp_path:
Path):
common = tmp_path / "common.fdl"
common.write_text(
@@ -677,6 +775,17 @@ def test_grpc_method_name_collisions_fail():
else:
raise AssertionError("Expected Go gRPC method name collision")
+ kotlin_generator = KotlinGenerator(
+ schema, GeneratorOptions(output_dir=Path("/tmp"), grpc=True)
+ )
+ try:
+ kotlin_generator.generate_services()
+ except ValueError as e:
+ assert "Kotlin gRPC method name collision" in str(e)
+ assert "Foo and foo" in str(e)
+ else:
+ raise AssertionError("Expected Kotlin gRPC method name collision")
+
def test_java_python_grpc_method_keywords_are_safe_names():
schema = parse_fdl(
@@ -705,6 +814,11 @@ def test_java_python_grpc_method_keywords_are_safe_names():
assert "servicer.class_" in python
assert ' "Class": grpc.unary_unary_rpc_method_handler(' in python
+ kotlin = next(iter(generate_service_files(schema,
KotlinGenerator).values()))
+ assert "public open suspend fun `class`(request: Req): Res" in kotlin
+ assert "public suspend fun `class`(" in kotlin
+ assert "implementation = ::`class`" in kotlin
+
def test_python_grpc_service_registration_collisions_fail():
schema = parse_fdl(
@@ -832,7 +946,7 @@ def
test_service_schema_produces_one_file_per_message_per_language():
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"):
+ for lang in ("java", "python", "rust", "go", "cpp", "csharp", "swift",
"kotlin"):
lang_dirs[lang] = tmp_path / lang
ok = compile_file(example_path, lang_dirs, grpc=True, generated_outputs={})
output = capsys.readouterr().out
@@ -846,6 +960,7 @@ def test_compile_service_schema_with_grpc_flag(tmp_path:
Path, capsys):
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()
+ assert (lang_dirs["kotlin"] / "demo" / "greeter" /
"GreeterGrpcKt.kt").exists()
def test_generated_message_contains_key_signatures():
diff --git a/docs/compiler/compiler-guide.md b/docs/compiler/compiler-guide.md
index dbad7568f..683bd3417 100644
--- a/docs/compiler/compiler-guide.md
+++ b/docs/compiler/compiler-guide.md
@@ -141,23 +141,24 @@ foryc schema.fdl --output ./src/generated
foryc user.fdl order.fdl product.fdl --output ./generated
```
-**Compile a simple schema containing service definitions (Java + Python + Rust
models):**
+**Compile a simple schema containing service definitions (Java + Python + Rust
+ Kotlin models):**
```bash
-foryc compiler/examples/service.fdl --java_out=./generated/java
--python_out=./generated/python --rust_out=./generated/rust
+foryc compiler/examples/service.fdl --java_out=./generated/java
--python_out=./generated/python --rust_out=./generated/rust
--kotlin_out=./generated/kotlin
```
-**Generate Java, Python, and Rust gRPC service companions:**
+**Generate Java, Python, Rust, and Kotlin gRPC service companions:**
```bash
-foryc compiler/examples/service.fdl --java_out=./generated/java
--python_out=./generated/python --rust_out=./generated/rust --grpc
+foryc compiler/examples/service.fdl --java_out=./generated/java
--python_out=./generated/python --rust_out=./generated/rust
--kotlin_out=./generated/kotlin --grpc
```
The generated gRPC service code uses Fory to serialize request and response
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.
+Rust output imports `tonic` and `bytes`. Kotlin output imports grpc-java and
+grpc-kotlin APIs and uses coroutine stubs. 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:**
@@ -443,6 +444,8 @@ generated/
- Enums use stable Fory enum IDs
- Unions use sealed classes with `@ForyUnion`, `@ForyCase`, and an
unknown-case carrier
- Schema module object included
+- With `--grpc`, one `<ServiceName>GrpcKt.kt` coroutine service companion per
+ service
### C# IDL Matrix Verification
diff --git a/docs/compiler/flatbuffers-idl.md b/docs/compiler/flatbuffers-idl.md
index 45e88e10a..fa29946ac 100644
--- a/docs/compiler/flatbuffers-idl.md
+++ b/docs/compiler/flatbuffers-idl.md
@@ -126,7 +126,7 @@ message Container {
FlatBuffers `rpc_service` definitions are translated to Fory services. With
`--grpc`, the compiler emits gRPC service companions for supported outputs such
-as Java, Python, Go, and Rust. These companions use Fory serialization for
+as Java, Python, Go, Rust, and Kotlin. These companions use Fory serialization
for
request and response payloads.
```fbs
@@ -137,12 +137,13 @@ rpc_service SearchService {
```
```bash
-foryc api.fbs --java_out=./generated/java --python_out=./generated/python
--rust_out=./generated/rust --grpc
+foryc api.fbs --java_out=./generated/java --python_out=./generated/python
--rust_out=./generated/rust --kotlin_out=./generated/kotlin --grpc
```
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.
+grpc-kotlin, `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 1764c47a1..d197fb86f 100644
--- a/docs/compiler/generated-code.md
+++ b/docs/compiler/generated-code.md
@@ -1376,6 +1376,77 @@ public object AddressbookForyModule : ForyModule {
`registerUnion` discovers the generated `<Target>_ForySerializer`; callers do
not pass a serializer instance.
+### gRPC Service Companions
+
+When a schema contains services and the compiler is run with `--grpc`, Kotlin
+generation emits one `<ServiceName>GrpcKt.kt` file per service next to the
model
+types. The file contains a grpc-kotlin coroutine companion object, not Java
+`*Grpc.java` source.
+
+```kotlin
+public object AddressBookServiceGrpcKt {
+ public const val SERVICE_NAME: String = "addressbook.AddressBookService"
+
+ @JvmStatic
+ public val serviceDescriptor: io.grpc.ServiceDescriptor
+ get() = serviceDescriptorValue
+
+ @JvmStatic
+ public val lookupMethod: io.grpc.MethodDescriptor<Person, AddressBook>
+ get() = lookupMethodValue
+
+ public abstract class AddressBookServiceCoroutineImplBase(
+ coroutineContext: kotlin.coroutines.CoroutineContext =
+ kotlin.coroutines.EmptyCoroutineContext,
+ ) : io.grpc.kotlin.AbstractCoroutineServerImpl(coroutineContext) {
+ public open suspend fun lookup(request: Person): AddressBook =
+ throw io.grpc.StatusException(
+ io.grpc.Status.UNIMPLEMENTED.withDescription(
+ "Method addressbook.AddressBookService/Lookup is unimplemented",
+ ),
+ )
+ }
+
+ public class AddressBookServiceCoroutineStub @JvmOverloads constructor(
+ channel: io.grpc.Channel,
+ callOptions: io.grpc.CallOptions = io.grpc.CallOptions.DEFAULT,
+ ) : io.grpc.kotlin.AbstractCoroutineStub<AddressBookServiceCoroutineStub>(
+ channel,
+ callOptions,
+ ) {
+ public suspend fun lookup(
+ request: Person,
+ headers: io.grpc.Metadata = io.grpc.Metadata(),
+ ): AddressBook =
+ io.grpc.kotlin.ClientCalls.unaryRpc(
+ channel,
+ lookupMethod,
+ request,
+ callOptions,
+ headers,
+ )
+ }
+}
+```
+
+Streaming RPCs use `kotlinx.coroutines.flow.Flow`:
+
+| IDL shape | Server method
| Client method |
+| ----------------------------------------- |
----------------------------------------- |
----------------------------------------- |
+| `rpc A (Req) returns (Res)` | `suspend fun a(request: Req):
Res` | `suspend fun a(request: Req): Res` |
+| `rpc A (Req) returns (stream Res)` | `fun a(request: Req): Flow<Res>`
| `fun a(request: Req): Flow<Res>` |
+| `rpc A (stream Req) returns (Res)` | `suspend fun a(requests:
Flow<Req>): Res` | `suspend fun a(requests: Flow<Req>): Res` |
+| `rpc A (stream Req) returns (stream Res)` | `fun a(requests: Flow<Req>):
Flow<Res>` | `fun a(requests: Flow<Req>): Flow<Res>` |
+
+Each method descriptor uses a Fory-backed `io.grpc.MethodDescriptor.Marshaller`
+that reuses the generated schema module's `ThreadSafeFory`. Generated service
+companions do not call protobuf parsers, do not expose KSP serializer class
+names, and do not create Fory instances per call.
+
+Applications compiling the generated Kotlin service files must provide
+grpc-java, grpc-kotlin, and `kotlinx-coroutines-core` dependencies. Fory Kotlin
+artifacts do not add those gRPC dependencies as hard dependencies.
+
## Scala
The Scala target emits Scala 3 source only. The `fory-scala` artifact still
diff --git a/docs/compiler/index.md b/docs/compiler/index.md
index 0bcc5ba6f..f9fea15b4 100644
--- a/docs/compiler/index.md
+++ b/docs/compiler/index.md
@@ -23,7 +23,7 @@ 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, Python, Go, and Rust, the compiler can
+describe RPC services; for Java, Python, Go, Rust, and Kotlin, the compiler can
generate gRPC service companions that use Fory serialization for request and
response payloads.
@@ -88,16 +88,16 @@ service AnimalService {
}
```
-Generate Java, Python, and Rust models plus gRPC service companions with:
+Generate Java, Python, Rust, and Kotlin models plus gRPC service companions
with:
```bash
-foryc animals.fdl --java_out=./generated/java --python_out=./generated/python
--rust_out=./generated/rust --grpc
+foryc animals.fdl --java_out=./generated/java --python_out=./generated/python
--rust_out=./generated/rust --kotlin_out=./generated/kotlin --grpc
```
The generated service code uses normal gRPC APIs, but request and response
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.
+grpc-kotlin, `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 66e0e4bc1..4ce825e37 100644
--- a/docs/compiler/protobuf-idl.md
+++ b/docs/compiler/protobuf-idl.md
@@ -41,17 +41,17 @@ how protobuf concepts map to Fory, and how to use
protobuf-only Fory extension o
## Protobuf vs Fory at a Glance
-| Aspect | Protocol Buffers | Fory
|
-| ------------------ | ----------------------------- |
------------------------------------- |
-| Primary purpose | RPC/message contracts | High-performance object
serialization |
-| Encoding model | Tag-length-value | Fory binary protocol
|
-| Reference tracking | Not built-in | First-class (`ref`)
|
-| Circular refs | Not supported | Supported
|
-| Unknown fields | Preserved | Not preserved
|
-| Generated types | Protobuf-specific model types | Native language
constructs |
-| gRPC ecosystem | Native | Java/Python/Go/Rust
service codegen |
-
-Fory can generate Java, Python, Go, and Rust gRPC service companions with
+| Aspect | Protocol Buffers | Fory
|
+| ------------------ | ----------------------------- |
------------------------------------------ |
+| Primary purpose | RPC/message contracts | High-performance object
serialization |
+| Encoding model | Tag-length-value | Fory binary protocol
|
+| Reference tracking | Not built-in | First-class (`ref`)
|
+| Circular refs | Not supported | Supported
|
+| Unknown fields | Preserved | Not preserved
|
+| Generated types | Protobuf-specific model types | Native language
constructs |
+| gRPC ecosystem | Native |
Java/Python/Go/Rust/Kotlin service codegen |
+
+Fory can generate Java, Python, Go, Rust, and Kotlin 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
@@ -314,16 +314,16 @@ languages.
For supported service outputs, add `--grpc` to emit gRPC companion code:
```bash
-foryc api.proto --java_out=./generated/java --python_out=./generated/python
--rust_out=./generated/rust --grpc
+foryc api.proto --java_out=./generated/java --python_out=./generated/python
--rust_out=./generated/rust --kotlin_out=./generated/kotlin --grpc
```
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.
+modules import `grpc`, generated Rust service files import `tonic` and `bytes`,
+and generated Kotlin service files compile against grpc-java and grpc-kotlin.
+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 c59bbe4c6..40f082108 100644
--- a/docs/compiler/schema-idl.md
+++ b/docs/compiler/schema-idl.md
@@ -908,7 +908,7 @@ 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 supported language
-outputs such as Java, Python, Go, and Rust.
+outputs such as Java, Python, Go, Rust, and Kotlin.
```protobuf
message GetPetRequest [id=200] {
@@ -950,8 +950,8 @@ service PetDirectory {
of a service contract.
- 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`.
+ dependency, such as grpc-java, grpc-kotlin, `grpcio`, grpc-go, or Rust
+ `tonic` and `bytes`.
**Grammar:**
diff --git a/docs/guide/java/grpc-support.md b/docs/guide/java/grpc-support.md
index bfa1781e3..4a233852d 100644
--- a/docs/guide/java/grpc-support.md
+++ b/docs/guide/java/grpc-support.md
@@ -30,6 +30,9 @@ 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.
+For Kotlin coroutine stubs and service bases, see
+[Kotlin gRPC Support](../kotlin/grpc-support.md).
+
## Add Dependencies
The generated Java service files compile against grpc-java. Fory Java artifacts
diff --git a/docs/guide/kotlin/android-support.md
b/docs/guide/kotlin/android-support.md
index 9ecafb4f2..3f8c66b67 100644
--- a/docs/guide/kotlin/android-support.md
+++ b/docs/guide/kotlin/android-support.md
@@ -1,6 +1,6 @@
---
title: Android Support
-sidebar_position: 6
+sidebar_position: 7
id: android_support
license: |
Licensed to the Apache Software Foundation (ASF) under one or more
diff --git a/docs/guide/kotlin/grpc-support.md
b/docs/guide/kotlin/grpc-support.md
new file mode 100644
index 000000000..ae12f9e0f
--- /dev/null
+++ b/docs/guide/kotlin/grpc-support.md
@@ -0,0 +1,179 @@
+---
+title: Kotlin gRPC Support
+sidebar_position: 6
+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 IDL can generate Kotlin coroutine gRPC companions. The generated gRPC
+files use normal grpc-java and grpc-kotlin APIs, while each request and
response
+message is serialized with Fory.
+
+## Dependencies
+
+Add Fory Kotlin, KSP, grpc-java, grpc-kotlin, coroutines, and one grpc-java
+transport to the application or service module that compiles the generated
+source.
+
+```kotlin
+plugins {
+ id("com.google.devtools.ksp") version "<ksp-version>"
+}
+
+dependencies {
+ implementation("org.apache.fory:fory-kotlin:<fory-version>")
+ ksp("org.apache.fory:fory-kotlin-ksp:<fory-version>")
+
+ implementation("io.grpc:grpc-api:<grpc-version>")
+ implementation("io.grpc:grpc-stub:<grpc-version>")
+ implementation("io.grpc:grpc-kotlin-stub:<grpc-kotlin-version>")
+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:<coroutines-version>")
+
+ runtimeOnly("io.grpc:grpc-netty-shaded:<grpc-version>")
+}
+```
+
+Use a different grpc-java transport if your application already standardizes on
+one. Generated Kotlin Fory gRPC does not require `grpc-protobuf` for payload
+encoding.
+
+## Generate Code
+
+For a schema such as:
+
+```protobuf
+package demo.greeter;
+
+message HelloRequest {
+ string name = 1;
+}
+
+message HelloReply {
+ string reply = 1;
+}
+
+service Greeter {
+ rpc SayHello (HelloRequest) returns (HelloReply);
+}
+```
+
+run:
+
+```bash
+foryc service.fdl --kotlin_out=./generated/kotlin --grpc
+```
+
+The compiler writes Kotlin model files, a schema module such as
+`ServiceForyModule.kt`, and one service companion such as `GreeterGrpcKt.kt`.
+Run KSP when compiling the generated model files so the schema serializers are
+available at runtime.
+
+## Server
+
+Implement the generated coroutine base class and register it with a normal
+grpc-java server.
+
+```kotlin
+import demo.greeter.GreeterGrpcKt
+import demo.greeter.HelloReply
+import demo.greeter.HelloRequest
+import io.grpc.ServerBuilder
+
+class GreeterService : GreeterGrpcKt.GreeterCoroutineImplBase() {
+ override suspend fun sayHello(request: HelloRequest): HelloReply =
+ HelloReply(reply = "Hello, ${request.name}")
+}
+
+val server = ServerBuilder
+ .forPort(50051)
+ .addService(GreeterService())
+ .build()
+ .start()
+```
+
+Unimplemented generated methods fail with gRPC `UNIMPLEMENTED`. Exceptions
+thrown by your service method follow grpc-kotlin server behavior.
+
+## Client
+
+Construct the generated coroutine stub directly from a grpc-java channel.
+
+```kotlin
+import demo.greeter.GreeterGrpcKt
+import demo.greeter.HelloRequest
+import io.grpc.ManagedChannelBuilder
+
+val channel = ManagedChannelBuilder
+ .forAddress("localhost", 50051)
+ .usePlaintext()
+ .build()
+
+val stub = GreeterGrpcKt.GreeterCoroutineStub(channel)
+val reply = stub.sayHello(HelloRequest(name = "Fory"))
+```
+
+Channel construction, shutdown, deadlines, credentials, interceptors, load
+balancing, retries, and server lifecycle stay normal grpc-java/grpc-kotlin
+responsibilities.
+
+## Streaming
+
+Streaming RPCs use `kotlinx.coroutines.flow.Flow`.
+
+| IDL shape | Server method
| Client method |
+| ----------------------------------------- |
----------------------------------------- |
----------------------------------------- |
+| `rpc A (Req) returns (Res)` | `suspend fun a(request: Req):
Res` | `suspend fun a(request: Req): Res` |
+| `rpc A (Req) returns (stream Res)` | `fun a(request: Req): Flow<Res>`
| `fun a(request: Req): Flow<Res>` |
+| `rpc A (stream Req) returns (Res)` | `suspend fun a(requests:
Flow<Req>): Res` | `suspend fun a(requests: Flow<Req>): Res` |
+| `rpc A (stream Req) returns (stream Res)` | `fun a(requests: Flow<Req>):
Flow<Res>` | `fun a(requests: Flow<Req>): Flow<Res>` |
+
+The generated method path keeps the exact service and method names from the
+schema, for example `/demo.greeter.Greeter/SayHello`.
+
+## Interoperability
+
+Generated Kotlin service companions use Fory binary payloads inside gRPC
+frames. They are interoperable with other Fory gRPC companions generated from
+the same schema, such as Java, Go, Python, and Rust companions. Generic
+protobuf gRPC clients cannot decode these payloads.
+
+Direct union request and response types are supported for Fory IDL services.
+For protobuf input, use the protobuf service shapes accepted by the protobuf
+frontend; protobuf `oneof` fields are translated into Fory union fields inside
+messages.
+
+## Troubleshooting
+
+**Generated service file is missing**
+
+Pass `--grpc` together with `--kotlin_out`. Schemas without service definitions
+only generate model files and the schema module.
+
+**Serializer class not found at runtime**
+
+Ensure KSP runs for the generated Kotlin model sources and that
+`fory-kotlin-ksp` uses the same Fory version as `fory-kotlin`.
+
+**gRPC classes are unresolved**
+
+Add grpc-java and grpc-kotlin dependencies to the application module. Fory
+Kotlin artifacts do not add those dependencies automatically.
+
+**A protobuf client cannot read responses**
+
+Fory gRPC uses Fory binary protocol payloads, not protobuf wire-format
messages.
+Use generated Fory gRPC companions on both sides for the same service schema.
diff --git a/docs/guide/kotlin/index.md b/docs/guide/kotlin/index.md
index a8bc9c431..92aeb09bb 100644
--- a/docs/guide/kotlin/index.md
+++ b/docs/guide/kotlin/index.md
@@ -40,6 +40,7 @@ Fory Kotlin inherits all features from Fory Java, plus
Kotlin-specific optimizat
- **Default Value Support**: Automatic handling of Kotlin data class default
parameters during schema evolution
- **Static Xlang Serializers**: KSP-generated schema serializers for
Kotlin/JVM and Android xlang mode
- **Schema IDL Generation**: Fory compiler output for Kotlin models, sealed
unions, and schema modules
+- **Kotlin gRPC Support**: Coroutine service companions that use Fory payload
serialization
- **Schema Evolution**: Forward/backward compatibility for class schema changes
See [Java Features](../java/index.md#features) for complete feature list.
@@ -130,4 +131,5 @@ Fory Kotlin is built on top of Fory Java. Most
configuration options, features,
- [Schema Metadata](schema-metadata.md) - Kotlin annotations, nullability,
references, and integer metadata
- [Default Values](default-values.md) - Kotlin data class default values
support
- [Static Generated Serializers](static-generated-serializers.md) - KSP
xlang/schema serializer generation
+- [Kotlin gRPC Support](grpc-support.md) - Coroutine stubs and service bases
for Fory IDL services
- [Android Support](android-support.md) - Android setup, R8 behavior, and
release-build validation
diff --git a/integration_tests/README.md b/integration_tests/README.md
index 845c07740..1bb28dce7 100644
--- a/integration_tests/README.md
+++ b/integration_tests/README.md
@@ -3,6 +3,8 @@
- [jdk_compatibility_tests](jdk_compatibility_tests): test fory compatibility
across multiple jdk versions.
- [graalvm_tests](graalvm_tests): test graalvm native image support.
- [jpms_tests](jpms_tests): test JPMS module names.
+- [idl_tests](idl_tests): test Fory IDL cross-language generation and round
trips.
+- [grpc_tests](grpc_tests): test Fory gRPC companion interoperability across
languages.
- [cpython_benchmark](cpython_benchmark): fory CPython microbenchmark.
> Note that this integration_tests is not designed as a maven multi-module
> project on purpose, so we can introduce features of higher jdk version
> without breaking compilation for lower jdk, and add integration tests for
> other languages.
diff --git a/integration_tests/grpc_tests/generate_grpc.py
b/integration_tests/grpc_tests/generate_grpc.py
index af3f3f44f..9f9c64332 100644
--- a/integration_tests/grpc_tests/generate_grpc.py
+++ b/integration_tests/grpc_tests/generate_grpc.py
@@ -35,6 +35,7 @@ OUTPUTS = {
"python": TEST_DIR / "python/grpc_tests/generated",
"go": TEST_DIR / "go/generated",
"rust": TEST_DIR / "rust/generated/src",
+ "kotlin": TEST_DIR / "kotlin/src/main/kotlin/generated",
}
@@ -76,6 +77,7 @@ def main() -> int:
f"--python_out={OUTPUTS['python']}",
f"--go_out={go_pkg_out}",
f"--rust_out={OUTPUTS['rust']}",
+ f"--kotlin_out={OUTPUTS['kotlin']}",
"--grpc",
],
env=env,
diff --git
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcTestBase.java
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcTestBase.java
index 730bfbf95..8e9fe616e 100644
---
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcTestBase.java
+++
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcTestBase.java
@@ -375,6 +375,28 @@ public abstract class GrpcTestBase {
return peerCommand;
}
+ protected PeerCommand kotlinCommand(String... args) {
+ Path grpcRoot =
repoRoot().resolve("integration_tests").resolve("grpc_tests");
+ Path kotlinRoot = grpcRoot.resolve("kotlin");
+ List<String> command = new ArrayList<>();
+ command.add("java");
+ command.add("-jar");
+
command.add(kotlinRoot.resolve("target").resolve("fory-kotlin-grpc-peer.jar").toString());
+ command.addAll(Arrays.asList(args));
+ PeerCommand peerCommand = new PeerCommand();
+ peerCommand.command = command;
+ peerCommand.workDir = kotlinRoot;
+ peerCommand.environment.put("ENABLE_FORY_DEBUG_OUTPUT", "1");
+ peerCommand.environment.put("NO_PROXY", "127.0.0.1,localhost");
+ peerCommand.environment.put("no_proxy", "127.0.0.1,localhost");
+ for (String proxyVar :
+ Arrays.asList(
+ "all_proxy", "http_proxy", "https_proxy", "ALL_PROXY",
"HTTP_PROXY", "HTTPS_PROXY")) {
+ peerCommand.environment.put(proxyVar, "");
+ }
+ return peerCommand;
+ }
+
protected void runPython(String peer, String... args) throws IOException,
InterruptedException {
Process process = startPeer(pythonCommand(args));
PeerOutputCollector outputCollector = new
PeerOutputCollector(process.getInputStream(), peer);
@@ -419,6 +441,28 @@ public abstract class GrpcTestBase {
outputCollector.awaitOutput();
}
+ protected void runKotlin(String peer, String... args) throws IOException,
InterruptedException {
+ Process process = startPeer(kotlinCommand(args));
+ PeerOutputCollector outputCollector = new
PeerOutputCollector(process.getInputStream(), peer);
+ outputCollector.start();
+ boolean finished = process.waitFor(180, TimeUnit.SECONDS);
+ if (!finished) {
+ process.destroyForcibly();
+ process.waitFor(10, TimeUnit.SECONDS);
+ Assert.fail("Peer process timed out for " + peer +
peerOutput(outputCollector));
+ }
+ int exitCode = process.exitValue();
+ if (exitCode != 0) {
+ Assert.fail(
+ "Peer process failed for "
+ + peer
+ + " with exit code "
+ + exitCode
+ + peerOutput(outputCollector));
+ }
+ outputCollector.awaitOutput();
+ }
+
private Process startPeer(PeerCommand command) throws IOException {
ProcessBuilder builder = new ProcessBuilder(command.command);
builder.redirectErrorStream(true);
diff --git
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/KotlinGrpcInteropTest.java
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/KotlinGrpcInteropTest.java
new file mode 100644
index 000000000..cc1d5e3a7
--- /dev/null
+++
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/KotlinGrpcInteropTest.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 KotlinGrpcInteropTest extends GrpcTestBase {
+
+ @Test
+ public void testJavaServerKotlinClient() throws Exception {
+ Server server = startJavaAllSchemasServer();
+ try {
+ runKotlin("kotlin-grpc-client", "client", "--target", "127.0.0.1:" +
server.getPort());
+ } finally {
+ server.shutdownNow();
+ server.awaitTermination(10, TimeUnit.SECONDS);
+ }
+ }
+
+ @Test
+ public void testKotlinServerJavaClient() throws Exception {
+ exercisePeerServer(
+ "kotlin-grpc",
+ "Kotlin",
+ "fory-grpc-kotlin-",
+ kotlinCommand("server"),
+ this::exerciseAllSchemas);
+ }
+}
diff --git a/integration_tests/grpc_tests/kotlin/pom.xml
b/integration_tests/grpc_tests/kotlin/pom.xml
new file mode 100644
index 000000000..c31056070
--- /dev/null
+++ b/integration_tests/grpc_tests/kotlin/pom.xml
@@ -0,0 +1,191 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.apache.fory</groupId>
+ <artifactId>fory-kotlin-parent</artifactId>
+ <version>1.2.0-SNAPSHOT</version>
+ <relativePath>../../../kotlin/pom.xml</relativePath>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>fory-kotlin-grpc-tests</artifactId>
+
+ <properties>
+ <grpc.version>1.75.0</grpc.version>
+ <grpc.kotlin.version>1.4.3</grpc.kotlin.version>
+ <kotlinx.coroutines.version>1.8.0</kotlinx.coroutines.version>
+ </properties>
+
+ <build>
+ <resources>
+ <resource>
+
<directory>${project.build.directory}/generated-resources/ksp</directory>
+ <includes>
+ <include>META-INF/proguard/**</include>
+ </includes>
+ </resource>
+ </resources>
+ <plugins>
+ <plugin>
+ <groupId>me.kpavlov.ksp.maven</groupId>
+ <artifactId>ksp-maven-plugin</artifactId>
+ <version>${ksp.maven.plugin.version}</version>
+ <executions>
+ <execution>
+ <id>ksp</id>
+ <goals>
+ <goal>process</goal>
+ </goals>
+ <phase>generate-sources</phase>
+ </execution>
+ </executions>
+ <configuration>
+
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
+ <sourceDirs>
+
<sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
+ </sourceDirs>
+ <processors>
+ <processor>
+ <groupId>org.apache.fory</groupId>
+ <artifactId>fory-kotlin-ksp</artifactId>
+ <version>${project.version}</version>
+ </processor>
+ </processors>
+ </configuration>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.fory</groupId>
+ <artifactId>fory-kotlin-ksp</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.devtools.ksp</groupId>
+
<artifactId>symbol-processing-aa-embeddable</artifactId>
+ <version>${ksp.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.devtools.ksp</groupId>
+ <artifactId>symbol-processing-api</artifactId>
+ <version>${ksp.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.devtools.ksp</groupId>
+ <artifactId>symbol-processing-common-deps</artifactId>
+ <version>${ksp.version}</version>
+ </dependency>
+ </dependencies>
+ </plugin>
+ <plugin>
+ <groupId>org.jetbrains.kotlin</groupId>
+ <artifactId>kotlin-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>compile</id>
+ <phase>compile</phase>
+ <goals>
+ <goal>compile</goal>
+ </goals>
+ <configuration>
+ <sourceDirs>
+
<sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
+
<sourceDir>${project.build.directory}/generated-sources/ksp</sourceDir>
+ </sourceDirs>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <finalName>fory-kotlin-grpc-peer</finalName>
+
<createDependencyReducedPom>false</createDependencyReducedPom>
+ <filters>
+ <filter>
+ <artifact>*:*</artifact>
+ <excludes>
+ <exclude>META-INF/*.SF</exclude>
+ <exclude>META-INF/*.DSA</exclude>
+ <exclude>META-INF/*.RSA</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ <transformers>
+ <transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"
/>
+ <transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+
<mainClass>org.apache.fory.grpc_tests.KotlinGrpcInteropKt</mainClass>
+ </transformer>
+ </transformers>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.fory</groupId>
+ <artifactId>fory-kotlin</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jetbrains.kotlin</groupId>
+ <artifactId>kotlin-stdlib</artifactId>
+ <version>${kotlin.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-api</artifactId>
+ <version>${grpc.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-netty-shaded</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-kotlin-stub</artifactId>
+ <version>${grpc.kotlin.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jetbrains.kotlinx</groupId>
+ <artifactId>kotlinx-coroutines-core-jvm</artifactId>
+ <version>${kotlinx.coroutines.version}</version>
+ </dependency>
+ </dependencies>
+</project>
diff --git
a/integration_tests/grpc_tests/kotlin/src/main/kotlin/org/apache/fory/grpc_tests/KotlinGrpcInterop.kt
b/integration_tests/grpc_tests/kotlin/src/main/kotlin/org/apache/fory/grpc_tests/KotlinGrpcInterop.kt
new file mode 100644
index 000000000..53e3cdfce
--- /dev/null
+++
b/integration_tests/grpc_tests/kotlin/src/main/kotlin/org/apache/fory/grpc_tests/KotlinGrpcInterop.kt
@@ -0,0 +1,420 @@
+/*
+ * 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 grpc_fbs.FbsGrpcServiceGrpcKt
+import grpc_fbs.GrpcFbsRequest
+import grpc_fbs.GrpcFbsResponse
+import grpc_fbs.GrpcFbsUnion
+import grpc_fdl.FdlGrpcServiceGrpcKt
+import grpc_fdl.GrpcFdlRequest
+import grpc_fdl.GrpcFdlResponse
+import grpc_fdl.GrpcFdlUnion
+import grpc_pb.GrpcPbRequest
+import grpc_pb.GrpcPbRequestPayload
+import grpc_pb.GrpcPbResponse
+import grpc_pb.GrpcPbResponsePayload
+import grpc_pb.PbGrpcServiceGrpcKt
+import io.grpc.ManagedChannel
+import io.grpc.ManagedChannelBuilder
+import io.grpc.ServerBuilder
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.runBlocking
+
+fun main(args: Array<String>): Unit = runBlocking {
+ when (val command = args.firstOrNull()) {
+ "client" -> runClient(requireOption(args, "--target"))
+ "server" -> runServer(requireOption(args, "--port-file"))
+ else -> error("Expected command 'client' or 'server', got '$command'")
+ }
+}
+
+private suspend fun runClient(target: String) {
+ val channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build()
+ try {
+ exerciseFdl(channel)
+ exerciseFbs(channel)
+ exercisePb(channel)
+ } finally {
+ channel.shutdownNow()
+ channel.awaitTermination(10, TimeUnit.SECONDS)
+ }
+}
+
+private fun runServer(portFile: String) {
+ val server =
+ ServerBuilder.forPort(0)
+ .addService(FdlService())
+ .addService(FbsService())
+ .addService(PbService())
+ .build()
+ .start()
+ Files.write(Paths.get(portFile),
server.port.toString().toByteArray(StandardCharsets.UTF_8))
+ server.awaitTermination()
+}
+
+private suspend fun exerciseFdl(channel: ManagedChannel) {
+ val stub = FdlGrpcServiceGrpcKt.FdlGrpcServiceCoroutineStub(channel)
+ val requests = listOf(fdlRequest("fdl-a", 1, "alpha"), fdlRequest("fdl-b",
2, "beta"))
+ exerciseMessages(
+ requests,
+ stub::unaryMessage,
+ stub::serverStreamMessage,
+ stub::clientStreamMessage,
+ stub::bidiStreamMessage,
+ ::fdlResponse,
+ ::fdlAggregate,
+ )
+
+ val unionRequests =
+ listOf(
+ fdlUnionRequest(fdlRequest("fdl-u-a", 3, "union-alpha")),
+ fdlUnionRequest(fdlRequest("fdl-u-b", 4, "union-beta")),
+ )
+ exerciseUnions(
+ unionRequests,
+ stub::unaryUnion,
+ stub::serverStreamUnion,
+ stub::clientStreamUnion,
+ stub::bidiStreamUnion,
+ ::fdlRequestFromUnion,
+ ::fdlUnionResponse,
+ ::fdlUnionAggregate,
+ )
+}
+
+private suspend fun exerciseFbs(channel: ManagedChannel) {
+ val stub = FbsGrpcServiceGrpcKt.FbsGrpcServiceCoroutineStub(channel)
+ val requests = listOf(fbsRequest("fbs-a", 5, "alpha"), fbsRequest("fbs-b",
6, "beta"))
+ exerciseMessages(
+ requests,
+ stub::unaryMessage,
+ stub::serverStreamMessage,
+ stub::clientStreamMessage,
+ stub::bidiStreamMessage,
+ ::fbsResponse,
+ ::fbsAggregate,
+ )
+
+ val unionRequests =
+ listOf(
+ fbsUnionRequest(fbsRequest("fbs-u-a", 7, "union-alpha")),
+ fbsUnionRequest(fbsRequest("fbs-u-b", 8, "union-beta")),
+ )
+ exerciseUnions(
+ unionRequests,
+ stub::unaryUnion,
+ stub::serverStreamUnion,
+ stub::clientStreamUnion,
+ stub::bidiStreamUnion,
+ ::fbsRequestFromUnion,
+ ::fbsUnionResponse,
+ ::fbsUnionAggregate,
+ )
+}
+
+private suspend fun exercisePb(channel: ManagedChannel) {
+ val stub = PbGrpcServiceGrpcKt.PbGrpcServiceCoroutineStub(channel)
+ val requests =
+ listOf(
+ pbRequest("pb-a", 9U, GrpcPbRequestPayload.Text("alpha")),
+ pbRequest("pb-b", 10U, GrpcPbRequestPayload.Number(42U)),
+ )
+ exerciseMessages(
+ requests,
+ stub::unaryMessage,
+ stub::serverStreamMessage,
+ stub::clientStreamMessage,
+ stub::bidiStreamMessage,
+ ::pbResponse,
+ ::pbAggregate,
+ )
+}
+
+private suspend fun <Request : Any, Response : Any> exerciseMessages(
+ requests: List<Request>,
+ unary: suspend (Request) -> Response,
+ serverStream: (Request) -> Flow<Response>,
+ clientStream: suspend (Flow<Request>) -> Response,
+ bidiStream: (Flow<Request>) -> Flow<Response>,
+ response: (Request, String, Int) -> Response,
+ aggregate: (List<Request>) -> Response,
+) {
+ val first = requests.first()
+ assertEquals("unary", unary(first), response(first, "unary", 10))
+ assertEquals(
+ "serverStream",
+ serverStream(first).toList(),
+ listOf(
+ response(first, "server-0", 0),
+ response(first, "server-1", 1),
+ response(first, "server-2", 2),
+ ),
+ )
+ assertEquals("clientStream", clientStream(requests.asFlow()),
aggregate(requests))
+ assertEquals(
+ "bidiStream",
+ bidiStream(requests.asFlow()).toList(),
+ requests.mapIndexed { index, request -> response(request, "bidi-$index",
index) },
+ )
+}
+
+private suspend fun <Request : Any, Union : Any> exerciseUnions(
+ requests: List<Union>,
+ unary: suspend (Union) -> Union,
+ serverStream: (Union) -> Flow<Union>,
+ clientStream: suspend (Flow<Union>) -> Union,
+ bidiStream: (Flow<Union>) -> Flow<Union>,
+ requestFromUnion: (Union) -> Request,
+ unionResponse: (Request, String, Int) -> Union,
+ unionAggregate: (List<Union>) -> Union,
+) {
+ val first = requests.first()
+ val firstRequest = requestFromUnion(first)
+ assertEquals("unaryUnion", unary(first), unionResponse(firstRequest,
"unary", 10))
+ assertEquals(
+ "serverStreamUnion",
+ serverStream(first).toList(),
+ listOf(
+ unionResponse(firstRequest, "server-0", 0),
+ unionResponse(firstRequest, "server-1", 1),
+ unionResponse(firstRequest, "server-2", 2),
+ ),
+ )
+ assertEquals("clientStreamUnion", clientStream(requests.asFlow()),
unionAggregate(requests))
+ assertEquals(
+ "bidiStreamUnion",
+ bidiStream(requests.asFlow()).toList(),
+ requests.mapIndexed { index, request ->
+ unionResponse(requestFromUnion(request), "bidi-$index", index)
+ },
+ )
+}
+
+private class FdlService :
FdlGrpcServiceGrpcKt.FdlGrpcServiceCoroutineImplBase() {
+ override suspend fun unaryMessage(request: GrpcFdlRequest): GrpcFdlResponse =
+ fdlResponse(request, "unary", 10)
+
+ override fun serverStreamMessage(request: GrpcFdlRequest):
Flow<GrpcFdlResponse> =
+ flow {
+ repeat(3) { index -> emit(fdlResponse(request, "server-$index", index)) }
+ }
+
+ override suspend fun clientStreamMessage(requests: Flow<GrpcFdlRequest>):
GrpcFdlResponse =
+ fdlAggregate(requests.toList())
+
+ override fun bidiStreamMessage(requests: Flow<GrpcFdlRequest>):
Flow<GrpcFdlResponse> =
+ flow {
+ var index = 0
+ requests.collect { request ->
+ emit(fdlResponse(request, "bidi-$index", index))
+ index++
+ }
+ }
+
+ override suspend fun unaryUnion(request: GrpcFdlUnion): GrpcFdlUnion =
+ fdlUnionResponse(fdlRequestFromUnion(request), "unary", 10)
+
+ override fun serverStreamUnion(request: GrpcFdlUnion): Flow<GrpcFdlUnion> =
+ flow {
+ val value = fdlRequestFromUnion(request)
+ repeat(3) { index -> emit(fdlUnionResponse(value, "server-$index",
index)) }
+ }
+
+ override suspend fun clientStreamUnion(requests: Flow<GrpcFdlUnion>):
GrpcFdlUnion =
+ fdlUnionAggregate(requests.toList())
+
+ override fun bidiStreamUnion(requests: Flow<GrpcFdlUnion>):
Flow<GrpcFdlUnion> =
+ flow {
+ var index = 0
+ requests.collect { request ->
+ emit(fdlUnionResponse(fdlRequestFromUnion(request), "bidi-$index",
index))
+ index++
+ }
+ }
+}
+
+private class FbsService :
FbsGrpcServiceGrpcKt.FbsGrpcServiceCoroutineImplBase() {
+ override suspend fun unaryMessage(request: GrpcFbsRequest): GrpcFbsResponse =
+ fbsResponse(request, "unary", 10)
+
+ override fun serverStreamMessage(request: GrpcFbsRequest):
Flow<GrpcFbsResponse> =
+ flow {
+ repeat(3) { index -> emit(fbsResponse(request, "server-$index", index)) }
+ }
+
+ override suspend fun clientStreamMessage(requests: Flow<GrpcFbsRequest>):
GrpcFbsResponse =
+ fbsAggregate(requests.toList())
+
+ override fun bidiStreamMessage(requests: Flow<GrpcFbsRequest>):
Flow<GrpcFbsResponse> =
+ flow {
+ var index = 0
+ requests.collect { request ->
+ emit(fbsResponse(request, "bidi-$index", index))
+ index++
+ }
+ }
+
+ override suspend fun unaryUnion(request: GrpcFbsUnion): GrpcFbsUnion =
+ fbsUnionResponse(fbsRequestFromUnion(request), "unary", 10)
+
+ override fun serverStreamUnion(request: GrpcFbsUnion): Flow<GrpcFbsUnion> =
+ flow {
+ val value = fbsRequestFromUnion(request)
+ repeat(3) { index -> emit(fbsUnionResponse(value, "server-$index",
index)) }
+ }
+
+ override suspend fun clientStreamUnion(requests: Flow<GrpcFbsUnion>):
GrpcFbsUnion =
+ fbsUnionAggregate(requests.toList())
+
+ override fun bidiStreamUnion(requests: Flow<GrpcFbsUnion>):
Flow<GrpcFbsUnion> =
+ flow {
+ var index = 0
+ requests.collect { request ->
+ emit(fbsUnionResponse(fbsRequestFromUnion(request), "bidi-$index",
index))
+ index++
+ }
+ }
+}
+
+private class PbService : PbGrpcServiceGrpcKt.PbGrpcServiceCoroutineImplBase()
{
+ override suspend fun unaryMessage(request: GrpcPbRequest): GrpcPbResponse =
+ pbResponse(request, "unary", 10)
+
+ override fun serverStreamMessage(request: GrpcPbRequest):
Flow<GrpcPbResponse> =
+ flow {
+ repeat(3) { index -> emit(pbResponse(request, "server-$index", index)) }
+ }
+
+ override suspend fun clientStreamMessage(requests: Flow<GrpcPbRequest>):
GrpcPbResponse =
+ pbAggregate(requests.toList())
+
+ override fun bidiStreamMessage(requests: Flow<GrpcPbRequest>):
Flow<GrpcPbResponse> =
+ flow {
+ var index = 0
+ requests.collect { request ->
+ emit(pbResponse(request, "bidi-$index", index))
+ index++
+ }
+ }
+}
+
+private fun fdlRequest(id: String, count: Int, payload: String):
GrpcFdlRequest =
+ GrpcFdlRequest(id, count, payload)
+
+private fun fdlResponse(request: GrpcFdlRequest, tag: String, offset: Int):
GrpcFdlResponse =
+ GrpcFdlResponse("$tag:${request.id}", request.count + offset,
"$tag:${request.payload}")
+
+private fun fdlAggregate(requests: List<GrpcFdlRequest>): GrpcFdlResponse =
+ GrpcFdlResponse(
+ "client:" + requests.joinToString("+") { it.id },
+ requests.sumOf { it.count },
+ "client:" + requests.joinToString("+") { it.payload },
+ )
+
+private fun fdlUnionRequest(request: GrpcFdlRequest): GrpcFdlUnion =
GrpcFdlUnion.Request(request)
+
+private fun fdlUnionResponse(request: GrpcFdlRequest, tag: String, offset:
Int): GrpcFdlUnion =
+ GrpcFdlUnion.Response(fdlResponse(request, tag, offset))
+
+private fun fdlUnionAggregate(requests: List<GrpcFdlUnion>): GrpcFdlUnion =
+ GrpcFdlUnion.Response(fdlAggregate(requests.map(::fdlRequestFromUnion)))
+
+private fun fdlRequestFromUnion(union: GrpcFdlUnion): GrpcFdlRequest =
+ when (union) {
+ is GrpcFdlUnion.Request -> union.value
+ else -> error("Expected GrpcFdlUnion.Request, got
${union::class.simpleName}")
+ }
+
+private fun fbsRequest(id: String, count: Int, payload: String):
GrpcFbsRequest =
+ GrpcFbsRequest(id, count, payload)
+
+private fun fbsResponse(request: GrpcFbsRequest, tag: String, offset: Int):
GrpcFbsResponse =
+ GrpcFbsResponse("$tag:${request.id}", request.count + offset,
"$tag:${request.payload}")
+
+private fun fbsAggregate(requests: List<GrpcFbsRequest>): GrpcFbsResponse =
+ GrpcFbsResponse(
+ "client:" + requests.joinToString("+") { it.id },
+ requests.sumOf { it.count },
+ "client:" + requests.joinToString("+") { it.payload },
+ )
+
+private fun fbsUnionRequest(request: GrpcFbsRequest): GrpcFbsUnion =
+ GrpcFbsUnion.GrpcFbsRequest(request)
+
+private fun fbsUnionResponse(request: GrpcFbsRequest, tag: String, offset:
Int): GrpcFbsUnion =
+ GrpcFbsUnion.GrpcFbsResponse(fbsResponse(request, tag, offset))
+
+private fun fbsUnionAggregate(requests: List<GrpcFbsUnion>): GrpcFbsUnion =
+
GrpcFbsUnion.GrpcFbsResponse(fbsAggregate(requests.map(::fbsRequestFromUnion)))
+
+private fun fbsRequestFromUnion(union: GrpcFbsUnion): GrpcFbsRequest =
+ when (union) {
+ is GrpcFbsUnion.GrpcFbsRequest -> union.value
+ else -> error("Expected GrpcFbsUnion.GrpcFbsRequest, got
${union::class.simpleName}")
+ }
+
+private fun pbRequest(id: String, count: UInt, payload: GrpcPbRequestPayload):
GrpcPbRequest =
+ GrpcPbRequest(id, count, payload)
+
+private fun pbResponse(request: GrpcPbRequest, tag: String, offset: Int):
GrpcPbResponse =
+ GrpcPbResponse(
+ "$tag:${request.id}",
+ request.count + offset.toUInt(),
+ pbResponsePayload(request.payload, tag, offset),
+ )
+
+private fun pbResponsePayload(
+ payload: GrpcPbRequestPayload?,
+ tag: String,
+ offset: Int,
+): GrpcPbResponsePayload? =
+ when (payload) {
+ null -> null
+ is GrpcPbRequestPayload.Text ->
GrpcPbResponsePayload.Text("$tag:${payload.value}")
+ is GrpcPbRequestPayload.Number ->
GrpcPbResponsePayload.Number(payload.value + offset.toUInt())
+ is GrpcPbRequestPayload.Unknown ->
+ error("Expected known GrpcPbRequestPayload, got
${payload::class.simpleName}")
+ }
+
+private fun pbAggregate(requests: List<GrpcPbRequest>): GrpcPbResponse =
+ GrpcPbResponse(
+ "client:" + requests.joinToString("+") { it.id },
+ requests.fold(0U) { total, request -> total + request.count },
+ GrpcPbResponsePayload.Text("client:" + requests.joinToString("+") { it.id
}),
+ )
+
+private fun requireOption(args: Array<String>, name: String): String {
+ val index = args.indexOf(name)
+ require(index >= 0 && index + 1 < args.size) { "Missing required option
$name" }
+ return args[index + 1]
+}
+
+private fun <T> assertEquals(name: String, actual: T, expected: T) {
+ check(actual == expected) { "$name: expected <$expected>, got <$actual>" }
+}
diff --git a/integration_tests/grpc_tests/run_tests.sh
b/integration_tests/grpc_tests/run_tests.sh
index 8028b3477..c1e532e94 100755
--- a/integration_tests/grpc_tests/run_tests.sh
+++ b/integration_tests/grpc_tests/run_tests.sh
@@ -21,7 +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}"
+TEST_CLASSES="${1:-PythonGrpcInteropTest,RustGrpcInteropTest,GoGrpcInteropTest,KotlinGrpcInteropTest}"
python -m pip install "grpcio>=1.62.2,<1.71"
python -m pip install -v -e "${ROOT_DIR}/python"
@@ -31,6 +31,8 @@ python "${SCRIPT_DIR}/generate_grpc.py"
cd "${SCRIPT_DIR}/go"
go build -o grpc-interop .
cargo build --manifest-path "${SCRIPT_DIR}/rust/Cargo.toml" --workspace --quiet
+cd "${SCRIPT_DIR}/kotlin"
+mvn --no-transfer-progress -DskipTests package
cd "${ROOT_DIR}/integration_tests/grpc_tests/java"
mvn -T16 --no-transfer-progress \
-Dtest="${TEST_CLASSES}" \
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]