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 3ab5cbce4 feat(compiler): support Rust gRPC code generation (#3738)
3ab5cbce4 is described below

commit 3ab5cbce4af85207c535815d146599476236a3fc
Author: Peiyang He <[email protected]>
AuthorDate: Fri Jun 12 14:15:35 2026 +0800

    feat(compiler): support Rust gRPC code generation (#3738)
    
    ## Why?
    
    Support Rust gRPC code generation.
    
    ## What does this PR do?
    
    **overview**:
    
    - support Rust unary/streaming gRPC code generation
    - add Rust&Java interop tests
    Note: in local environment such as Ubuntu, run the test using virtual
    environment:
      ```shell
      cd integration_tests/grpc_tests
      python3 -m venv .venv
      source .venv/bin/activate
      ./run_tests.sh
      ```
    
    **details**:
    
    1. Before generating code, first validate that there is no
    `thread_safe=false` ref usage in gRPC payload types since tonic requires
    all payload types to be thread safe; and move naming collision logic for
    service definition from `compiler/fory_compiler/generators/rust.py` to
    `compiler/fory_compiler/generators/services/rust.py`.
    
    2. For the given IDL, generate tonic-compatiable client and server stub
    code, i.e. `<module_name>_service.rs`, `<module_name>_service_grpc.rs`.
    
    **Note**: considering that codec logic is relatively small, codec code
    is also generated as a separate `mod` inside
    `<module_name>_service_grpc.rs`, instead of defining a new crate under
    `rust/`.
    
      <details>
      <summary>Example</summary>
    
      For this simple service definition:
    
      ```
      package hello;
    
      message HelloRequest {
        string name = 1;
      }
    
      message HelloResponse {
        string greeting = 1;
      }
    
      service Greeter {
        rpc SayHello (HelloRequest) returns (HelloResponse);
      }
      ```
    
      The generated `service.rs` (ignore the copyright header):
    
      ```rust
      #[::tonic::async_trait]
    pub trait Greeter: ::std::marker::Send + ::std::marker::Sync + 'static {
          async fn say_hello(
              &self,
              request: ::tonic::Request<crate::hello::HelloRequest>,
          ) -> ::std::result::Result<
              ::tonic::Response<crate::hello::HelloResponse>,
              ::tonic::Status,
          >;
      }
    
      pub const GREETER_SERVICE_NAME: &str = "hello.Greeter";
      pub const GREETER_SAY_HELLO_PATH: &str = "/hello.Greeter/SayHello";
      ```
    
      The generated `service_grpc.rs` (also ignore the copyright header):
    
      ```rust
      pub mod codec {
          pub trait ForyGrpcPayload: Sized + ::std::marker::Send + 'static {
              fn encode_fory_payload(
                  &self,
    ) -> ::std::result::Result<::std::vec::Vec<u8>, ::fory::Error>;
    
              fn decode_fory_payload(
                  payload: &[u8],
              ) -> ::std::result::Result<Self, ::fory::Error>;
          }
    
          #[derive(Debug, Clone)]
          pub struct ForyCodec<Encode, Decode> {
              marker: ::std::marker::PhantomData<(Encode, Decode)>,
          }
    
          impl<Encode, Decode> ForyCodec<Encode, Decode> {
              pub fn new() -> Self {
                  Self {
                      marker: ::std::marker::PhantomData,
                  }
              }
          }
    
    impl<Encode, Decode> ::std::default::Default for ForyCodec<Encode,
    Decode> {
              fn default() -> Self {
                  Self::new()
              }
          }
    
    impl<Encode, Decode> ::tonic::codec::Codec for ForyCodec<Encode, Decode>
          where
              Encode: ForyGrpcPayload,
              Decode: ForyGrpcPayload,
          {
              type Encode = Encode;
              type Decode = Decode;
              type Encoder = ForyEncoder<Encode>;
              type Decoder = ForyDecoder<Decode>;
    
              fn encoder(&mut self) -> Self::Encoder {
                  ForyEncoder::default()
              }
    
              fn decoder(&mut self) -> Self::Decoder {
                  ForyDecoder::default()
              }
          }
    
          #[derive(Debug, Clone)]
          pub struct ForyEncoder<T> {
              marker: ::std::marker::PhantomData<T>,
          }
    
          impl<T> ::std::default::Default for ForyEncoder<T> {
              fn default() -> Self {
                  Self {
                      marker: ::std::marker::PhantomData,
                  }
              }
          }
    
          #[derive(Debug, Clone)]
          pub struct ForyDecoder<T> {
              marker: ::std::marker::PhantomData<T>,
          }
    
          impl<T> ::std::default::Default for ForyDecoder<T> {
              fn default() -> Self {
                  Self {
                      marker: ::std::marker::PhantomData,
                  }
              }
          }
    
    fn fory_error_to_tonic_status(error: ::fory::Error) -> ::tonic::Status {
              ::tonic::Status::internal(error.to_string())
          }
    
          impl<T> ::tonic::codec::Encoder for ForyEncoder<T>
          where
              T: ForyGrpcPayload,
          {
              type Item = T;
              type Error = ::tonic::Status;
    
              fn encode(
                  &mut self,
                  item: Self::Item,
                  dst: &mut ::tonic::codec::EncodeBuf<'_>,
              ) -> ::std::result::Result<(), Self::Error> {
    let bytes =
    item.encode_fory_payload().map_err(fory_error_to_tonic_status)?;
                  ::tonic::codec::EncodeBuf::reserve(dst, bytes.len());
                  ::bytes::BufMut::put_slice(dst, &bytes);
                  Ok(())
              }
          }
    
          impl<T> ::tonic::codec::Decoder for ForyDecoder<T>
          where
              T: ForyGrpcPayload,
          {
              type Item = T;
              type Error = ::tonic::Status;
    
              fn decode(
                  &mut self,
                  src: &mut ::tonic::codec::DecodeBuf<'_>,
    ) -> ::std::result::Result<::std::option::Option<Self::Item>,
    Self::Error> {
                  let len = ::bytes::Buf::remaining(src);
                  if len == 0 {
                      return Ok(None);
                  }
    
                  let chunk = ::bytes::Buf::chunk(src);
                  if chunk.len() == len {
                      let result =
    T::decode_fory_payload(chunk).map_err(fory_error_to_tonic_status);
                      ::bytes::Buf::advance(src, len);
                      return result.map(Some);
                  }
    
                  let payload = ::bytes::Buf::copy_to_bytes(src, len);
                  T::decode_fory_payload(&payload)
                      .map(Some)
                      .map_err(fory_error_to_tonic_status)
              }
          }
      }
    
      impl codec::ForyGrpcPayload for crate::hello::HelloRequest {
    fn encode_fory_payload(&self) ->
    ::std::result::Result<::std::vec::Vec<u8>, ::fory::Error> {
              self.to_bytes()
          }
    
    fn decode_fory_payload(payload: &[u8]) -> ::std::result::Result<Self,
    ::fory::Error> {
              Self::from_bytes(payload)
          }
      }
    
      impl codec::ForyGrpcPayload for crate::hello::HelloResponse {
    fn encode_fory_payload(&self) ->
    ::std::result::Result<::std::vec::Vec<u8>, ::fory::Error> {
              self.to_bytes()
          }
    
    fn decode_fory_payload(payload: &[u8]) -> ::std::result::Result<Self,
    ::fory::Error> {
              Self::from_bytes(payload)
          }
      }
    
      pub mod greeter_client {
          #[derive(Debug, Clone)]
          pub struct GreeterClient<T> {
              inner: ::tonic::client::Grpc<T>,
          }
    
          impl GreeterClient<::tonic::transport::Channel> {
              pub async fn connect<D>(
                  dst: D,
              ) -> ::std::result::Result<Self, ::tonic::transport::Error>
              where
                  D: ::std::convert::TryInto<::tonic::transport::Endpoint>,
                  D::Error: Into<::tonic::codegen::StdError>,
              {
    let conn = ::tonic::transport::Endpoint::new(dst)?.connect().await?;
                  Ok(Self::new(conn))
              }
          }
    
          impl<T> GreeterClient<T>
          where
              T: ::tonic::client::GrpcService<::tonic::body::Body>,
              T::Error: Into<::tonic::codegen::StdError>,
    T::ResponseBody: ::tonic::codegen::Body<Data = ::tonic::codegen::Bytes>
                  + ::std::marker::Send
                  + 'static,
              <T::ResponseBody as ::tonic::codegen::Body>::Error:
                  Into<::tonic::codegen::StdError> + ::std::marker::Send,
          {
              pub fn new(inner: T) -> Self {
                  let inner = ::tonic::client::Grpc::new(inner);
                  Self { inner }
              }
    
              pub async fn say_hello(
                  &mut self,
    request: impl ::tonic::IntoRequest<crate::hello::HelloRequest>,
              ) -> ::std::result::Result<
                  ::tonic::Response<crate::hello::HelloResponse>,
                  ::tonic::Status,
              > {
                  self.inner.ready().await.map_err(|e| {
    ::tonic::Status::unknown(format!("Service was not ready: {}", e.into()))
                  })?;
                  let codec = super::codec::ForyCodec::<
                      crate::hello::HelloRequest,
                      crate::hello::HelloResponse,
                  >::default();
    let path = ::tonic::codegen::http::uri::PathAndQuery::from_static(
                      crate::service::GREETER_SAY_HELLO_PATH,
                  );
                  let mut req = request.into_request();
    req.extensions_mut().insert(::tonic::codegen::GrpcMethod::new(
                      crate::service::GREETER_SERVICE_NAME,
                      "SayHello",
                  ));
                  self.inner.unary(req, path, codec).await
              }
          }
      }
    
      pub mod greeter_server {
          #[derive(Debug)]
          pub struct GreeterServer<T> {
              inner: ::std::sync::Arc<T>,
          }
    
          impl<T> GreeterServer<T> {
              pub fn new(inner: T) -> Self {
                  Self::from_arc(::std::sync::Arc::new(inner))
              }
    
              pub fn from_arc(inner: ::std::sync::Arc<T>) -> Self {
                  Self { inner }
              }
          }
    
    impl<T, B> ::tonic::codegen::Service<::tonic::codegen::http::Request<B>>
    for GreeterServer<T>
          where
              T: crate::service::Greeter,
              B: ::tonic::codegen::Body + ::std::marker::Send + 'static,
    B::Error: Into<::tonic::codegen::StdError> + ::std::marker::Send +
    'static,
          {
    type Response = ::tonic::codegen::http::Response<::tonic::body::Body>;
              type Error = ::std::convert::Infallible;
    type Future = ::tonic::codegen::BoxFuture<Self::Response, Self::Error>;
    
              fn poll_ready(
                  &mut self,
                  _cx: &mut ::std::task::Context<'_>,
    ) -> ::std::task::Poll<::std::result::Result<(), Self::Error>> {
                  ::std::task::Poll::Ready(Ok(()))
              }
    
    fn call(&mut self, req: ::tonic::codegen::http::Request<B>) ->
    Self::Future {
                  match req.uri().path() {
                      crate::service::GREETER_SAY_HELLO_PATH => {
    struct SayHelloSvc<T: crate::service::Greeter>(pub ::std::sync::Arc<T>);
    
    impl<T: crate::service::Greeter>
    ::tonic::server::UnaryService<crate::hello::HelloRequest>
                              for SayHelloSvc<T>
                          {
                              type Response = crate::hello::HelloResponse;
                              type Future = ::tonic::codegen::BoxFuture<
                                  ::tonic::Response<Self::Response>,
                                  ::tonic::Status,
                              >;
    
                              fn call(
                                  &mut self,
    request: ::tonic::Request<crate::hello::HelloRequest>,
                              ) -> Self::Future {
    let inner = ::std::sync::Arc::clone(&self.0);
                                  let fut = async move {
    <T as crate::service::Greeter>::say_hello(&inner, request).await
                                  };
                                  ::std::boxed::Box::pin(fut)
                              }
                          }
    
                          let inner = self.inner.clone();
                          let fut = async move {
                              let method = SayHelloSvc(inner);
                              let codec = super::codec::ForyCodec::<
                                  crate::hello::HelloResponse,
                                  crate::hello::HelloRequest,
                              >::default();
    let mut grpc = ::tonic::server::Grpc::new(codec);
                              let res = grpc.unary(method, req).await;
                              Ok(res)
                          };
                          ::std::boxed::Box::pin(fut)
                      }
                      _ => ::std::boxed::Box::pin(async move {
    let mut response = ::tonic::codegen::http::Response::new(
                              ::tonic::body::Body::default(),
                          );
                          let headers = response.headers_mut();
                          headers.insert(
                              ::tonic::Status::GRPC_STATUS,
                              (::tonic::Code::Unimplemented as i32).into(),
                          );
                          headers.insert(
                              ::tonic::codegen::http::header::CONTENT_TYPE,
                              ::tonic::metadata::GRPC_CONTENT_TYPE,
                          );
                          Ok(response)
                      }),
                  }
              }
          }
    
          impl<T> ::std::clone::Clone for GreeterServer<T> {
              fn clone(&self) -> Self {
                  Self {
                      inner: self.inner.clone(),
                  }
              }
          }
    
    pub const SERVICE_NAME: &str = crate::service::GREETER_SERVICE_NAME;
    
          impl<T> ::tonic::server::NamedService for GreeterServer<T> {
              const NAME: &'static str = SERVICE_NAME;
          }
      }
      ```
      </details>
    
    3. Add interop testing logic for Java and Rust, implement gRPC handlers
    and assertions so that Rust&Java interop testing can work, and also
    reserve a placeholder crate `generated` to hold the generated gRPC code.
    
    **next:**
    
    - add documentation when above implementation is stable.
    - performance (mainly deserializaion) benchmarking and tuning.
    
    ## Related issues
    
    https://github.com/apache/fory/issues/3275
    
    ## 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?
    
    N/A.
    
    ## Benchmark
    
    N/A.
---
 .github/workflows/ci.yml                           |   9 +-
 .gitignore                                         |   8 +
 compiler/fory_compiler/generators/rust.py          | 106 +--
 compiler/fory_compiler/generators/services/rust.py | 900 ++++++++++++++++++++
 .../fory_compiler/tests/test_service_codegen.py    | 102 ++-
 integration_tests/grpc_tests/generate_grpc.py      |   2 +
 .../apache/fory/grpc_tests/GrpcInteropTest.java    | 118 ++-
 integration_tests/grpc_tests/run_tests.sh          |   1 +
 .../grpc_tests/{run_tests.sh => rust/Cargo.toml}   |  33 +-
 .../{run_tests.sh => rust/generated/Cargo.toml}    |  27 +-
 .../grpc_tests/rust/generated/src/lib.rs           |  26 +
 .../{run_tests.sh => rust/interop/Cargo.toml}      |  27 +-
 .../grpc_tests/rust/interop/src/main.rs            | 909 +++++++++++++++++++++
 13 files changed, 2121 insertions(+), 147 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d9c794f51..6506f5152 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -688,7 +688,7 @@ jobs:
         run: python ./ci/run_ci.py java --version integration_tests
 
   grpc_tests:
-    name: Java/Python gRPC Tests
+    name: Java/Python/Go/Rust gRPC Tests
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v5
@@ -709,11 +709,16 @@ jobs:
           key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
           restore-keys: |
             ${{ runner.os }}-maven-
+      - name: Cache Cargo dependencies
+        uses: Swatinem/rust-cache@v2
+        with:
+          workspaces: integration_tests/grpc_tests/rust
+          cache-bin: "false"
       - 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: Run Java/Python gRPC Tests
+      - name: Run Java/Python/Go/Rust gRPC Tests
         run: ./integration_tests/grpc_tests/run_tests.sh
 
   javascript:
diff --git a/.gitignore b/.gitignore
index 55c36b011..ba7a5f9ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,6 +38,12 @@ bazel-*/
 bazel-fory/
 bazel-fory/**
 **/generated
+!integration_tests/grpc_tests/rust/generated/
+integration_tests/grpc_tests/rust/generated/*
+!integration_tests/grpc_tests/rust/generated/Cargo.toml
+!integration_tests/grpc_tests/rust/generated/src/
+integration_tests/grpc_tests/rust/generated/src/*
+!integration_tests/grpc_tests/rust/generated/src/lib.rs
 java/**/generated
 
 javascript/**/dist/
@@ -78,6 +84,7 @@ bazel-genfiles//
 
 # Go
 go/**/build/
+integration_tests/grpc_tests/go/grpc-interop
 
 # Rust
 rust/target/
@@ -131,6 +138,7 @@ benchmarks/python/proto/
 benchmarks/java/dependency-reduced-pom.xml
 benchmarks/go/go.test
 benchmarks/rust/Cargo.lock
+integration_tests/grpc_tests/rust/Cargo.lock
 docs/superpowers
 test.md
 
diff --git a/compiler/fory_compiler/generators/rust.py 
b/compiler/fory_compiler/generators/rust.py
index 4569b26e6..54ba2f758 100644
--- a/compiler/fory_compiler/generators/rust.py
+++ b/compiler/fory_compiler/generators/rust.py
@@ -20,6 +20,7 @@
 from pathlib import Path
 from typing import Dict, List, Optional, Set, Tuple
 
+from fory_compiler.generators.services.rust import RustServiceGeneratorMixin
 from fory_compiler.generators.base import BaseGenerator, GeneratedFile
 from fory_compiler.frontend.utils import parse_idl_file
 from fory_compiler.ir.ast import (
@@ -39,7 +40,7 @@ from fory_compiler.ir.ast import (
 from fory_compiler.ir.types import PrimitiveKind
 
 
-class RustGenerator(BaseGenerator):
+class RustGenerator(RustServiceGeneratorMixin, BaseGenerator):
     """Generates Rust types with Fory derive macros."""
 
     language_name = "rust"
@@ -461,87 +462,6 @@ class RustGenerator(BaseGenerator):
     def generate(self) -> List[GeneratedFile]:
         """Generate Rust files for the schema."""
         files = []
-        if self.options.grpc:
-            # Allocate and validate identifier naming for gRPC service 
definition.
-            self._ensure_name_caches(self.schema)
-            schema_id = id(self.schema)
-            if schema_id not in self._named_service_schema_ids:
-                schema_source_key = 
str(Path(self.schema.source_file).resolve())
-                services = [
-                    service
-                    for service in self.schema.services
-                    if str(Path(service.location.file).resolve()) == 
schema_source_key
-                ]
-                used_traits: Dict[str, str] = {}
-                used_modules: Dict[str, str] = {}
-                used_constants: Dict[str, str] = {}
-                for service in services:
-                    service_key = self._cache_key(service)
-                    self._service_trait_identifier_cache[service_key] = (
-                        self._allocate_scoped_identifier(
-                            self.to_pascal_case(service.name),
-                            used_traits,
-                            "Rust gRPC service traits",
-                            service.name,
-                        )
-                    )
-                    self._service_client_module_identifier_cache[service_key] 
= (
-                        self._allocate_scoped_identifier(
-                            f"{self.to_snake_case(service.name)}_client",
-                            used_modules,
-                            "Rust gRPC service modules",
-                            f"{service.name} client module",
-                        )
-                    )
-                    self._service_server_module_identifier_cache[service_key] 
= (
-                        self._allocate_scoped_identifier(
-                            f"{self.to_snake_case(service.name)}_server",
-                            used_modules,
-                            "Rust gRPC service modules",
-                            f"{service.name} server module",
-                        )
-                    )
-                    self._service_name_constant_identifier_cache[service_key] 
= (
-                        self._allocate_scoped_identifier(
-                            
f"{self.to_upper_snake_case(service.name)}_SERVICE_NAME",
-                            used_constants,
-                            "Rust gRPC service constants",
-                            service.name,
-                        )
-                    )
-                    used_methods: Dict[str, str] = {}
-                    used_stream_types: Dict[str, str] = {}
-                    method_names: Dict[Tuple[object, ...], str] = {}
-                    stream_types: Dict[Tuple[object, ...], str] = {}
-                    path_constants: Dict[Tuple[object, ...], str] = {}
-                    for method in service.methods:
-                        method_key = self._cache_key(method)
-                        method_names[method_key] = 
self._allocate_scoped_identifier(
-                            self.to_snake_case(method.name),
-                            used_methods,
-                            f"Rust gRPC service {service.name} methods",
-                            method.name,
-                        )
-                        if method.server_streaming:
-                            stream_types[method_key] = 
self._allocate_scoped_identifier(
-                                f"{self.to_pascal_case(method.name)}Stream",
-                                used_stream_types,
-                                f"Rust gRPC service {service.name} stream 
types",
-                                method.name,
-                            )
-                        path_constants[method_key] = 
self._allocate_scoped_identifier(
-                            f"{self.to_upper_snake_case(service.name)}_"
-                            f"{self.to_upper_snake_case(method.name)}_PATH",
-                            used_constants,
-                            "Rust gRPC service constants",
-                            f"{service.name}.{method.name}",
-                        )
-                    self._rpc_method_identifier_cache[service_key] = 
method_names
-                    self._rpc_stream_type_identifier_cache[service_key] = 
stream_types
-                    self._rpc_path_constant_identifier_cache[service_key] = (
-                        path_constants
-                    )
-                self._named_service_schema_ids.add(schema_id)
 
         # Generate a single module file with all types
         files.append(self.generate_module())
@@ -1063,6 +983,28 @@ class RustGenerator(BaseGenerator):
                 break
         return lineage
 
+    def _parent_stack_for_type(self, type_def: object) -> List[Message]:
+        def visit(current: Message, parents: List[Message]) -> 
Optional[List[Message]]:
+            if current is type_def:
+                return parents
+            for nested_union in current.nested_unions:
+                if nested_union is type_def:
+                    return parents + [current]
+            for nested_enum in current.nested_enums:
+                if nested_enum is type_def:
+                    return parents + [current]
+            for nested_message in current.nested_messages:
+                found = visit(nested_message, parents + [current])
+                if found is not None:
+                    return found
+            return None
+
+        for top in self.schema.messages:
+            found = visit(top, [])
+            if found is not None:
+                return found
+        return []
+
     def union_supports_trait(
         self,
         union: Union,
diff --git a/compiler/fory_compiler/generators/services/rust.py 
b/compiler/fory_compiler/generators/services/rust.py
new file mode 100644
index 000000000..16cf5954e
--- /dev/null
+++ b/compiler/fory_compiler/generators/services/rust.py
@@ -0,0 +1,900 @@
+# 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.
+
+"""Rust gRPC service generator helpers."""
+
+from typing import Dict, List, Optional, Set, Tuple
+
+from fory_compiler.generators.base import GeneratedFile
+from fory_compiler.ir.ast import (
+    ArrayType,
+    Field,
+    FieldType,
+    ListType,
+    MapType,
+    Message,
+    NamedType,
+    PrimitiveType,
+    RpcMethod,
+    Service,
+    Union,
+)
+
+
+class RustServiceGeneratorMixin:
+    """Generates Rust gRPC service companions."""
+
+    def generate_services(self) -> List[GeneratedFile]:
+        """Generate Rust grpc service companion module."""
+        local_services = [
+            service
+            for service in self.schema.services
+            if not self.is_imported_type(service)
+        ]
+        if not local_services:
+            return []
+        self.validate_grpc_payload_safe(local_services)
+        self.allocate_grpc_service_identifiers(local_services)
+        return [
+            self.generate_service_api_module(local_services),
+            self.generate_service_grpc_module(local_services),
+        ]
+
+    def allocate_grpc_service_identifiers(self, services: List[Service]) -> 
None:
+        """Allocate sanitized identifiers used by generated gRPC code."""
+        self._ensure_name_caches(self.schema)
+        schema_id = id(self.schema)
+        if schema_id in self._named_service_schema_ids:
+            return
+        used_traits: Dict[str, str] = {}
+        used_modules: Dict[str, str] = {}
+        used_constants: Dict[str, str] = {}
+        for service in services:
+            service_key = self._cache_key(service)
+            self._service_trait_identifier_cache[service_key] = (
+                self._allocate_scoped_identifier(
+                    self.to_pascal_case(service.name),
+                    used_traits,
+                    "Rust gRPC service traits",
+                    service.name,
+                )
+            )
+            self._service_client_module_identifier_cache[service_key] = (
+                self._allocate_scoped_identifier(
+                    f"{self.to_snake_case(service.name)}_client",
+                    used_modules,
+                    "Rust gRPC service modules",
+                    f"{service.name} client module",
+                )
+            )
+            self._service_server_module_identifier_cache[service_key] = (
+                self._allocate_scoped_identifier(
+                    f"{self.to_snake_case(service.name)}_server",
+                    used_modules,
+                    "Rust gRPC service modules",
+                    f"{service.name} server module",
+                )
+            )
+            self._service_name_constant_identifier_cache[service_key] = (
+                self._allocate_scoped_identifier(
+                    f"{self.to_upper_snake_case(service.name)}_SERVICE_NAME",
+                    used_constants,
+                    "Rust gRPC service constants",
+                    service.name,
+                )
+            )
+            used_methods: Dict[str, str] = {}
+            used_stream_types: Dict[str, str] = {}
+            method_names: Dict[Tuple[object, ...], str] = {}
+            stream_types: Dict[Tuple[object, ...], str] = {}
+            path_constants: Dict[Tuple[object, ...], str] = {}
+            for method in service.methods:
+                method_key = self._cache_key(method)
+                method_name = self.to_snake_case(method.name)
+                # Client stub code uses `new` and `connect` as helper method 
names.
+                if method_name in {"connect", "new"}:
+                    method_name = f"{method_name}_"
+                method_names[method_key] = self._allocate_scoped_identifier(
+                    method_name,
+                    used_methods,
+                    f"Rust gRPC service {service.name} methods",
+                    method.name,
+                )
+                if method.server_streaming:
+                    stream_types[method_key] = 
self._allocate_scoped_identifier(
+                        f"{self.to_pascal_case(method.name)}Stream",
+                        used_stream_types,
+                        f"Rust gRPC service {service.name} stream types",
+                        method.name,
+                    )
+                path_constants[method_key] = self._allocate_scoped_identifier(
+                    f"{self.to_upper_snake_case(service.name)}_"
+                    f"{self.to_upper_snake_case(method.name)}_PATH",
+                    used_constants,
+                    "Rust gRPC service constants",
+                    f"{service.name}.{method.name}",
+                )
+            self._rpc_method_identifier_cache[service_key] = method_names
+            self._rpc_stream_type_identifier_cache[service_key] = stream_types
+            self._rpc_path_constant_identifier_cache[service_key] = 
path_constants
+        self._named_service_schema_ids.add(schema_id)
+
+    def grpc_payload_root_names(self, services: List[Service]) -> Set[str]:
+        names: Set[str] = set()
+        for service in services:
+            for method in service.methods:
+                names.add(method.request_type.name)
+                names.add(method.response_type.name)
+        return names
+
+    def validate_grpc_payload_safe(self, services: List[Service]) -> None:
+        # Make sure all gRPC payload types are thread-safe. For now only 
reject pointer with `thread_safe=false`.
+        reachable: Set[int] = set()
+
+        def reject_explicit_non_thread_safe_ref(
+            path: List[str], description: str, ref_options: dict
+        ) -> None:
+            if ref_options.get("thread_safe_pointer") is False:
+                raise ValueError(
+                    f"Rust gRPC payload type {'.'.join(path)} uses 
non-thread-safe {description}; "
+                    "remove thread_safe=false or use the default thread-safe 
pointer"
+                )
+
+        def visit_named(
+            type_name: str,
+            path: List[str],
+            parent_stack: Optional[List[Message]] = None,
+        ) -> None:
+            resolved = self.resolve_named_type(type_name, parent_stack)
+            if resolved is None:
+                return
+            key = id(resolved)
+            if key in reachable:
+                return
+            reachable.add(key)
+            owner_stack = self._parent_stack_for_type(resolved)
+            if isinstance(resolved, Message):
+                lineage = owner_stack + [resolved]
+                for field in resolved.fields:
+                    visit_field(field, path + [field.name], lineage)
+            elif isinstance(resolved, Union):
+                for field in resolved.fields:
+                    visit_field(
+                        field,
+                        path + [self.to_pascal_case(field.name)],
+                        owner_stack,
+                    )
+
+        def visit_field(
+            field: Field,
+            path: List[str],
+            parent_stack: Optional[List[Message]],
+        ) -> None:
+            if field.ref:
+                reject_explicit_non_thread_safe_ref(path, "ref", 
field.ref_options)
+            if isinstance(field.field_type, ListType) and field.element_ref:
+                reject_explicit_non_thread_safe_ref(
+                    path, "list element ref", field.element_ref_options
+                )
+            visit_field_type(field.field_type, path, parent_stack)
+
+        def visit_field_type(
+            field_type: FieldType,
+            path: List[str],
+            parent_stack: Optional[List[Message]],
+        ) -> None:
+            if isinstance(field_type, PrimitiveType):
+                return
+            if isinstance(field_type, NamedType):
+                visit_named(field_type.name, path + [field_type.name], 
parent_stack)
+            elif isinstance(field_type, ListType):
+                if field_type.element_ref:
+                    reject_explicit_non_thread_safe_ref(
+                        path, "list element ref", 
field_type.element_ref_options
+                    )
+                visit_field_type(field_type.element_type, path, parent_stack)
+            elif isinstance(field_type, ArrayType):
+                visit_field_type(field_type.element_type, path, parent_stack)
+            elif isinstance(field_type, MapType):
+                if field_type.value_ref:
+                    reject_explicit_non_thread_safe_ref(
+                        path, "map value ref", field_type.value_ref_options
+                    )
+                visit_field_type(field_type.key_type, path, parent_stack)
+                visit_field_type(field_type.value_type, path, parent_stack)
+
+        for root in self.grpc_payload_root_names(services):
+            visit_named(root, [root])
+
+    def generate_service_api_module(self, services: List[Service]) -> 
GeneratedFile:
+        """Generate Rust service API module (service.rs)."""
+        lines: List[str] = []
+        lines.append(self.get_license_header("//"))
+        lines.append("")
+        for i, service in enumerate(services):
+            if i > 0:
+                lines.append("")
+            lines.extend(self.generate_service_trait(service))
+            lines.append("")
+            lines.extend(self.generate_service_constants(service))
+        return GeneratedFile(
+            path=f"{self.get_module_name()}_service.rs",
+            content="\n".join(lines).rstrip() + "\n",
+        )
+
+    def generate_service_trait(self, service: Service) -> List[str]:
+        """Generate tonic service trait."""
+        service_key = self._cache_key(service)
+        trait_name = self._service_trait_identifier_cache[service_key]
+        lines: List[str] = []
+        lines.append("#[::tonic::async_trait]")
+        lines.append(
+            f"pub trait {trait_name}: ::std::marker::Send + 
::std::marker::Sync + 'static {{"
+        )
+        for i, method in enumerate(service.methods):
+            if i > 0:
+                lines.append("")
+            lines.extend(
+                self.indent_lines(
+                    self.generate_grpc_method_signature(service, method), 1
+                )
+            )
+        lines.append("}")
+        return lines
+
+    def generate_grpc_method_signature(
+        self, service: Service, method: RpcMethod
+    ) -> List[str]:
+        """Generate tonic service trait method signature."""
+        service_key = self._cache_key(service)
+        method_key = self._cache_key(method)
+        method_name = 
self._rpc_method_identifier_cache[service_key][method_key]
+        request_type = self.service_type_path(method.request_type)
+        response_type = self.service_type_path(method.response_type)
+        lines: List[str] = []
+        if method.client_streaming:
+            request_arg_type = 
f"::tonic::Request<::tonic::Streaming<{request_type}>>"
+        else:
+            request_arg_type = f"::tonic::Request<{request_type}>"
+        if method.server_streaming:
+            stream_type = self._rpc_stream_type_identifier_cache[service_key][
+                method_key
+            ]
+            response_return_type = f"::tonic::Response<Self::{stream_type}>"
+            lines.extend(
+                [
+                    f"type {stream_type}: 
::tonic::codegen::tokio_stream::Stream<",
+                    f"    Item = ::std::result::Result<{response_type}, 
::tonic::Status>,",
+                    "> + ::std::marker::Send + 'static;",
+                    "",
+                ]
+            )
+        else:
+            response_return_type = f"::tonic::Response<{response_type}>"
+        lines.extend(
+            [
+                f"async fn {method_name}(",
+                "    &self,",
+                f"    request: {request_arg_type},",
+                ") -> ::std::result::Result<",
+                f"    {response_return_type},",
+                "    ::tonic::Status,",
+                ">;",
+            ]
+        )
+        return lines
+
+    def service_type_path(self, named_type: NamedType) -> str:
+        """Get Rust path for a gRPC request or response type."""
+        resolved = self.resolve_named_type(named_type.name, parent_stack=None)
+        if resolved is None:
+            raise ValueError(f"Unknown gRPC payload type {named_type.name!r}")
+        type_path = self.get_type_path(resolved, 
self._parent_stack_for_type(resolved))
+        if self.is_imported_type(resolved):
+            module = self._module_name_for_type(resolved)
+            return f"crate::{module}::{type_path}"
+        module = self.get_top_level_module_identifier(self.package)
+        return f"crate::{module}::{type_path}"
+
+    def generate_service_constants(self, service: Service) -> List[str]:
+        """Generate service and gRPC path constants."""
+        # e.g. `pub const GREETER_SERVICE_NAME: &str = 
"demo.greeter.Greeter";` and
+        # `pub const GREETER_SAY_HELLO_PATH: &str = 
"/demo.greeter.Greeter/SayHello";`
+        lines: List[str] = []
+        service_key = self._cache_key(service)
+        service_name_const = 
self._service_name_constant_identifier_cache[service_key]
+        service_name = self.get_grpc_service_name(service)
+        lines.append(
+            f"pub const {service_name_const}: &str = "
+            f"{self.rust_string_literal(service_name)};"
+        )
+        for method in service.methods:
+            method_key = self._cache_key(method)
+            rpc_path_const = 
self._rpc_path_constant_identifier_cache[service_key][
+                method_key
+            ]
+            lines.append(
+                f"pub const {rpc_path_const}: &str = "
+                f"{self.rust_string_literal(self.get_grpc_method_path(service, 
method))};"
+            )
+        return lines
+
+    def generate_service_grpc_module(self, services: List[Service]) -> 
GeneratedFile:
+        """Generate Rust service tonic binding module (service_grpc.rs)."""
+        lines: List[str] = []
+        lines.append(self.get_license_header("//"))
+        lines.append("")
+        lines.extend(self.generate_grpc_codec_module())
+        payload_impls = self.generate_grpc_payload_impls(services)
+        if payload_impls:
+            lines.append("")
+            lines.extend(payload_impls)
+        for service in services:
+            lines.append("")
+            lines.extend(self.generate_grpc_client_module(service))
+            lines.append("")
+            lines.extend(self.generate_grpc_server_module(service))
+        return GeneratedFile(
+            path=f"{self.get_module_name()}_service_grpc.rs",
+            content="\n".join(lines).rstrip() + "\n",
+        )
+
+    def generate_grpc_codec_module(self) -> List[str]:
+        """Generate Fory-backed codec used by Rust gRPC stubs, instead of 
relying on a separate crate."""
+        return [
+            "pub mod codec {",
+            "    pub trait ForyGrpcPayload: Sized + ::std::marker::Send + 
'static {",
+            "        fn encode_fory_payload(",
+            "            &self,",
+            "        ) -> ::std::result::Result<::std::vec::Vec<u8>, 
::fory::Error>;",
+            "",
+            "        fn decode_fory_payload(",
+            "            payload: &[u8],",
+            "        ) -> ::std::result::Result<Self, ::fory::Error>;",
+            "    }",
+            "",
+            "    #[derive(Debug, Clone)]",
+            "    pub struct ForyCodec<Encode, Decode> {",
+            "        marker: ::std::marker::PhantomData<(Encode, Decode)>,",
+            "    }",
+            "",
+            "    impl<Encode, Decode> ForyCodec<Encode, Decode> {",
+            "        pub fn new() -> Self {",
+            "            Self {",
+            "                marker: ::std::marker::PhantomData,",
+            "            }",
+            "        }",
+            "    }",
+            "",
+            "    impl<Encode, Decode> ::std::default::Default for 
ForyCodec<Encode, Decode> {",
+            "        fn default() -> Self {",
+            "            Self::new()",
+            "        }",
+            "    }",
+            "",
+            "    impl<Encode, Decode> ::tonic::codec::Codec for 
ForyCodec<Encode, Decode>",
+            "    where",
+            "        Encode: ForyGrpcPayload,",
+            "        Decode: ForyGrpcPayload,",
+            "    {",
+            "        type Encode = Encode;",
+            "        type Decode = Decode;",
+            "        type Encoder = ForyEncoder<Encode>;",
+            "        type Decoder = ForyDecoder<Decode>;",
+            "",
+            "        fn encoder(&mut self) -> Self::Encoder {",
+            "            ForyEncoder::default()",
+            "        }",
+            "",
+            "        fn decoder(&mut self) -> Self::Decoder {",
+            "            ForyDecoder::default()",
+            "        }",
+            "    }",
+            "",
+            "    #[derive(Debug, Clone)]",
+            "    pub struct ForyEncoder<T> {",
+            "        marker: ::std::marker::PhantomData<T>,",
+            "    }",
+            "",
+            "    impl<T> ::std::default::Default for ForyEncoder<T> {",
+            "        fn default() -> Self {",
+            "            Self {",
+            "                marker: ::std::marker::PhantomData,",
+            "            }",
+            "        }",
+            "    }",
+            "",
+            "    #[derive(Debug, Clone)]",
+            "    pub struct ForyDecoder<T> {",
+            "        marker: ::std::marker::PhantomData<T>,",
+            "    }",
+            "",
+            "    impl<T> ::std::default::Default for ForyDecoder<T> {",
+            "        fn default() -> Self {",
+            "            Self {",
+            "                marker: ::std::marker::PhantomData,",
+            "            }",
+            "        }",
+            "    }",
+            "",
+            "    fn fory_error_to_tonic_status(error: ::fory::Error) -> 
::tonic::Status {",
+            "        ::tonic::Status::internal(error.to_string())",
+            "    }",
+            "",
+            "    impl<T> ::tonic::codec::Encoder for ForyEncoder<T>",
+            "    where",
+            "        T: ForyGrpcPayload,",
+            "    {",
+            "        type Item = T;",
+            "        type Error = ::tonic::Status;",
+            "",
+            "        fn encode(",
+            "            &mut self,",
+            "            item: Self::Item,",
+            "            dst: &mut ::tonic::codec::EncodeBuf<'_>,",
+            "        ) -> ::std::result::Result<(), Self::Error> {",
+            "            let bytes = 
item.encode_fory_payload().map_err(fory_error_to_tonic_status)?;",
+            "            ::tonic::codec::EncodeBuf::reserve(dst, 
bytes.len());",
+            "            ::bytes::BufMut::put_slice(dst, &bytes);",
+            "            Ok(())",
+            "        }",
+            "    }",
+            "",
+            "    impl<T> ::tonic::codec::Decoder for ForyDecoder<T>",
+            "    where",
+            "        T: ForyGrpcPayload,",
+            "    {",
+            "        type Item = T;",
+            "        type Error = ::tonic::Status;",
+            "",
+            "        fn decode(",
+            "            &mut self,",
+            "            src: &mut ::tonic::codec::DecodeBuf<'_>,",
+            "        ) -> 
::std::result::Result<::std::option::Option<Self::Item>, Self::Error> {",
+            "            let len = ::bytes::Buf::remaining(src);",
+            "            if len == 0 {",
+            "                return Ok(None);",
+            "            }",
+            "",
+            "            let chunk = ::bytes::Buf::chunk(src);",
+            "            if chunk.len() == len {",
+            "                let result =",
+            "                    
T::decode_fory_payload(chunk).map_err(fory_error_to_tonic_status);",
+            "                ::bytes::Buf::advance(src, len);",
+            "                return result.map(Some);",
+            "            }",
+            "",
+            "            let payload = ::bytes::Buf::copy_to_bytes(src, len);",
+            "            T::decode_fory_payload(&payload)",
+            "                .map(Some)",
+            "                .map_err(fory_error_to_tonic_status)",
+            "        }",
+            "    }",
+            "}",
+        ]
+
+    def generate_grpc_payload_impls(self, services: List[Service]) -> 
List[str]:
+        """Generate `ForyGrpcPayload` trait impls for all gRPC payload 
types."""
+        lines: List[str] = []
+        for i, type_path in enumerate(self.grpc_payload_type_paths(services)):
+            if i > 0:
+                lines.append("")
+            lines.extend(self.generate_grpc_payload_impl(type_path))
+        return lines
+
+    def grpc_payload_type_paths(self, services: List[Service]) -> List[str]:
+        """Get unique request and response type paths in service order."""
+        seen: Set[str] = set()
+        type_paths: List[str] = []
+        for service in services:
+            for method in service.methods:
+                for named_type in (method.request_type, method.response_type):
+                    type_path = self.service_type_path(named_type)
+                    if type_path in seen:
+                        continue
+                    seen.add(type_path)
+                    type_paths.append(type_path)
+        return type_paths
+
+    def generate_grpc_payload_impl(self, type_path: str) -> List[str]:
+        """Generate `ForyGrpcPayload` trait impl for one gRPC request or 
response type."""
+        return [
+            f"impl codec::ForyGrpcPayload for {type_path} {{",
+            "    fn encode_fory_payload(&self) -> 
::std::result::Result<::std::vec::Vec<u8>, ::fory::Error> {",
+            "        self.to_bytes()",
+            "    }",
+            "",
+            "    fn decode_fory_payload(payload: &[u8]) -> 
::std::result::Result<Self, ::fory::Error> {",
+            "        Self::from_bytes(payload)",
+            "    }",
+            "}",
+        ]
+
+    def generate_grpc_client_module(self, service: Service) -> List[str]:
+        """Generate tonic client module for one service."""
+        lines: List[str] = []
+        service_key = self._cache_key(service)
+        client_module = 
self._service_client_module_identifier_cache[service_key]
+        lines.append(f"pub mod {client_module} {{")
+        
lines.extend(self.indent_lines(self.generate_grpc_client_struct(service), 1))
+        lines.append("")
+        lines.extend(
+            self.indent_lines(self.generate_grpc_client_connect_impl(service), 
1)
+        )
+        lines.append("")
+        
lines.extend(self.indent_lines(self.generate_grpc_client_impl(service), 1))
+        lines.append("}")
+        return lines
+
+    def generate_grpc_client_struct(self, service: Service) -> List[str]:
+        """Generate tonic client wrapper struct."""
+        service_key = self._cache_key(service)
+        client_name = 
f"{self._service_trait_identifier_cache[service_key]}Client"
+        return [
+            "#[derive(Debug, Clone)]",
+            f"pub struct {client_name}<T> {{",
+            "    inner: ::tonic::client::Grpc<T>,",
+            "}",
+        ]
+
+    def generate_grpc_client_connect_impl(self, service: Service) -> List[str]:
+        """Generate tonic channel connect constructor."""
+        service_key = self._cache_key(service)
+        client_name = 
f"{self._service_trait_identifier_cache[service_key]}Client"
+        return [
+            f"impl {client_name}<::tonic::transport::Channel> {{",
+            "    pub async fn connect<D>(",
+            "        dst: D,",
+            "    ) -> ::std::result::Result<Self, ::tonic::transport::Error>",
+            "    where",
+            "        D: 
::std::convert::TryInto<::tonic::transport::Endpoint>,",
+            "        D::Error: Into<::tonic::codegen::StdError>,",
+            "    {",
+            "        let conn = 
::tonic::transport::Endpoint::new(dst)?.connect().await?;",
+            "        Ok(Self::new(conn))",
+            "    }",
+            "}",
+        ]
+
+    def generate_grpc_client_impl(self, service: Service) -> List[str]:
+        """Generate generic tonic client implementation."""
+        service_key = self._cache_key(service)
+        client_name = 
f"{self._service_trait_identifier_cache[service_key]}Client"
+        lines: List[str] = [
+            f"impl<T> {client_name}<T>",
+            "where",
+            "    T: ::tonic::client::GrpcService<::tonic::body::Body>,",
+            "    T::Error: Into<::tonic::codegen::StdError>,",
+            "    T::ResponseBody: ::tonic::codegen::Body<Data = 
::tonic::codegen::Bytes>",
+            "        + ::std::marker::Send",
+            "        + 'static,",
+            "    <T::ResponseBody as ::tonic::codegen::Body>::Error:",
+            "        Into<::tonic::codegen::StdError> + ::std::marker::Send,",
+            "{",
+            "    pub fn new(inner: T) -> Self {",
+            "        let inner = ::tonic::client::Grpc::new(inner);",
+            "        Self { inner }",
+            "    }",
+        ]
+        for method in service.methods:
+            lines.append("")
+            lines.extend(
+                self.indent_lines(self.generate_grpc_client_method(service, 
method), 1)
+            )
+        lines.append("}")
+        return lines
+
+    def generate_grpc_client_method(
+        self, service: Service, method: RpcMethod
+    ) -> List[str]:
+        """Generate a tonic client method."""
+        service_key = self._cache_key(service)
+        method_key = self._cache_key(method)
+        method_name = 
self._rpc_method_identifier_cache[service_key][method_key]
+        request_type = self.service_type_path(method.request_type)
+        response_type = self.service_type_path(method.response_type)
+        if method.client_streaming:
+            request_arg_type = (
+                f"impl ::tonic::IntoStreamingRequest<Message = {request_type}>"
+            )
+            request_into_method = "into_streaming_request"
+        else:
+            request_arg_type = f"impl ::tonic::IntoRequest<{request_type}>"
+            request_into_method = "into_request"
+        if method.server_streaming:
+            response_return_type = (
+                
f"::tonic::Response<::tonic::codec::Streaming<{response_type}>>"
+            )
+        else:
+            response_return_type = f"::tonic::Response<{response_type}>"
+        if method.client_streaming and method.server_streaming:
+            grpc_call = "streaming"
+        elif method.client_streaming:
+            grpc_call = "client_streaming"
+        elif method.server_streaming:
+            grpc_call = "server_streaming"
+        else:
+            grpc_call = "unary"
+        service_module = f"crate::{self.get_module_name()}_service"
+        service_name_const = 
f"{service_module}::{self._service_name_constant_identifier_cache[service_key]}"
+        rpc_path_const = 
f"{service_module}::{self._rpc_path_constant_identifier_cache[service_key][method_key]}"
+        return [
+            f"pub async fn {method_name}(",
+            "    &mut self,",
+            f"    request: {request_arg_type},",
+            ") -> ::std::result::Result<",
+            f"    {response_return_type},",
+            "    ::tonic::Status,",
+            "> {",
+            "    self.inner.ready().await.map_err(|e| {",
+            '        ::tonic::Status::unknown(format!("Service was not ready: 
{}", e.into()))',
+            "    })?;",
+            "    let codec = super::codec::ForyCodec::<",
+            f"        {request_type},",
+            f"        {response_type},",
+            "    >::default();",
+            "    let path = 
::tonic::codegen::http::uri::PathAndQuery::from_static(",
+            f"        {rpc_path_const},",
+            "    );",
+            f"    let mut req = request.{request_into_method}();",
+            "    
req.extensions_mut().insert(::tonic::codegen::GrpcMethod::new(",
+            f"        {service_name_const},",
+            f"        {self.rust_string_literal(method.name)},",
+            "    ));",
+            f"    self.inner.{grpc_call}(req, path, codec).await",
+            "}",
+        ]
+
+    def generate_grpc_server_module(self, service: Service) -> List[str]:
+        """Generate tonic server module for one service."""
+        lines: List[str] = []
+        service_key = self._cache_key(service)
+        server_module = 
self._service_server_module_identifier_cache[service_key]
+        lines.append(f"pub mod {server_module} {{")
+        
lines.extend(self.indent_lines(self.generate_grpc_server_struct(service), 1))
+        lines.append("")
+        
lines.extend(self.indent_lines(self.generate_grpc_server_impl(service), 1))
+        lines.append("")
+        lines.extend(
+            self.indent_lines(self.generate_grpc_server_service_impl(service), 
1)
+        )
+        lines.append("")
+        lines.extend(
+            self.indent_lines(self.generate_grpc_server_clone_impl(service), 1)
+        )
+        lines.append("")
+        lines.extend(
+            self.indent_lines(self.generate_grpc_server_named_impl(service), 1)
+        )
+        lines.append("}")
+        return lines
+
+    def generate_grpc_server_struct(self, service: Service) -> List[str]:
+        """Generate tonic server wrapper struct."""
+        service_key = self._cache_key(service)
+        server_name = 
f"{self._service_trait_identifier_cache[service_key]}Server"
+        return [
+            "#[derive(Debug)]",
+            f"pub struct {server_name}<T> {{",
+            "    inner: ::std::sync::Arc<T>,",
+            "}",
+        ]
+
+    def generate_grpc_server_impl(self, service: Service) -> List[str]:
+        """Generate constructors for the tonic server wrapper."""
+        service_key = self._cache_key(service)
+        server_name = 
f"{self._service_trait_identifier_cache[service_key]}Server"
+        return [
+            f"impl<T> {server_name}<T> {{",
+            "    pub fn new(inner: T) -> Self {",
+            "        Self::from_arc(::std::sync::Arc::new(inner))",
+            "    }",
+            "",
+            "    pub fn from_arc(inner: ::std::sync::Arc<T>) -> Self {",
+            "        Self { inner }",
+            "    }",
+            "}",
+        ]
+
+    def generate_grpc_server_service_impl(self, service: Service) -> List[str]:
+        """Generate tonic Service impl for the server wrapper."""
+        service_key = self._cache_key(service)
+        service_trait_identifier = 
self._service_trait_identifier_cache[service_key]
+        server_name = f"{service_trait_identifier}Server"
+        service_trait = (
+            
f"crate::{self.get_module_name()}_service::{service_trait_identifier}"
+        )
+        lines: List[str] = [
+            (
+                f"impl<T, B> ::tonic::codegen::Service<"
+                f"::tonic::codegen::http::Request<B>> for {server_name}<T>"
+            ),
+            "where",
+            f"    T: {service_trait},",
+            "    B: ::tonic::codegen::Body + ::std::marker::Send + 'static,",
+            "    B::Error: Into<::tonic::codegen::StdError> + 
::std::marker::Send + 'static,",
+            "{",
+            "    type Response = 
::tonic::codegen::http::Response<::tonic::body::Body>;",
+            "    type Error = ::std::convert::Infallible;",
+            "    type Future = ::tonic::codegen::BoxFuture<Self::Response, 
Self::Error>;",
+            "",
+            "    fn poll_ready(",
+            "        &mut self,",
+            "        _cx: &mut ::std::task::Context<'_>,",
+            "    ) -> ::std::task::Poll<::std::result::Result<(), 
Self::Error>> {",
+            "        ::std::task::Poll::Ready(Ok(()))",
+            "    }",
+            "",
+            (
+                "    fn call("
+                "&mut self, req: ::tonic::codegen::http::Request<B>"
+                ") -> Self::Future {"
+            ),
+            "        match req.uri().path() {",
+        ]
+        for method in service.methods:
+            lines.extend(
+                self.indent_lines(self.generate_grpc_server_route(service, 
method), 3)
+            )
+        lines.extend(
+            self.indent_lines(self.generate_grpc_server_unimplemented_route(), 
3)
+        )
+        lines.extend(
+            [
+                "        }",
+                "    }",
+                "}",
+            ]
+        )
+        return lines
+
+    def generate_grpc_server_route(
+        self, service: Service, method: RpcMethod
+    ) -> List[str]:
+        """Generate one tonic server route arm."""
+        service_key = self._cache_key(service)
+        method_key = self._cache_key(method)
+        service_trait_identifier = 
self._service_trait_identifier_cache[service_key]
+        method_name = 
self._rpc_method_identifier_cache[service_key][method_key]
+        service_trait = (
+            
f"crate::{self.get_module_name()}_service::{service_trait_identifier}"
+        )
+        request_type = self.service_type_path(method.request_type)
+        response_type = self.service_type_path(method.response_type)
+        method_service_name_base = (
+            method_name[2:] if method_name.startswith("r#") else method_name
+        )
+        method_service_name = 
f"{self.to_pascal_case(method_service_name_base)}Svc"
+        rpc_path_const = 
f"crate::{self.get_module_name()}_service::{self._rpc_path_constant_identifier_cache[service_key][method_key]}"
+        if method.client_streaming:
+            request_arg_type = 
f"::tonic::Request<::tonic::Streaming<{request_type}>>"
+        else:
+            request_arg_type = f"::tonic::Request<{request_type}>"
+        if method.client_streaming and method.server_streaming:
+            tonic_service_trait = "StreamingService"
+            grpc_call = "streaming"
+        elif method.client_streaming:
+            tonic_service_trait = "ClientStreamingService"
+            grpc_call = "client_streaming"
+        elif method.server_streaming:
+            tonic_service_trait = "ServerStreamingService"
+            grpc_call = "server_streaming"
+        else:
+            tonic_service_trait = "UnaryService"
+            grpc_call = "unary"
+        lines = [
+            f"{rpc_path_const} => {{",
+            (
+                f"    struct {method_service_name}<T: {service_trait}>"
+                "(pub ::std::sync::Arc<T>);"
+            ),
+            "",
+            f"    impl<T: {service_trait}> 
::tonic::server::{tonic_service_trait}<{request_type}>",
+            f"        for {method_service_name}<T>",
+            "    {",
+            f"        type Response = {response_type};",
+        ]
+        if method.server_streaming:
+            stream_type = self._rpc_stream_type_identifier_cache[service_key][
+                method_key
+            ]
+            future_response_type = "Self::ResponseStream"
+            lines.append(f"        type ResponseStream = T::{stream_type};")
+        else:
+            future_response_type = "Self::Response"
+        lines.extend(
+            [
+                "        type Future = ::tonic::codegen::BoxFuture<",
+                f"            ::tonic::Response<{future_response_type}>,",
+                "            ::tonic::Status,",
+                "        >;",
+                "",
+                "        fn call(",
+                "            &mut self,",
+                f"            request: {request_arg_type},",
+                "        ) -> Self::Future {",
+                "            let inner = ::std::sync::Arc::clone(&self.0);",
+                "            let fut = async move {",
+                f"                <T as 
{service_trait}>::{method_name}(&inner, request).await",
+                "            };",
+                "            ::std::boxed::Box::pin(fut)",
+                "        }",
+                "    }",
+                "",
+                "    let inner = self.inner.clone();",
+                "    let fut = async move {",
+                f"        let method = {method_service_name}(inner);",
+                "        let codec = super::codec::ForyCodec::<",
+                f"            {response_type},",
+                f"            {request_type},",
+                "        >::default();",
+                "        let mut grpc = ::tonic::server::Grpc::new(codec);",
+                f"        let res = grpc.{grpc_call}(method, req).await;",
+                "        Ok(res)",
+                "    };",
+                "    ::std::boxed::Box::pin(fut)",
+                "}",
+            ]
+        )
+        return lines
+
+    def generate_grpc_server_unimplemented_route(self) -> List[str]:
+        """Generate fallback route for unknown gRPC paths."""
+        return [
+            "_ => ::std::boxed::Box::pin(async move {",
+            "    let mut response = ::tonic::codegen::http::Response::new(",
+            "        ::tonic::body::Body::default(),",
+            "    );",
+            "    let headers = response.headers_mut();",
+            "    headers.insert(",
+            "        ::tonic::Status::GRPC_STATUS,",
+            "        (::tonic::Code::Unimplemented as i32).into(),",
+            "    );",
+            "    headers.insert(",
+            "        ::tonic::codegen::http::header::CONTENT_TYPE,",
+            "        ::tonic::metadata::GRPC_CONTENT_TYPE,",
+            "    );",
+            "    Ok(response)",
+            "}),",
+        ]
+
+    def generate_grpc_server_clone_impl(self, service: Service) -> List[str]:
+        """Generate Clone impl for the tonic server wrapper."""
+        service_key = self._cache_key(service)
+        server_name = 
f"{self._service_trait_identifier_cache[service_key]}Server"
+        return [
+            f"impl<T> ::std::clone::Clone for {server_name}<T> {{",
+            "    fn clone(&self) -> Self {",
+            "        Self {",
+            "            inner: self.inner.clone(),",
+            "        }",
+            "    }",
+            "}",
+        ]
+
+    def generate_grpc_server_named_impl(self, service: Service) -> List[str]:
+        """Generate NamedService impl for the tonic server wrapper."""
+        service_key = self._cache_key(service)
+        server_name = 
f"{self._service_trait_identifier_cache[service_key]}Server"
+        service_name_const = 
f"crate::{self.get_module_name()}_service::{self._service_name_constant_identifier_cache[service_key]}"
+        return [
+            f"pub const SERVICE_NAME: &str = {service_name_const};",
+            "",
+            f"impl<T> ::tonic::server::NamedService for {server_name}<T> {{",
+            "    const NAME: &'static str = SERVICE_NAME;",
+            "}",
+        ]
diff --git a/compiler/fory_compiler/tests/test_service_codegen.py 
b/compiler/fory_compiler/tests/test_service_codegen.py
index 58269d0f7..8170b9988 100644
--- a/compiler/fory_compiler/tests/test_service_codegen.py
+++ b/compiler/fory_compiler/tests/test_service_codegen.py
@@ -21,6 +21,8 @@ from pathlib import Path
 from textwrap import dedent
 from typing import Dict, Tuple, Type
 
+import pytest
+
 from fory_compiler.cli import compile_file, resolve_imports
 from fory_compiler.frontend.fdl.lexer import Lexer
 from fory_compiler.frontend.fdl.parser import Parser
@@ -129,7 +131,12 @@ def 
test_service_definition_does_not_affect_message_codegen():
 def test_generate_services_returns_empty_list_for_unsupported_generators():
     schema = parse_fdl(_GREETER_WITH_SERVICE)
     for generator_cls in GENERATOR_CLASSES:
-        if generator_cls in (JavaGenerator, PythonGenerator, GoGenerator):
+        if generator_cls in (
+            JavaGenerator,
+            PythonGenerator,
+            GoGenerator,
+            RustGenerator,
+        ):
             continue
         options = GeneratorOptions(output_dir=Path("/tmp"))
         generator = generator_cls(schema, options)
@@ -368,6 +375,13 @@ def 
test_proto_grpc_services_use_imported_qualified_type_references(tmp_path: Pa
     )
     assert "marshaller(com.example.common.CommonModels.Shared.class)" in java
 
+    rust_files = generate_service_files(schema, RustGenerator)
+    rust_service = rust_files["api_service.rs"]
+    assert "request: ::tonic::Request<crate::common::Shared>," in rust_service
+    assert "::tonic::Response<crate::api::Local>" in rust_service
+    assert "crate::common::common::Shared" not in rust_service
+    assert "crate::api::api::Local" not in rust_service
+
 
 def test_proto_grpc_absolute_rpc_type_uses_package_type_not_nested_shadow():
     schema = parse_proto(
@@ -396,6 +410,13 @@ def 
test_proto_grpc_absolute_rpc_type_uses_package_type_not_nested_shadow():
     assert "io.grpc.MethodDescriptor<Request, Response>" in java
     assert "io.grpc.MethodDescriptor<demo.Request, Response>" not in java
 
+    rust_files = generate_service_files(schema, RustGenerator)
+    rust_service = rust_files["demo_service.rs"]
+    assert "request: ::tonic::Request<crate::demo::Request>," in rust_service
+    assert "::tonic::Response<crate::demo::Response>" in rust_service
+    assert "crate::demo::demo::Request" not in rust_service
+    assert "crate::demo::::demo::Request" not in rust_service
+
 
 def test_proto_grpc_absolute_rpc_type_prefers_longest_package_prefix(tmp_path: 
Path):
     common = tmp_path / "common.proto"
@@ -441,6 +462,12 @@ def 
test_proto_grpc_absolute_rpc_type_prefers_longest_package_prefix(tmp_path: P
     assert "marshaller(pkg.two.C.class)" in java
     assert "io.grpc.MethodDescriptor<beta.C, beta.C>" not in java
 
+    rust_files = generate_service_files(schema, RustGenerator)
+    rust_service = rust_files["alpha_service.rs"]
+    assert "request: ::tonic::Request<crate::alpha_beta::C>," in rust_service
+    assert "::tonic::Response<crate::alpha_beta::C>" in rust_service
+    assert "crate::alpha::beta::C" not in rust_service
+
 
 def test_java_grpc_service_class_collision_fails():
     schema = parse_fdl(
@@ -593,7 +620,7 @@ def test_grpc_method_name_collisions_fail():
         schema, GeneratorOptions(output_dir=Path("/tmp"), grpc=True)
     )
     try:
-        rust_generator.generate()
+        rust_generator.generate_services()
     except ValueError as e:
         assert "Rust name collision" in str(e)
     else:
@@ -667,7 +694,7 @@ def test_rust_grpc_service_module_collisions_fail():
         schema, GeneratorOptions(output_dir=Path("/tmp"), grpc=True)
     )
     try:
-        generator.generate()
+        generator.generate_services()
     except ValueError as e:
         assert "Rust name collision" in str(e)
     else:
@@ -763,6 +790,8 @@ def test_compile_service_schema_with_grpc_flag(tmp_path: 
Path):
         assert len(files) >= 1, f"{lang}: expected at least one file with 
grpc=True"
     assert (lang_dirs["java"] / "demo" / "greeter" / 
"GreeterGrpc.java").exists()
     assert (lang_dirs["python"] / "demo_greeter_grpc.py").exists()
+    assert (lang_dirs["rust"] / "demo_greeter_service.rs").exists()
+    assert (lang_dirs["rust"] / "demo_greeter_service_grpc.rs").exists()
 
 
 def test_generated_message_contains_key_signatures():
@@ -778,3 +807,70 @@ def test_generated_message_contains_key_signatures():
     all_python = "\n".join(python_files.values())
     assert "HelloRequest" in all_python
     assert "HelloReply" in all_python
+
+
+def test_rust_grpc_rejects_non_thread_safe_refs():
+    cases = [
+        (
+            "Rust gRPC payload type Request.node uses non-thread-safe ref",
+            """
+            package demo.grpc;
+
+            message Node {}
+
+            message Request {
+                ref(thread_safe=false) Node node = 1;
+            }
+
+            message Response {}
+
+            service Api {
+                rpc Call (Request) returns (Response);
+            }
+            """,
+        ),
+        (
+            "Rust gRPC payload type Request.groups uses non-thread-safe list 
element ref",
+            """
+            package demo.grpc;
+
+            message Node {}
+
+            message Request {
+                list<ref(thread_safe=false) Node> groups = 1;
+            }
+
+            message Response {}
+
+            service Api {
+                rpc Call (Request) returns (Response);
+            }
+            """,
+        ),
+        (
+            "Rust gRPC payload type Request.nodes uses non-thread-safe map 
value ref",
+            """
+            package demo.grpc;
+
+            message Node {}
+
+            message Request {
+                map<string, ref(thread_safe=false) Node> nodes = 1;
+            }
+
+            message Response {}
+
+            service Api {
+                rpc Call (Request) returns (Response);
+            }
+            """,
+        ),
+    ]
+
+    for message, source in cases:
+        schema = parse_fdl(dedent(source))
+        generator = RustGenerator(
+            schema, GeneratorOptions(output_dir=Path("/tmp"), grpc=True)
+        )
+        with pytest.raises(ValueError, match=message):
+            generator.generate_services()
diff --git a/integration_tests/grpc_tests/generate_grpc.py 
b/integration_tests/grpc_tests/generate_grpc.py
index 5d3131640..af3f3f44f 100644
--- a/integration_tests/grpc_tests/generate_grpc.py
+++ b/integration_tests/grpc_tests/generate_grpc.py
@@ -34,6 +34,7 @@ OUTPUTS = {
     "java": TEST_DIR / "java/src/main/java/generated",
     "python": TEST_DIR / "python/grpc_tests/generated",
     "go": TEST_DIR / "go/generated",
+    "rust": TEST_DIR / "rust/generated/src",
 }
 
 
@@ -74,6 +75,7 @@ def main() -> int:
                 f"--java_out={OUTPUTS['java']}",
                 f"--python_out={OUTPUTS['python']}",
                 f"--go_out={go_pkg_out}",
+                f"--rust_out={OUTPUTS['rust']}",
                 "--grpc",
             ],
             env=env,
diff --git 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcInteropTest.java
 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcInteropTest.java
index c494c6dc2..bbf6a8629 100644
--- 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcInteropTest.java
+++ 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcInteropTest.java
@@ -71,7 +71,57 @@ public class GrpcInteropTest {
         new PeerOutputCollector(process.getInputStream(), 
"python-grpc-server");
     outputCollector.start();
     try {
-      int port = waitForPort(process, outputCollector, portFile);
+      int port = waitForPort(process, outputCollector, portFile, "Python");
+      ManagedChannel channel =
+          ManagedChannelBuilder.forAddress("127.0.0.1", 
port).usePlaintext().build();
+      try {
+        exerciseFdl(channel);
+        exerciseFbs(channel);
+        exercisePb(channel);
+      } finally {
+        channel.shutdownNow();
+        channel.awaitTermination(10, TimeUnit.SECONDS);
+      }
+    } finally {
+      process.destroy();
+      process.waitFor(10, TimeUnit.SECONDS);
+      if (process.isAlive()) {
+        process.destroyForcibly();
+        process.waitFor(10, TimeUnit.SECONDS);
+      }
+      outputCollector.awaitOutput();
+      Files.deleteIfExists(portFile);
+    }
+  }
+
+  @Test
+  public void testJavaServerRustClient() throws Exception {
+    Server server =
+        ServerBuilder.forPort(0)
+            .addService(new FdlService())
+            .addService(new FbsService())
+            .addService(new PbService())
+            .build()
+            .start();
+    try {
+      runRust("rust-grpc-client", "client", "--target", "127.0.0.1:" + 
server.getPort());
+    } finally {
+      server.shutdownNow();
+      server.awaitTermination(10, TimeUnit.SECONDS);
+    }
+  }
+
+  @Test
+  public void testJavaClientRustServer() throws Exception {
+    Path portFile = Files.createTempFile("fory-grpc-rust-", ".port");
+    Files.deleteIfExists(portFile);
+    PeerCommand command = rustCommand("server", "--port-file", 
portFile.toString());
+    Process process = startPeer(command);
+    PeerOutputCollector outputCollector =
+        new PeerOutputCollector(process.getInputStream(), "rust-grpc-server");
+    outputCollector.start();
+    try {
+      int port = waitForPort(process, outputCollector, portFile, "Rust");
       ManagedChannel channel =
           ManagedChannelBuilder.forAddress("127.0.0.1", 
port).usePlaintext().build();
       try {
@@ -280,11 +330,7 @@ public class GrpcInteropTest {
 
   @Test
   public void testJavaServerGoClient() throws Exception {
-    Server server =
-        ServerBuilder.forPort(0)
-            .addService(new FdlService())
-            .build()
-            .start();
+    Server server = ServerBuilder.forPort(0).addService(new 
FdlService()).build().start();
     try {
       runGo("go-grpc-client", "client", "--target", "127.0.0.1:" + 
server.getPort());
     } finally {
@@ -303,7 +349,7 @@ public class GrpcInteropTest {
         new PeerOutputCollector(process.getInputStream(), "go-grpc-server");
     outputCollector.start();
     try {
-      int port = waitForPort(process, outputCollector, portFile);
+      int port = waitForPort(process, outputCollector, portFile, "Go");
       ManagedChannel channel =
           ManagedChannelBuilder.forAddress("127.0.0.1", 
port).usePlaintext().build();
       try {
@@ -397,6 +443,34 @@ public class GrpcInteropTest {
     return peerCommand;
   }
 
+  private 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;
+  }
+
   private void runPython(String peer, String... args) throws IOException, 
InterruptedException {
     Process process = startPeer(pythonCommand(args));
     PeerOutputCollector outputCollector = new 
PeerOutputCollector(process.getInputStream(), peer);
@@ -419,6 +493,28 @@ public class GrpcInteropTest {
     outputCollector.awaitOutput();
   }
 
+  private 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();
+  }
+
   private Process startPeer(PeerCommand command) throws IOException {
     ProcessBuilder builder = new ProcessBuilder(command.command);
     builder.redirectErrorStream(true);
@@ -427,12 +523,13 @@ public class GrpcInteropTest {
     return builder.start();
   }
 
-  private int waitForPort(Process process, PeerOutputCollector 
outputCollector, Path portFile)
+  private int waitForPort(
+      Process process, PeerOutputCollector outputCollector, Path portFile, 
String language)
       throws IOException, InterruptedException {
     long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(60);
     while (System.nanoTime() < deadline) {
       if (!process.isAlive()) {
-        Assert.fail("Python gRPC server exited early" + 
peerOutput(outputCollector));
+        Assert.fail(language + " gRPC server exited early" + 
peerOutput(outputCollector));
       }
       if (Files.exists(portFile)) {
         String value = new String(Files.readAllBytes(portFile), 
StandardCharsets.UTF_8).trim();
@@ -444,7 +541,8 @@ public class GrpcInteropTest {
     }
     process.destroyForcibly();
     process.waitFor(10, TimeUnit.SECONDS);
-    Assert.fail("Timed out waiting for Python gRPC server port" + 
peerOutput(outputCollector));
+    Assert.fail(
+        "Timed out waiting for " + language + " gRPC server port" + 
peerOutput(outputCollector));
     return -1;
   }
 
diff --git a/integration_tests/grpc_tests/run_tests.sh 
b/integration_tests/grpc_tests/run_tests.sh
index 033d27c56..22b9cd3aa 100755
--- a/integration_tests/grpc_tests/run_tests.sh
+++ b/integration_tests/grpc_tests/run_tests.sh
@@ -29,6 +29,7 @@ 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 "${ROOT_DIR}/integration_tests/grpc_tests/java"
 mvn -T16 --no-transfer-progress \
   -Dtest=GrpcInteropTest \
diff --git a/integration_tests/grpc_tests/run_tests.sh 
b/integration_tests/grpc_tests/rust/Cargo.toml
old mode 100755
new mode 100644
similarity index 61%
copy from integration_tests/grpc_tests/run_tests.sh
copy to integration_tests/grpc_tests/rust/Cargo.toml
index 033d27c56..de5fe6244
--- a/integration_tests/grpc_tests/run_tests.sh
+++ b/integration_tests/grpc_tests/rust/Cargo.toml
@@ -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,19 +15,22 @@
 # 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)"
-
-python -m pip install "grpcio>=1.62.2,<1.71"
-python -m pip install -v -e "${ROOT_DIR}/python"
+[workspace]
+members = [
+    "generated",
+    "interop",
+]
+resolver = "2"
 
-python "${SCRIPT_DIR}/generate_grpc.py"
+[workspace.package]
+version = "1.2.0-alpha.0"
+edition = "2021"
+license = "Apache-2.0"
 
-cd "${SCRIPT_DIR}/go"
-go build -o grpc-interop .
-cd "${ROOT_DIR}/integration_tests/grpc_tests/java"
-mvn -T16 --no-transfer-progress \
-  -Dtest=GrpcInteropTest \
-  test
+[workspace.dependencies]
+bytes = "1"
+fory = { path = "../../../rust/fory" }
+fory-core = { path = "../../../rust/fory-core" }
+tokio = { version = "1", features = ["fs", "macros", "net", "rt-multi-thread", 
"sync"] }
+tokio-stream = { version = "0.1", features = ["net"] }
+tonic = { version = "0.14", features = ["transport"] }
diff --git a/integration_tests/grpc_tests/run_tests.sh 
b/integration_tests/grpc_tests/rust/generated/Cargo.toml
old mode 100755
new mode 100644
similarity index 64%
copy from integration_tests/grpc_tests/run_tests.sh
copy to integration_tests/grpc_tests/rust/generated/Cargo.toml
index 033d27c56..ea65f83f4
--- a/integration_tests/grpc_tests/run_tests.sh
+++ b/integration_tests/grpc_tests/rust/generated/Cargo.toml
@@ -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,19 +15,14 @@
 # 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)"
-
-python -m pip install "grpcio>=1.62.2,<1.71"
-python -m pip install -v -e "${ROOT_DIR}/python"
-
-python "${SCRIPT_DIR}/generate_grpc.py"
+[package]
+name = "grpc_tests_generated"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
 
-cd "${SCRIPT_DIR}/go"
-go build -o grpc-interop .
-cd "${ROOT_DIR}/integration_tests/grpc_tests/java"
-mvn -T16 --no-transfer-progress \
-  -Dtest=GrpcInteropTest \
-  test
+[dependencies]
+bytes.workspace = true
+fory.workspace = true
+fory-core.workspace = true
+tonic.workspace = true
diff --git a/integration_tests/grpc_tests/rust/generated/src/lib.rs 
b/integration_tests/grpc_tests/rust/generated/src/lib.rs
new file mode 100644
index 000000000..82ba47079
--- /dev/null
+++ b/integration_tests/grpc_tests/rust/generated/src/lib.rs
@@ -0,0 +1,26 @@
+// 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.
+
+pub mod grpc_fbs;
+pub mod grpc_fbs_service;
+pub mod grpc_fbs_service_grpc;
+pub mod grpc_fdl;
+pub mod grpc_fdl_service;
+pub mod grpc_fdl_service_grpc;
+pub mod grpc_pb;
+pub mod grpc_pb_service;
+pub mod grpc_pb_service_grpc;
diff --git a/integration_tests/grpc_tests/run_tests.sh 
b/integration_tests/grpc_tests/rust/interop/Cargo.toml
old mode 100755
new mode 100644
similarity index 64%
copy from integration_tests/grpc_tests/run_tests.sh
copy to integration_tests/grpc_tests/rust/interop/Cargo.toml
index 033d27c56..3c06b6349
--- a/integration_tests/grpc_tests/run_tests.sh
+++ b/integration_tests/grpc_tests/rust/interop/Cargo.toml
@@ -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,19 +15,14 @@
 # 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)"
-
-python -m pip install "grpcio>=1.62.2,<1.71"
-python -m pip install -v -e "${ROOT_DIR}/python"
-
-python "${SCRIPT_DIR}/generate_grpc.py"
+[package]
+name = "grpc_tests_interop"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
 
-cd "${SCRIPT_DIR}/go"
-go build -o grpc-interop .
-cd "${ROOT_DIR}/integration_tests/grpc_tests/java"
-mvn -T16 --no-transfer-progress \
-  -Dtest=GrpcInteropTest \
-  test
+[dependencies]
+grpc_tests_generated = { path = "../generated" }
+tokio.workspace = true
+tokio-stream.workspace = true
+tonic.workspace = true
diff --git a/integration_tests/grpc_tests/rust/interop/src/main.rs 
b/integration_tests/grpc_tests/rust/interop/src/main.rs
new file mode 100644
index 000000000..30a85ad73
--- /dev/null
+++ b/integration_tests/grpc_tests/rust/interop/src/main.rs
@@ -0,0 +1,909 @@
+// 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.
+
+use std::env;
+use std::io::{Error as IoError, ErrorKind};
+use std::path::PathBuf;
+use std::pin::Pin;
+
+use grpc_tests_generated::grpc_fbs::{GrpcFbsRequest, GrpcFbsResponse, 
GrpcFbsUnion};
+use grpc_tests_generated::grpc_fbs_service::FbsGrpcService;
+use 
grpc_tests_generated::grpc_fbs_service_grpc::fbs_grpc_service_client::FbsGrpcServiceClient;
+use 
grpc_tests_generated::grpc_fbs_service_grpc::fbs_grpc_service_server::FbsGrpcServiceServer;
+use grpc_tests_generated::grpc_fdl::{GrpcFdlRequest, GrpcFdlResponse, 
GrpcFdlUnion};
+use grpc_tests_generated::grpc_fdl_service::FdlGrpcService;
+use 
grpc_tests_generated::grpc_fdl_service_grpc::fdl_grpc_service_client::FdlGrpcServiceClient;
+use 
grpc_tests_generated::grpc_fdl_service_grpc::fdl_grpc_service_server::FdlGrpcServiceServer;
+use grpc_tests_generated::grpc_pb::grpc_pb_request;
+use grpc_tests_generated::grpc_pb::grpc_pb_response;
+use grpc_tests_generated::grpc_pb::{GrpcPbRequest, GrpcPbResponse};
+use grpc_tests_generated::grpc_pb_service::PbGrpcService;
+use 
grpc_tests_generated::grpc_pb_service_grpc::pb_grpc_service_client::PbGrpcServiceClient;
+use 
grpc_tests_generated::grpc_pb_service_grpc::pb_grpc_service_server::PbGrpcServiceServer;
+use tokio_stream::wrappers::{ReceiverStream, TcpListenerStream};
+use tokio_stream::Stream;
+use tonic::transport::{Channel, Server};
+use tonic::{Request, Response, Status};
+
+type AnyError = Box<dyn std::error::Error + Send + Sync>;
+type AnyResult<T> = std::result::Result<T, AnyError>;
+type StatusResult<T> = std::result::Result<T, Status>;
+type GrpcStream<T> = Pin<Box<dyn Stream<Item = StatusResult<T>> + Send + 
'static>>;
+
+/// Runs a gRPC client or server according to the CLI arguments.
+#[tokio::main]
+async fn main() -> AnyResult<()> {
+    let args = env::args().skip(1).collect::<Vec<_>>();
+    match args.first().map(String::as_str) {
+        Some("client") => run_client(required_arg(&args[1..], 
"--target")?).await,
+        Some("server") => run_server(PathBuf::from(required_arg(&args[1..], 
"--port-file")?)).await,
+        _ => Err(invalid_args(
+            "usage: grpc_tests_interop client --target HOST:PORT | server 
--port-file PATH",
+        )),
+    }
+}
+
+/// Returns the value following a required CLI flag.
+fn required_arg(args: &[String], name: &str) -> AnyResult<String> {
+    let Some(index) = args.iter().position(|arg| arg == name) else {
+        return Err(invalid_args(format!("missing required argument {name}")));
+    };
+    args.get(index + 1)
+        .cloned()
+        .ok_or_else(|| invalid_args(format!("missing value for {name}")))
+}
+
+fn invalid_args(message: impl Into<String>) -> AnyError {
+    Box::new(IoError::new(ErrorKind::InvalidInput, message.into()))
+}
+
+// Connects to a gRPC server and exercises every schema service as a client.
+async fn run_client(target: String) -> AnyResult<()> {
+    let target = if target.contains("://") {
+        target
+    } else {
+        format!("http://{target}";)
+    };
+    let channel = Channel::from_shared(target)?.connect().await?;
+
+    exercise_fdl(channel.clone()).await?;
+    exercise_fbs(channel.clone()).await?;
+    exercise_pb(channel).await?;
+    Ok(())
+}
+
+// Starts a gRPC server registers services.
+async fn run_server(port_file: PathBuf) -> AnyResult<()> {
+    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
+    let port = listener.local_addr()?.port();
+    tokio::fs::write(&port_file, port.to_string()).await?;
+    Server::builder()
+        .add_service(FdlGrpcServiceServer::new(FdlServiceImpl))
+        .add_service(FbsGrpcServiceServer::new(FbsServiceImpl))
+        .add_service(PbGrpcServiceServer::new(PbServiceImpl))
+        .serve_with_incoming(TcpListenerStream::new(listener))
+        .await?;
+    Ok(())
+}
+
+// Drains a streaming RPC response into a vector for equality assertions.
+async fn collect_stream<T>(mut stream: tonic::codec::Streaming<T>) -> 
StatusResult<Vec<T>> {
+    let mut values = Vec::new();
+    while let Some(value) = stream.message().await? {
+        values.push(value);
+    }
+    Ok(values)
+}
+
+// Builds a FDL request.
+fn fdl_request(id: &str, count: i32, payload: &str) -> GrpcFdlRequest {
+    GrpcFdlRequest {
+        id: id.to_string(),
+        count,
+        payload: payload.to_string(),
+    }
+}
+
+// Builds the expected FDL response for a FDL request.
+fn fdl_response(request: &GrpcFdlRequest, tag: &str, offset: i32) -> 
GrpcFdlResponse {
+    GrpcFdlResponse {
+        id: format!("{tag}:{}", request.id),
+        count: request.count + offset,
+        payload: format!("{tag}:{}", request.payload),
+    }
+}
+
+// Builds the aggregate FDL response used by client-streaming RPCs.
+fn fdl_aggregate(requests: &[GrpcFdlRequest]) -> GrpcFdlResponse {
+    GrpcFdlResponse {
+        id: format!(
+            "client:{}",
+            requests
+                .iter()
+                .map(|request| request.id.as_str())
+                .collect::<Vec<_>>()
+                .join("+")
+        ),
+        count: requests.iter().map(|request| request.count).sum(),
+        payload: format!(
+            "client:{}",
+            requests
+                .iter()
+                .map(|request| request.payload.as_str())
+                .collect::<Vec<_>>()
+                .join("+")
+        ),
+    }
+}
+
+// Extracts the request arm from a FDL union.
+fn fdl_request_from_union(union: GrpcFdlUnion) -> StatusResult<GrpcFdlRequest> 
{
+    match union {
+        GrpcFdlUnion::Request(request) => Ok(request),
+        _ => Err(Status::invalid_argument("expected GrpcFdlUnion::Request")),
+    }
+}
+
+async fn exercise_fdl(channel: Channel) -> AnyResult<()> {
+    let mut client = FdlGrpcServiceClient::new(channel);
+    let requests = vec![
+        fdl_request("fdl-a", 1, "alpha"),
+        fdl_request("fdl-b", 2, "beta"),
+    ];
+    let first = requests[0].clone();
+    assert_eq!(
+        client.unary_message(first.clone()).await?.into_inner(),
+        fdl_response(&first, "unary", 10)
+    );
+    assert_eq!(
+        collect_stream(
+            client
+                .server_stream_message(first.clone())
+                .await?
+                .into_inner()
+        )
+        .await?,
+        (0..3)
+            .map(|index| fdl_response(&first, &format!("server-{index}"), 
index))
+            .collect::<Vec<_>>()
+    );
+    assert_eq!(
+        client
+            .client_stream_message(tokio_stream::iter(requests.clone()))
+            .await?
+            .into_inner(),
+        fdl_aggregate(&requests)
+    );
+    assert_eq!(
+        collect_stream(
+            client
+                .bidi_stream_message(tokio_stream::iter(requests.clone()))
+                .await?
+                .into_inner()
+        )
+        .await?,
+        requests
+            .iter()
+            .enumerate()
+            .map(|(index, request)| fdl_response(request, 
&format!("bidi-{index}"), index as i32))
+            .collect::<Vec<_>>()
+    );
+
+    let union_requests = vec![
+        fdl_request("fdl-u-a", 3, "union-alpha"),
+        fdl_request("fdl-u-b", 4, "union-beta"),
+    ];
+    let union_payloads = union_requests
+        .iter()
+        .cloned()
+        .map(GrpcFdlUnion::Request)
+        .collect::<Vec<_>>();
+    let first_union = union_payloads[0].clone();
+    assert_eq!(
+        client.unary_union(first_union.clone()).await?.into_inner(),
+        GrpcFdlUnion::Response(fdl_response(&union_requests[0], "unary", 10))
+    );
+    assert_eq!(
+        
collect_stream(client.server_stream_union(first_union).await?.into_inner()).await?,
+        (0..3)
+            .map(|index| {
+                GrpcFdlUnion::Response(fdl_response(
+                    &union_requests[0],
+                    &format!("server-{index}"),
+                    index,
+                ))
+            })
+            .collect::<Vec<_>>()
+    );
+    assert_eq!(
+        client
+            .client_stream_union(tokio_stream::iter(union_payloads.clone()))
+            .await?
+            .into_inner(),
+        GrpcFdlUnion::Response(fdl_aggregate(&union_requests))
+    );
+    assert_eq!(
+        collect_stream(
+            client
+                .bidi_stream_union(tokio_stream::iter(union_payloads))
+                .await?
+                .into_inner()
+        )
+        .await?,
+        union_requests
+            .iter()
+            .enumerate()
+            .map(|(index, request)| {
+                GrpcFdlUnion::Response(fdl_response(
+                    request,
+                    &format!("bidi-{index}"),
+                    index as i32,
+                ))
+            })
+            .collect::<Vec<_>>()
+    );
+    Ok(())
+}
+
+struct FdlServiceImpl;
+
+#[tonic::async_trait]
+impl FdlGrpcService for FdlServiceImpl {
+    type ServerStreamMessageStream = GrpcStream<GrpcFdlResponse>;
+    type BidiStreamMessageStream = GrpcStream<GrpcFdlResponse>;
+    type ServerStreamUnionStream = GrpcStream<GrpcFdlUnion>;
+    type BidiStreamUnionStream = GrpcStream<GrpcFdlUnion>;
+
+    async fn unary_message(
+        &self,
+        request: Request<GrpcFdlRequest>,
+    ) -> StatusResult<Response<GrpcFdlResponse>> {
+        Ok(Response::new(fdl_response(
+            &request.into_inner(),
+            "unary",
+            10,
+        )))
+    }
+
+    async fn server_stream_message(
+        &self,
+        request: Request<GrpcFdlRequest>,
+    ) -> StatusResult<Response<Self::ServerStreamMessageStream>> {
+        let request = request.into_inner();
+        Ok(Response::new(Box::pin(tokio_stream::iter(
+            (0..3)
+                .map(|index| Ok(fdl_response(&request, 
&format!("server-{index}"), index)))
+                .collect::<Vec<_>>(),
+        ))))
+    }
+
+    async fn client_stream_message(
+        &self,
+        request: Request<tonic::Streaming<GrpcFdlRequest>>,
+    ) -> StatusResult<Response<GrpcFdlResponse>> {
+        let mut stream = request.into_inner();
+        let mut requests = Vec::new();
+        while let Some(request) = stream.message().await? {
+            requests.push(request);
+        }
+        Ok(Response::new(fdl_aggregate(&requests)))
+    }
+
+    async fn bidi_stream_message(
+        &self,
+        request: Request<tonic::Streaming<GrpcFdlRequest>>,
+    ) -> StatusResult<Response<Self::BidiStreamMessageStream>> {
+        let mut stream = request.into_inner();
+        let (tx, rx) = tokio::sync::mpsc::channel(4);
+        tokio::spawn(async move {
+            let mut index = 0;
+            loop {
+                match stream.message().await {
+                    Ok(Some(request)) => {
+                        let response = fdl_response(&request, 
&format!("bidi-{index}"), index);
+                        if tx.send(Ok(response)).await.is_err() {
+                            break;
+                        }
+                        index += 1;
+                    }
+                    Ok(None) => break,
+                    Err(error) => {
+                        let _ = tx.send(Err(error)).await;
+                        break;
+                    }
+                }
+            }
+        });
+        Ok(Response::new(Box::pin(ReceiverStream::new(rx))))
+    }
+
+    async fn unary_union(
+        &self,
+        request: Request<GrpcFdlUnion>,
+    ) -> StatusResult<Response<GrpcFdlUnion>> {
+        let request = fdl_request_from_union(request.into_inner())?;
+        Ok(Response::new(GrpcFdlUnion::Response(fdl_response(
+            &request, "unary", 10,
+        ))))
+    }
+
+    async fn server_stream_union(
+        &self,
+        request: Request<GrpcFdlUnion>,
+    ) -> StatusResult<Response<Self::ServerStreamUnionStream>> {
+        let request = fdl_request_from_union(request.into_inner())?;
+        Ok(Response::new(Box::pin(tokio_stream::iter(
+            (0..3)
+                .map(|index| {
+                    Ok(GrpcFdlUnion::Response(fdl_response(
+                        &request,
+                        &format!("server-{index}"),
+                        index,
+                    )))
+                })
+                .collect::<Vec<_>>(),
+        ))))
+    }
+
+    async fn client_stream_union(
+        &self,
+        request: Request<tonic::Streaming<GrpcFdlUnion>>,
+    ) -> StatusResult<Response<GrpcFdlUnion>> {
+        let mut stream = request.into_inner();
+        let mut requests = Vec::new();
+        while let Some(union) = stream.message().await? {
+            requests.push(fdl_request_from_union(union)?);
+        }
+        Ok(Response::new(GrpcFdlUnion::Response(fdl_aggregate(
+            &requests,
+        ))))
+    }
+
+    async fn bidi_stream_union(
+        &self,
+        request: Request<tonic::Streaming<GrpcFdlUnion>>,
+    ) -> StatusResult<Response<Self::BidiStreamUnionStream>> {
+        let mut stream = request.into_inner();
+        let (tx, rx) = tokio::sync::mpsc::channel(4);
+        tokio::spawn(async move {
+            let mut index = 0;
+            loop {
+                match stream.message().await {
+                    Ok(Some(union)) => match fdl_request_from_union(union) {
+                        Ok(request) => {
+                            let response = GrpcFdlUnion::Response(fdl_response(
+                                &request,
+                                &format!("bidi-{index}"),
+                                index,
+                            ));
+                            if tx.send(Ok(response)).await.is_err() {
+                                break;
+                            }
+                            index += 1;
+                        }
+                        Err(error) => {
+                            let _ = tx.send(Err(error)).await;
+                            break;
+                        }
+                    },
+                    Ok(None) => break,
+                    Err(error) => {
+                        let _ = tx.send(Err(error)).await;
+                        break;
+                    }
+                }
+            }
+        });
+        Ok(Response::new(Box::pin(ReceiverStream::new(rx))))
+    }
+}
+
+// Builds a FBS request.
+fn fbs_request(id: &str, count: i32, payload: &str) -> GrpcFbsRequest {
+    GrpcFbsRequest {
+        id: id.to_string(),
+        count,
+        payload: payload.to_string(),
+    }
+}
+
+// Builds the expected FBS response for a request.
+fn fbs_response(request: &GrpcFbsRequest, tag: &str, offset: i32) -> 
GrpcFbsResponse {
+    GrpcFbsResponse {
+        id: format!("{tag}:{}", request.id),
+        count: request.count + offset,
+        payload: format!("{tag}:{}", request.payload),
+    }
+}
+
+// Builds the aggregate FBS response used by client-streaming RPCs.
+fn fbs_aggregate(requests: &[GrpcFbsRequest]) -> GrpcFbsResponse {
+    GrpcFbsResponse {
+        id: format!(
+            "client:{}",
+            requests
+                .iter()
+                .map(|request| request.id.as_str())
+                .collect::<Vec<_>>()
+                .join("+")
+        ),
+        count: requests.iter().map(|request| request.count).sum(),
+        payload: format!(
+            "client:{}",
+            requests
+                .iter()
+                .map(|request| request.payload.as_str())
+                .collect::<Vec<_>>()
+                .join("+")
+        ),
+    }
+}
+
+// Extracts the request arm from a FBS union.
+fn fbs_request_from_union(union: GrpcFbsUnion) -> StatusResult<GrpcFbsRequest> 
{
+    match union {
+        GrpcFbsUnion::GrpcFbsRequest(request) => Ok(request),
+        _ => Err(Status::invalid_argument(
+            "expected GrpcFbsUnion::GrpcFbsRequest",
+        )),
+    }
+}
+
+async fn exercise_fbs(channel: Channel) -> AnyResult<()> {
+    let mut client = FbsGrpcServiceClient::new(channel);
+    let requests = vec![
+        fbs_request("fbs-a", 5, "alpha"),
+        fbs_request("fbs-b", 6, "beta"),
+    ];
+    let first = requests[0].clone();
+    assert_eq!(
+        client.unary_message(first.clone()).await?.into_inner(),
+        fbs_response(&first, "unary", 10)
+    );
+    assert_eq!(
+        collect_stream(
+            client
+                .server_stream_message(first.clone())
+                .await?
+                .into_inner()
+        )
+        .await?,
+        (0..3)
+            .map(|index| fbs_response(&first, &format!("server-{index}"), 
index))
+            .collect::<Vec<_>>()
+    );
+    assert_eq!(
+        client
+            .client_stream_message(tokio_stream::iter(requests.clone()))
+            .await?
+            .into_inner(),
+        fbs_aggregate(&requests)
+    );
+    assert_eq!(
+        collect_stream(
+            client
+                .bidi_stream_message(tokio_stream::iter(requests.clone()))
+                .await?
+                .into_inner()
+        )
+        .await?,
+        requests
+            .iter()
+            .enumerate()
+            .map(|(index, request)| fbs_response(request, 
&format!("bidi-{index}"), index as i32))
+            .collect::<Vec<_>>()
+    );
+
+    let union_requests = vec![
+        fbs_request("fbs-u-a", 7, "union-alpha"),
+        fbs_request("fbs-u-b", 8, "union-beta"),
+    ];
+    let union_payloads = union_requests
+        .iter()
+        .cloned()
+        .map(GrpcFbsUnion::GrpcFbsRequest)
+        .collect::<Vec<_>>();
+    let first_union = union_payloads[0].clone();
+    assert_eq!(
+        client.unary_union(first_union.clone()).await?.into_inner(),
+        GrpcFbsUnion::GrpcFbsResponse(fbs_response(&union_requests[0], 
"unary", 10))
+    );
+    assert_eq!(
+        
collect_stream(client.server_stream_union(first_union).await?.into_inner()).await?,
+        (0..3)
+            .map(|index| {
+                GrpcFbsUnion::GrpcFbsResponse(fbs_response(
+                    &union_requests[0],
+                    &format!("server-{index}"),
+                    index,
+                ))
+            })
+            .collect::<Vec<_>>()
+    );
+    assert_eq!(
+        client
+            .client_stream_union(tokio_stream::iter(union_payloads.clone()))
+            .await?
+            .into_inner(),
+        GrpcFbsUnion::GrpcFbsResponse(fbs_aggregate(&union_requests))
+    );
+    assert_eq!(
+        collect_stream(
+            client
+                .bidi_stream_union(tokio_stream::iter(union_payloads))
+                .await?
+                .into_inner()
+        )
+        .await?,
+        union_requests
+            .iter()
+            .enumerate()
+            .map(|(index, request)| {
+                GrpcFbsUnion::GrpcFbsResponse(fbs_response(
+                    request,
+                    &format!("bidi-{index}"),
+                    index as i32,
+                ))
+            })
+            .collect::<Vec<_>>()
+    );
+    Ok(())
+}
+
+struct FbsServiceImpl;
+
+#[tonic::async_trait]
+impl FbsGrpcService for FbsServiceImpl {
+    type ServerStreamMessageStream = GrpcStream<GrpcFbsResponse>;
+    type BidiStreamMessageStream = GrpcStream<GrpcFbsResponse>;
+    type ServerStreamUnionStream = GrpcStream<GrpcFbsUnion>;
+    type BidiStreamUnionStream = GrpcStream<GrpcFbsUnion>;
+
+    async fn unary_message(
+        &self,
+        request: Request<GrpcFbsRequest>,
+    ) -> StatusResult<Response<GrpcFbsResponse>> {
+        Ok(Response::new(fbs_response(
+            &request.into_inner(),
+            "unary",
+            10,
+        )))
+    }
+
+    async fn server_stream_message(
+        &self,
+        request: Request<GrpcFbsRequest>,
+    ) -> StatusResult<Response<Self::ServerStreamMessageStream>> {
+        let request = request.into_inner();
+        Ok(Response::new(Box::pin(tokio_stream::iter(
+            (0..3)
+                .map(|index| Ok(fbs_response(&request, 
&format!("server-{index}"), index)))
+                .collect::<Vec<_>>(),
+        ))))
+    }
+
+    async fn client_stream_message(
+        &self,
+        request: Request<tonic::Streaming<GrpcFbsRequest>>,
+    ) -> StatusResult<Response<GrpcFbsResponse>> {
+        let mut stream = request.into_inner();
+        let mut requests = Vec::new();
+        while let Some(request) = stream.message().await? {
+            requests.push(request);
+        }
+        Ok(Response::new(fbs_aggregate(&requests)))
+    }
+
+    async fn bidi_stream_message(
+        &self,
+        request: Request<tonic::Streaming<GrpcFbsRequest>>,
+    ) -> StatusResult<Response<Self::BidiStreamMessageStream>> {
+        let mut stream = request.into_inner();
+        let (tx, rx) = tokio::sync::mpsc::channel(4);
+        tokio::spawn(async move {
+            let mut index = 0;
+            loop {
+                match stream.message().await {
+                    Ok(Some(request)) => {
+                        let response = fbs_response(&request, 
&format!("bidi-{index}"), index);
+                        if tx.send(Ok(response)).await.is_err() {
+                            break;
+                        }
+                        index += 1;
+                    }
+                    Ok(None) => break,
+                    Err(error) => {
+                        let _ = tx.send(Err(error)).await;
+                        break;
+                    }
+                }
+            }
+        });
+        Ok(Response::new(Box::pin(ReceiverStream::new(rx))))
+    }
+
+    async fn unary_union(
+        &self,
+        request: Request<GrpcFbsUnion>,
+    ) -> StatusResult<Response<GrpcFbsUnion>> {
+        let request = fbs_request_from_union(request.into_inner())?;
+        Ok(Response::new(GrpcFbsUnion::GrpcFbsResponse(fbs_response(
+            &request, "unary", 10,
+        ))))
+    }
+
+    async fn server_stream_union(
+        &self,
+        request: Request<GrpcFbsUnion>,
+    ) -> StatusResult<Response<Self::ServerStreamUnionStream>> {
+        let request = fbs_request_from_union(request.into_inner())?;
+        Ok(Response::new(Box::pin(tokio_stream::iter(
+            (0..3)
+                .map(|index| {
+                    Ok(GrpcFbsUnion::GrpcFbsResponse(fbs_response(
+                        &request,
+                        &format!("server-{index}"),
+                        index,
+                    )))
+                })
+                .collect::<Vec<_>>(),
+        ))))
+    }
+
+    async fn client_stream_union(
+        &self,
+        request: Request<tonic::Streaming<GrpcFbsUnion>>,
+    ) -> StatusResult<Response<GrpcFbsUnion>> {
+        let mut stream = request.into_inner();
+        let mut requests = Vec::new();
+        while let Some(union) = stream.message().await? {
+            requests.push(fbs_request_from_union(union)?);
+        }
+        Ok(Response::new(GrpcFbsUnion::GrpcFbsResponse(fbs_aggregate(
+            &requests,
+        ))))
+    }
+
+    async fn bidi_stream_union(
+        &self,
+        request: Request<tonic::Streaming<GrpcFbsUnion>>,
+    ) -> StatusResult<Response<Self::BidiStreamUnionStream>> {
+        let mut stream = request.into_inner();
+        let (tx, rx) = tokio::sync::mpsc::channel(4);
+        tokio::spawn(async move {
+            let mut index = 0;
+            loop {
+                match stream.message().await {
+                    Ok(Some(union)) => match fbs_request_from_union(union) {
+                        Ok(request) => {
+                            let response = 
GrpcFbsUnion::GrpcFbsResponse(fbs_response(
+                                &request,
+                                &format!("bidi-{index}"),
+                                index,
+                            ));
+                            if tx.send(Ok(response)).await.is_err() {
+                                break;
+                            }
+                            index += 1;
+                        }
+                        Err(error) => {
+                            let _ = tx.send(Err(error)).await;
+                            break;
+                        }
+                    },
+                    Ok(None) => break,
+                    Err(error) => {
+                        let _ = tx.send(Err(error)).await;
+                        break;
+                    }
+                }
+            }
+        });
+        Ok(Response::new(Box::pin(ReceiverStream::new(rx))))
+    }
+}
+
+// Builds a protobuf request.
+fn pb_request(id: &str, count: u32, payload: grpc_pb_request::Payload) -> 
GrpcPbRequest {
+    GrpcPbRequest {
+        id: id.to_string(),
+        count,
+        payload: Some(payload),
+    }
+}
+
+// Maps a protobuf request oneof payload to the expected response payload.
+fn pb_response_payload(
+    payload: &Option<grpc_pb_request::Payload>,
+    tag: &str,
+    offset: u32,
+) -> StatusResult<Option<grpc_pb_response::Payload>> {
+    match payload {
+        None => Ok(None),
+        Some(grpc_pb_request::Payload::Text(value)) => 
Ok(Some(grpc_pb_response::Payload::Text(
+            format!("{tag}:{value}"),
+        ))),
+        Some(grpc_pb_request::Payload::Number(value)) => {
+            Ok(Some(grpc_pb_response::Payload::Number(value + offset)))
+        }
+        Some(grpc_pb_request::Payload::Unknown(_)) => 
Err(Status::invalid_argument(
+            "expected grpc_pb_request::Payload text or number",
+        )),
+    }
+}
+
+// Builds the expected protobuf response for a request.
+fn pb_response(request: &GrpcPbRequest, tag: &str, offset: u32) -> 
StatusResult<GrpcPbResponse> {
+    Ok(GrpcPbResponse {
+        id: format!("{tag}:{}", request.id),
+        count: request.count + offset,
+        payload: pb_response_payload(&request.payload, tag, offset)?,
+    })
+}
+
+// Builds the aggregate protobuf response used by client-streaming RPCs.
+fn pb_aggregate(requests: &[GrpcPbRequest]) -> GrpcPbResponse {
+    GrpcPbResponse {
+        id: format!(
+            "client:{}",
+            requests
+                .iter()
+                .map(|request| request.id.as_str())
+                .collect::<Vec<_>>()
+                .join("+")
+        ),
+        count: requests.iter().map(|request| request.count).sum(),
+        payload: Some(grpc_pb_response::Payload::Text(format!(
+            "client:{}",
+            requests
+                .iter()
+                .map(|request| request.id.as_str())
+                .collect::<Vec<_>>()
+                .join("+")
+        ))),
+    }
+}
+
+async fn exercise_pb(channel: Channel) -> AnyResult<()> {
+    let mut client = PbGrpcServiceClient::new(channel);
+    let requests = vec![
+        pb_request(
+            "pb-a",
+            9,
+            grpc_pb_request::Payload::Text("alpha".to_string()),
+        ),
+        pb_request("pb-b", 10, grpc_pb_request::Payload::Number(42)),
+    ];
+    let first = requests[0].clone();
+    assert_eq!(
+        client.unary_message(first.clone()).await?.into_inner(),
+        pb_response(&first, "unary", 10)?
+    );
+    assert_eq!(
+        collect_stream(
+            client
+                .server_stream_message(first.clone())
+                .await?
+                .into_inner()
+        )
+        .await?,
+        (0..3)
+            .map(|index| pb_response(&first, &format!("server-{index}"), 
index))
+            .collect::<StatusResult<Vec<_>>>()?
+    );
+    assert_eq!(
+        client
+            .client_stream_message(tokio_stream::iter(requests.clone()))
+            .await?
+            .into_inner(),
+        pb_aggregate(&requests)
+    );
+    assert_eq!(
+        collect_stream(
+            client
+                .bidi_stream_message(tokio_stream::iter(requests.clone()))
+                .await?
+                .into_inner()
+        )
+        .await?,
+        requests
+            .iter()
+            .enumerate()
+            .map(|(index, request)| pb_response(request, 
&format!("bidi-{index}"), index as u32))
+            .collect::<StatusResult<Vec<_>>>()?
+    );
+    Ok(())
+}
+
+struct PbServiceImpl;
+
+#[tonic::async_trait]
+impl PbGrpcService for PbServiceImpl {
+    type ServerStreamMessageStream = GrpcStream<GrpcPbResponse>;
+    type BidiStreamMessageStream = GrpcStream<GrpcPbResponse>;
+
+    async fn unary_message(
+        &self,
+        request: Request<GrpcPbRequest>,
+    ) -> StatusResult<Response<GrpcPbResponse>> {
+        Ok(Response::new(pb_response(
+            &request.into_inner(),
+            "unary",
+            10,
+        )?))
+    }
+
+    async fn server_stream_message(
+        &self,
+        request: Request<GrpcPbRequest>,
+    ) -> StatusResult<Response<Self::ServerStreamMessageStream>> {
+        let request = request.into_inner();
+        let responses = (0..3)
+            .map(|index| pb_response(&request, &format!("server-{index}"), 
index))
+            .collect::<StatusResult<Vec<_>>>()?;
+        Ok(Response::new(Box::pin(tokio_stream::iter(
+            responses.into_iter().map(Ok).collect::<Vec<_>>(),
+        ))))
+    }
+
+    async fn client_stream_message(
+        &self,
+        request: Request<tonic::Streaming<GrpcPbRequest>>,
+    ) -> StatusResult<Response<GrpcPbResponse>> {
+        let mut stream = request.into_inner();
+        let mut requests = Vec::new();
+        while let Some(request) = stream.message().await? {
+            requests.push(request);
+        }
+        Ok(Response::new(pb_aggregate(&requests)))
+    }
+
+    async fn bidi_stream_message(
+        &self,
+        request: Request<tonic::Streaming<GrpcPbRequest>>,
+    ) -> StatusResult<Response<Self::BidiStreamMessageStream>> {
+        let mut stream = request.into_inner();
+        let (tx, rx) = tokio::sync::mpsc::channel(4);
+        tokio::spawn(async move {
+            let mut index = 0;
+            loop {
+                match stream.message().await {
+                    Ok(Some(request)) => {
+                        match pb_response(&request, &format!("bidi-{index}"), 
index) {
+                            Ok(response) => {
+                                if tx.send(Ok(response)).await.is_err() {
+                                    break;
+                                }
+                                index += 1;
+                            }
+                            Err(error) => {
+                                let _ = tx.send(Err(error)).await;
+                                break;
+                            }
+                        }
+                    }
+                    Ok(None) => break,
+                    Err(error) => {
+                        let _ = tx.send(Err(error)).await;
+                        break;
+                    }
+                }
+            }
+        });
+        Ok(Response::new(Box::pin(ReceiverStream::new(rx))))
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to