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]

Reply via email to