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//! The DDS-RPC spec defines two language-binding styles:
7//! 1. **Request/reply style** (low-level) — `crates/rpc/src/{requester,
8//!    replier}.rs`.
9//! 2. **Function-call style** (high-level) — *this module*.
10//!
11//! Function-call style uses stubs (client-side proxy) and
12//! skeletons (service-side dispatch), generated at codegen time from a
13//! service definition (IDL `interface Foo { void op(); }`).
14//! The stubs look like native function calls, but internally
15//! encapsulate the request/reply path.
16//!
17//! # Architecture
18//!
19//! Here we provide the **runtime foundation** for generated stubs
20//! and skeletons:
21//!
22//! * The [`FunctionStub`] trait for client-side proxies (each generated
23//!   stub class implements it).
24//! * The [`FunctionSkeleton`] trait for service-side dispatch — calls the
25//!   right operation from the `request_data` union discriminator
26//!   and returns the reply.
27//! * The [`dispatch_request`] helper for skeleton implementations.
28//!
29//! Codegen templates live in `crates/idl-cpp/src/rpc_template.rs`
30//! (C++) and `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: every generated client-side proxy implements it.
40///
41/// The stub encapsulates the request/reply mechanism behind a
42/// type-safe method signature (e.g. `fn add(a: i32, b: i32) -> i32`).
43pub trait FunctionStub {
44    /// Service name from the IDL `interface` name.
45    fn service_name(&self) -> &str;
46}
47
48/// Skeleton trait: every generated service-side dispatch implements
49/// it. The skeleton unpacks the request discriminator, calls the
50/// matching operation in the user implementation and packs the reply
51/// back as a union.
52pub trait FunctionSkeleton {
53    /// Service name.
54    fn service_name(&self) -> &str;
55
56    /// List of all operations this skeleton accepts.
57    /// Each entry is `(operation_name, opcode)`. Opcodes are
58    /// assigned monotonically and automatically at codegen time (Spec §7.2.2.1).
59    fn operations(&self) -> &[(&'static str, u32)];
60}
61
62/// Operation descriptor for codegen.
63///
64/// For each IDL operation `void op(in t1 x, out t2 y) raises (E)` the
65/// codegen produces an [`OperationDescriptor`].
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct OperationDescriptor {
68    /// Method name from IDL.
69    pub name: String,
70    /// Monotonically assigned opcode.
71    pub opcode: u32,
72    /// `true` for a `oneway` spec — no reply expected.
73    pub one_way: bool,
74    /// List of `in`/`inout` parameters (order as in IDL).
75    pub in_params: Vec<String>,
76    /// List of `out`/`inout` parameters + return type (Spec
77    /// §7.2.4.2 maps the return to the first member position).
78    pub out_params: Vec<String>,
79}
80
81/// Service descriptor for codegen — collection of [`OperationDescriptor`]s.
82#[derive(Debug, Clone, PartialEq, Eq, Default)]
83pub struct ServiceDescriptor {
84    /// IDL-Interface-Name.
85    pub name: String,
86    /// Operations in IDL order.
87    pub operations: Vec<OperationDescriptor>,
88}
89
90impl ServiceDescriptor {
91    /// Constructor.
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    /// Registers an operation. The opcode is assigned automatically.
101    ///
102    /// # Errors
103    /// `RpcError::Codec` if the operation count exceeds `u32::MAX`.
104    pub fn add_operation(
105        &mut self,
106        name: impl Into<String>,
107        one_way: bool,
108        in_params: Vec<String>,
109        out_params: Vec<String>,
110    ) -> RpcResult<&OperationDescriptor> {
111        let opcode = u32::try_from(self.operations.len()).map_err(|_| {
112            RpcError::Codec("ServiceDescriptor: too many operations (>u32::MAX)".into())
113        })?;
114        self.operations.push(OperationDescriptor {
115            name: name.into(),
116            opcode,
117            one_way,
118            in_params,
119            out_params,
120        });
121        self.operations
122            .last()
123            .ok_or_else(|| RpcError::Codec("ServiceDescriptor: push failed".into()))
124    }
125
126    /// Lookup by name.
127    #[must_use]
128    pub fn operation(&self, name: &str) -> Option<&OperationDescriptor> {
129        self.operations.iter().find(|o| o.name == name)
130    }
131
132    /// Lookup by opcode.
133    #[must_use]
134    pub fn operation_by_opcode(&self, opcode: u32) -> Option<&OperationDescriptor> {
135        self.operations.iter().find(|o| o.opcode == opcode)
136    }
137}
138
139/// Dispatcher helper for skeleton implementations.
140///
141/// Reads the opcode from the request discriminator, calls the
142/// matching handler closure and returns the encoded reply.
143///
144/// # Errors
145/// `OperationNotFound` if the opcode is not present in the service.
146pub fn dispatch_request<F, T>(service: &ServiceDescriptor, opcode: u32, handler: F) -> RpcResult<T>
147where
148    F: FnOnce(&OperationDescriptor) -> RpcResult<T>,
149{
150    let op = service
151        .operation_by_opcode(opcode)
152        .ok_or_else(|| RpcError::Codec("function-call: unknown opcode".into()))?;
153    handler(op)
154}
155
156#[cfg(test)]
157#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
158mod tests {
159    use super::*;
160
161    fn calculator_service() -> ServiceDescriptor {
162        let mut s = ServiceDescriptor::new("Calculator");
163        s.add_operation(
164            "add",
165            false,
166            alloc::vec!["a".into(), "b".into()],
167            alloc::vec!["result".into()],
168        )
169        .expect("add");
170        s.add_operation(
171            "subtract",
172            false,
173            alloc::vec!["a".into(), "b".into()],
174            alloc::vec!["result".into()],
175        )
176        .expect("subtract");
177        s.add_operation("ping", true, alloc::vec![], alloc::vec![])
178            .expect("ping");
179        s
180    }
181
182    #[test]
183    fn service_descriptor_assigns_monotonic_opcodes() {
184        let s = calculator_service();
185        assert_eq!(s.operations[0].opcode, 0);
186        assert_eq!(s.operations[1].opcode, 1);
187        assert_eq!(s.operations[2].opcode, 2);
188    }
189
190    #[test]
191    fn service_descriptor_lookup_by_name() {
192        let s = calculator_service();
193        assert_eq!(s.operation("add").map(|o| o.opcode), Some(0));
194        assert_eq!(s.operation("subtract").map(|o| o.opcode), Some(1));
195        assert!(s.operation("nonexistent").is_none());
196    }
197
198    #[test]
199    fn service_descriptor_lookup_by_opcode() {
200        let s = calculator_service();
201        assert_eq!(
202            s.operation_by_opcode(0).map(|o| o.name.as_str()),
203            Some("add")
204        );
205        assert!(s.operation_by_opcode(99).is_none());
206    }
207
208    #[test]
209    fn one_way_operation_marked_correctly() {
210        let s = calculator_service();
211        let ping = s.operation("ping").expect("ping");
212        assert!(ping.one_way);
213        let add = s.operation("add").expect("add");
214        assert!(!add.one_way);
215    }
216
217    #[test]
218    fn dispatch_request_routes_by_opcode() {
219        let s = calculator_service();
220        let result = dispatch_request(&s, 0, |op| Ok::<String, RpcError>(op.name.clone()))
221            .expect("dispatch");
222        assert_eq!(result, "add");
223    }
224
225    #[test]
226    fn dispatch_request_unknown_opcode_returns_codec_error() {
227        let s = calculator_service();
228        let err = dispatch_request(&s, 99, |_op| Ok::<(), RpcError>(())).expect_err("unknown");
229        assert!(matches!(err, RpcError::Codec(_)));
230    }
231
232    #[test]
233    fn out_params_first_member_is_return_value() {
234        // Spec §7.2.4.2: the return type is mapped to the first member of
235        // `out_params`.
236        let s = calculator_service();
237        let add = s.operation("add").expect("add");
238        assert_eq!(add.out_params.first().map(String::as_str), Some("result"));
239    }
240
241    /// Test stub as an example codegen output.
242    struct CalculatorStub {
243        service_name: String,
244    }
245    impl FunctionStub for CalculatorStub {
246        fn service_name(&self) -> &str {
247            &self.service_name
248        }
249    }
250
251    /// Test skeleton as an example codegen output.
252    struct CalculatorSkeleton;
253    impl FunctionSkeleton for CalculatorSkeleton {
254        fn service_name(&self) -> &str {
255            "Calculator"
256        }
257        fn operations(&self) -> &[(&'static str, u32)] {
258            &[("add", 0), ("subtract", 1), ("ping", 2)]
259        }
260    }
261
262    #[test]
263    fn stub_and_skeleton_traits_are_object_safe() {
264        let stub: alloc::boxed::Box<dyn FunctionStub> = alloc::boxed::Box::new(CalculatorStub {
265            service_name: "Calc".into(),
266        });
267        let skel: alloc::boxed::Box<dyn FunctionSkeleton> =
268            alloc::boxed::Box::new(CalculatorSkeleton);
269        assert_eq!(stub.service_name(), "Calc");
270        assert_eq!(skel.service_name(), "Calculator");
271        assert_eq!(skel.operations().len(), 3);
272    }
273}