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 0ebf1bab4 feat(compiler): add dart gRPC codegen (#3723)
0ebf1bab4 is described below

commit 0ebf1bab48180686f61e5e667b671c23c8090890
Author: Yash Agarwal <[email protected]>
AuthorDate: Sun Jun 21 18:23:49 2026 +0530

    feat(compiler): add dart gRPC codegen (#3723)
    
    ## Why?
    
    Dart users need gRPC support from FDL/proto/fbs schemas
    
    ## What does this PR do?
    
    Adds DartServiceGeneratorMixin so foryc --grpc --dart_out=… emits a
    <package>/<stem>_grpc.dart next to the messages file. Includes:
    
    - <Service>Client over package:grpc's Client with one method per RPC,
    covering unary and all three streaming modes (server-stream,
    client-stream, bidi).
    - abstract <Service>ServiceBase over Service with $addMethod
    registrations and a _Pre shim per method.
    - Top-level _serialize / _deserialize helpers routing through the schema
    module's getFory(), with reference tracking on so payloads round-trip
    with the Java/Python peers.
    - Works from FDL, Protobuf, and FlatBuffers IDL service definitions.
    - Class- and method-name collision detection.
    - Opt-in dart analyze + dart format smoke test on the emitted file.
    - A Java↔Dart gRPC interop test exercising all four modes in both
    directions.
    
    ## Related issues
    Part of #3266
    Part of #3279
    Closes #3266
    
    ## 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?
    
    - [x] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    - New _grpc.dart companion when --grpc --dart_out is used on a schema
    with services.
    
    ## Benchmark
    
    ---------
    
    Co-authored-by: chaokunyang <[email protected]>
---
 .agents/languages/dart.md                          |   2 +
 .github/workflows/ci.yml                           |  49 ++
 compiler/fory_compiler/generators/dart.py          |  11 +-
 compiler/fory_compiler/generators/services/dart.py | 390 +++++++++++++
 .../fory_compiler/tests/test_service_codegen.py    | 634 +++++++++++++++++++++
 docs/compiler/compiler-guide.md                    |  14 +-
 docs/compiler/flatbuffers-idl.md                   |  17 +-
 docs/compiler/generated-code.md                    |  41 ++
 docs/compiler/index.md                             |  18 +-
 docs/compiler/protobuf-idl.md                      |  41 +-
 docs/compiler/schema-idl.md                        |   8 +-
 docs/guide/dart/grpc-support.md                    | 292 ++++++++++
 docs/guide/dart/index.md                           |   1 +
 docs/guide/dart/troubleshooting.md                 |  17 +
 integration_tests/grpc_tests/dart/.gitignore       |   4 +
 integration_tests/grpc_tests/dart/bin/interop.dart | 520 +++++++++++++++++
 .../grpc_tests/{run_tests.sh => dart/pubspec.yaml} |  31 +-
 integration_tests/grpc_tests/generate_grpc.py      |   2 +
 .../{RustGrpcTest.java => DartGrpcTest.java}       |  29 +-
 .../org/apache/fory/grpc_tests/GoGrpcTest.java     |  16 +-
 .../org/apache/fory/grpc_tests/GrpcTestBase.java   | 197 +------
 .../org/apache/fory/grpc_tests/KotlinGrpcTest.java |  22 +-
 .../fory/grpc_tests/PythonAsyncGrpcTest.java       |  38 +-
 .../apache/fory/grpc_tests/PythonSyncGrpcTest.java |  41 +-
 .../org/apache/fory/grpc_tests/RustGrpcTest.java   |  26 +-
 integration_tests/grpc_tests/run_tests.sh          |  35 +-
 26 files changed, 2228 insertions(+), 268 deletions(-)

diff --git a/.agents/languages/dart.md b/.agents/languages/dart.md
index f257ece60..d4f6bebbb 100644
--- a/.agents/languages/dart.md
+++ b/.agents/languages/dart.md
@@ -26,6 +26,8 @@ Load this file when changing `dart/`.
 - Generated struct serializers should use serializer-owned field descriptors 
for runtime resolver decisions and emit direct field-specific write/read code 
for static schemas. Do not route generated hot writes through generic 
field-info value helpers such as `writeGeneratedStructFieldInfoValue`.
 - Dart xlang or runtime ownership changes need local Dart package tests plus 
the Java-driven `DartXlangTest`; package-only smoke tests are not enough.
 - When claiming non-VM Dart support, prove a relevant non-VM compile path such 
as `dart compile js` against active runtime or example code.
+- Generated Dart gRPC service companions (`<stem>_grpc.dart`) are 
compiler-owned files that depend on the application-provided `grpc` package, 
not `dart/packages/fory`. Keep gRPC dependencies out of the Fory Dart runtime 
package.
+- Dart generated schema modules (`<Stem>ForyModule`) are the source-file 
owners and own a ready `Fory` runtime: `getFory()` initializes a default 
runtime and registers the schema's types on first use, so generated gRPC 
companions never require a manual `install(...)`; `install(customFory)` stays 
optional injection. Keep `getFory()` ready by construction, and do not 
introduce package-derived aliases or duplicate serializer registration paths.
 
 ## Commands
 
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1a9ee4dd5..d618e65cc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -855,6 +855,55 @@ jobs:
           cd integration_tests/grpc_tests/java
           mvn -T16 --no-transfer-progress -Dtest=KotlinGrpcTest test
 
+  grpc_java_dart_tests:
+    name: Java/Dart gRPC Tests
+    needs: changes
+    if: needs.changes.outputs.grpc_tests == 'true'
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v5
+      - name: Set up JDK 21
+        uses: actions/setup-java@v4
+        with:
+          java-version: 21
+          distribution: "temurin"
+      - name: Set up Python 3.11
+        uses: actions/setup-python@v5
+        with:
+          python-version: 3.11
+          cache: "pip"
+      - name: Set up Dart
+        uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c  # 
v1.7.1
+        with:
+          sdk: stable
+      - name: Cache Maven local repository
+        uses: actions/cache@v4
+        with:
+          path: ~/.m2/repository
+          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+          restore-keys: |
+            ${{ runner.os }}-maven-
+      - name: Install Java artifacts for gRPC tests
+        run: |
+          cd java
+          mvn -T16 --no-transfer-progress clean install -DskipTests 
-Dmaven.javadoc.skip=true -Dmaven.source.skip=true
+      - name: Generate gRPC test sources
+        run: python integration_tests/grpc_tests/generate_grpc.py
+      - name: Build Dart gRPC peer
+        run: |
+          cd integration_tests/grpc_tests/dart
+          dart pub get
+          dart run build_runner build
+      - name: Analyze and format-check generated Dart gRPC companions
+        run: |
+          cd integration_tests/grpc_tests/dart
+          dart analyze bin lib/generated/*/*_grpc.dart
+          dart format --output=none --set-exit-if-changed bin 
lib/generated/*/*_grpc.dart
+      - name: Run Java/Dart gRPC Tests
+        run: |
+          cd integration_tests/grpc_tests/java
+          mvn -T16 --no-transfer-progress -Dtest=DartGrpcTest test
+
   javascript:
     name: JavaScript CI
     needs: changes
diff --git a/compiler/fory_compiler/generators/dart.py 
b/compiler/fory_compiler/generators/dart.py
index 557cd19b9..620dc28e7 100644
--- a/compiler/fory_compiler/generators/dart.py
+++ b/compiler/fory_compiler/generators/dart.py
@@ -23,6 +23,7 @@ from typing import Dict, List, Optional, Set, Tuple
 
 from fory_compiler.frontend.utils import parse_idl_file
 from fory_compiler.generators.base import BaseGenerator, GeneratedFile
+from fory_compiler.generators.services.dart import DartServiceGeneratorMixin
 from fory_compiler.ir.ast import (
     ArrayType,
     Enum,
@@ -40,7 +41,7 @@ from fory_compiler.ir.ast import (
 from fory_compiler.ir.types import PrimitiveKind
 
 
-class DartGenerator(BaseGenerator):
+class DartGenerator(DartServiceGeneratorMixin, BaseGenerator):
     language_name = "dart"
     file_extension = ".dart"
 
@@ -245,6 +246,8 @@ class DartGenerator(BaseGenerator):
         names: Set[str] = set()
         for defs in (self.schema.enums, self.schema.unions, 
self.schema.messages):
             for item in defs:
+                if self.is_imported_type(item):
+                    continue
                 
names.add(self.safe_type_identifier(self.to_pascal_case(item.name)))
         return names
 
@@ -1291,8 +1294,10 @@ class DartGenerator(BaseGenerator):
         lines.extend(
             [
                 f"{self.indent_str * (indent + 1)}static Fory getFory() {{",
-                f"{self.indent_str * (indent + 2)}final fory = _fory;",
-                f"{self.indent_str * (indent + 2)}if (fory == null) throw 
StateError('Call {self.module_type_name()}.install(...) before using generated 
helpers.');",
+                f"{self.indent_str * (indent + 2)}final existing = _fory;",
+                f"{self.indent_str * (indent + 2)}if (existing != null) return 
existing;",
+                f"{self.indent_str * (indent + 2)}final fory = Fory();",
+                f"{self.indent_str * (indent + 2)}install(fory);",
                 f"{self.indent_str * (indent + 2)}return fory;",
                 f"{self.indent_str * (indent + 1)}}}",
                 "",
diff --git a/compiler/fory_compiler/generators/services/dart.py 
b/compiler/fory_compiler/generators/services/dart.py
new file mode 100644
index 000000000..e3da08d0f
--- /dev/null
+++ b/compiler/fory_compiler/generators/services/dart.py
@@ -0,0 +1,390 @@
+# 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.
+
+"""Dart gRPC service generator helpers."""
+
+from pathlib import Path
+from typing import Dict, List, Set, Tuple
+
+from fory_compiler.generators.base import GeneratedFile
+from fory_compiler.ir.ast import RpcMethod, Service
+
+
+class DartServiceGeneratorMixin:
+    """Generates Dart gRPC service companions for all four RPC modes."""
+
+    _DART_RESERVED_METHOD_NAMES = frozenset(
+        {"toString", "hashCode", "noSuchMethod", "runtimeType"}
+    )
+    _DART_GRPC_SUPPORT_NAMES = frozenset(
+        {
+            "CallOptions",
+            "Client",
+            "ClientMethod",
+            "Future",
+            "List",
+            "ResponseFuture",
+            "ResponseStream",
+            "Service",
+            "ServiceCall",
+            "ServiceMethod",
+            "Stream",
+            "String",
+            "T",
+            "Uint8List",
+            "bytes",
+            "call",
+            "int",
+            "options",
+            "request",
+            "u8",
+            "value",
+        }
+    )
+
+    def generate_services(self) -> List[GeneratedFile]:
+        local_services = [
+            service
+            for service in self.schema.services
+            if not self.is_imported_type(service)
+        ]
+        if not local_services:
+            return []
+        self.check_dart_grpc_service_collisions(local_services)
+        self.check_dart_grpc_method_collisions(local_services)
+        self.check_dart_grpc_reserved_method_names(local_services)
+        return [self.generate_grpc_module(local_services)]
+
+    def check_dart_grpc_service_collisions(self, services: List[Service]) -> 
None:
+        generated_names = set(self._top_level_names())
+        service_names = set()
+        for service in services:
+            for emitted in (f"{service.name}Client", 
f"{service.name}ServiceBase"):
+                if emitted in generated_names or emitted in service_names:
+                    raise ValueError(
+                        f"Dart gRPC class {emitted} conflicts with a generated 
"
+                        "type or another service; rename the service or type"
+                    )
+                service_names.add(emitted)
+
+    def check_dart_grpc_method_collisions(self, services: List[Service]) -> 
None:
+        for service in services:
+            seen = {}
+            for method in service.methods:
+                emitted = self.dart_grpc_method_name(method)
+                if emitted in seen:
+                    raise ValueError(
+                        f"Dart gRPC method name collision in service 
{service.name}: "
+                        f"{seen[emitted]} and {method.name} both generate 
{emitted}"
+                    )
+                seen[emitted] = method.name
+
+    def check_dart_grpc_reserved_method_names(self, services: List[Service]) 
-> None:
+        offenders = []
+        for service in services:
+            for method in service.methods:
+                emitted = self.dart_grpc_method_name(method)
+                if emitted in self._DART_RESERVED_METHOD_NAMES:
+                    offenders.append(f"{service.name}.{method.name} -> 
{emitted}")
+        if offenders:
+            joined = "\n  - " + "\n  - ".join(offenders)
+            raise ValueError(
+                "Dart gRPC method name collides with an inherited Dart member "
+                "(Object/Client/Service) and would produce an invalid 
override; "
+                "rename the RPC method:" + joined
+            )
+
+    def generate_grpc_module(self, services: List[Service]) -> GeneratedFile:
+        """Emit a grpc-dart companion module for schema services."""
+        models_output = Path(
+            self.output_file_path()
+        )  # e.g. "demo/greeter/greeter.dart"
+        models_stem = models_output.stem  # e.g. "greeter"
+        grpc_path = str(models_output.with_name(f"{models_stem}_grpc.dart"))
+
+        self._grpc_model_alias = "_models"
+        self._grpc_payload_imports: Dict[str, Tuple[str, str]] = {}
+        self._grpc_used_import_aliases = 
self._dart_grpc_reserved_import_aliases(
+            services
+        )
+
+        body: List[str] = []
+        for service in services:
+            body.extend(self.generate_dart_grpc_client(service))
+            body.append("")
+            body.extend(self.generate_dart_grpc_service_base(service))
+            body.append("")
+
+        lines: List[str] = []
+        lines.append(self.get_license_header("//"))
+        lines.append("")
+        lines.append(
+            "// ignore_for_file: camel_case_types, constant_identifier_names, "
+            "non_constant_identifier_names"
+        )
+        lines.append("")
+        lines.append("import 'dart:async';")
+        lines.append("import 'dart:typed_data';")
+        lines.append("")
+        lines.append("import 'package:grpc/grpc.dart';")
+        lines.append("")
+        lines.append(f"import '{models_stem}.dart' as 
{self._grpc_model_alias};")
+        for path, alias in sorted(self._grpc_payload_imports.values()):
+            lines.append(f"import '{path}' as {alias};")
+        lines.append("")
+        lines.append(
+            "// grpc-dart Service self-registers via $methods; "
+            "no separate registration helper needed."
+        )
+        lines.append("")
+        fory = f"{self._grpc_model_alias}.{self.module_type_name()}.getFory()"
+        lines.append("List<int> _serialize<T>(T value) =>")
+        lines.append(f"    {fory}.serialize(value, trackRef: true);")
+        lines.append("")
+        lines.append("T _deserialize<T>(List<int> bytes) {")
+        lines.append(
+            "  final u8 = bytes is Uint8List ? bytes : 
Uint8List.fromList(bytes);"
+        )
+        lines.append(f"  return {fory}.deserialize<T>(u8);")
+        lines.append("}")
+        lines.append("")
+        lines.extend(body)
+
+        return GeneratedFile(path=grpc_path, content="\n".join(lines))
+
+    def _dart_grpc_type_ref(self, named_type) -> str:
+        """Return the Dart reference for an RPC request/response type.
+
+        Resolves through the Dart generator's type machinery so nested types
+        use their flattened symbol (`Envelope.Request` -> `Envelope_Request`)
+        and imported types use an alias-qualified reference plus an emitted
+        import, instead of the raw IDL name.
+        """
+        type_def = self.resolve_type(named_type.name)
+        if type_def is None:
+            return f"{self._grpc_model_alias}.{named_type.name}"
+        if self.is_imported_type(type_def):
+            alias = self._dart_grpc_import_alias(type_def)
+            return f"{alias}.{self.local_name(type_def)}"
+        return f"{self._grpc_model_alias}.{self.local_name(type_def)}"
+
+    def _dart_grpc_import_alias(self, type_def) -> str:
+        file = type_def.location.file
+        existing = self._grpc_payload_imports.get(file)
+        if existing is not None:
+            return existing[1]
+        schema = self._load_schema(file)
+        if schema is None:
+            return self.ref_name(type_def)
+        candidate = self.safe_identifier(
+            schema.package.replace(".", "_") if schema.package else 
Path(file).stem
+        )
+        alias = self._dart_grpc_unique_import_alias(candidate)
+        self._grpc_payload_imports[file] = 
(self._relative_import_path(schema), alias)
+        return alias
+
+    def _dart_grpc_unique_import_alias(self, candidate: str) -> str:
+        alias = candidate
+        index = 2
+        while alias in self._grpc_used_import_aliases:
+            alias = f"{candidate}_{index}"
+            index += 1
+        self._grpc_used_import_aliases.add(alias)
+        return alias
+
+    def _dart_grpc_reserved_import_aliases(self, services: List[Service]) -> 
Set[str]:
+        aliases = set(self._DART_GRPC_SUPPORT_NAMES)
+        aliases.update({self._grpc_model_alias, "_serialize", "_deserialize"})
+        for service in services:
+            aliases.add(f"{service.name}Client")
+            aliases.add(f"{service.name}ServiceBase")
+            for method in service.methods:
+                method_name = self.dart_grpc_method_name(method)
+                aliases.add(method_name)
+                aliases.add(f"{method_name}_Pre")
+        return aliases
+
+    def generate_dart_grpc_client(self, service: Service) -> List[str]:
+        lines: List[str] = []
+        lines.append(f"class {service.name}Client extends Client {{")
+        for method in service.methods:
+            method_const = f"_${self.dart_grpc_method_name(method)}"
+            req_t = self._dart_grpc_type_ref(method.request_type)
+            res_t = self._dart_grpc_type_ref(method.response_type)
+            full_path = self.get_grpc_method_path(service, method)
+            lines.append(f"  static final {method_const} =")
+            lines.append(f"      ClientMethod<{req_t}, {res_t}>(")
+            lines.append(f"        '{full_path}',")
+            lines.append("        _serialize,")
+            lines.append("        _deserialize,")
+            lines.append("      );")
+            lines.append("")
+        lines.append(
+            f"  {service.name}Client(super.channel, "
+            "{super.options, super.interceptors});"
+        )
+        for method in service.methods:
+            lines.append("")
+            lines.extend(self._dart_grpc_client_method(method))
+        lines.append("}")
+        return lines
+
+    def _dart_grpc_client_method(self, method: RpcMethod) -> List[str]:
+        streaming_request, streaming_response = 
self._dart_grpc_call_kind(method)
+        method_const = f"_${self.dart_grpc_method_name(method)}"
+        req_t = self._dart_grpc_type_ref(method.request_type)
+        res_t = self._dart_grpc_type_ref(method.response_type)
+        method_name = self.dart_grpc_method_name(method)
+
+        return_type = (
+            f"ResponseStream<{res_t}>"
+            if streaming_response
+            else (f"ResponseFuture<{res_t}>")
+        )
+        request_param = (
+            f"Stream<{req_t}> request" if streaming_request else (f"{req_t} 
request")
+        )
+
+        if not streaming_request and not streaming_response:
+            call_fn = "$createUnaryCall"
+            request_arg = "request"
+            single = ""
+        else:
+            call_fn = "$createStreamingCall"
+            request_arg = "request" if streaming_request else 
"Stream.value(request)"
+            # client-stream returns a single response; ResponseStream.single
+            # adapts the streaming call to ResponseFuture<R>.
+            single = "" if streaming_response else ".single"
+
+        lines: List[str] = []
+        lines.append(f"  {return_type} {method_name}(")
+        lines.append(f"    {request_param}, {{")
+        lines.append("    CallOptions? options,")
+        lines.append("  }) {")
+        lines.extend(
+            self._dart_grpc_call_body(call_fn, method_const, request_arg, 
single)
+        )
+        lines.append("  }")
+        return lines
+
+    def _dart_grpc_call_body(
+        self, call_fn: str, method_const: str, request_arg: str, single: str
+    ) -> List[str]:
+        """Emit `return <call>;`, wrapping when dart format would.
+
+        dart format keeps the call on one line when it fits in 80 columns and
+        otherwise wraps each argument onto its own line with a trailing comma.
+        Matching that here keeps the emitted file format-clean.
+        """
+        single_line = (
+            f"    return {call_fn}({method_const}, {request_arg}, "
+            f"options: options){single};"
+        )
+        if len(single_line) <= 80:
+            return [single_line]
+        return [
+            f"    return {call_fn}(",
+            f"      {method_const},",
+            f"      {request_arg},",
+            "      options: options,",
+            f"    ){single};",
+        ]
+
+    def _dart_grpc_call_kind(self, method: RpcMethod):
+        """Return (streaming_request, streaming_response) for an RPC method."""
+        return bool(method.client_streaming), bool(method.server_streaming)
+
+    def generate_dart_grpc_service_base(self, service: Service) -> List[str]:
+        lines: List[str] = []
+        lines.append(f"abstract class {service.name}ServiceBase extends 
Service {{")
+        lines.append("  @override")
+        lines.append(f"  String get $name => 
'{self.get_grpc_service_name(service)}';")
+        lines.append("")
+        lines.append(f"  {service.name}ServiceBase() {{")
+        for method in service.methods:
+            streaming_request, streaming_response = 
self._dart_grpc_call_kind(method)
+            req_t = self._dart_grpc_type_ref(method.request_type)
+            res_t = self._dart_grpc_type_ref(method.response_type)
+            method_name = self.dart_grpc_method_name(method)
+            lines.append("    $addMethod(")
+            lines.append(f"      ServiceMethod<{req_t}, {res_t}>(")
+            lines.append(f"        '{method.name}',")
+            lines.append(f"        {method_name}_Pre,")
+            lines.append(f"        {str(streaming_request).lower()},")
+            lines.append(f"        {str(streaming_response).lower()},")
+            lines.append(f"        (List<int> value) => 
_deserialize<{req_t}>(value),")
+            lines.append(f"        ({res_t} value) => _serialize(value),")
+            lines.append("      ),")
+            lines.append("    );")
+        lines.append("  }")
+        lines.append("")
+        for idx, method in enumerate(service.methods):
+            lines.extend(self._dart_grpc_service_method(method))
+            if idx != len(service.methods) - 1:
+                lines.append("")
+        lines.append("}")
+        return lines
+
+    def _dart_grpc_service_method(self, method: RpcMethod) -> List[str]:
+        streaming_request, streaming_response = 
self._dart_grpc_call_kind(method)
+        req_t = self._dart_grpc_type_ref(method.request_type)
+        res_t = self._dart_grpc_type_ref(method.response_type)
+        method_name = self.dart_grpc_method_name(method)
+
+        # grpc-dart hands the handler a Future<Q> for a single request and a
+        # Stream<Q> for a client-streaming request, and consumes the handler's
+        # return value directly as the response (a Future<R> for a single
+        # response, a Stream<R> for a streaming response). The _Pre shim adapts
+        # that handler signature to the user-overridable method:
+        #   - single request  -> await $request before delegating
+        #   - stream request   -> pass $request straight through
+        #   - stream response  -> the shim is async* and yield*s the delegate
+        user_return = f"Stream<{res_t}>" if streaming_response else 
f"Future<{res_t}>"
+        user_param = f"Stream<{req_t}>" if streaming_request else req_t
+        shim_param = f"Stream<{req_t}>" if streaming_request else 
f"Future<{req_t}>"
+        request_arg = "$request" if streaming_request else "await $request"
+
+        lines: List[str] = []
+        lines.append(
+            "  // protoc_plugin parity: _Pre shim adapts the grpc-dart handler"
+        )
+        lines.append("  // signature to the user-overridable method.")
+        lines.append(f"  {user_return} {method_name}_Pre(")
+        lines.append("    ServiceCall $call,")
+        lines.append(f"    {shim_param} $request,")
+        if streaming_response and not streaming_request:
+            # server-stream: must await the single request, then stream 
results.
+            lines.append("  ) async* {")
+            lines.append(f"    yield* {method_name}($call, {request_arg});")
+        elif not streaming_request:
+            # unary: await the single request, return the single response.
+            lines.append("  ) async {")
+            lines.append(f"    return {method_name}($call, {request_arg});")
+        else:
+            # client-stream / bidi: pass the request stream straight through.
+            lines.append("  ) {")
+            lines.append(f"    return {method_name}($call, {request_arg});")
+        lines.append("  }")
+        lines.append("")
+        lines.append(f"  {user_return} {method_name}(")
+        lines.append("    ServiceCall call,")
+        lines.append(f"    {user_param} request,")
+        lines.append("  );")
+        return lines
+
+    def dart_grpc_method_name(self, method: RpcMethod) -> str:
+        return self.safe_identifier(self.to_camel_case(method.name))
diff --git a/compiler/fory_compiler/tests/test_service_codegen.py 
b/compiler/fory_compiler/tests/test_service_codegen.py
index b7d2d2a97..2d63b2ece 100644
--- a/compiler/fory_compiler/tests/test_service_codegen.py
+++ b/compiler/fory_compiler/tests/test_service_codegen.py
@@ -44,6 +44,7 @@ from fory_compiler.frontend.proto.translator import 
ProtoTranslator
 from fory_compiler.generators.base import BaseGenerator, GeneratorOptions
 from fory_compiler.generators.cpp import CppGenerator
 from fory_compiler.generators.csharp import CSharpGenerator
+from fory_compiler.generators.dart import DartGenerator
 from fory_compiler.generators.go import GoGenerator
 from fory_compiler.generators.java import JavaGenerator
 from fory_compiler.generators.javascript import JavaScriptGenerator
@@ -67,6 +68,7 @@ GENERATOR_CLASSES: Tuple[Type[BaseGenerator], ...] = (
     SwiftGenerator,
     ScalaGenerator,
     KotlinGenerator,
+    DartGenerator,
 )
 
 _GREETER_WITH_SERVICE = dedent(
@@ -155,6 +157,7 @@ def test_unsupported_generators_no_services():
             ScalaGenerator,
             KotlinGenerator,
             JavaScriptGenerator,
+            DartGenerator,
         ):
             continue
         options = GeneratorOptions(output_dir=Path("/tmp"))
@@ -2968,3 +2971,634 @@ def test_rust_grpc_rejects_unsafe_refs():
         )
         with pytest.raises(ValueError, match=message):
             generator.generate_services()
+
+
+def test_dart_grpc_streaming_shapes():
+    from fory_compiler.generators.dart import DartGenerator
+
+    schema = parse_fdl(
+        dedent(
+            """
+            package demo.streams;
+
+            message Req {}
+            message Res {}
+
+            service Streamer {
+                rpc UnaryMessage (Req) returns (Res);
+                rpc ServerStreamMessage (Req) returns (stream Res);
+                rpc ClientStreamMessage (stream Req) returns (Res);
+                rpc BidiStreamMessage (stream Req) returns (stream Res);
+            }
+            """
+        )
+    )
+
+    content = generate_service_files(schema, DartGenerator)[
+        "demo/streams/demo_streams_grpc.dart"
+    ]
+
+    assert "ResponseFuture<_models.Res> unaryMessage(" in content
+    assert "$createUnaryCall(_$unaryMessage, request, options: options);" in 
content
+
+    assert "ResponseStream<_models.Res> serverStreamMessage(" in content
+    assert (
+        "    return $createStreamingCall(\n"
+        "      _$serverStreamMessage,\n"
+        "      Stream.value(request),\n"
+        "      options: options,\n"
+        "    );"
+    ) in content
+
+    assert "ResponseFuture<_models.Res> clientStreamMessage(" in content
+    assert "Stream<_models.Req> request, {" in content
+    assert (
+        "    return $createStreamingCall(\n"
+        "      _$clientStreamMessage,\n"
+        "      request,\n"
+        "      options: options,\n"
+        "    ).single;"
+    ) in content
+
+    assert "ResponseStream<_models.Res> bidiStreamMessage(" in content
+    assert (
+        "$createStreamingCall(_$bidiStreamMessage, request, options: options);"
+    ) in content
+
+    assert (
+        "'UnaryMessage',\n        unaryMessage_Pre,\n        false,\n        
false,"
+    ) in content
+    assert (
+        "'ServerStreamMessage',\n        serverStreamMessage_Pre,\n"
+        "        false,\n        true,"
+    ) in content
+    assert (
+        "'ClientStreamMessage',\n        clientStreamMessage_Pre,\n"
+        "        true,\n        false,"
+    ) in content
+    assert (
+        "'BidiStreamMessage',\n        bidiStreamMessage_Pre,\n"
+        "        true,\n        true,"
+    ) in content
+
+    assert "Future<_models.Res> unaryMessage_Pre(" in content
+    assert "Stream<_models.Res> serverStreamMessage_Pre(" in content
+    assert (
+        "  ) async* {\n    yield* serverStreamMessage($call, await $request);"
+    ) in content
+    assert "Future<_models.Res> clientStreamMessage_Pre(" in content
+    assert "  ) {\n    return clientStreamMessage($call, $request);" in content
+    assert "Stream<_models.Res> bidiStreamMessage_Pre(" in content
+
+    assert (
+        "Future<_models.Res> unaryMessage(\n    ServiceCall call,\n"
+        "    _models.Req request,\n  );"
+    ) in content
+    assert (
+        "Stream<_models.Res> serverStreamMessage(\n    ServiceCall call,\n"
+        "    _models.Req request,\n  );"
+    ) in content
+    assert (
+        "Future<_models.Res> clientStreamMessage(\n    ServiceCall call,\n"
+        "    Stream<_models.Req> request,\n  );"
+    ) in content
+    assert (
+        "Stream<_models.Res> bidiStreamMessage(\n    ServiceCall call,\n"
+        "    Stream<_models.Req> request,\n  );"
+    ) in content
+
+
+def test_dart_grpc_fory_codec():
+    from fory_compiler.generators.dart import DartGenerator
+
+    schema = parse_fdl(_GREETER_WITH_SERVICE)
+    files = generate_service_files(schema, DartGenerator)
+    assert set(files) == {"demo/greeter/demo_greeter_grpc.dart"}
+    content = files["demo/greeter/demo_greeter_grpc.dart"]
+
+    assert "This file is generated by Apache Fory compiler." in content
+    assert "library;" not in content
+
+    assert "import 'dart:typed_data';" in content
+    assert "import 'package:grpc/grpc.dart';" in content
+    assert "import 'demo_greeter.dart' as _models;" in content
+
+    assert (
+        "_models.DemoGreeterForyModule.getFory().serialize(value, trackRef: 
true)"
+        in content
+    )
+    assert "_models.DemoGreeterForyModule.getFory().deserialize<T>" in content
+    assert "is Uint8List ? bytes : Uint8List.fromList(bytes)" in content
+
+    assert "class GreeterClient extends Client {" in content
+    assert "static final _$sayHello =" in content
+    assert "ClientMethod<_models.HelloRequest, _models.HelloReply>(" in content
+    assert "'/demo.greeter.Greeter/SayHello'," in content
+    assert (
+        "GreeterClient(super.channel, {super.options, super.interceptors});" 
in content
+    )
+    assert "ResponseFuture<_models.HelloReply> sayHello(" in content
+    assert "_models.HelloRequest request, {" in content
+    assert "$createUnaryCall(_$sayHello, request, options: options);" in 
content
+
+    assert "abstract class GreeterServiceBase extends Service {" in content
+    assert "String get $name => 'demo.greeter.Greeter';" in content
+    assert "GreeterServiceBase() {" in content
+    assert "$addMethod(" in content
+    assert ("ServiceMethod<_models.HelloRequest, _models.HelloReply>(") in 
content
+    assert "'SayHello'," in content
+    assert "sayHello_Pre," in content
+    assert "Future<_models.HelloReply> sayHello_Pre(" in content
+    assert "Future<_models.HelloReply> sayHello(" in content
+    assert "ServiceCall call," in content
+
+
+def test_dart_grpc_service_class_collision():
+    from fory_compiler.generators.dart import DartGenerator
+
+    schema = parse_fdl(
+        dedent(
+            """
+            package demo.collide;
+
+            message GreeterClient {
+                string name = 1;
+            }
+
+            message HelloRequest {}
+            message HelloReply {}
+
+            service Greeter {
+                rpc SayHello (HelloRequest) returns (HelloReply);
+            }
+            """
+        )
+    )
+
+    import pytest
+
+    with pytest.raises(ValueError) as excinfo:
+        generate_service_files(schema, DartGenerator)
+    msg = str(excinfo.value)
+    assert "GreeterClient" in msg
+    assert "conflicts" in msg
+
+
+def test_dart_grpc_method_collision():
+    from fory_compiler.generators.dart import DartGenerator
+
+    schema = parse_fdl(
+        dedent(
+            """
+            package demo.dupes;
+
+            message Req {}
+            message Res {}
+
+            service Greeter {
+                rpc SayHello   (Req) returns (Res);
+                rpc say_hello  (Req) returns (Res);
+            }
+            """
+        )
+    )
+
+    import pytest
+
+    with pytest.raises(ValueError) as excinfo:
+        generate_service_files(schema, DartGenerator)
+    msg = str(excinfo.value)
+    assert "Greeter" in msg
+    assert "SayHello" in msg
+    assert "say_hello" in msg
+    assert "sayHello" in msg
+
+
+def test_dart_grpc_proto_fbs():
+    from fory_compiler.generators.dart import DartGenerator
+
+    proto_schema = parse_proto(
+        dedent(
+            """
+            syntax = "proto3";
+            package demo.proto;
+
+            message Req {}
+            message Res {}
+
+            service ProtoSvc {
+                rpc Call (Req) returns (Res);
+            }
+            """
+        )
+    )
+    proto_dart = generate_service_files(proto_schema, DartGenerator)
+    assert "demo/proto/demo_proto_grpc.dart" in proto_dart
+    assert (
+        "'/demo.proto.ProtoSvc/Call'," in 
proto_dart["demo/proto/demo_proto_grpc.dart"]
+    )
+    assert (
+        "class ProtoSvcClient extends Client {"
+        in proto_dart["demo/proto/demo_proto_grpc.dart"]
+    )
+
+    fbs_schema = parse_fbs(
+        dedent(
+            """
+            namespace demo.fbs;
+
+            table Req {}
+            table Res {}
+
+            rpc_service FbsSvc {
+                Call(Req):Res;
+            }
+            """
+        )
+    )
+    fbs_dart = generate_service_files(fbs_schema, DartGenerator)
+    assert "demo/fbs/demo_fbs_grpc.dart" in fbs_dart
+    assert "'/demo.fbs.FbsSvc/Call'," in 
fbs_dart["demo/fbs/demo_fbs_grpc.dart"]
+    assert (
+        "class FbsSvcClient extends Client {" in 
fbs_dart["demo/fbs/demo_fbs_grpc.dart"]
+    )
+
+
+def test_dart_grpc_nested_rpc_payloads():
+    from fory_compiler.generators.dart import DartGenerator
+
+    schema = parse_fdl(
+        dedent(
+            """
+            package demo.nested;
+
+            message Envelope {
+                message Request {
+                    string name = 1;
+                }
+                message Reply {
+                    string name = 1;
+                }
+            }
+
+            service Nested {
+                rpc Call (Envelope.Request) returns (Envelope.Reply);
+            }
+            """
+        )
+    )
+    content = generate_service_files(schema, DartGenerator)[
+        "demo/nested/demo_nested_grpc.dart"
+    ]
+    assert "ClientMethod<_models.Envelope_Request, _models.Envelope_Reply>(" 
in content
+    assert "Future<_models.Envelope_Reply> call(" in content
+    assert "_models.Envelope.Request" not in content
+
+
+def test_dart_grpc_imported_rpc_payloads(tmp_path: Path):
+    from fory_compiler.generators.dart import DartGenerator
+
+    common = tmp_path / "common.fdl"
+    common.write_text(
+        dedent(
+            """
+            package demo.common;
+
+            message Shared {
+                string id = 1;
+            }
+            """
+        )
+    )
+    service = tmp_path / "service.fdl"
+    service.write_text(
+        dedent(
+            """
+            package demo.api;
+
+            import "common.fdl";
+
+            service Api {
+                rpc Call (demo.common.Shared) returns (demo.common.Shared);
+            }
+            """
+        )
+    )
+    schema = resolve_imports(service)
+    generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path, 
grpc=True))
+    content = generator.generate_services()[0].content
+
+    assert "import '../common/common.dart' as demo_common;" in content
+    assert "ClientMethod<demo_common.Shared, demo_common.Shared>(" in content
+    assert "_models.demo.common.Shared" not in content
+
+
+def test_dart_imported_model_collision(tmp_path: Path):
+    from fory_compiler.generators.dart import DartGenerator
+
+    common = tmp_path / "common.fdl"
+    common.write_text(
+        dedent(
+            """
+            package demo.common;
+
+            message GreeterClient {
+                string id = 1;
+            }
+            """
+        )
+    )
+    service = tmp_path / "service.fdl"
+    service.write_text(
+        dedent(
+            """
+            package demo.api;
+
+            import "common.fdl";
+
+            message Req {}
+            message Res {}
+
+            service Greeter {
+                rpc Call (Req) returns (Res);
+            }
+            """
+        )
+    )
+    schema = resolve_imports(service)
+    generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path, 
grpc=True))
+    content = generator.generate_services()[0].content
+
+    assert "class GreeterClient extends Client {" in content
+    assert "ClientMethod<_models.Req, _models.Res>(" in content
+
+
+def test_dart_grpc_import_alias_collision(tmp_path: Path):
+    from fory_compiler.generators.dart import DartGenerator
+
+    common = tmp_path / "common.fdl"
+    common.write_text(
+        dedent(
+            """
+            package _models;
+
+            message Req {
+                string id = 1;
+            }
+            """
+        )
+    )
+    service = tmp_path / "service.fdl"
+    service.write_text(
+        dedent(
+            """
+            package demo.api;
+
+            import "common.fdl";
+
+            message Req {}
+            message Res {}
+
+            service Api {
+                rpc Call (_models.Req) returns (Res);
+            }
+            """
+        )
+    )
+    schema = resolve_imports(service)
+    generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path, 
grpc=True))
+    content = generator.generate_services()[0].content
+
+    assert "import 'api.dart' as _models;" in content
+    assert "import '../../_models/_models.dart' as _models_2;" in content
+    assert "ClientMethod<_models_2.Req, _models.Res>(" in content
+    assert "ServiceMethod<_models_2.Req, _models.Res>(" in content
+
+
+def test_dart_grpc_helper_alias(tmp_path: Path):
+    from fory_compiler.generators.dart import DartGenerator
+
+    common = tmp_path / "common.fdl"
+    common.write_text(
+        dedent(
+            """
+            package _serialize;
+
+            message Shared {
+                string id = 1;
+            }
+            """
+        )
+    )
+    service = tmp_path / "service.fdl"
+    service.write_text(
+        dedent(
+            """
+            package demo.api;
+
+            import "common.fdl";
+
+            message Res {}
+
+            service Api {
+                rpc Call (_serialize.Shared) returns (Res);
+            }
+            """
+        )
+    )
+    schema = resolve_imports(service)
+    generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path, 
grpc=True))
+    content = generator.generate_services()[0].content
+
+    assert "import '../../_serialize/_serialize.dart' as _serialize_2;" in 
content
+    assert "ClientMethod<_serialize_2.Shared, _models.Res>(" in content
+    assert "ServiceMethod<_serialize_2.Shared, _models.Res>(" in content
+
+
+def test_dart_grpc_service_alias(tmp_path: Path):
+    from fory_compiler.generators.dart import DartGenerator
+
+    common = tmp_path / "common.fdl"
+    common.write_text(
+        dedent(
+            """
+            package ApiClient;
+
+            message Shared {
+                string id = 1;
+            }
+            """
+        )
+    )
+    service = tmp_path / "service.fdl"
+    service.write_text(
+        dedent(
+            """
+            package demo.api;
+
+            import "common.fdl";
+
+            message Res {}
+
+            service Api {
+                rpc Call (ApiClient.Shared) returns (Res);
+            }
+            """
+        )
+    )
+    schema = resolve_imports(service)
+    generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path, 
grpc=True))
+    content = generator.generate_services()[0].content
+
+    assert "import '../../ApiClient/ApiClient.dart' as ApiClient_2;" in content
+    assert "class ApiClient extends Client {" in content
+    assert "ClientMethod<ApiClient_2.Shared, _models.Res>(" in content
+    assert "ServiceMethod<ApiClient_2.Shared, _models.Res>(" in content
+
+
+def test_dart_grpc_support_aliases(tmp_path: Path):
+    from fory_compiler.generators.dart import DartGenerator
+
+    common = tmp_path / "common.fdl"
+    common.write_text(
+        dedent(
+            """
+            package Client;
+
+            message Shared {
+                string id = 1;
+            }
+            """
+        )
+    )
+    service = tmp_path / "service.fdl"
+    service.write_text(
+        dedent(
+            """
+            package demo.api;
+
+            import "common.fdl";
+
+            message Res {}
+
+            service Api {
+                rpc Call (Client.Shared) returns (Res);
+            }
+            """
+        )
+    )
+    schema = resolve_imports(service)
+    generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path, 
grpc=True))
+    content = generator.generate_services()[0].content
+
+    assert "import '../../Client/Client.dart' as Client_2;" in content
+    assert "class ApiClient extends Client {" in content
+    assert "ClientMethod<Client_2.Shared, _models.Res>(" in content
+    assert "ServiceMethod<Client_2.Shared, _models.Res>(" in content
+
+
+def test_dart_grpc_local_aliases(tmp_path: Path):
+    from fory_compiler.generators.dart import DartGenerator
+
+    value_schema = tmp_path / "value.fdl"
+    value_schema.write_text(
+        dedent(
+            """
+            package value;
+
+            message Shared {
+                string id = 1;
+            }
+            """
+        )
+    )
+    service = tmp_path / "service.fdl"
+    service.write_text(
+        dedent(
+            """
+            package demo.api;
+
+            import "value.fdl";
+
+            service Api {
+                rpc Echo (Shared) returns (Shared);
+            }
+            """
+        )
+    )
+    schema = resolve_imports(service)
+    generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path, 
grpc=True))
+    content = generator.generate_services()[0].content
+
+    assert "import '../../value/value.dart' as value_2;" in content
+    assert "ClientMethod<value_2.Shared, value_2.Shared>(" in content
+    assert "ServiceMethod<value_2.Shared, value_2.Shared>(" in content
+    assert "_deserialize<value_2.Shared>(value)" in content
+    assert "(value_2.Shared value) => _serialize(value)" in content
+    assert "_models.value.Shared" not in content
+
+
+def test_dart_grpc_method_aliases(tmp_path: Path):
+    from fory_compiler.generators.dart import DartGenerator
+
+    foo_schema = tmp_path / "foo.fdl"
+    foo_schema.write_text(
+        dedent(
+            """
+            package foo;
+
+            message Shared {
+                string id = 1;
+            }
+            """
+        )
+    )
+    service = tmp_path / "service.fdl"
+    service.write_text(
+        dedent(
+            """
+            package demo.api;
+
+            import "foo.fdl";
+
+            service Api {
+                rpc Foo (Shared) returns (Shared);
+            }
+            """
+        )
+    )
+    schema = resolve_imports(service)
+    generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path, 
grpc=True))
+    content = generator.generate_services()[0].content
+
+    assert "import '../../foo/foo.dart' as foo_2;" in content
+    assert "ResponseFuture<foo_2.Shared> foo(" in content
+    assert "Future<foo_2.Shared> foo_Pre(" in content
+    assert "foo.Shared" not in content
+
+
+def test_dart_grpc_reserved_methods():
+    from fory_compiler.generators.dart import DartGenerator
+
+    import pytest
+
+    for rpc_name, emitted in [("ToString", "toString"), ("HashCode", 
"hashCode")]:
+        schema = parse_fdl(
+            dedent(
+                f"""
+                package demo.names;
+
+                message Req {{}}
+                message Res {{}}
+
+                service Svc {{
+                    rpc {rpc_name} (Req) returns (Res);
+                }}
+                """
+            )
+        )
+        with pytest.raises(ValueError) as excinfo:
+            generate_service_files(schema, DartGenerator)
+        msg = str(excinfo.value)
+        assert "inherited Dart member" in msg
+        assert f"Svc.{rpc_name} -> {emitted}" in msg
diff --git a/docs/compiler/compiler-guide.md b/docs/compiler/compiler-guide.md
index 0c5ebd491..89a2427b6 100644
--- a/docs/compiler/compiler-guide.md
+++ b/docs/compiler/compiler-guide.md
@@ -143,16 +143,16 @@ foryc schema.fdl --output ./src/generated
 foryc user.fdl order.fdl product.fdl --output ./generated
 ```
 
-**Compile a simple schema containing service definitions (Java + Python + Go + 
Rust + C# + Scala + Kotlin + JavaScript models):**
+**Compile a simple schema containing service definitions (Java + Python + Go + 
Rust + C# + Dart + Scala + Kotlin + JavaScript models):**
 
 ```bash
-foryc compiler/examples/service.fdl --java_out=./generated/java 
--python_out=./generated/python --go_out=./generated/go 
--rust_out=./generated/rust --csharp_out=./generated/csharp 
--scala_out=./generated/scala --kotlin_out=./generated/kotlin 
--javascript_out=./generated/javascript
+foryc compiler/examples/service.fdl --java_out=./generated/java 
--python_out=./generated/python --go_out=./generated/go 
--rust_out=./generated/rust --csharp_out=./generated/csharp 
--dart_out=./generated/dart --scala_out=./generated/scala 
--kotlin_out=./generated/kotlin --javascript_out=./generated/javascript
 ```
 
-**Generate Java, Python, Go, Rust, C#, Scala, Kotlin, and Node.js JavaScript 
gRPC service companions:**
+**Generate Java, Python, Go, Rust, C#, Dart, Scala, Kotlin, and Node.js 
JavaScript gRPC service companions:**
 
 ```bash
-foryc compiler/examples/service.fdl --java_out=./generated/java 
--python_out=./generated/python --go_out=./generated/go 
--rust_out=./generated/rust --csharp_out=./generated/csharp 
--scala_out=./generated/scala --kotlin_out=./generated/kotlin 
--javascript_out=./generated/javascript --grpc
+foryc compiler/examples/service.fdl --java_out=./generated/java 
--python_out=./generated/python --go_out=./generated/go 
--rust_out=./generated/rust --csharp_out=./generated/csharp 
--dart_out=./generated/dart --scala_out=./generated/scala 
--kotlin_out=./generated/kotlin --javascript_out=./generated/javascript --grpc
 ```
 
 The generated gRPC service code uses Fory to serialize request and response
@@ -161,7 +161,8 @@ payloads. Java output imports grpc-java APIs, Python output 
defaults to
 Scala output imports grpc-java APIs, and Kotlin output imports grpc-java and
 grpc-kotlin APIs and uses coroutine stubs. C# output imports `Grpc.Core.Api`
 types and can be hosted with normal .NET gRPC packages such as 
`Grpc.AspNetCore`
-or called through `Grpc.Net.Client`. JavaScript output imports `@grpc/grpc-js`.
+or called through `Grpc.Net.Client`. Dart output imports `package:grpc`.
+JavaScript output imports `@grpc/grpc-js`.
 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.
@@ -440,6 +441,9 @@ generated/
 - IDL module class included in the main file; generated serializer metadata is
   included in the part file
 - Typed arrays used for non-optional, non-ref primitive lists (e.g., 
`Int32List`)
+- With `--grpc`, one `<stem>_grpc.dart` companion per schema is generated next 
to
+  the model file, containing each service's `Client` and `ServiceBase`; it
+  imports `package:grpc`
 
 ### Scala
 
diff --git a/docs/compiler/flatbuffers-idl.md b/docs/compiler/flatbuffers-idl.md
index 89b375153..0ad11a648 100644
--- a/docs/compiler/flatbuffers-idl.md
+++ b/docs/compiler/flatbuffers-idl.md
@@ -126,8 +126,8 @@ 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, Rust, C#, Scala, Kotlin, and JavaScript. JavaScript 
browser
-clients are generated with `--grpc-web`. These companions use Fory
+as Java, Python, Go, Rust, C#, Dart, Scala, Kotlin, and JavaScript. JavaScript
+browser clients are generated with `--grpc-web`. These companions use Fory
 serialization for request and response payloads.
 
 ```fbs
@@ -138,16 +138,17 @@ rpc_service SearchService {
 ```
 
 ```bash
-foryc api.fbs --java_out=./generated/java --python_out=./generated/python 
--go_out=./generated/go --rust_out=./generated/rust 
--csharp_out=./generated/csharp --scala_out=./generated/scala 
--kotlin_out=./generated/kotlin --javascript_out=./generated/javascript --grpc
+foryc api.fbs --java_out=./generated/java --python_out=./generated/python 
--go_out=./generated/go --rust_out=./generated/rust 
--csharp_out=./generated/csharp --dart_out=./generated/dart 
--scala_out=./generated/scala --kotlin_out=./generated/kotlin 
--javascript_out=./generated/javascript --grpc
 ```
 
 Generated service code imports grpc APIs, so applications must provide 
grpc-java,
 grpc-kotlin, Scala grpc-java APIs, `grpcio`, grpc-go, Rust `tonic` and `bytes`,
-`@grpc/grpc-js`, or C# `Grpc.Core.Api` plus server/client dependencies when 
they
-compile or run those files. Python companions use `grpc.aio` by default and can
-be generated in sync mode with `--grpc-python-mode=sync`. Fory packages do not
-add gRPC as a hard dependency. Use `--grpc-web` with JavaScript output to
-generate browser clients that import `grpc-web`.
+`@grpc/grpc-js`, C# `Grpc.Core.Api` plus server/client dependencies, or Dart
+`package:grpc` when they compile or run those files. Python companions use
+`grpc.aio` by default and can be generated in sync mode with
+`--grpc-python-mode=sync`. Fory packages do not add gRPC as a hard dependency.
+Use `--grpc-web` with JavaScript output to generate browser clients that import
+`grpc-web`.
 
 ### Defaults and Metadata
 
diff --git a/docs/compiler/generated-code.md b/docs/compiler/generated-code.md
index 1f52db67e..b3e5ebc91 100644
--- a/docs/compiler/generated-code.md
+++ b/docs/compiler/generated-code.md
@@ -1380,6 +1380,47 @@ void main() {
 }
 ```
 
+### gRPC Service Companions
+
+When a schema contains services and the compiler is run with `--grpc`, Dart
+generation emits one `<module>_grpc.dart` file per schema next to the model
+types. It targets `package:grpc`. Request and response serialization uses a 
Fory
+runtime the companion obtains automatically and that registers the schema's
+types on first use, so no manual registration is required; an application may
+optionally inject a custom `Fory` via the schema module's `install(...)` before
+the first call.
+
+All four RPC modes are generated: unary, server-streaming, client-streaming, 
and
+bidirectional. The client class extends `Client`; the service base class 
extends
+`Service` and self-registers each method with `$addMethod`.
+
+```dart
+class GreeterClient extends Client {
+  // Single response: ResponseFuture. Streaming response: ResponseStream.
+  ResponseFuture<HelloReply> sayHello(HelloRequest request, {CallOptions? 
options}) { ... }
+  ResponseStream<HelloReply> sayHellos(HelloRequest request, {CallOptions? 
options}) { ... }
+  ResponseFuture<HelloReply> collectHellos(Stream<HelloRequest> request, 
{CallOptions? options}) { ... }
+  ResponseStream<HelloReply> chatHellos(Stream<HelloRequest> request, 
{CallOptions? options}) { ... }
+}
+
+abstract class GreeterServiceBase extends Service {
+  Future<HelloReply> sayHello(ServiceCall call, HelloRequest request);
+  Stream<HelloReply> sayHellos(ServiceCall call, HelloRequest request);
+  Future<HelloReply> collectHellos(ServiceCall call, Stream<HelloRequest> 
request);
+  Stream<HelloReply> chatHellos(ServiceCall call, Stream<HelloRequest> 
request);
+}
+```
+
+A single-response client method returns `ResponseFuture<R>` (client-streaming
+adapts the streaming call with `.single`); a streaming-response method returns
+`ResponseStream<R>`. On the server, implementations override the abstract 
methods,
+which receive a single request as `Q` and a client-streaming request as
+`Stream<Q>`, and return a `Future` for single responses or a `Stream` for
+streaming responses.
+Applications compiling these files must provide a `grpc` dependency; the Fory 
Dart
+runtime does not add one. The original IDL method names are used in the gRPC 
wire
+paths.
+
 ## Kotlin
 
 The Kotlin target emits Kotlin source only. The compiler does not generate Java
diff --git a/docs/compiler/index.md b/docs/compiler/index.md
index da10b2cd1..caa69a6fc 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, Rust, C#, Scala, Kotlin, and
+describe RPC services; for Java, Python, Go, Rust, C#, Dart, Scala, Kotlin, and
 JavaScript, the compiler can generate gRPC service companions that use Fory
 serialization for request and response payloads.
 
@@ -88,21 +88,21 @@ service AnimalService {
 }
 ```
 
-Generate Java, Python, Go, Rust, C#, Scala, Kotlin, and JavaScript models plus
-gRPC service companions with:
+Generate Java, Python, Go, Rust, C#, Dart, Scala, Kotlin, and JavaScript models
+plus gRPC service companions with:
 
 ```bash
-foryc animals.fdl --java_out=./generated/java --python_out=./generated/python 
--go_out=./generated/go --rust_out=./generated/rust 
--csharp_out=./generated/csharp --scala_out=./generated/scala 
--kotlin_out=./generated/kotlin --javascript_out=./generated/javascript --grpc
+foryc animals.fdl --java_out=./generated/java --python_out=./generated/python 
--go_out=./generated/go --rust_out=./generated/rust 
--csharp_out=./generated/csharp --dart_out=./generated/dart 
--scala_out=./generated/scala --kotlin_out=./generated/kotlin 
--javascript_out=./generated/javascript --grpc
 ```
 
 The generated service code uses normal gRPC APIs, but request and response
 objects are serialized with Fory. Applications provide their own grpc-java,
 grpc-kotlin, Scala grpc-java APIs, `grpcio`, grpc-go, Rust `tonic` and `bytes`,
-or C# `Grpc.Core.Api` and hosting/client dependencies; Fory packages do not add
-gRPC as a hard dependency. Python companions use `grpc.aio` by default and can
-be generated in sync mode with `--grpc-python-mode=sync`. JavaScript Node.js
-companions use `@grpc/grpc-js`; browser clients are generated separately with
-`--grpc-web` and use `grpc-web`.
+C# `Grpc.Core.Api` and hosting/client dependencies, or Dart `package:grpc`; 
Fory
+packages do not add gRPC as a hard dependency. Python companions use `grpc.aio`
+by default and can be generated in sync mode with `--grpc-python-mode=sync`.
+JavaScript Node.js companions use `@grpc/grpc-js`; browser clients are 
generated
+separately with `--grpc-web` and use `grpc-web`.
 
 ## Why Fory IDL?
 
diff --git a/docs/compiler/protobuf-idl.md b/docs/compiler/protobuf-idl.md
index da5deba31..d9355deec 100644
--- a/docs/compiler/protobuf-idl.md
+++ b/docs/compiler/protobuf-idl.md
@@ -41,19 +41,19 @@ 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/C#/Scala/Kotlin/JavaScript service codegen |
-
-Fory can generate Java, Python, Go, Rust, C#, Scala, Kotlin, and JavaScript 
gRPC
-service companions with `--grpc`. JavaScript browser clients are generated with
-`--grpc-web`. Those services use normal gRPC transports but serialize request
+| 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/C#/Dart/Scala/Kotlin/JavaScript service codegen |
+
+Fory can generate Java, Python, Go, Rust, C#, Dart, Scala, Kotlin, and
+JavaScript gRPC service companions with `--grpc`. JavaScript browser clients 
are
+generated with `--grpc-web`. Those services use normal gRPC transports but 
serialize request
 and response payloads with Fory rather than protobuf. For broad gRPC ecosystem
 tooling, schema reflection, and protobuf-native interceptors, protobuf remains
 the mature/default choice.
@@ -315,19 +315,20 @@ languages.
 For supported service outputs, add `--grpc` to emit gRPC companion code:
 
 ```bash
-foryc api.proto --java_out=./generated/java --python_out=./generated/python 
--go_out=./generated/go --rust_out=./generated/rust 
--csharp_out=./generated/csharp --scala_out=./generated/scala 
--kotlin_out=./generated/kotlin --javascript_out=./generated/javascript --grpc
+foryc api.proto --java_out=./generated/java --python_out=./generated/python 
--go_out=./generated/go --rust_out=./generated/rust 
--csharp_out=./generated/csharp --dart_out=./generated/dart 
--scala_out=./generated/scala --kotlin_out=./generated/kotlin 
--javascript_out=./generated/javascript --grpc
 ```
 
 Generated Java service files compile against grpc-java, generated Python 
service
 modules use `grpc.aio` by default, generated Rust service files import `tonic`
 and `bytes`, generated Go service files import grpc-go, generated JavaScript
 Node.js service files import `@grpc/grpc-js`, generated C# service files import
-`Grpc.Core.Api` types, generated Scala service files compile against grpc-java,
-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. Use `--grpc-python-mode=sync` for sync Python `grpcio`
-companions. Use `--grpc-web` with JavaScript output to generate browser clients
-that import `grpc-web`.
+`Grpc.Core.Api` types, generated Dart service files import `package:grpc`,
+generated Scala service files compile against grpc-java, 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.
+Use `--grpc-python-mode=sync` for sync Python `grpcio` companions. Use
+`--grpc-web` with JavaScript output to generate browser clients that import
+`grpc-web`.
 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.
diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md
index d2e12c989..633b90f1e 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, Rust, C#, Scala, Kotlin, and JavaScript.
+outputs such as Java, Python, Go, Rust, C#, Dart, Scala, Kotlin, and 
JavaScript.
 JavaScript browser gRPC-Web clients are generated with `--grpc-web`.
 
 ```protobuf
@@ -952,9 +952,9 @@ service PetDirectory {
 - 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, grpc-kotlin, `grpcio`, grpc-go, Rust `tonic`
-  and `bytes`, Scala grpc-java APIs, `@grpc/grpc-js`, `grpc-web`, or C#
-  `Grpc.Core.Api` plus a server or client package. Python companions use
-  `grpc.aio` by default and can be generated in sync mode with
+  and `bytes`, Scala grpc-java APIs, `@grpc/grpc-js`, `grpc-web`, C#
+  `Grpc.Core.Api` plus a server or client package, or Dart `package:grpc`. 
Python
+  companions use `grpc.aio` by default and can be generated in sync mode with
   `--grpc-python-mode=sync`.
 
 **Grammar:**
diff --git a/docs/guide/dart/grpc-support.md b/docs/guide/dart/grpc-support.md
new file mode 100644
index 000000000..cda566adc
--- /dev/null
+++ b/docs/guide/dart/grpc-support.md
@@ -0,0 +1,292 @@
+---
+title: gRPC Support
+sidebar_position: 12
+id: grpc_support
+license: |
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+---
+
+Fory can generate Dart gRPC service companions for schemas that define 
services.
+The generated code uses normal `package:grpc` clients, service bases, method
+descriptors, call options, deadlines, cancellations, and status codes, while
+request and response objects are serialized with Fory instead of protobuf.
+
+Use this mode when both RPC peers are generated from the same Fory IDL, 
protobuf
+IDL, or FlatBuffers IDL and both sides expect Fory-encoded message bodies. Use
+normal protobuf gRPC generation for APIs that must be consumed by generic
+protobuf clients, reflection tools, or components that expect protobuf message
+bytes.
+
+## Add Dependencies
+
+The `fory` package does not add gRPC dependencies. Add `grpc` (and the
+`build_runner` dev dependency that generates the Fory serializer code) in the
+application that compiles or runs generated service companions:
+
+```yaml
+dependencies:
+  fory: ^1.1.0
+  grpc: ^4.0.0
+
+dev_dependencies:
+  build_runner: ^2.4.0
+```
+
+The same dependencies cover both client and server applications.
+
+## Define a Service
+
+Service definitions can come from Fory IDL, protobuf IDL, or FlatBuffers
+`rpc_service` definitions. A Fory IDL service looks like this:
+
+```protobuf
+package demo.greeter;
+
+message HelloRequest {
+  string name = 1;
+}
+
+message HelloReply {
+  string reply = 1;
+}
+
+service Greeter {
+  rpc SayHello (HelloRequest) returns (HelloReply);
+}
+```
+
+Generate Dart model and gRPC companion code with `--grpc`:
+
+```bash
+foryc service.fdl --dart_out=./lib/generated --grpc
+```
+
+Then run `build_runner` once to emit the Fory serializer part file for the
+generated models (this step is required before the code can run):
+
+```bash
+dart run build_runner build --delete-conflicting-outputs
+```
+
+For this schema, the Dart generator emits (the model file and module are named
+from the package leaf, `greeter`):
+
+| File                                        | Purpose                        
                      |
+| ------------------------------------------- | 
---------------------------------------------------- |
+| `demo/greeter/greeter.dart`                 | Fory model types and the 
schema module               |
+| `demo/greeter/greeter.fory.dart`            | Serializers and registration 
(built by build_runner) |
+| `demo/greeter/greeter_grpc.dart`            | gRPC client, service base, and 
method descriptors    |
+| `GreeterForyModule` in `greeter.dart`       | Fory registration module for 
generated types         |
+| `GreeterServiceBase` in `greeter_grpc.dart` | Base class for server 
implementations                |
+| `GreeterClient` in `greeter_grpc.dart`      | Client stub for gRPC calls     
                      |
+
+The generated client and service base obtain a ready `Fory` automatically and
+register the schema's types on first use, so no manual registration step is
+required. To share a custom `Fory` (for example one configured with extra
+modules), call `GreeterForyModule.install(yourFory)` once before the first RPC;
+this is optional.
+
+## Implement a Server
+
+Extend the generated `GreeterServiceBase` and host it with grpc-dart's 
`Server`:
+
+```dart
+import 'dart:io';
+
+import 'package:grpc/grpc.dart';
+import 'demo/greeter/greeter.dart';
+import 'demo/greeter/greeter_grpc.dart';
+
+class GreeterService extends GreeterServiceBase {
+  @override
+  Future<HelloReply> sayHello(ServiceCall call, HelloRequest request) async {
+    final reply = HelloReply()..reply = 'Hello, ${request.name}';
+    return reply;
+  }
+}
+
+Future<void> main() async {
+  final server = Server.create(services: [GreeterService()]);
+  await server.serve(address: InternetAddress.loopbackIPv4, port: 50051);
+}
+```
+
+## Create a Client
+
+Use the generated client with a `ClientChannel`:
+
+```dart
+import 'package:grpc/grpc.dart';
+import 'demo/greeter/greeter.dart';
+import 'demo/greeter/greeter_grpc.dart';
+
+Future<void> main() async {
+  final channel = ClientChannel(
+    'localhost',
+    port: 50051,
+    options: const ChannelOptions(
+      credentials: ChannelCredentials.insecure(),
+    ),
+  );
+  final client = GreeterClient(channel);
+
+  final reply = await client.sayHello(HelloRequest()..name = 'Fory');
+  print(reply.reply);
+
+  await channel.shutdown();
+}
+```
+
+## Streaming RPCs
+
+Fory service definitions can use the same gRPC streaming shapes:
+
+```protobuf
+service Greeter {
+  rpc SayHello (HelloRequest) returns (HelloReply);
+  rpc LotsOfReplies (HelloRequest) returns (stream HelloReply);
+  rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply);
+  rpc Chat (stream HelloRequest) returns (stream HelloReply);
+}
+```
+
+Generated Dart methods follow grpc-dart conventions. Single responses return a
+`ResponseFuture<R>` (client-streaming adapts the call with `.single`); 
streaming
+responses return a `ResponseStream<R>`. On the server, single requests arrive 
as
+the message type and streaming requests as a `Stream`; the method returns a
+`Future` for single responses and a `Stream` for streaming responses:
+
+| IDL shape                                 | Client method                    
                    | Server method (override)                               |
+| ----------------------------------------- | 
---------------------------------------------------- | 
------------------------------------------------------ |
+| `rpc A (Req) returns (Res)`               | `ResponseFuture<Res> a(Req 
request, {CallOptions?})` | `Future<Res> a(ServiceCall call, Req request)`      
   |
+| `rpc A (Req) returns (stream Res)`        | `ResponseStream<Res> a(Req 
request, {CallOptions?})` | `Stream<Res> a(ServiceCall call, Req request)`      
   |
+| `rpc A (stream Req) returns (Res)`        | `ResponseFuture<Res> 
a(Stream<Req> request, {...})`  | `Future<Res> a(ServiceCall call, Stream<Req> 
request)` |
+| `rpc A (stream Req) returns (stream Res)` | `ResponseStream<Res> 
a(Stream<Req> request, {...})`  | `Stream<Res> a(ServiceCall call, Stream<Req> 
request)` |
+
+Server implementations use the generated streaming method shapes directly:
+
+```dart
+class GreeterService extends GreeterServiceBase {
+  @override
+  Stream<HelloReply> lotsOfReplies(
+    ServiceCall call,
+    HelloRequest request,
+  ) async* {
+    for (final greeting in ['Hello, ${request.name}', 'Welcome, 
${request.name}']) {
+      yield HelloReply()..reply = greeting;
+    }
+  }
+
+  @override
+  Future<HelloReply> lotsOfGreetings(
+    ServiceCall call,
+    Stream<HelloRequest> request,
+  ) async {
+    final names = <String>[];
+    await for (final message in request) {
+      names.add(message.name);
+    }
+    return HelloReply()..reply = names.join(', ');
+  }
+
+  @override
+  Stream<HelloReply> chat(
+    ServiceCall call,
+    Stream<HelloRequest> request,
+  ) async* {
+    await for (final message in request) {
+      yield HelloReply()..reply = 'Hello, ${message.name}';
+    }
+  }
+}
+```
+
+Generated clients return the standard grpc-dart call objects:
+
+```dart
+// Server streaming.
+await for (final reply in client.lotsOfReplies(HelloRequest()..name = 'Fory')) 
{
+  print(reply.reply);
+}
+
+// Client streaming.
+final summary = await client.lotsOfGreetings(
+  Stream.fromIterable([
+    HelloRequest()..name = 'Ada',
+    HelloRequest()..name = 'Grace',
+  ]),
+);
+print(summary.reply);
+
+// Bidirectional streaming.
+await for (final reply in client.chat(
+  Stream.fromIterable([HelloRequest()..name = 'Fory']),
+)) {
+  print(reply.reply);
+}
+```
+
+The generated descriptors preserve the exact IDL service and method names for
+the gRPC path, while the Dart methods use camelCase names.
+
+## Generated Module Names
+
+Dart model files and schema modules are named after the package's last segment,
+not the gRPC service name. (When a schema has no package, the source file stem 
is
+used instead.)
+
+| Schema input (package)          | Model file          | Schema module        
   |
+| ------------------------------- | ------------------- | 
----------------------- |
+| `service.fdl` (`demo.greeter`)  | `greeter.dart`      | `GreeterForyModule`  
   |
+| `api.fdl` (`demo.order_events`) | `order_events.dart` | 
`OrderEventsForyModule` |
+| `greeter.fdl` (`demo.greeter`)  | `greeter.dart`      | `GreeterForyModule`  
   |
+
+A gRPC service named `Greeter` still generates the companion
+`<stem>_grpc.dart` with `GreeterClient` and `GreeterServiceBase`; it does not
+change the schema module name. If several schema files use the same package
+leaf, place them in distinct output directories or choose package/file names 
that
+produce distinct Dart model files.
+
+## Operations
+
+The generated service code only replaces request and response serialization.
+All normal gRPC operational features still belong to your gRPC stack:
+
+- Deadlines and cancellations
+- TLS and authentication
+- Name resolution and load balancing
+- Client and server interceptors
+- Status codes and metadata
+- Channel lifecycle management
+
+## Troubleshooting
+
+### Missing `package:grpc` Types
+
+Add `grpc` to your application dependencies. Generated Fory service files 
import
+grpc-dart APIs, but `fory` intentionally does not depend on gRPC.
+
+### Generated Code References a Missing `.fory.dart` Part
+
+Run `dart run build_runner build --delete-conflicting-outputs` after generating
+or regenerating the Dart sources. The serializer part file is produced by
+`build_runner`, not by `foryc`.
+
+### Protobuf Clients Cannot Decode the Service
+
+Fory gRPC companions do not use protobuf wire encoding for messages. Use a
+Fory-generated client for Fory-generated services, or expose a separate 
protobuf
+service endpoint for generic protobuf clients.
diff --git a/docs/guide/dart/index.md b/docs/guide/dart/index.md
index 6c467bbe1..d779afc99 100644
--- a/docs/guide/dart/index.md
+++ b/docs/guide/dart/index.md
@@ -138,6 +138,7 @@ dart run build_runner build --delete-conflicting-outputs
 | [Supported Types](supported-types.md)           | Built-in xlang values, 
wrappers, collections, and structs       |
 | [Schema Evolution](schema-evolution.md)         | Compatible structs and 
evolving schemas                         |
 | [Web Platform Support](web-platform-support.md) | Dart VM/AOT, Flutter, and 
web support, limits, and validation   |
+| [gRPC Support](grpc-support.md)                 | Generated Fory-backed gRPC 
service companions                   |
 | [Troubleshooting](troubleshooting.md)           | Common errors, 
diagnostics, and validation steps                |
 
 ## Related Resources
diff --git a/docs/guide/dart/troubleshooting.md 
b/docs/guide/dart/troubleshooting.md
index d94ac4e79..5f066920a 100644
--- a/docs/guide/dart/troubleshooting.md
+++ b/docs/guide/dart/troubleshooting.md
@@ -138,9 +138,26 @@ dart run build_runner build --delete-conflicting-outputs
 dart test
 ```
 
+## Generated gRPC files cannot find `package:grpc` types
+
+**Cause**: gRPC packages are application dependencies. The `fory` package does
+not add gRPC as a hard dependency.
+
+**Fix**: Add `grpc` to your `pubspec.yaml` (and the `build_runner` dev
+dependency), then run `dart pub get`. See [gRPC Support](grpc-support.md).
+
+## A protobuf client cannot decode a Fory gRPC service
+
+**Cause**: Fory gRPC companions use gRPC transports with Fory-encoded message
+bodies, not protobuf wire encoding.
+
+**Fix**: Use a Fory-generated client for Fory-generated services, or expose a
+separate protobuf service endpoint for generic protobuf clients.
+
 ## Related Topics
 
 - [Xlang Serialization](xlang-serialization.md)
 - [Code Generation](code-generation.md)
 - [Custom Serializers](custom-serializers.md)
 - [Web Platform Support](web-platform-support.md)
+- [gRPC Support](grpc-support.md)
diff --git a/integration_tests/grpc_tests/dart/.gitignore 
b/integration_tests/grpc_tests/dart/.gitignore
new file mode 100644
index 000000000..304132b85
--- /dev/null
+++ b/integration_tests/grpc_tests/dart/.gitignore
@@ -0,0 +1,4 @@
+.dart_tool/
+.packages
+pubspec.lock
+lib/generated/
diff --git a/integration_tests/grpc_tests/dart/bin/interop.dart 
b/integration_tests/grpc_tests/dart/bin/interop.dart
new file mode 100644
index 000000000..21bd4736c
--- /dev/null
+++ b/integration_tests/grpc_tests/dart/bin/interop.dart
@@ -0,0 +1,520 @@
+// 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.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:grpc/grpc.dart';
+
+import 'package:fory_grpc_interop/generated/grpc_fdl/grpc_fdl.dart';
+import 'package:fory_grpc_interop/generated/grpc_fdl/grpc_fdl_grpc.dart';
+import 'package:fory_grpc_interop/generated/grpc_fbs/grpc_fbs.dart';
+import 'package:fory_grpc_interop/generated/grpc_fbs/grpc_fbs_grpc.dart';
+import 'package:fory_grpc_interop/generated/grpc_pb/grpc_pb.dart';
+import 'package:fory_grpc_interop/generated/grpc_pb/grpc_pb_grpc.dart';
+
+void _expect(Object? actual, Object? expected, String what) {
+  if (actual != expected) {
+    throw StateError(
+      'mismatch [$what]\n  actual:   $actual\n  expected: $expected',
+    );
+  }
+}
+
+void _expectList(List<Object?> actual, List<Object?> expected, String what) {
+  if (actual.length != expected.length) {
+    throw StateError(
+      'length mismatch [$what]: ${actual.length} != ${expected.length}',
+    );
+  }
+  for (var i = 0; i < actual.length; i++) {
+    _expect(actual[i], expected[i], '$what[$i]');
+  }
+}
+
+GrpcFdlRequest _fdlRequest(String id, int count, String payload) {
+  return GrpcFdlRequest()
+    ..id = id
+    ..count = count
+    ..payload = payload;
+}
+
+GrpcFdlResponse _fdlResponse(GrpcFdlRequest request, String tag, int offset) {
+  return GrpcFdlResponse()
+    ..id = '$tag:${request.id}'
+    ..count = request.count + offset
+    ..payload = '$tag:${request.payload}';
+}
+
+GrpcFdlResponse _fdlAggregate(List<GrpcFdlRequest> requests) {
+  return GrpcFdlResponse()
+    ..id = 'client:${requests.map((e) => e.id).join('+')}'
+    ..count = requests.fold(0, (sum, e) => sum + e.count)
+    ..payload = 'client:${requests.map((e) => e.payload).join('+')}';
+}
+
+GrpcFdlUnion _fdlUnionRequest(GrpcFdlRequest request) =>
+    GrpcFdlUnion.request(request);
+
+GrpcFdlUnion _fdlUnionResponse(
+  GrpcFdlRequest request,
+  String tag,
+  int offset,
+) => GrpcFdlUnion.response(_fdlResponse(request, tag, offset));
+
+GrpcFdlUnion _fdlUnionAggregate(List<GrpcFdlRequest> requests) =>
+    GrpcFdlUnion.response(_fdlAggregate(requests));
+
+GrpcFdlRequest _fdlRequestFromUnion(GrpcFdlUnion union) => union.requestValue;
+
+GrpcFbsRequest _fbsRequest(String id, int count, String payload) {
+  return GrpcFbsRequest()
+    ..id = id
+    ..count = count
+    ..payload = payload;
+}
+
+GrpcFbsResponse _fbsResponse(GrpcFbsRequest request, String tag, int offset) {
+  return GrpcFbsResponse()
+    ..id = '$tag:${request.id}'
+    ..count = request.count + offset
+    ..payload = '$tag:${request.payload}';
+}
+
+GrpcFbsResponse _fbsAggregate(List<GrpcFbsRequest> requests) {
+  return GrpcFbsResponse()
+    ..id = 'client:${requests.map((e) => e.id).join('+')}'
+    ..count = requests.fold(0, (sum, e) => sum + e.count)
+    ..payload = 'client:${requests.map((e) => e.payload).join('+')}';
+}
+
+GrpcFbsUnion _fbsUnionRequest(GrpcFbsRequest request) =>
+    GrpcFbsUnion.grpcFbsRequest(request);
+
+GrpcFbsUnion _fbsUnionResponse(
+  GrpcFbsRequest request,
+  String tag,
+  int offset,
+) => GrpcFbsUnion.grpcFbsResponse(_fbsResponse(request, tag, offset));
+
+GrpcFbsUnion _fbsUnionAggregate(List<GrpcFbsRequest> requests) =>
+    GrpcFbsUnion.grpcFbsResponse(_fbsAggregate(requests));
+
+GrpcFbsRequest _fbsRequestFromUnion(GrpcFbsUnion union) =>
+    union.grpcFbsRequestValue;
+
+GrpcPbRequest _pbRequest(String id, int count, GrpcPbRequest_Payload payload) {
+  return GrpcPbRequest()
+    ..id = id
+    ..count = count
+    ..payload = payload;
+}
+
+GrpcPbResponse_Payload? _pbResponsePayload(
+  GrpcPbRequest_Payload? payload,
+  String tag,
+  int offset,
+) {
+  if (payload == null) return null;
+  if (payload.isText) {
+    return GrpcPbResponse_Payload.text('$tag:${payload.textValue}');
+  }
+  return GrpcPbResponse_Payload.number(payload.numberValue + offset);
+}
+
+GrpcPbResponse _pbResponse(GrpcPbRequest request, String tag, int offset) {
+  return GrpcPbResponse()
+    ..id = '$tag:${request.id}'
+    ..count = request.count + offset
+    ..payload = _pbResponsePayload(request.payload, tag, offset);
+}
+
+GrpcPbResponse _pbAggregate(List<GrpcPbRequest> requests) {
+  final ids = requests.map((e) => e.id).join('+');
+  return GrpcPbResponse()
+    ..id = 'client:$ids'
+    ..count = requests.fold(0, (sum, e) => sum + e.count)
+    ..payload = GrpcPbResponse_Payload.text('client:$ids');
+}
+
+class FdlService extends FdlGrpcServiceServiceBase {
+  @override
+  Future<GrpcFdlResponse> unaryMessage(ServiceCall c, GrpcFdlRequest r) async 
=>
+      _fdlResponse(r, 'unary', 10);
+
+  @override
+  Stream<GrpcFdlResponse> serverStreamMessage(
+    ServiceCall c,
+    GrpcFdlRequest r,
+  ) async* {
+    for (var i = 0; i < 3; i++) {
+      yield _fdlResponse(r, 'server-$i', i);
+    }
+  }
+
+  @override
+  Future<GrpcFdlResponse> clientStreamMessage(
+    ServiceCall c,
+    Stream<GrpcFdlRequest> r,
+  ) async => _fdlAggregate(await r.toList());
+
+  @override
+  Stream<GrpcFdlResponse> bidiStreamMessage(
+    ServiceCall c,
+    Stream<GrpcFdlRequest> r,
+  ) async* {
+    var i = 0;
+    await for (final v in r) {
+      yield _fdlResponse(v, 'bidi-$i', i);
+      i++;
+    }
+  }
+
+  @override
+  Future<GrpcFdlUnion> unaryUnion(ServiceCall c, GrpcFdlUnion r) async =>
+      _fdlUnionResponse(_fdlRequestFromUnion(r), 'unary', 10);
+
+  @override
+  Stream<GrpcFdlUnion> serverStreamUnion(ServiceCall c, GrpcFdlUnion r) async* 
{
+    final item = _fdlRequestFromUnion(r);
+    for (var i = 0; i < 3; i++) {
+      yield _fdlUnionResponse(item, 'server-$i', i);
+    }
+  }
+
+  @override
+  Future<GrpcFdlUnion> clientStreamUnion(
+    ServiceCall c,
+    Stream<GrpcFdlUnion> r,
+  ) async {
+    final requests = <GrpcFdlRequest>[];
+    await for (final item in r) {
+      requests.add(_fdlRequestFromUnion(item));
+    }
+    return _fdlUnionAggregate(requests);
+  }
+
+  @override
+  Stream<GrpcFdlUnion> bidiStreamUnion(
+    ServiceCall c,
+    Stream<GrpcFdlUnion> r,
+  ) async* {
+    var i = 0;
+    await for (final item in r) {
+      yield _fdlUnionResponse(_fdlRequestFromUnion(item), 'bidi-$i', i);
+      i++;
+    }
+  }
+}
+
+class FbsService extends FbsGrpcServiceServiceBase {
+  @override
+  Future<GrpcFbsResponse> unaryMessage(ServiceCall c, GrpcFbsRequest r) async 
=>
+      _fbsResponse(r, 'unary', 10);
+
+  @override
+  Stream<GrpcFbsResponse> serverStreamMessage(
+    ServiceCall c,
+    GrpcFbsRequest r,
+  ) async* {
+    for (var i = 0; i < 3; i++) {
+      yield _fbsResponse(r, 'server-$i', i);
+    }
+  }
+
+  @override
+  Future<GrpcFbsResponse> clientStreamMessage(
+    ServiceCall c,
+    Stream<GrpcFbsRequest> r,
+  ) async => _fbsAggregate(await r.toList());
+
+  @override
+  Stream<GrpcFbsResponse> bidiStreamMessage(
+    ServiceCall c,
+    Stream<GrpcFbsRequest> r,
+  ) async* {
+    var i = 0;
+    await for (final v in r) {
+      yield _fbsResponse(v, 'bidi-$i', i);
+      i++;
+    }
+  }
+
+  @override
+  Future<GrpcFbsUnion> unaryUnion(ServiceCall c, GrpcFbsUnion r) async =>
+      _fbsUnionResponse(_fbsRequestFromUnion(r), 'unary', 10);
+
+  @override
+  Stream<GrpcFbsUnion> serverStreamUnion(ServiceCall c, GrpcFbsUnion r) async* 
{
+    final item = _fbsRequestFromUnion(r);
+    for (var i = 0; i < 3; i++) {
+      yield _fbsUnionResponse(item, 'server-$i', i);
+    }
+  }
+
+  @override
+  Future<GrpcFbsUnion> clientStreamUnion(
+    ServiceCall c,
+    Stream<GrpcFbsUnion> r,
+  ) async {
+    final requests = <GrpcFbsRequest>[];
+    await for (final item in r) {
+      requests.add(_fbsRequestFromUnion(item));
+    }
+    return _fbsUnionAggregate(requests);
+  }
+
+  @override
+  Stream<GrpcFbsUnion> bidiStreamUnion(
+    ServiceCall c,
+    Stream<GrpcFbsUnion> r,
+  ) async* {
+    var i = 0;
+    await for (final item in r) {
+      yield _fbsUnionResponse(_fbsRequestFromUnion(item), 'bidi-$i', i);
+      i++;
+    }
+  }
+}
+
+class PbService extends PbGrpcServiceServiceBase {
+  @override
+  Future<GrpcPbResponse> unaryMessage(ServiceCall c, GrpcPbRequest r) async =>
+      _pbResponse(r, 'unary', 10);
+
+  @override
+  Stream<GrpcPbResponse> serverStreamMessage(
+    ServiceCall c,
+    GrpcPbRequest r,
+  ) async* {
+    for (var i = 0; i < 3; i++) {
+      yield _pbResponse(r, 'server-$i', i);
+    }
+  }
+
+  @override
+  Future<GrpcPbResponse> clientStreamMessage(
+    ServiceCall c,
+    Stream<GrpcPbRequest> r,
+  ) async => _pbAggregate(await r.toList());
+
+  @override
+  Stream<GrpcPbResponse> bidiStreamMessage(
+    ServiceCall c,
+    Stream<GrpcPbRequest> r,
+  ) async* {
+    var i = 0;
+    await for (final v in r) {
+      yield _pbResponse(v, 'bidi-$i', i);
+      i++;
+    }
+  }
+}
+
+Future<void> _exerciseFdl(FdlGrpcServiceClient stub) async {
+  final messages = [
+    _fdlRequest('fdl-a', 1, 'alpha'),
+    _fdlRequest('fdl-b', 2, 'beta'),
+  ];
+  final first = messages[0];
+  _expect(
+    await stub.unaryMessage(first),
+    _fdlResponse(first, 'unary', 10),
+    'fdl.unaryMessage',
+  );
+  _expectList(await stub.serverStreamMessage(first).toList(), [
+    for (var i = 0; i < 3; i++) _fdlResponse(first, 'server-$i', i),
+  ], 'fdl.serverStreamMessage');
+  _expect(
+    await stub.clientStreamMessage(Stream.fromIterable(messages)),
+    _fdlAggregate(messages),
+    'fdl.clientStreamMessage',
+  );
+  _expectList(
+    await stub.bidiStreamMessage(Stream.fromIterable(messages)).toList(),
+    [
+      for (var i = 0; i < messages.length; i++)
+        _fdlResponse(messages[i], 'bidi-$i', i),
+    ],
+    'fdl.bidiStreamMessage',
+  );
+
+  final unionReqs = [
+    _fdlRequest('fdl-u-a', 3, 'union-alpha'),
+    _fdlRequest('fdl-u-b', 4, 'union-beta'),
+  ];
+  final unions = [for (final r in unionReqs) _fdlUnionRequest(r)];
+  final unionFirst = unionReqs[0];
+  _expect(
+    await stub.unaryUnion(unions[0]),
+    _fdlUnionResponse(unionFirst, 'unary', 10),
+    'fdl.unaryUnion',
+  );
+  _expectList(await stub.serverStreamUnion(unions[0]).toList(), [
+    for (var i = 0; i < 3; i++) _fdlUnionResponse(unionFirst, 'server-$i', i),
+  ], 'fdl.serverStreamUnion');
+  _expect(
+    await stub.clientStreamUnion(Stream.fromIterable(unions)),
+    _fdlUnionAggregate(unionReqs),
+    'fdl.clientStreamUnion',
+  );
+  _expectList(
+    await stub.bidiStreamUnion(Stream.fromIterable(unions)).toList(),
+    [
+      for (var i = 0; i < unionReqs.length; i++)
+        _fdlUnionResponse(unionReqs[i], 'bidi-$i', i),
+    ],
+    'fdl.bidiStreamUnion',
+  );
+}
+
+Future<void> _exerciseFbs(FbsGrpcServiceClient stub) async {
+  final messages = [
+    _fbsRequest('fbs-a', 5, 'alpha'),
+    _fbsRequest('fbs-b', 6, 'beta'),
+  ];
+  final first = messages[0];
+  _expect(
+    await stub.unaryMessage(first),
+    _fbsResponse(first, 'unary', 10),
+    'fbs.unaryMessage',
+  );
+  _expectList(await stub.serverStreamMessage(first).toList(), [
+    for (var i = 0; i < 3; i++) _fbsResponse(first, 'server-$i', i),
+  ], 'fbs.serverStreamMessage');
+  _expect(
+    await stub.clientStreamMessage(Stream.fromIterable(messages)),
+    _fbsAggregate(messages),
+    'fbs.clientStreamMessage',
+  );
+  _expectList(
+    await stub.bidiStreamMessage(Stream.fromIterable(messages)).toList(),
+    [
+      for (var i = 0; i < messages.length; i++)
+        _fbsResponse(messages[i], 'bidi-$i', i),
+    ],
+    'fbs.bidiStreamMessage',
+  );
+
+  final unionReqs = [
+    _fbsRequest('fbs-u-a', 7, 'union-alpha'),
+    _fbsRequest('fbs-u-b', 8, 'union-beta'),
+  ];
+  final unions = [for (final r in unionReqs) _fbsUnionRequest(r)];
+  final unionFirst = unionReqs[0];
+  _expect(
+    await stub.unaryUnion(unions[0]),
+    _fbsUnionResponse(unionFirst, 'unary', 10),
+    'fbs.unaryUnion',
+  );
+  _expectList(await stub.serverStreamUnion(unions[0]).toList(), [
+    for (var i = 0; i < 3; i++) _fbsUnionResponse(unionFirst, 'server-$i', i),
+  ], 'fbs.serverStreamUnion');
+  _expect(
+    await stub.clientStreamUnion(Stream.fromIterable(unions)),
+    _fbsUnionAggregate(unionReqs),
+    'fbs.clientStreamUnion',
+  );
+  _expectList(
+    await stub.bidiStreamUnion(Stream.fromIterable(unions)).toList(),
+    [
+      for (var i = 0; i < unionReqs.length; i++)
+        _fbsUnionResponse(unionReqs[i], 'bidi-$i', i),
+    ],
+    'fbs.bidiStreamUnion',
+  );
+}
+
+Future<void> _exercisePb(PbGrpcServiceClient stub) async {
+  final messages = [
+    _pbRequest('pb-a', 9, GrpcPbRequest_Payload.text('alpha')),
+    _pbRequest('pb-b', 10, GrpcPbRequest_Payload.number(42)),
+  ];
+  final first = messages[0];
+  _expect(
+    await stub.unaryMessage(first),
+    _pbResponse(first, 'unary', 10),
+    'pb.unaryMessage',
+  );
+  _expectList(await stub.serverStreamMessage(first).toList(), [
+    for (var i = 0; i < 3; i++) _pbResponse(first, 'server-$i', i),
+  ], 'pb.serverStreamMessage');
+  _expect(
+    await stub.clientStreamMessage(Stream.fromIterable(messages)),
+    _pbAggregate(messages),
+    'pb.clientStreamMessage',
+  );
+  _expectList(
+    await stub.bidiStreamMessage(Stream.fromIterable(messages)).toList(),
+    [
+      for (var i = 0; i < messages.length; i++)
+        _pbResponse(messages[i], 'bidi-$i', i),
+    ],
+    'pb.bidiStreamMessage',
+  );
+}
+
+Future<void> _runClient(String target) async {
+  final parts = target.split(':');
+  final channel = ClientChannel(
+    parts[0],
+    port: int.parse(parts[1]),
+    options: const ChannelOptions(credentials: ChannelCredentials.insecure()),
+  );
+  try {
+    await _exerciseFdl(FdlGrpcServiceClient(channel));
+    await _exerciseFbs(FbsGrpcServiceClient(channel));
+    await _exercisePb(PbGrpcServiceClient(channel));
+  } finally {
+    await channel.shutdown();
+  }
+}
+
+Future<void> _runServer(String portFilePath) async {
+  final server = Server.create(
+    services: [FdlService(), FbsService(), PbService()],
+  );
+  await server.serve(address: InternetAddress.loopbackIPv4, port: 0);
+  await File(portFilePath).writeAsString('${server.port!}', flush: true);
+  await Completer<void>().future;
+}
+
+String _flag(List<String> args, String name) {
+  final i = args.indexOf(name);
+  if (i < 0 || i + 1 >= args.length) {
+    throw ArgumentError('missing $name');
+  }
+  return args[i + 1];
+}
+
+Future<void> main(List<String> args) async {
+  try {
+    if (args.isNotEmpty && args[0] == 'client') {
+      await _runClient(_flag(args, '--target'));
+    } else if (args.isNotEmpty && args[0] == 'server') {
+      await _runServer(_flag(args, '--port-file'));
+    } else {
+      stderr.writeln(
+        'usage: interop.dart <client --target H:P | server --port-file PATH>',
+      );
+      exit(2);
+    }
+  } catch (e, st) {
+    stderr.writeln('interop peer failed: $e\n$st');
+    exit(1);
+  }
+}
diff --git a/integration_tests/grpc_tests/run_tests.sh 
b/integration_tests/grpc_tests/dart/pubspec.yaml
old mode 100755
new mode 100644
similarity index 53%
copy from integration_tests/grpc_tests/run_tests.sh
copy to integration_tests/grpc_tests/dart/pubspec.yaml
index 34fdcc59b..14e21dbf7
--- a/integration_tests/grpc_tests/run_tests.sh
+++ b/integration_tests/grpc_tests/dart/pubspec.yaml
@@ -1,5 +1,3 @@
-#!/bin/bash
-
 # 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
@@ -17,23 +15,18 @@
 # specific language governing permissions and limitations
 # under the License.
 
-set -euo pipefail
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
-TEST_CLASSES="${1:-PythonAsyncGrpcTest,PythonSyncGrpcTest,RustGrpcTest,GoGrpcTest,KotlinGrpcTest}"
+name: fory_grpc_interop
+description: Apache Fory Java/Dart gRPC interop peer
+publish_to: none
+version: 0.0.0
 
-python -m pip install "grpcio>=1.62.2,<1.71"
-python -m pip install -v -e "${ROOT_DIR}/python"
+environment:
+  sdk: ^3.7.0
 
-python "${SCRIPT_DIR}/generate_grpc.py"
+dependencies:
+  grpc: ^4.0.0
+  fory:
+    path: ../../../dart/packages/fory
 
-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}" \
-  test
+dev_dependencies:
+  build_runner: ">=2.7.0 <3.0.0"
diff --git a/integration_tests/grpc_tests/generate_grpc.py 
b/integration_tests/grpc_tests/generate_grpc.py
index e89622b5c..f2950c1fe 100644
--- a/integration_tests/grpc_tests/generate_grpc.py
+++ b/integration_tests/grpc_tests/generate_grpc.py
@@ -38,6 +38,7 @@ OUTPUTS = {
     "rust": TEST_DIR / "rust/generated/src",
     "csharp": TEST_DIR / "csharp/generated",
     "kotlin": TEST_DIR / "kotlin/src/main/kotlin/generated",
+    "dart": TEST_DIR / "dart/lib/generated",
 }
 
 
@@ -81,6 +82,7 @@ def main() -> int:
                 f"--rust_out={OUTPUTS['rust']}",
                 f"--csharp_out={OUTPUTS['csharp']}",
                 f"--kotlin_out={OUTPUTS['kotlin']}",
+                f"--dart_out={OUTPUTS['dart']}",
                 "--grpc",
             ],
             env=env,
diff --git 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcTest.java
 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/DartGrpcTest.java
similarity index 55%
copy from 
integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcTest.java
copy to 
integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/DartGrpcTest.java
index c764a5f9e..743595d23 100644
--- 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcTest.java
+++ 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/DartGrpcTest.java
@@ -20,16 +20,21 @@
 package org.apache.fory.grpc_tests;
 
 import io.grpc.Server;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 import org.testng.annotations.Test;
 
-public class RustGrpcTest extends GrpcTestBase {
+public class DartGrpcTest extends GrpcTestBase {
 
   @Test
-  public void testJavaServerRustClient() throws Exception {
+  public void testJavaServerDartClient() throws Exception {
     Server server = startJavaAllSchemasServer();
     try {
-      runRust("rust-grpc-client", "client", "--target", "127.0.0.1:" + 
server.getPort());
+      runPeer(
+          "dart-grpc-client", dartCommand("client", "--target", "127.0.0.1:" + 
server.getPort()));
     } finally {
       server.shutdownNow();
       server.awaitTermination(10, TimeUnit.SECONDS);
@@ -37,8 +42,22 @@ public class RustGrpcTest extends GrpcTestBase {
   }
 
   @Test
-  public void testJavaClientRustServer() throws Exception {
+  public void testDartServerJavaClient() throws Exception {
     exercisePeerServer(
-        "rust-grpc", "Rust", "fory-grpc-rust-", rustCommand("server"), 
this::exerciseAllSchemas);
+        "dart-grpc", "Dart", "fory-grpc-dart-", dartCommand("server"), 
this::exerciseAllSchemas);
+  }
+
+  private PeerCommand dartCommand(String... args) {
+    Path dartRoot = grpcRoot().resolve("dart");
+    List<String> command = new ArrayList<>();
+    command.add("dart");
+    command.add("run");
+    command.add("bin/interop.dart");
+    command.addAll(Arrays.asList(args));
+    PeerCommand peerCommand = newPeerCommand(dartRoot, command);
+    putEnv(peerCommand, "ENABLE_FORY_DEBUG_OUTPUT", "1");
+    setLocalhostNoProxy(peerCommand);
+    clearProxyEnv(peerCommand);
+    return peerCommand;
   }
 }
diff --git 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GoGrpcTest.java
 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GoGrpcTest.java
index 8bcb3d5ef..fed51db84 100644
--- 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GoGrpcTest.java
+++ 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GoGrpcTest.java
@@ -20,6 +20,10 @@
 package org.apache.fory.grpc_tests;
 
 import io.grpc.Server;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 import org.testng.annotations.Test;
 
@@ -29,7 +33,7 @@ public class GoGrpcTest extends GrpcTestBase {
   public void testJavaServerGoClient() throws Exception {
     Server server = startJavaFdlServer();
     try {
-      runGo("go-grpc-client", "client", "--target", "127.0.0.1:" + 
server.getPort());
+      runPeer("go-grpc-client", goCommand("client", "--target", "127.0.0.1:" + 
server.getPort()));
     } finally {
       server.shutdownNow();
       server.awaitTermination(10, TimeUnit.SECONDS);
@@ -40,4 +44,14 @@ public class GoGrpcTest extends GrpcTestBase {
   public void testGoServerJavaClient() throws Exception {
     exercisePeerServer("go-grpc", "Go", "fory-grpc-go-", goCommand("server"), 
this::exerciseFdl);
   }
+
+  private PeerCommand goCommand(String... args) {
+    Path goRoot = grpcRoot().resolve("go");
+    List<String> command = new ArrayList<>();
+    command.add(goRoot.resolve("grpc-interop").toString());
+    command.addAll(Arrays.asList(args));
+    PeerCommand peerCommand = newPeerCommand(goRoot, command);
+    setLocalhostNoProxy(peerCommand);
+    return peerCommand;
+  }
 }
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 cfec07151..9776493ff 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
@@ -25,7 +25,6 @@ import io.grpc.Server;
 import io.grpc.ServerBuilder;
 import io.grpc.stub.StreamObserver;
 import java.io.ByteArrayOutputStream;
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
@@ -274,155 +273,33 @@ public abstract class GrpcTestBase {
             pbResponse(requests.get(0), "bidi-0", 0), 
pbResponse(requests.get(1), "bidi-1", 1)));
   }
 
-  protected PeerCommand goCommand(String... args) {
-    Path grpcRoot = 
repoRoot().resolve("integration_tests").resolve("grpc_tests");
-    Path goRoot = grpcRoot.resolve("go");
-    List<String> command = new ArrayList<>();
-    command.add(goRoot.resolve("grpc-interop").toString());
-    command.addAll(Arrays.asList(args));
+  protected PeerCommand newPeerCommand(Path workDir, List<String> command) {
     PeerCommand peerCommand = new PeerCommand();
     peerCommand.command = command;
-    peerCommand.workDir = goRoot;
-    peerCommand.environment.put("NO_PROXY", "127.0.0.1,localhost");
-    peerCommand.environment.put("no_proxy", "127.0.0.1,localhost");
+    peerCommand.workDir = workDir;
     return peerCommand;
   }
 
-  protected void runGo(String peer, String... args) throws IOException, 
InterruptedException {
-    Process process = startPeer(goCommand(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();
+  protected void putEnv(PeerCommand command, String key, String value) {
+    command.environment.put(key, value);
   }
 
-  protected PeerCommand pythonCommand(String... args) {
-    Path pythonRoot =
-        
repoRoot().resolve("integration_tests").resolve("grpc_tests").resolve("python");
-    return pythonCommand(
-        "grpc_tests.grpc_peer", 
pythonRoot.resolve("grpc_tests").resolve("generated"), args);
+  protected void setLocalhostNoProxy(PeerCommand command) {
+    command.environment.put("NO_PROXY", "127.0.0.1,localhost");
+    command.environment.put("no_proxy", "127.0.0.1,localhost");
   }
 
-  protected PeerCommand pythonSyncCommand(String... args) {
-    Path pythonRoot =
-        
repoRoot().resolve("integration_tests").resolve("grpc_tests").resolve("python");
-    return pythonCommand(
-        "grpc_sync_tests.grpc_peer",
-        pythonRoot.resolve("grpc_sync_tests").resolve("generated"),
-        args);
-  }
-
-  private PeerCommand pythonCommand(String moduleName, Path generatedRoot, 
String... args) {
-    Path repoRoot = repoRoot();
-    Path grpcRoot = 
repoRoot.resolve("integration_tests").resolve("grpc_tests");
-    Path pythonRoot = grpcRoot.resolve("python");
-    String pythonPath =
-        generatedRoot
-            + File.pathSeparator
-            + pythonRoot
-            + File.pathSeparator
-            + repoRoot.resolve("python");
-    String existingPythonPath = System.getenv("PYTHONPATH");
-    if (existingPythonPath != null && !existingPythonPath.isEmpty()) {
-      pythonPath = pythonPath + File.pathSeparator + existingPythonPath;
-    }
-    List<String> command = new ArrayList<>();
-    command.add("python");
-    command.add("-m");
-    command.add(moduleName);
-    command.addAll(Arrays.asList(args));
-    PeerCommand peerCommand = new PeerCommand();
-    peerCommand.command = command;
-    peerCommand.workDir = grpcRoot;
-    peerCommand.environment.put("PYTHONPATH", pythonPath);
-    peerCommand.environment.put("ENABLE_FORY_CYTHON_SERIALIZATION", "0");
-    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");
-    // Some developer and CI environments set proxy variables that grpcio 
honors
-    // even for localhost unless no_proxy is configured correctly.
+  protected void clearProxyEnv(PeerCommand command) {
+    // Some developer and CI environments set proxy variables that peer clients
+    // can honor even for localhost unless no_proxy is configured correctly.
     for (String proxyVar :
         Arrays.asList(
             "all_proxy", "http_proxy", "https_proxy", "ALL_PROXY", 
"HTTP_PROXY", "HTTPS_PROXY")) {
-      peerCommand.environment.put(proxyVar, "");
+      command.environment.put(proxyVar, "");
     }
-    return peerCommand;
-  }
-
-  protected PeerCommand rustCommand(String... args) {
-    Path repoRoot = repoRoot();
-    Path grpcRoot = 
repoRoot.resolve("integration_tests").resolve("grpc_tests");
-    Path rustRoot = grpcRoot.resolve("rust");
-    List<String> command = new ArrayList<>();
-    command.add("cargo");
-    command.add("run");
-    command.add("--quiet");
-    command.add("--manifest-path");
-    command.add(rustRoot.resolve("interop").resolve("Cargo.toml").toString());
-    command.add("--");
-    command.addAll(Arrays.asList(args));
-    PeerCommand peerCommand = new PeerCommand();
-    peerCommand.command = command;
-    peerCommand.workDir = rustRoot;
-    peerCommand.environment.put("CARGO_TERM_COLOR", "never");
-    peerCommand.environment.put("ENABLE_FORY_DEBUG_OUTPUT", "1");
-    peerCommand.environment.put("RUST_BACKTRACE", "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 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 {
-    runPythonPeer(peer, pythonCommand(args));
-  }
-
-  protected void runPythonSync(String peer, String... args)
-      throws IOException, InterruptedException {
-    runPythonPeer(peer, pythonSyncCommand(args));
-  }
-
-  private void runPythonPeer(String peer, PeerCommand command)
+  protected void runPeer(String peer, PeerCommand command)
       throws IOException, InterruptedException {
     Process process = startPeer(command);
     PeerOutputCollector outputCollector = new 
PeerOutputCollector(process.getInputStream(), peer);
@@ -445,50 +322,6 @@ public abstract class GrpcTestBase {
     outputCollector.awaitOutput();
   }
 
-  protected void runRust(String peer, String... args) throws IOException, 
InterruptedException {
-    Process process = startPeer(rustCommand(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();
-  }
-
-  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);
@@ -526,7 +359,11 @@ public abstract class GrpcTestBase {
     return output.isEmpty() ? "" : "\noutput:\n" + output;
   }
 
-  private Path repoRoot() {
+  protected Path grpcRoot() {
+    return repoRoot().resolve("integration_tests").resolve("grpc_tests");
+  }
+
+  protected Path repoRoot() {
     Path moduleDir = java.nio.file.Paths.get("").toAbsolutePath();
     return moduleDir.getParent().getParent().getParent();
   }
diff --git 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/KotlinGrpcTest.java
 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/KotlinGrpcTest.java
index 3a84e693c..19630dbc5 100644
--- 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/KotlinGrpcTest.java
+++ 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/KotlinGrpcTest.java
@@ -20,6 +20,10 @@
 package org.apache.fory.grpc_tests;
 
 import io.grpc.Server;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 import org.testng.annotations.Test;
 
@@ -29,7 +33,9 @@ public class KotlinGrpcTest extends GrpcTestBase {
   public void testJavaServerKotlinClient() throws Exception {
     Server server = startJavaAllSchemasServer();
     try {
-      runKotlin("kotlin-grpc-client", "client", "--target", "127.0.0.1:" + 
server.getPort());
+      runPeer(
+          "kotlin-grpc-client",
+          kotlinCommand("client", "--target", "127.0.0.1:" + 
server.getPort()));
     } finally {
       server.shutdownNow();
       server.awaitTermination(10, TimeUnit.SECONDS);
@@ -45,4 +51,18 @@ public class KotlinGrpcTest extends GrpcTestBase {
         kotlinCommand("server"),
         this::exerciseAllSchemas);
   }
+
+  private PeerCommand kotlinCommand(String... args) {
+    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 = newPeerCommand(kotlinRoot, command);
+    putEnv(peerCommand, "ENABLE_FORY_DEBUG_OUTPUT", "1");
+    setLocalhostNoProxy(peerCommand);
+    clearProxyEnv(peerCommand);
+    return peerCommand;
+  }
 }
diff --git 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonAsyncGrpcTest.java
 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonAsyncGrpcTest.java
index b0e47838b..25ebb20f2 100644
--- 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonAsyncGrpcTest.java
+++ 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonAsyncGrpcTest.java
@@ -20,6 +20,11 @@
 package org.apache.fory.grpc_tests;
 
 import io.grpc.Server;
+import java.io.File;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 import org.testng.annotations.Test;
 
@@ -29,7 +34,9 @@ public class PythonAsyncGrpcTest extends GrpcTestBase {
   public void testJavaServerPythonClient() throws Exception {
     Server server = startJavaAllSchemasServer();
     try {
-      runPython("python-async-grpc-client", "client", "--target", "127.0.0.1:" 
+ server.getPort());
+      runPeer(
+          "python-async-grpc-client",
+          pythonCommand("client", "--target", "127.0.0.1:" + 
server.getPort()));
     } finally {
       server.shutdownNow();
       server.awaitTermination(10, TimeUnit.SECONDS);
@@ -45,4 +52,33 @@ public class PythonAsyncGrpcTest extends GrpcTestBase {
         pythonCommand("server"),
         this::exerciseAllSchemas);
   }
+
+  private PeerCommand pythonCommand(String... args) {
+    Path repoRoot = repoRoot();
+    Path grpcRoot = grpcRoot();
+    Path pythonRoot = grpcRoot.resolve("python");
+    Path generatedRoot = pythonRoot.resolve("grpc_tests").resolve("generated");
+    String pythonPath =
+        generatedRoot
+            + File.pathSeparator
+            + pythonRoot
+            + File.pathSeparator
+            + repoRoot.resolve("python");
+    String existingPythonPath = System.getenv("PYTHONPATH");
+    if (existingPythonPath != null && !existingPythonPath.isEmpty()) {
+      pythonPath = pythonPath + File.pathSeparator + existingPythonPath;
+    }
+    List<String> command = new ArrayList<>();
+    command.add("python");
+    command.add("-m");
+    command.add("grpc_tests.grpc_peer");
+    command.addAll(Arrays.asList(args));
+    PeerCommand peerCommand = newPeerCommand(grpcRoot, command);
+    putEnv(peerCommand, "PYTHONPATH", pythonPath);
+    putEnv(peerCommand, "ENABLE_FORY_CYTHON_SERIALIZATION", "0");
+    putEnv(peerCommand, "ENABLE_FORY_DEBUG_OUTPUT", "1");
+    setLocalhostNoProxy(peerCommand);
+    clearProxyEnv(peerCommand);
+    return peerCommand;
+  }
 }
diff --git 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonSyncGrpcTest.java
 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonSyncGrpcTest.java
index 3ea572c21..6870f4b8c 100644
--- 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonSyncGrpcTest.java
+++ 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonSyncGrpcTest.java
@@ -20,6 +20,11 @@
 package org.apache.fory.grpc_tests;
 
 import io.grpc.Server;
+import java.io.File;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 import org.testng.annotations.Test;
 
@@ -29,8 +34,9 @@ public class PythonSyncGrpcTest extends GrpcTestBase {
   public void testJavaServerPythonClient() throws Exception {
     Server server = startJavaAllSchemasServer();
     try {
-      runPythonSync(
-          "python-sync-grpc-client", "client", "--target", "127.0.0.1:" + 
server.getPort());
+      runPeer(
+          "python-sync-grpc-client",
+          pythonCommand("client", "--target", "127.0.0.1:" + 
server.getPort()));
     } finally {
       server.shutdownNow();
       server.awaitTermination(10, TimeUnit.SECONDS);
@@ -43,7 +49,36 @@ public class PythonSyncGrpcTest extends GrpcTestBase {
         "python-sync-grpc",
         "Python",
         "fory-grpc-python-sync-",
-        pythonSyncCommand("server"),
+        pythonCommand("server"),
         this::exerciseAllSchemas);
   }
+
+  private PeerCommand pythonCommand(String... args) {
+    Path repoRoot = repoRoot();
+    Path grpcRoot = grpcRoot();
+    Path pythonRoot = grpcRoot.resolve("python");
+    Path generatedRoot = 
pythonRoot.resolve("grpc_sync_tests").resolve("generated");
+    String pythonPath =
+        generatedRoot
+            + File.pathSeparator
+            + pythonRoot
+            + File.pathSeparator
+            + repoRoot.resolve("python");
+    String existingPythonPath = System.getenv("PYTHONPATH");
+    if (existingPythonPath != null && !existingPythonPath.isEmpty()) {
+      pythonPath = pythonPath + File.pathSeparator + existingPythonPath;
+    }
+    List<String> command = new ArrayList<>();
+    command.add("python");
+    command.add("-m");
+    command.add("grpc_sync_tests.grpc_peer");
+    command.addAll(Arrays.asList(args));
+    PeerCommand peerCommand = newPeerCommand(grpcRoot, command);
+    putEnv(peerCommand, "PYTHONPATH", pythonPath);
+    putEnv(peerCommand, "ENABLE_FORY_CYTHON_SERIALIZATION", "0");
+    putEnv(peerCommand, "ENABLE_FORY_DEBUG_OUTPUT", "1");
+    setLocalhostNoProxy(peerCommand);
+    clearProxyEnv(peerCommand);
+    return peerCommand;
+  }
 }
diff --git 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcTest.java
 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcTest.java
index c764a5f9e..1db867290 100644
--- 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcTest.java
+++ 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcTest.java
@@ -20,6 +20,10 @@
 package org.apache.fory.grpc_tests;
 
 import io.grpc.Server;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 import org.testng.annotations.Test;
 
@@ -29,7 +33,8 @@ public class RustGrpcTest extends GrpcTestBase {
   public void testJavaServerRustClient() throws Exception {
     Server server = startJavaAllSchemasServer();
     try {
-      runRust("rust-grpc-client", "client", "--target", "127.0.0.1:" + 
server.getPort());
+      runPeer(
+          "rust-grpc-client", rustCommand("client", "--target", "127.0.0.1:" + 
server.getPort()));
     } finally {
       server.shutdownNow();
       server.awaitTermination(10, TimeUnit.SECONDS);
@@ -41,4 +46,23 @@ public class RustGrpcTest extends GrpcTestBase {
     exercisePeerServer(
         "rust-grpc", "Rust", "fory-grpc-rust-", rustCommand("server"), 
this::exerciseAllSchemas);
   }
+
+  private PeerCommand rustCommand(String... args) {
+    Path rustRoot = grpcRoot().resolve("rust");
+    List<String> command = new ArrayList<>();
+    command.add("cargo");
+    command.add("run");
+    command.add("--quiet");
+    command.add("--manifest-path");
+    command.add(rustRoot.resolve("interop").resolve("Cargo.toml").toString());
+    command.add("--");
+    command.addAll(Arrays.asList(args));
+    PeerCommand peerCommand = newPeerCommand(rustRoot, command);
+    putEnv(peerCommand, "CARGO_TERM_COLOR", "never");
+    putEnv(peerCommand, "ENABLE_FORY_DEBUG_OUTPUT", "1");
+    putEnv(peerCommand, "RUST_BACKTRACE", "1");
+    setLocalhostNoProxy(peerCommand);
+    clearProxyEnv(peerCommand);
+    return peerCommand;
+  }
 }
diff --git a/integration_tests/grpc_tests/run_tests.sh 
b/integration_tests/grpc_tests/run_tests.sh
index 34fdcc59b..784f2f05e 100755
--- a/integration_tests/grpc_tests/run_tests.sh
+++ b/integration_tests/grpc_tests/run_tests.sh
@@ -21,18 +21,37 @@ set -euo pipefail
 
 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
-TEST_CLASSES="${1:-PythonAsyncGrpcTest,PythonSyncGrpcTest,RustGrpcTest,GoGrpcTest,KotlinGrpcTest}"
+TEST_CLASSES="${1:-PythonAsyncGrpcTest,PythonSyncGrpcTest,RustGrpcTest,GoGrpcTest,KotlinGrpcTest,DartGrpcTest}"
 
-python -m pip install "grpcio>=1.62.2,<1.71"
-python -m pip install -v -e "${ROOT_DIR}/python"
+has_test_class() {
+  [[ ",${TEST_CLASSES}," == *",$1,"* ]]
+}
+
+if has_test_class "PythonAsyncGrpcTest" || has_test_class 
"PythonSyncGrpcTest"; then
+  python -m pip install "grpcio>=1.62.2,<1.71"
+  python -m pip install -v -e "${ROOT_DIR}/python"
+fi
 
 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
+if has_test_class "GoGrpcTest"; then
+  cd "${SCRIPT_DIR}/go"
+  go build -o grpc-interop .
+fi
+if has_test_class "RustGrpcTest"; then
+  cargo build --manifest-path "${SCRIPT_DIR}/rust/Cargo.toml" --workspace 
--quiet
+fi
+if has_test_class "KotlinGrpcTest"; then
+  cd "${SCRIPT_DIR}/kotlin"
+  mvn --no-transfer-progress -DskipTests package
+fi
+if has_test_class "DartGrpcTest"; then
+  cd "${SCRIPT_DIR}/dart"
+  dart pub get
+  dart run build_runner build
+  dart analyze bin lib/generated/*/*_grpc.dart
+  dart format --output=none --set-exit-if-changed bin 
lib/generated/*/*_grpc.dart
+fi
 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