Skip to main content

ClientTrait

Trait ClientTrait 

Source
pub trait ClientTrait {
    type Error;

    // Required methods
    fn send_request_raw(&mut self, request: &[u8]) -> Result<i64, Self::Error>;
    fn take_response_raw(
        &mut self,
        reply_buf: &mut [u8],
    ) -> Result<Option<(usize, i64)>, Self::Error>;

    // Provided methods
    fn refused_response_len(&self) -> Option<usize> { ... }
    fn send_request<S: RosService>(
        &mut self,
        request: &S::Request,
        req_buf: &mut [u8],
    ) -> Result<(), Self::Error>
       where Self::Error: From<TransportError> { ... }
    fn take_response<S: RosService>(
        &mut self,
        reply_buf: &mut [u8],
    ) -> Result<Option<S::Reply>, Self::Error>
       where Self::Error: From<TransportError> { ... }
    fn register_waker(&self, _waker: &Waker) { ... }
    fn service_is_ready(&self) -> Result<bool, Self::Error>
       where Self::Error: From<TransportError> { ... }
    fn request_publisher_actual_qos(&self) -> QoSProfile { ... }
    fn response_subscription_actual_qos(&self) -> QoSProfile { ... }
}
Expand description

Service client trait for sending requests.

§Threading

&mut self on every method — the client is single-owner. For fan-out request patterns, create one client per worker thread.

§Calling pattern

All in-tree backends route blocking waits through the executor:

  1. send_request_raw(buf) — non-blocking; returns once the request is queued for transmission.
  2. The executor’s drive_io runs.
  3. take_response_raw(buf) — non-blocking; returns Ok(Some(len)) when the reply is back.

Phase-301 (issue 0240): the deprecated blocking call_raw path is DELETED — send_request_raw + take_response_raw is the one request/reply path, and both are required for service-capable backends.

Required Associated Types§

Source

type Error

Error type for service operations

Required Methods§

Source

fn send_request_raw(&mut self, request: &[u8]) -> Result<i64, Self::Error>

Send a service request without waiting for a reply (non-blocking).

Returns the SEQUENCE ID the backend assigned. The caller must subsequently poll take_response_raw and match that id against the one the reply carries.

Issue 0778 — this returned () until 2026-08-25, and every backend computed an id and discarded it. With nothing to correlate by, a client with two calls outstanding could not tell the replies apart, so each backend picked a policy: cyclonedds abandoned the older request, zenoh took the first reply. Both are wrong for send_goal and SetParameters, which travel this path and are not idempotent.

Source

fn take_response_raw( &mut self, reply_buf: &mut [u8], ) -> Result<Option<(usize, i64)>, Self::Error>

Poll for a reply (non-blocking).

Returns Ok(Some((len, sequence_id))) when a reply has arrived, Ok(None) if not yet available, or Err on failure. The sequence_id is the one send_request_raw returned for the request this answers.

It used to say “a reply to the MOST RECENTLY sent request”, which was the single-outstanding-call assumption written into the contract.

Provided Methods§

Source

fn refused_response_len(&self) -> Option<usize>

Issue 1632 — how many bytes the reply the most recent take_response_raw REFUSED as too big for the caller’s buffer needed. Subscription::refused_sample_len’s contract, for the client: None is “the backend does not know”.

Source

fn send_request<S: RosService>( &mut self, request: &S::Request, req_buf: &mut [u8], ) -> Result<(), Self::Error>
where Self::Error: From<TransportError>,

Send a typed service request without waiting for a reply (non-blocking).

Serializes the request into req_buf and calls send_request_raw.

Source

fn take_response<S: RosService>( &mut self, reply_buf: &mut [u8], ) -> Result<Option<S::Reply>, Self::Error>
where Self::Error: From<TransportError>,

Poll for a typed reply to the most recently sent request (non-blocking).

Calls take_response_raw and deserializes if available.

Source

fn register_waker(&self, _waker: &Waker)

Register an async waker to be notified when a reply arrives.

Called from Future::poll() implementations to store the waker. The transport backend calls waker.wake() from its reply callback when a response is available, enabling event-driven async without busy-polling.

Default: no-op (backends that don’t support waking simply ignore this).

Source

fn service_is_ready(&self) -> Result<bool, Self::Error>
where Self::Error: From<TransportError>,

Whether a matching service server is currently discoverable.

Mirrors rclcpp::ClientBase::service_is_ready — the NAME is upstream’s. The SHAPE is rcl’s: rcl_service_server_is_available(node, client, bool *is_available) returns RCL_RET_OK “if the check was made successfully (regardless of the service readiness)”, i.e. the return code says whether the CHECK worked and the out-param carries the ANSWER. rclcpp collapses that to a bare bool and moves the error to exceptions; RFC-0018 forbids exceptions, so Result<bool, _> is how the same contract is expressed here (phase-379 W6, RFC-0036).

Returns Ok(true) if at least one matching server is CURRENTLY discoverable, Ok(false) if none is, or Err(_) if the backend cannot answer (e.g. XRCE — micro-XRCE-DDS-Client has no participant enumeration).

CURRENT, not latched: upstream rmw.h says the outcome reflects a change in either direction, and rclcpp::ClientBase::service_is_ready calls rcl_service_server_is_available on every invocation. A backend answers from the discovery state it maintains — the DDS built-in topics, or the zenoh matched-server set fed by liveliness PUT/DELETE (phase-428 W13) — so a server that goes away reads unavailable again. Every wait loop in the tree (wait_for_service, wait_for_action_server, their C and C++ bindings) is a spin on this one method; there is no separate asynchronous probe (issue 1087).

Those are three answers, not two, and keeping them apart is the whole point: is_server_ready used to sit beside this method collapsing “don’t know” and “no server” into one false, its trait default was true, and only zenoh overrode it — so on every other image wait_for_service returned success without probing anything. Issue 1008 deleted it rather than fixing it, because a caller cannot recover from an answer that does not distinguish the two.

User-facing surface: Client<S>::server_available() in Rust, rclc_client_server_available() in C/C++. Clients use this to gate the first request so a startup-ordering race (client opens before server’s discovery announcement lands) doesn’t surface as a request-side timeout.

Default impl: Err(TransportError::Unsupported) — backends that support graph introspection (zenoh queryable interest, DDS built-in topic readers) opt in by overriding.

Source

fn request_publisher_actual_qos(&self) -> QoSProfile

The profile this entity is ACTUALLY running — what the backend GRANTED, not what the call site requested.

Upstream’s rmw_client_request_publisher_get_actual_qos. The two differ whenever a backend serves something other than what it was asked for, and the difference is exactly what answers “why is nothing arriving”: a RELIABLE reader does not match a BEST_EFFORT writer, and a clamped history depth drops samples the caller believed were kept.

Per-field, and an unreportable field is an ABSENCE. A backend that determines four policies and not the fifth returns the four and leaves the fifth at its Unknown variant. Never echo the request into a field you did not read back — that is the inverse of this method’s meaning, and it is what the six C-ABI slots did until this phase.

The default is QoSProfile::QOS_PROFILE_UNKNOWN — every field an absence — which is the honest answer for a backend with no read-back at all (XRCE has none; zenoh-pico’s QoS is per-message flags). It is NOT the request, and it is NOT QOS_PROFILE_DEFAULT.

Source

fn response_subscription_actual_qos(&self) -> QoSProfile

The profile this entity is ACTUALLY running — what the backend GRANTED, not what the call site requested.

Upstream’s rmw_client_response_subscription_get_actual_qos. The two differ whenever a backend serves something other than what it was asked for, and the difference is exactly what answers “why is nothing arriving”: a RELIABLE reader does not match a BEST_EFFORT writer, and a clamped history depth drops samples the caller believed were kept.

Per-field, and an unreportable field is an ABSENCE. A backend that determines four policies and not the fifth returns the four and leaves the fifth at its Unknown variant. Never echo the request into a field you did not read back — that is the inverse of this method’s meaning, and it is what the six C-ABI slots did until this phase.

The default is QoSProfile::QOS_PROFILE_UNKNOWN — every field an absence — which is the honest answer for a backend with no read-back at all (XRCE has none; zenoh-pico’s QoS is per-message flags). It is NOT the request, and it is NOT QOS_PROFILE_DEFAULT.

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§