From 57950b9ca063888c9f6d7045dda7fd46a3fc18e3 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Thu, 1 Jan 2026 17:57:36 +0100 Subject: [PATCH 1/7] Add service introspection for clients --- rclrs/src/client.rs | 38 +++++++++++++++++++++++++++++++++++++- rclrs/src/service.rs | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/rclrs/src/client.rs b/rclrs/src/client.rs index ecf4c379..3456f72b 100644 --- a/rclrs/src/client.rs +++ b/rclrs/src/client.rs @@ -10,7 +10,8 @@ use rosidl_runtime_rs::Message; use crate::{ error::ToResult, log_fatal, rcl_bindings::*, IntoPrimitiveOptions, MessageCow, Node, Promise, QoSProfile, RclPrimitive, RclPrimitiveHandle, RclPrimitiveKind, RclReturnCode, RclrsError, - ReadyKind, ServiceInfo, Waitable, WaitableLifecycle, ENTITY_LIFECYCLE_MUTEX, + ReadyKind, ServiceInfo, ServiceIntrospectionState, Waitable, WaitableLifecycle, + ENTITY_LIFECYCLE_MUTEX, }; mod client_async_callback; @@ -369,6 +370,41 @@ where lifecycle, })) } + + /// Configure service introspection for this client. + /// Service introspection allows tools to monitor service requests and responses. + /// Service introspection can be set to either + /// - Off: Disabled + /// - Metadata: Only metadata without any user data contents + /// - Contents: User data contents with metadata + pub fn configure_introspection( + &self, + introspection_state: ServiceIntrospectionState, + ) -> Result<(), RclrsError> { + let client = &mut *self.handle.rcl_client.lock().unwrap(); + let node = &mut *self.handle.node.handle().rcl_node.lock().unwrap(); + let clock = self.handle.node.get_clock(); + let rcl_clock = &mut *clock.get_rcl_clock().lock().unwrap(); + let type_support = ::get_type_support() + as *const rosidl_service_type_support_t; + + // SAFETY: No preconditions for this function. + let publisher_options = unsafe { rcl_publisher_get_default_options() }; + + unsafe { + rcl_client_configure_service_introspection( + client, + node, + rcl_clock, + type_support, + publisher_options, + introspection_state.into(), + ) + .ok()?; + } + + Ok(()) + } } /// `ClientOptions` are used by [`Node::create_client`][1] to initialize a diff --git a/rclrs/src/service.rs b/rclrs/src/service.rs index 1450c272..668df834 100644 --- a/rclrs/src/service.rs +++ b/rclrs/src/service.rs @@ -391,6 +391,45 @@ impl Drop for ServiceHandle { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ServiceIntrospectionState { + Off, + Metadata, + Contents, +} + +impl From for ServiceIntrospectionState { + fn from(value: rcl_service_introspection_state_e) -> Self { + match value { + rcl_service_introspection_state_e::RCL_SERVICE_INTROSPECTION_OFF => { + ServiceIntrospectionState::Off + } + rcl_service_introspection_state_e::RCL_SERVICE_INTROSPECTION_METADATA => { + ServiceIntrospectionState::Metadata + } + rcl_service_introspection_state_e::RCL_SERVICE_INTROSPECTION_CONTENTS => { + ServiceIntrospectionState::Contents + } + } + } +} + +impl From for rcl_service_introspection_state_e { + fn from(value: ServiceIntrospectionState) -> Self { + match value { + ServiceIntrospectionState::Off => { + rcl_service_introspection_state_e::RCL_SERVICE_INTROSPECTION_OFF + } + ServiceIntrospectionState::Metadata => { + rcl_service_introspection_state_e::RCL_SERVICE_INTROSPECTION_METADATA + } + ServiceIntrospectionState::Contents => { + rcl_service_introspection_state_e::RCL_SERVICE_INTROSPECTION_CONTENTS + } + } + } +} + #[cfg(test)] mod tests { use super::*; From 4908152c51d3bc4be80053ca1339d73f344f7274 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Thu, 1 Jan 2026 18:42:58 +0100 Subject: [PATCH 2/7] Add service introspection for service --- rclrs/src/node.rs | 8 ++++---- rclrs/src/service.rs | 45 +++++++++++++++++++++++++++++++++++++++----- rclrs/src/worker.rs | 2 +- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/rclrs/src/node.rs b/rclrs/src/node.rs index ed436df2..c03dcde6 100644 --- a/rclrs/src/node.rs +++ b/rclrs/src/node.rs @@ -632,7 +632,7 @@ impl NodeState { /// The advantage of creating a service directly from the [`NodeState`] is you /// can create async services using [`NodeState::create_async_service`]. pub fn create_service<'a, T, Args>( - &self, + self: &Arc, options: impl Into>, callback: impl IntoNodeServiceCallback, ) -> Result, RclrsError> @@ -642,7 +642,7 @@ impl NodeState { ServiceState::::create( options, callback.into_node_service_callback(), - &self.handle, + self, self.commands.async_worker_commands(), ) } @@ -723,7 +723,7 @@ impl NodeState { /// # Ok::<(), RclrsError>(()) /// ``` pub fn create_async_service<'a, T, Args>( - &self, + self: &Arc, options: impl Into>, callback: impl IntoAsyncServiceCallback, ) -> Result, RclrsError> @@ -733,7 +733,7 @@ impl NodeState { ServiceState::::create( options, callback.into_async_service_callback(), - &self.handle, + self, self.commands.async_worker_commands(), ) } diff --git a/rclrs/src/service.rs b/rclrs/src/service.rs index 668df834..986131cf 100644 --- a/rclrs/src/service.rs +++ b/rclrs/src/service.rs @@ -101,7 +101,7 @@ where pub(crate) fn create<'a>( options: impl Into>, callback: AnyServiceCallback, - node_handle: &Arc, + node: &Node, commands: &Arc, ) -> Result, RclrsError> { let ServiceOptions { name, qos } = options.into(); @@ -120,7 +120,7 @@ where service_options.qos = qos.into(); { - let rcl_node = node_handle.rcl_node.lock().unwrap(); + let rcl_node = node.handle().rcl_node.lock().unwrap(); let _lifecycle_lock = ENTITY_LIFECYCLE_MUTEX.lock().unwrap(); unsafe { // SAFETY: @@ -143,7 +143,7 @@ where let handle = Arc::new(ServiceHandle { rcl_service: Mutex::new(rcl_service), - node_handle: Arc::clone(&node_handle), + node: Arc::clone(node), }); let (waitable, lifecycle) = Waitable::new( @@ -164,6 +164,41 @@ where Ok(service) } + + /// Configure service introspection for this service. + /// Service introspection allows tools to monitor service requests and responses. + /// Service introspection can be set to either + /// - Off: Disabled + /// - Metadata: Only metadata without any user data contents + /// - Contents: User data contents with metadata + pub fn configure_introspection( + &self, + introspection_state: ServiceIntrospectionState, + ) -> Result<(), RclrsError> { + let service = &mut *self.handle.rcl_service.lock().unwrap(); + let node = &mut *self.handle.node.handle().rcl_node.lock().unwrap(); + let clock = self.handle.node.get_clock(); + let rcl_clock = &mut *clock.get_rcl_clock().lock().unwrap(); + let type_support = ::get_type_support() + as *const rosidl_service_type_support_t; + + // SAFETY: No preconditions for this function. + let publisher_options = unsafe { rcl_publisher_get_default_options() }; + + unsafe { + rcl_service_configure_service_introspection( + service, + node, + rcl_clock, + type_support, + publisher_options, + introspection_state.into(), + ) + .ok()?; + } + + Ok(()) + } } impl ServiceState { @@ -281,7 +316,7 @@ unsafe impl Send for rcl_service_t {} /// [1]: pub struct ServiceHandle { rcl_service: Mutex, - node_handle: Arc, + node: Node, } impl ServiceHandle { @@ -381,7 +416,7 @@ impl ServiceHandle { impl Drop for ServiceHandle { fn drop(&mut self) { let rcl_service = self.rcl_service.get_mut().unwrap(); - let mut rcl_node = self.node_handle.rcl_node.lock().unwrap(); + let mut rcl_node = self.node.handle().rcl_node.lock().unwrap(); let _lifecycle_lock = ENTITY_LIFECYCLE_MUTEX.lock().unwrap(); // SAFETY: The entity lifecycle mutex is locked to protect against the risk of // global variables in the rmw implementation being unsafely modified during cleanup. diff --git a/rclrs/src/worker.rs b/rclrs/src/worker.rs index 73a3b09d..6e390129 100644 --- a/rclrs/src/worker.rs +++ b/rclrs/src/worker.rs @@ -428,7 +428,7 @@ impl WorkerState { ServiceState::>::create( options, callback.into_worker_service_callback(), - self.node.handle(), + &self.node, &self.commands, ) } From a305501047ea5ce490202609b1df105962f05933 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Thu, 1 Jan 2026 18:51:38 +0100 Subject: [PATCH 3/7] fixup! Add service introspection for service --- rclrs/src/service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rclrs/src/service.rs b/rclrs/src/service.rs index 986131cf..a68b1b6f 100644 --- a/rclrs/src/service.rs +++ b/rclrs/src/service.rs @@ -143,7 +143,7 @@ where let handle = Arc::new(ServiceHandle { rcl_service: Mutex::new(rcl_service), - node: Arc::clone(node), + node: Arc::clone(&node), }); let (waitable, lifecycle) = Waitable::new( From 531533ba5096414f77c96cced3eb68a945b7c082 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Thu, 1 Jan 2026 20:32:01 +0100 Subject: [PATCH 4/7] Fix circular NodeState Arc reference By making the Service keep a reference to the Node instead of the NodeHandle we were creating a circular reference that prevented the Node from being dropped. NodeState -> ParameterInterface -> ParameterService -> Service -> ServiceHandle -> NodeState This change modifies the Service to keep a reference to the NodeHandle again and to the Clock breaking the circular reference. --- rclrs/src/service.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/rclrs/src/service.rs b/rclrs/src/service.rs index a68b1b6f..57ccac9d 100644 --- a/rclrs/src/service.rs +++ b/rclrs/src/service.rs @@ -8,7 +8,7 @@ use std::{ use rosidl_runtime_rs::{Message, Service as ServiceIDL}; use crate::{ - error::ToResult, rcl_bindings::*, IntoPrimitiveOptions, MessageCow, Node, NodeHandle, + error::ToResult, rcl_bindings::*, Clock, IntoPrimitiveOptions, MessageCow, Node, NodeHandle, QoSProfile, RclPrimitive, RclPrimitiveHandle, RclPrimitiveKind, RclrsError, ReadyKind, Waitable, WaitableLifecycle, WorkScope, Worker, WorkerCommands, ENTITY_LIFECYCLE_MUTEX, }; @@ -143,7 +143,8 @@ where let handle = Arc::new(ServiceHandle { rcl_service: Mutex::new(rcl_service), - node: Arc::clone(&node), + node_handle: Arc::clone(node.handle()), + clock: node.get_clock(), }); let (waitable, lifecycle) = Waitable::new( @@ -176,8 +177,8 @@ where introspection_state: ServiceIntrospectionState, ) -> Result<(), RclrsError> { let service = &mut *self.handle.rcl_service.lock().unwrap(); - let node = &mut *self.handle.node.handle().rcl_node.lock().unwrap(); - let clock = self.handle.node.get_clock(); + let node = &mut *self.handle.node_handle.rcl_node.lock().unwrap(); + let clock = &self.handle.clock; let rcl_clock = &mut *clock.get_rcl_clock().lock().unwrap(); let type_support = ::get_type_support() as *const rosidl_service_type_support_t; @@ -316,7 +317,8 @@ unsafe impl Send for rcl_service_t {} /// [1]: pub struct ServiceHandle { rcl_service: Mutex, - node: Node, + node_handle: Arc, + clock: Clock, } impl ServiceHandle { @@ -416,7 +418,7 @@ impl ServiceHandle { impl Drop for ServiceHandle { fn drop(&mut self) { let rcl_service = self.rcl_service.get_mut().unwrap(); - let mut rcl_node = self.node.handle().rcl_node.lock().unwrap(); + let mut rcl_node = self.node_handle.rcl_node.lock().unwrap(); let _lifecycle_lock = ENTITY_LIFECYCLE_MUTEX.lock().unwrap(); // SAFETY: The entity lifecycle mutex is locked to protect against the risk of // global variables in the rmw implementation being unsafely modified during cleanup. From 4742bd9cfa71abfef9bed598cca8d19a5ba31943 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Sun, 25 Jan 2026 00:43:42 +0100 Subject: [PATCH 5/7] Fix API missing in humble --- rclrs/src/client.rs | 9 +++++++-- rclrs/src/service.rs | 8 ++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/rclrs/src/client.rs b/rclrs/src/client.rs index 3456f72b..e5fb1087 100644 --- a/rclrs/src/client.rs +++ b/rclrs/src/client.rs @@ -10,10 +10,13 @@ use rosidl_runtime_rs::Message; use crate::{ error::ToResult, log_fatal, rcl_bindings::*, IntoPrimitiveOptions, MessageCow, Node, Promise, QoSProfile, RclPrimitive, RclPrimitiveHandle, RclPrimitiveKind, RclReturnCode, RclrsError, - ReadyKind, ServiceInfo, ServiceIntrospectionState, Waitable, WaitableLifecycle, - ENTITY_LIFECYCLE_MUTEX, + ReadyKind, ServiceInfo, Waitable, WaitableLifecycle, ENTITY_LIFECYCLE_MUTEX, }; +// The API for service introspection was added in Jazzy. +#[cfg(not(ros_distro = "humble"))] +use crate::ServiceIntrospectionState; + mod client_async_callback; pub use client_async_callback::*; @@ -377,6 +380,8 @@ where /// - Off: Disabled /// - Metadata: Only metadata without any user data contents /// - Contents: User data contents with metadata + // The API for service introspection was added in Jazzy. + #[cfg(not(ros_distro = "humble"))] pub fn configure_introspection( &self, introspection_state: ServiceIntrospectionState, diff --git a/rclrs/src/service.rs b/rclrs/src/service.rs index 57ccac9d..408bf1c3 100644 --- a/rclrs/src/service.rs +++ b/rclrs/src/service.rs @@ -172,6 +172,8 @@ where /// - Off: Disabled /// - Metadata: Only metadata without any user data contents /// - Contents: User data contents with metadata + // The API for service introspection was added in Jazzy. + #[cfg(not(ros_distro = "humble"))] pub fn configure_introspection( &self, introspection_state: ServiceIntrospectionState, @@ -428,6 +430,8 @@ impl Drop for ServiceHandle { } } +// The API for service introspection was added in Jazzy. +#[cfg(not(ros_distro = "humble"))] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ServiceIntrospectionState { Off, @@ -435,6 +439,8 @@ pub enum ServiceIntrospectionState { Contents, } +// The API for service introspection was added in Jazzy. +#[cfg(not(ros_distro = "humble"))] impl From for ServiceIntrospectionState { fn from(value: rcl_service_introspection_state_e) -> Self { match value { @@ -451,6 +457,8 @@ impl From for ServiceIntrospectionState { } } +// The API for service introspection was added in Jazzy. +#[cfg(not(ros_distro = "humble"))] impl From for rcl_service_introspection_state_e { fn from(value: ServiceIntrospectionState) -> Self { match value { From 8a16f8126c267393061863d8d1b2039dbf80853b Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Sun, 25 Jan 2026 01:35:05 +0100 Subject: [PATCH 6/7] Add missing docs --- rclrs/src/service.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rclrs/src/service.rs b/rclrs/src/service.rs index 408bf1c3..d9bc5814 100644 --- a/rclrs/src/service.rs +++ b/rclrs/src/service.rs @@ -430,12 +430,16 @@ impl Drop for ServiceHandle { } } +/// The possible service introspection configurations // The API for service introspection was added in Jazzy. #[cfg(not(ros_distro = "humble"))] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ServiceIntrospectionState { + /// Disable service introspection Off, + /// Enable service introspection with metadata only Metadata, + /// Enable service introspection with metadata and contents Contents, } From 8d8e8cf99b44ad5465150d428c1c2b810f043255 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Sat, 14 Feb 2026 13:25:32 +0100 Subject: [PATCH 7/7] Review feedback --- rclrs/src/node.rs | 2 +- rclrs/src/service.rs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/rclrs/src/node.rs b/rclrs/src/node.rs index c03dcde6..a48ad42f 100644 --- a/rclrs/src/node.rs +++ b/rclrs/src/node.rs @@ -632,7 +632,7 @@ impl NodeState { /// The advantage of creating a service directly from the [`NodeState`] is you /// can create async services using [`NodeState::create_async_service`]. pub fn create_service<'a, T, Args>( - self: &Arc, + &self, options: impl Into>, callback: impl IntoNodeServiceCallback, ) -> Result, RclrsError> diff --git a/rclrs/src/service.rs b/rclrs/src/service.rs index d9bc5814..f5e43bae 100644 --- a/rclrs/src/service.rs +++ b/rclrs/src/service.rs @@ -9,8 +9,9 @@ use rosidl_runtime_rs::{Message, Service as ServiceIDL}; use crate::{ error::ToResult, rcl_bindings::*, Clock, IntoPrimitiveOptions, MessageCow, Node, NodeHandle, - QoSProfile, RclPrimitive, RclPrimitiveHandle, RclPrimitiveKind, RclrsError, ReadyKind, - Waitable, WaitableLifecycle, WorkScope, Worker, WorkerCommands, ENTITY_LIFECYCLE_MUTEX, + NodeState, QoSProfile, RclPrimitive, RclPrimitiveHandle, RclPrimitiveKind, RclrsError, + ReadyKind, Waitable, WaitableLifecycle, WorkScope, Worker, WorkerCommands, + ENTITY_LIFECYCLE_MUTEX, }; mod any_service_callback; @@ -101,7 +102,7 @@ where pub(crate) fn create<'a>( options: impl Into>, callback: AnyServiceCallback, - node: &Node, + node: &NodeState, commands: &Arc, ) -> Result, RclrsError> { let ServiceOptions { name, qos } = options.into();