Skip to main content

zerodds_rpc/
function_call.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Function-Call-Style Service-API (Spec §7.2.2.1, §7.9.2.1, §7.10.1).
5//!
6//! Das DDS-RPC-Spec definiert zwei Language-Binding-Styles:
7//! 1. **Request/Reply-Style** (low-level) — `crates/rpc/src/{requester,
8//!    replier}.rs`.
9//! 2. **Function-Call-Style** (high-level) — *dieses Modul*.
10//!
11//! Function-Call-Style verwendet Stubs (Client-Side-Proxy) und
12//! Skeletons (Service-Side-Dispatch), die zur Codegen-Zeit aus einer
13//! Service-Definition (IDL `interface Foo { void op(); }`) generiert
14//! werden. Die Stubs sehen wie native Function-Calls aus, kapseln
15//! aber intern den Request/Reply-Pfad.
16//!
17//! # Architektur
18//!
19//! Wir liefern hier die **Runtime-Foundation** fuer generierte Stubs
20//! und Skeletons:
21//!
22//! * [`FunctionStub`]-Trait fuer Client-Side-Proxies (jede generierte
23//!   Stub-Klasse implementiert es).
24//! * [`FunctionSkeleton`]-Trait fuer Service-Side-Dispatch — ruft die
25//!   richtige Operation aus dem `request_data`-Union-Discriminator
26//!   und liefert die Reply.
27//! * [`dispatch_request`]-Helper fuer Skeleton-Implementations.
28//!
29//! Codegen-Templates leben in `crates/idl-cpp/src/rpc_template.rs`
30//! (C++) und `crates/idl-java/src/rpc_template.rs` (Java).
31
32extern crate alloc;
33
34use alloc::string::String;
35use alloc::vec::Vec;
36
37use crate::error::{RpcError, RpcResult};
38
39/// Stub-Trait: jeder generierte Client-Side-Proxy implementiert das.
40///
41/// Der Stub kapselt den Request/Reply-Mechanismus hinter einer
42/// type-safe Method-Signatur (z.B. `fn add(a: i32, b: i32) -> i32`).
43pub trait FunctionStub {
44    /// Service-Name aus IDL-`interface`-Namen.
45    fn service_name(&self) -> &str;
46}
47
48/// Skeleton-Trait: jeder generierte Service-Side-Dispatch implementiert
49/// das. Der Skeleton entpackt den Request-Discriminator, ruft die
50/// passende Operation in der User-Implementation und packt die Reply
51/// als Union zurueck.
52pub trait FunctionSkeleton {
53    /// Service-Name.
54    fn service_name(&self) -> &str;
55
56    /// Liste aller Operations, die dieser Skeleton entgegen nimmt.
57    /// Jeder Eintrag ist `(operation_name, opcode)`. Opcodes werden
58    /// bei der Codegen automatisch monoton vergeben (Spec §7.2.2.1).
59    fn operations(&self) -> &[(&'static str, u32)];
60}
61
62/// Operation-Descriptor fuer Codegen.
63///
64/// Pro IDL-Operation `void op(in t1 x, out t2 y) raises (E)` erzeugt
65/// der Codegen einen [`OperationDescriptor`].
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct OperationDescriptor {
68    /// Method-Name aus IDL.
69    pub name: String,
70    /// Monoton vergebener Opcode.
71    pub opcode: u32,
72    /// `true` wenn `oneway`-Spec — kein Reply erwartet.
73    pub one_way: bool,
74    /// Liste der `in`/`inout`-Parameter (Reihenfolge wie in IDL).
75    pub in_params: Vec<String>,
76    /// Liste der `out`/`inout`-Parameter + Return-Type (Spec
77    /// §7.2.4.2 mappt Return zur ersten Member-Position).
78    pub out_params: Vec<String>,
79}
80
81/// Service-Descriptor fuer Codegen — Sammlung von [`OperationDescriptor`]s.
82#[derive(Debug, Clone, PartialEq, Eq, Default)]
83pub struct ServiceDescriptor {
84    /// IDL-Interface-Name.
85    pub name: String,
86    /// Operations in IDL-Reihenfolge.
87    pub operations: Vec<OperationDescriptor>,
88}
89
90impl ServiceDescriptor {
91    /// Konstruktor.
92    #[must_use]
93    pub fn new(name: impl Into<String>) -> Self {
94        Self {
95            name: name.into(),
96            operations: Vec::new(),
97        }
98    }
99
100    /// Registriert eine Operation. Opcode wird automatisch vergeben.
101    ///
102    /// # Errors
103    /// `RpcError::Codec` wenn die Operations-Anzahl `u32::MAX`
104    /// ueberschreitet.
105    pub fn add_operation(
106        &mut self,
107        name: impl Into<String>,
108        one_way: bool,
109        in_params: Vec<String>,
110        out_params: Vec<String>,
111    ) -> RpcResult<&OperationDescriptor> {
112        let opcode = u32::try_from(self.operations.len()).map_err(|_| {
113            RpcError::Codec("ServiceDescriptor: too many operations (>u32::MAX)".into())
114        })?;
115        self.operations.push(OperationDescriptor {
116            name: name.into(),
117            opcode,
118            one_way,
119            in_params,
120            out_params,
121        });
122        self.operations
123            .last()
124            .ok_or_else(|| RpcError::Codec("ServiceDescriptor: push failed".into()))
125    }
126
127    /// Lookup nach Name.
128    #[must_use]
129    pub fn operation(&self, name: &str) -> Option<&OperationDescriptor> {
130        self.operations.iter().find(|o| o.name == name)
131    }
132
133    /// Lookup nach Opcode.
134    #[must_use]
135    pub fn operation_by_opcode(&self, opcode: u32) -> Option<&OperationDescriptor> {
136        self.operations.iter().find(|o| o.opcode == opcode)
137    }
138}
139
140/// Dispatcher-Helper fuer Skeleton-Implementations.
141///
142/// Liest den Opcode aus dem Request-Discriminator, ruft den
143/// passenden Handler-Closure und liefert die kodierte Reply zurueck.
144///
145/// # Errors
146/// `OperationNotFound` wenn der Opcode nicht im Service vorhanden ist.
147pub fn dispatch_request<F, T>(service: &ServiceDescriptor, opcode: u32, handler: F) -> RpcResult<T>
148where
149    F: FnOnce(&OperationDescriptor) -> RpcResult<T>,
150{
151    let op = service
152        .operation_by_opcode(opcode)
153        .ok_or_else(|| RpcError::Codec("function-call: unknown opcode".into()))?;
154    handler(op)
155}
156
157#[cfg(test)]
158#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
159mod tests {
160    use super::*;
161
162    fn calculator_service() -> ServiceDescriptor {
163        let mut s = ServiceDescriptor::new("Calculator");
164        s.add_operation(
165            "add",
166            false,
167            alloc::vec!["a".into(), "b".into()],
168            alloc::vec!["result".into()],
169        )
170        .expect("add");
171        s.add_operation(
172            "subtract",
173            false,
174            alloc::vec!["a".into(), "b".into()],
175            alloc::vec!["result".into()],
176        )
177        .expect("subtract");
178        s.add_operation("ping", true, alloc::vec![], alloc::vec![])
179            .expect("ping");
180        s
181    }
182
183    #[test]
184    fn service_descriptor_assigns_monotonic_opcodes() {
185        let s = calculator_service();
186        assert_eq!(s.operations[0].opcode, 0);
187        assert_eq!(s.operations[1].opcode, 1);
188        assert_eq!(s.operations[2].opcode, 2);
189    }
190
191    #[test]
192    fn service_descriptor_lookup_by_name() {
193        let s = calculator_service();
194        assert_eq!(s.operation("add").map(|o| o.opcode), Some(0));
195        assert_eq!(s.operation("subtract").map(|o| o.opcode), Some(1));
196        assert!(s.operation("nonexistent").is_none());
197    }
198
199    #[test]
200    fn service_descriptor_lookup_by_opcode() {
201        let s = calculator_service();
202        assert_eq!(
203            s.operation_by_opcode(0).map(|o| o.name.as_str()),
204            Some("add")
205        );
206        assert!(s.operation_by_opcode(99).is_none());
207    }
208
209    #[test]
210    fn one_way_operation_marked_correctly() {
211        let s = calculator_service();
212        let ping = s.operation("ping").expect("ping");
213        assert!(ping.one_way);
214        let add = s.operation("add").expect("add");
215        assert!(!add.one_way);
216    }
217
218    #[test]
219    fn dispatch_request_routes_by_opcode() {
220        let s = calculator_service();
221        let result = dispatch_request(&s, 0, |op| Ok::<String, RpcError>(op.name.clone()))
222            .expect("dispatch");
223        assert_eq!(result, "add");
224    }
225
226    #[test]
227    fn dispatch_request_unknown_opcode_returns_codec_error() {
228        let s = calculator_service();
229        let err = dispatch_request(&s, 99, |_op| Ok::<(), RpcError>(())).expect_err("unknown");
230        assert!(matches!(err, RpcError::Codec(_)));
231    }
232
233    #[test]
234    fn out_params_first_member_is_return_value() {
235        // Spec §7.2.4.2: Return-Type mapped auf ersten Member von
236        // `out_params`.
237        let s = calculator_service();
238        let add = s.operation("add").expect("add");
239        assert_eq!(add.out_params.first().map(String::as_str), Some("result"));
240    }
241
242    /// Test-Stub als Beispiel-Codegen-Output.
243    struct CalculatorStub {
244        service_name: String,
245    }
246    impl FunctionStub for CalculatorStub {
247        fn service_name(&self) -> &str {
248            &self.service_name
249        }
250    }
251
252    /// Test-Skeleton als Beispiel-Codegen-Output.
253    struct CalculatorSkeleton;
254    impl FunctionSkeleton for CalculatorSkeleton {
255        fn service_name(&self) -> &str {
256            "Calculator"
257        }
258        fn operations(&self) -> &[(&'static str, u32)] {
259            &[("add", 0), ("subtract", 1), ("ping", 2)]
260        }
261    }
262
263    #[test]
264    fn stub_and_skeleton_traits_are_object_safe() {
265        let stub: alloc::boxed::Box<dyn FunctionStub> = alloc::boxed::Box::new(CalculatorStub {
266            service_name: "Calc".into(),
267        });
268        let skel: alloc::boxed::Box<dyn FunctionSkeleton> =
269            alloc::boxed::Box::new(CalculatorSkeleton);
270        assert_eq!(stub.service_name(), "Calc");
271        assert_eq!(skel.service_name(), "Calculator");
272        assert_eq!(skel.operations().len(), 3);
273    }
274}