From 65ffdf3b2a4dd3e5993a518ebfeb8fc390af0d1a Mon Sep 17 00:00:00 2001 From: Saurav Date: Wed, 29 Oct 2025 07:54:13 +0000 Subject: [PATCH] Implement AsView and AsMut traits for Grpc messages These traits are supposed to provide abstractions over standard references and protobuf views and muts to eliminate hard dependency on protobuf The biggest challenge in the message module is to solve the long-standing difficulty of reborrowing mutable message views. The Problem: Reborrowing GATs When working with Generic Associated Types (GATs) for mutable views (e.g., type Mut<'a>), we frequently need to "reborrow" a view to pass it to a child function without consuming the original. However, traditional approaches fail here: 1. Foreign Types: We cannot add a .reborrow() method directly to protobuf::Mut as it is a foreign type. 2. HRTB Hell: Defining a separate Reborrow trait and requiring for<'a> T::Mut<'a>: Reborrow leads to complex Higher-Ranked Trait Bound errors (e.g., "implementation is not general enough") that are extremely difficult to satisfy in Rust's current type system. The Solution: Static Operator Pattern We bypass these issues by moving the reborrowing logic into the provider trait itself as a static operator. * Introduced AsMut::reborrow_view<'a, 'b>(view: &'b mut Self::Mut<'a>) -> Self::Mut<'b>. * This bundles the reborrowing logic with the type definition, avoiding the need for extra traits or complex HRTB bounds on the view type itself. Changes: * as_mut.rs: Defined AsMut with the reborrow_view static operator. Implemented it for protobuf::Message and added tests verifying that views can be reborrowed and mutated without consuming the original. * as_view.rs: Defined AsView for immutable views. --- grpc/Cargo.toml | 7 ++- grpc/src/server/message.rs | 20 +++++++ grpc/src/server/message/as_mut.rs | 78 +++++++++++++++++++++++++++ grpc/src/server/message/as_view.rs | 37 +++++++++++++ grpc/src/server/message/prost.rs | 83 +++++++++++++++++++++++++++++ grpc/src/server/message/protobuf.rs | 79 +++++++++++++++++++++++++++ grpc/src/server/mod.rs | 2 + 7 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 grpc/src/server/message.rs create mode 100644 grpc/src/server/message/as_mut.rs create mode 100644 grpc/src/server/message/as_view.rs create mode 100644 grpc/src/server/message/prost.rs create mode 100644 grpc/src/server/message/protobuf.rs diff --git a/grpc/Cargo.toml b/grpc/Cargo.toml index 352e65219..4dfd03c1c 100644 --- a/grpc/Cargo.toml +++ b/grpc/Cargo.toml @@ -15,7 +15,9 @@ allowed_external_types = [ ] [features] -default = ["dns", "_runtime-tokio", "tls-rustls"] +default = ["dns", "_runtime-tokio","tls-rustls", "protobuf"] +protobuf = ["dep:protobuf"] +prost = ["dep:prost"] dns = ["dep:hickory-resolver", "_runtime-tokio"] # The following feature is used to ensure all modules use the runtime # abstraction instead of using tokio directly. @@ -41,6 +43,8 @@ tls-rustls = [ ] [dependencies] +protobuf = { version = "4.33.0-release", optional = true } +prost = { version = "0.14.0", optional = true } bytes = "1.10.1" hickory-resolver = { version = "0.25.1", optional = true } http = "1.1.0" @@ -89,3 +93,4 @@ tonic = { version = "0.14.0", path = "../tonic", default-features = false, featu "router", ] } tonic-prost = { version = "0.14.0", path = "../tonic-prost" } +protobuf-well-known-types = "4.33.0-release" diff --git a/grpc/src/server/message.rs b/grpc/src/server/message.rs new file mode 100644 index 000000000..6331698c1 --- /dev/null +++ b/grpc/src/server/message.rs @@ -0,0 +1,20 @@ +//! Traits for accessing view and mutable view types of messages. +//! +//! These traits are needed to support Protobuf's design decision to prefer view and mut proxy types +//! over references. See +//! for more details. +//! +//! These traits allow for support of view and mut types while defaulting to regular references for +//! non-Protobuf classes that need gRPC support. + +pub mod as_mut; +pub mod as_view; + +pub use as_mut::AsMut; +pub use as_view::AsView; + +#[cfg(feature = "protobuf")] +pub mod protobuf; + +#[cfg(all(feature = "prost", not(feature = "protobuf")))] +pub mod prost; diff --git a/grpc/src/server/message/as_mut.rs b/grpc/src/server/message/as_mut.rs new file mode 100644 index 000000000..b43d89a7b --- /dev/null +++ b/grpc/src/server/message/as_mut.rs @@ -0,0 +1,78 @@ +use crate::server::message::AsView; + +pub trait AsMut { + /// We let this be whatever the library uses (e.g. protobuf::Mut). + type Mut<'a>: Send + AsView + 'a; + + fn as_mut(&mut self) -> Self::Mut<'_>; + + /// Implement `reborrow` to make it feel it feel like a mut reference. + /// Since we can't attach .reborrow() to the alias 'Self::Mut', + /// we define the logic here as a static utility function. + #[doc(hidden)] + fn reborrow_view<'a, 'b>(view: &'b mut Self::Mut<'a>) -> Self::Mut<'b> + where + 'a: 'b; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_custom_as_mut() { + struct TestMsg { + val: i32, + } + + struct TestMsgMut<'a> { + val: &'a mut i32, + } + + impl AsView for TestMsgMut<'_> { + type View<'msg> + = i32 + where + Self: 'msg; + fn as_view(&self) -> Self::View<'_> { + *self.val + } + } + + impl AsMut for TestMsg { + type Mut<'a> = TestMsgMut<'a>; + + fn as_mut(&mut self) -> Self::Mut<'_> { + TestMsgMut { val: &mut self.val } + } + + fn reborrow_view<'a, 'b>(view: &'b mut Self::Mut<'a>) -> Self::Mut<'b> + where + 'a: 'b, + { + TestMsgMut { val: view.val } + } + } + + let mut msg = TestMsg { val: 10 }; + + // 1. Test as_mut() + let mut view = msg.as_mut(); + assert_eq!(*view.val, 10); + *view.val = 20; + + // 2. Test reborrow_view() + { + let reborrowed = ::reborrow_view(&mut view); + assert_eq!(*reborrowed.val, 20); + *reborrowed.val = 30; + } + + // 3. Verify view is still usable (reborrow, not move) + assert_eq!(*view.val, 30); + *view.val = 40; + + // Verify changes persisted + assert_eq!(msg.val, 40); + } +} diff --git a/grpc/src/server/message/as_view.rs b/grpc/src/server/message/as_view.rs new file mode 100644 index 000000000..875eee754 --- /dev/null +++ b/grpc/src/server/message/as_view.rs @@ -0,0 +1,37 @@ +pub trait AsView { + /// A GAT for the view type. + type View<'msg>: Send + Copy + where + Self: 'msg; + + /// Creates a view of the object. + fn as_view(&self) -> Self::View<'_>; +} + +#[cfg(test)] +mod tests { + use super::AsView; + + #[test] + fn test_non_protobuf_view() { + #[derive(Debug, PartialEq)] + struct TestMessage { + id: i32, + } + + impl AsView for TestMessage { + type View<'msg> + = &'msg TestMessage + where + Self: 'msg; + fn as_view(&self) -> Self::View<'_> { + self + } + } + + let msg = TestMessage { id: 42 }; + let view = AsView::as_view(&msg); + assert_eq!(view.id, 42); + assert!(std::ptr::eq(view, &msg)); + } +} diff --git a/grpc/src/server/message/prost.rs b/grpc/src/server/message/prost.rs new file mode 100644 index 000000000..74adfe28c --- /dev/null +++ b/grpc/src/server/message/prost.rs @@ -0,0 +1,83 @@ +use super::{AsMut, AsView}; + +impl AsView for T +where + T: prost::Message + Sync + Send, +{ + type View<'msg> + = &'msg T + where + Self: 'msg; + + fn as_view(&self) -> Self::View<'_> { + self + } +} + +impl AsMut for T +where + T: prost::Message + Send + 'static, +{ + type Mut<'a> = &'a mut T; + + fn as_mut(&mut self) -> Self::Mut<'_> { + self + } + + fn reborrow_view<'a, 'b>(view: &'b mut Self::Mut<'a>) -> Self::Mut<'b> + where + 'a: 'b, + { + &mut **view + } +} + +#[cfg(test)] +mod tests { + use crate::server::message::{AsMut, AsView}; + use prost::Message; + + // A simple prost message for testing + #[derive(Clone, PartialEq, Message)] + struct ValidateRequest { + #[prost(string, tag = "1")] + pub service_name: String, + } + + #[test] + fn test_prost_view() { + let msg = ValidateRequest { + service_name: "test_service".to_string(), + }; + let view = AsView::as_view(&msg); + assert_eq!(view.service_name, "test_service"); + // For prost, the view is just a reference + assert_eq!(view as *const _, &msg as *const _); + } + + #[test] + fn test_prost_as_mut() { + let mut msg = ValidateRequest { + service_name: "test_service".to_string(), + }; + + // 1. Test as_mut() + let mut view = AsMut::as_mut(&mut msg); + assert_eq!(view.service_name, "test_service"); + view.service_name = "updated_service".to_string(); + + // 2. Test reborrow_view() + { + let reborrowed = ::reborrow_view(&mut view); + assert_eq!(reborrowed.service_name, "updated_service"); + reborrowed.service_name = "reborrowed_service".to_string(); + } + + // 3. Verify view is still usable + assert_eq!(view.service_name, "reborrowed_service"); + view.service_name = "final_service".to_string(); + + // Verify changes persisted + assert_eq!(msg.service_name, "final_service"); + } +} diff --git a/grpc/src/server/message/protobuf.rs b/grpc/src/server/message/protobuf.rs new file mode 100644 index 000000000..e3b2e39f0 --- /dev/null +++ b/grpc/src/server/message/protobuf.rs @@ -0,0 +1,79 @@ +use super::{AsMut, AsView}; + +impl AsView for T +where + T: protobuf::AsView + Sync, + // Require `Copy` to emulate "all shared references are copyable" + for<'msg> protobuf::View<'msg, ::Proxied>: Copy, +{ + type View<'msg> + = protobuf::View<'msg, ::Proxied> + where + Self: 'msg; + + fn as_view(&self) -> Self::View<'_> { + self.as_view() + } +} + +impl AsMut for T +where + T: protobuf::AsMut + Send + 'static, + for<'msg> protobuf::Mut<'msg, ::MutProxied>: Send + AsView, +{ + type Mut<'a> = protobuf::Mut<'a, ::MutProxied>; + + fn as_mut(&mut self) -> Self::Mut<'_> { + protobuf::AsMut::as_mut(self) + } + + fn reborrow_view<'a, 'b>(view: &'b mut Self::Mut<'a>) -> Self::Mut<'b> + where + 'a: 'b, + { + protobuf::AsMut::as_mut(view) + } +} + +#[cfg(test)] +mod tests { + use crate::server::message::{AsMut, AsView}; + + #[test] + fn test_protobuf_view() { + use protobuf_well_known_types::Timestamp; + let mut ts = Timestamp::new(); + ts.set_seconds(1234567890); + let view = AsView::as_view(&ts); + assert_eq!(view.seconds(), 1234567890); + } + + #[test] + fn test_protobuf_as_mut() { + use protobuf_well_known_types::Timestamp; + + let mut msg = Timestamp::new(); + msg.set_seconds(123); + msg.set_nanos(456); + + // 1. Test as_mut() + let mut view = AsMut::as_mut(&mut msg); + assert_eq!(view.seconds(), 123); + view.set_seconds(789); + + // 2. Test reborrow_view() + { + let mut reborrowed = ::reborrow_view(&mut view); + assert_eq!(reborrowed.seconds(), 789); + reborrowed.set_nanos(999); + } + + // 3. Verify view is still usable (reborrow, not move) + assert_eq!(view.nanos(), 999); + view.set_seconds(111); + + // Verify changes persisted + assert_eq!(msg.seconds(), 111); + assert_eq!(msg.nanos(), 999); + } +} diff --git a/grpc/src/server/mod.rs b/grpc/src/server/mod.rs index afef95dd2..dc61decd4 100644 --- a/grpc/src/server/mod.rs +++ b/grpc/src/server/mod.rs @@ -34,6 +34,8 @@ use crate::service::Request; use crate::service::Response; use crate::service::Service; +pub mod message; + pub struct Server { handler: Option>, }