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 response_info(&self) -> ServiceInfo { ... }
    fn send_request<S: RosService>(
        &mut self,
        request: &S::Request,
        req_buf: &mut [u8],
    ) -> Result<i64, Self::Error>
       where Self::Error: From<TransportError> { ... }
    fn take_response<S: RosService>(
        &mut self,
        reply_buf: &mut [u8],
    ) -> Result<Option<(S::Reply, i64)>, Self::Error>
       where Self::Error: From<TransportError> { ... }
    fn take_response_for<S: RosService>(
        &mut self,
        sequence_id: i64,
        reply_buf: &mut [u8],
    ) -> Result<Option<S::Reply>, Self::Error>
       where Self::Error: From<TransportError> { ... }
    fn register_waker(&self, _waker: &Waker) -> bool { ... }
    fn service_is_ready(&self) -> Result<bool, Self::Error>
       where Self::Error: From<TransportError> { ... }
    fn get_gid(&self) -> Result<[u8; 24], 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 response_info(&self) -> ServiceInfo

Issue 2136 — the metadata of the reply the most recent successful take_response_raw returned: upstream rmw_take_response’s rmw_service_info_t, the client twin of ServiceTrait::request_info.

  • request_id — the request this reply answers: this client’s own GID and the sequence number of its request;
  • source_timestamp — when the SERVER sent the reply, on its clock;
  • received_timestamp — when this client received it, on its clock.

Read it after a take and before the next one. A field the backend cannot report is None, and the default — for a backend with no per-reply metadata at all — is ServiceInfo::NOT_REPORTED.

Source

fn send_request<S: RosService>( &mut self, request: &S::Request, req_buf: &mut [u8], ) -> Result<i64, 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, returning the SEQUENCE ID the backend assigned — crate::SEQUENCE_ID_UNKNOWN on a backend that cannot assign one. Hand it to take_response_for so this call takes only its own reply.

Issue 2033 — this returned () after issue 2009, discarding the id the raw path had carried since issue 0778, so a typed caller had nothing to correlate by.

Source

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

Poll for the next typed reply (non-blocking), WHICHEVER request it answers.

Returns Ok(Some((reply, sequence_id))): sequence_id is the id send_request returned for the request this reply answers, and the caller decides whom it belongs to — upstream’s rcl_take_response request header. A caller with ONE request in mind wants take_response_for, which drops the replies of requests it abandoned.

Issue 2033 — this used to return the reply alone and was documented as “the reply to the most recently sent request”, which it never checked: after a resend or a timeout, the abandoned request’s late reply was handed to the new one.

Source

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

Poll for the typed reply to the request sequence_id names (non-blocking), dropping any reply that answers a different one.

sequence_id is what send_request returned. The rule is crate::correlation’s, the one every other client surface applies (issue 2009): a reply is delivered only to the request whose id it carries, a stale reply is DROPPED (at most crate::MAX_STALE_DRAIN_PER_POLL per call), and an id-less reply or request (crate::SEQUENCE_ID_UNKNOWN) matches the sole request in mind. Ok(None) — nothing for this request yet.

Source

fn register_waker(&self, _waker: &Waker) -> bool

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.

Returns false when the backend cannot wake waker (issue 1940); the caller then polls. Default: false.

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 get_gid(&self) -> Result<[u8; 24], Self::Error>
where Self::Error: From<TransportError>,

This client’s own global identifier — upstream (Jazzy) rmw_get_gid_for_client, the vtable’s get_gid_for_client. Issue 1978.

The client twin of Publisher::get_gid: the identity a server keys a request id on (zenoh: the gid stamped into every request attachment; Cyclone: the request writer’s GUID), zero-extended to PUBLISHER_GID_SIZE by pad_publisher_gid.

The default is Err(Unsupported), never an all-zero id — the same discipline, for the same reason, as Publisher::get_gid.

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§