#[repr(C)]pub struct nros_rmw_vtable_t {Show 78 fields
pub create_session: Option<unsafe extern "C" fn(locator: *const c_char, mode: u8, domain_id: u32, node_name: *const c_char, options: *const rmw_session_options_t, out: *mut rmw_session_t) -> rmw_ret_t>,
pub destroy_session: Option<unsafe extern "C" fn(session: *mut rmw_session_t) -> rmw_ret_t>,
pub drive_io: Option<unsafe extern "C" fn(session: *mut rmw_session_t, timeout_ms: i32) -> rmw_ret_t>,
pub create_publisher: Option<unsafe extern "C" fn(node: *const rmw_node_t, type_support: *const rmw_message_type_support_t, topic_name: *const c_char, domain_id: u32, qos: *const rmw_qos_profile_t, options: *const rmw_publisher_options_t, out: *mut rmw_publisher_t) -> rmw_ret_t>,
pub destroy_publisher: Option<unsafe extern "C" fn(publisher: *mut rmw_publisher_t) -> rmw_ret_t>,
pub publish: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, payload: rmw_byte_span_t) -> rmw_ret_t>,
pub create_subscription: Option<unsafe extern "C" fn(node: *const rmw_node_t, type_support: *const rmw_message_type_support_t, topic_name: *const c_char, domain_id: u32, qos: *const rmw_qos_profile_t, options: *const rmw_subscription_options_t, out: *mut rmw_subscription_t) -> rmw_ret_t>,
pub destroy_subscription: Option<unsafe extern "C" fn(subscription: *mut rmw_subscription_t) -> rmw_ret_t>,
pub take: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, out: *mut rmw_mut_byte_span_t, taken: *mut bool) -> rmw_ret_t>,
pub has_data: Option<unsafe extern "C" fn(subscription: *mut rmw_subscription_t, out_has_data: *mut bool) -> rmw_ret_t>,
pub create_service: Option<unsafe extern "C" fn(node: *const rmw_node_t, type_support: *const rmw_service_type_support_t, service_name: *const c_char, domain_id: u32, qos: *const rmw_qos_profile_t, out: *mut rmw_service_t) -> rmw_ret_t>,
pub destroy_service: Option<unsafe extern "C" fn(server: *mut rmw_service_t) -> rmw_ret_t>,
pub take_request: Option<unsafe extern "C" fn(server: *const rmw_service_t, request: *mut rmw_mut_byte_span_t, seq_out: *mut i64, taken: *mut bool) -> rmw_ret_t>,
pub has_request: Option<unsafe extern "C" fn(server: *mut rmw_service_t, out_has_request: *mut bool) -> rmw_ret_t>,
pub send_response: Option<unsafe extern "C" fn(server: *const rmw_service_t, seq: i64, response: rmw_byte_span_t) -> rmw_ret_t>,
pub create_client: Option<unsafe extern "C" fn(node: *const rmw_node_t, type_support: *const rmw_service_type_support_t, service_name: *const c_char, domain_id: u32, qos: *const rmw_qos_profile_t, out: *mut rmw_client_t) -> rmw_ret_t>,
pub destroy_client: Option<unsafe extern "C" fn(client: *mut rmw_client_t) -> rmw_ret_t>,
pub send_request: Option<unsafe extern "C" fn(client: *const rmw_client_t, request: rmw_byte_span_t, sequence_id: *mut i64) -> rmw_ret_t>,
pub take_response: Option<unsafe extern "C" fn(client: *const rmw_client_t, reply: *mut rmw_mut_byte_span_t, seq_out: *mut i64, taken: *mut bool) -> rmw_ret_t>,
pub subscription_event_init: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, kind: Type, deadline_ms: u32, cb: rmw_status_event_callback_t, user_context: *mut c_void) -> rmw_ret_t>,
pub subscription_take_event: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, kind: Type, out: *mut rmw_event_payload_t, taken: *mut bool) -> rmw_ret_t>,
pub publisher_take_event: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, kind: Type, out: *mut rmw_event_payload_t, taken: *mut bool) -> rmw_ret_t>,
pub publisher_event_init: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, kind: Type, deadline_ms: u32, cb: rmw_status_event_callback_t, user_context: *mut c_void) -> rmw_ret_t>,
pub publisher_assert_liveliness: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t) -> rmw_ret_t>,
pub next_deadline_ms: Option<unsafe extern "C" fn(session: *const rmw_session_t, out_ms: *mut u32, has_deadline: *mut bool) -> rmw_ret_t>,
pub set_wake_callback: Option<unsafe extern "C" fn(session: *mut rmw_session_t, cb: Option<unsafe extern "C" fn(ctx: *mut c_void)>, ctx: *mut c_void) -> rmw_ret_t>,
pub borrow_loaned_message: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, requested_len: usize, out_slot: *mut rmw_mut_byte_span_t, out_token: *mut *mut rmw_loan_token_t) -> rmw_ret_t>,
pub publish_loaned_message: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, token: *mut rmw_loan_token_t, actual_len: usize) -> rmw_ret_t>,
pub return_loaned_message_from_publisher: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, token: *mut rmw_loan_token_t) -> rmw_ret_t>,
pub take_loaned_message: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, out_view: *mut rmw_byte_span_t, out_token: *mut *mut rmw_loan_token_t, taken: *mut bool) -> rmw_ret_t>,
pub return_loaned_message_from_subscription: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, token: *mut rmw_loan_token_t) -> rmw_ret_t>,
pub service_server_is_available: Option<unsafe extern "C" fn(client: *const rmw_client_t, out_available: *mut bool) -> rmw_ret_t>,
pub take_sequence: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, buf: *mut u8, per_msg_cap: usize, max_msgs: usize, out_lens: *mut usize, taken: *mut usize) -> rmw_ret_t>,
pub publish_streamed: Option<unsafe extern "C" fn(publisher: *mut rmw_publisher_t, size_cb: Option<unsafe extern "C" fn(out_total_len: *mut usize, user_ctx: *mut c_void)>, chunk_cb: Option<unsafe extern "C" fn(out_buf: *mut u8, cap: usize, out_written: *mut usize, user_ctx: *mut c_void)>, user_ctx: *mut c_void) -> rmw_ret_t>,
pub ping_session: Option<unsafe extern "C" fn(session: *mut rmw_session_t, timeout_ms: i32) -> rmw_ret_t>,
pub subscription_supports_in_place: Option<unsafe extern "C" fn(subscription: *mut rmw_subscription_t, out_supports: *mut bool) -> rmw_ret_t>,
pub process_raw_in_place: Option<unsafe extern "C" fn(subscription: *mut rmw_subscription_t, ctx: *mut c_void, cb: Option<unsafe extern "C" fn(ctx: *mut c_void, message: rmw_byte_span_t)>, out_processed: *mut bool) -> rmw_ret_t>,
pub get_implementation_identifier: Option<unsafe extern "C" fn() -> *const c_char>,
pub get_serialization_format: Option<unsafe extern "C" fn() -> *const c_char>,
pub feature_supported: Option<unsafe extern "C" fn(feature: Type) -> bool>,
pub get_gid_for_publisher: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, gid: *mut rmw_gid_t) -> rmw_ret_t>,
pub publisher_count_matched_subscriptions: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, subscription_count: *mut usize) -> rmw_ret_t>,
pub subscription_count_matched_publishers: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, publisher_count: *mut usize) -> rmw_ret_t>,
pub publisher_get_actual_qos: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, qos: *mut rmw_qos_profile_t) -> rmw_ret_t>,
pub subscription_get_actual_qos: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, qos: *mut rmw_qos_profile_t) -> rmw_ret_t>,
pub client_request_publisher_get_actual_qos: Option<unsafe extern "C" fn(client: *const rmw_client_t, qos: *mut rmw_qos_profile_t) -> rmw_ret_t>,
pub client_response_subscription_get_actual_qos: Option<unsafe extern "C" fn(client: *const rmw_client_t, qos: *mut rmw_qos_profile_t) -> rmw_ret_t>,
pub service_request_subscription_get_actual_qos: Option<unsafe extern "C" fn(service: *const rmw_service_t, qos: *mut rmw_qos_profile_t) -> rmw_ret_t>,
pub service_response_publisher_get_actual_qos: Option<unsafe extern "C" fn(service: *const rmw_service_t, qos: *mut rmw_qos_profile_t) -> rmw_ret_t>,
pub publisher_wait_for_all_acked: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, timeout_ms: u32) -> rmw_ret_t>,
pub take_with_info: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, message: *mut rmw_mut_byte_span_t, taken: *mut bool, message_info: *mut rmw_message_info_t) -> rmw_ret_t>,
pub take_loaned_message_with_info: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, out_view: *mut rmw_byte_span_t, out_token: *mut *mut rmw_loan_token_t, taken: *mut bool, message_info: *mut rmw_message_info_t) -> rmw_ret_t>,
pub get_node_names: Option<unsafe extern "C" fn(session: *const rmw_session_t, visitor: rmw_node_visitor_t) -> rmw_ret_t>,
pub get_topic_names_and_types: Option<unsafe extern "C" fn(session: *const rmw_session_t, no_demangle: bool, visitor: rmw_names_and_types_visitor_t) -> rmw_ret_t>,
pub get_service_names_and_types: Option<unsafe extern "C" fn(session: *const rmw_session_t, visitor: rmw_names_and_types_visitor_t) -> rmw_ret_t>,
pub get_publisher_names_and_types_by_node: Option<unsafe extern "C" fn(session: *const rmw_session_t, node_name: *const c_char, node_namespace: *const c_char, no_demangle: bool, visitor: rmw_names_and_types_visitor_t) -> rmw_ret_t>,
pub get_subscriber_names_and_types_by_node: Option<unsafe extern "C" fn(session: *const rmw_session_t, node_name: *const c_char, node_namespace: *const c_char, no_demangle: bool, visitor: rmw_names_and_types_visitor_t) -> rmw_ret_t>,
pub get_service_names_and_types_by_node: Option<unsafe extern "C" fn(session: *const rmw_session_t, node_name: *const c_char, node_namespace: *const c_char, visitor: rmw_names_and_types_visitor_t) -> rmw_ret_t>,
pub get_client_names_and_types_by_node: Option<unsafe extern "C" fn(session: *const rmw_session_t, node_name: *const c_char, node_namespace: *const c_char, visitor: rmw_names_and_types_visitor_t) -> rmw_ret_t>,
pub get_publishers_info_by_topic: Option<unsafe extern "C" fn(session: *const rmw_session_t, topic_name: *const c_char, no_mangle: bool, visitor: rmw_topic_endpoint_info_visitor_t) -> rmw_ret_t>,
pub get_subscriptions_info_by_topic: Option<unsafe extern "C" fn(session: *const rmw_session_t, topic_name: *const c_char, no_mangle: bool, visitor: rmw_topic_endpoint_info_visitor_t) -> rmw_ret_t>,
pub count_publishers: Option<unsafe extern "C" fn(session: *const rmw_session_t, topic_name: *const c_char, count: *mut usize) -> rmw_ret_t>,
pub count_subscribers: Option<unsafe extern "C" fn(session: *const rmw_session_t, topic_name: *const c_char, count: *mut usize) -> rmw_ret_t>,
pub node_get_graph_guard_condition: Option<unsafe extern "C" fn(session: *mut rmw_session_t, callback: rmw_event_callback_t, user_data: *const c_void) -> rmw_ret_t>,
pub create_node: Option<unsafe extern "C" fn(session: *mut rmw_session_t, name: *const c_char, namespace_: *const c_char, out: *mut rmw_node_t) -> rmw_ret_t>,
pub destroy_node: Option<unsafe extern "C" fn(node: *mut rmw_node_t) -> rmw_ret_t>,
pub set_log_severity: Option<unsafe extern "C" fn(severity: Type) -> rmw_ret_t>,
pub supported_qos_policies: Option<unsafe extern "C" fn(session: *const rmw_session_t, out_mask: *mut u32) -> rmw_ret_t>,
pub subscription_set_on_new_message_callback: Option<unsafe extern "C" fn(subscription: *mut rmw_subscription_t, callback: rmw_event_callback_t, user_data: *const c_void) -> rmw_ret_t>,
pub service_set_on_new_request_callback: Option<unsafe extern "C" fn(service: *mut rmw_service_t, callback: rmw_event_callback_t, user_data: *const c_void) -> rmw_ret_t>,
pub client_set_on_new_response_callback: Option<unsafe extern "C" fn(client: *mut rmw_client_t, callback: rmw_event_callback_t, user_data: *const c_void) -> rmw_ret_t>,
pub count_clients: Option<unsafe extern "C" fn(session: *const rmw_session_t, service_name: *const c_char, count: *mut usize) -> rmw_ret_t>,
pub count_services: Option<unsafe extern "C" fn(session: *const rmw_session_t, service_name: *const c_char, count: *mut usize) -> rmw_ret_t>,
pub get_gid_for_client: Option<unsafe extern "C" fn(client: *const rmw_client_t, gid: *mut rmw_gid_t) -> rmw_ret_t>,
pub event_type_is_supported: Option<unsafe extern "C" fn(event_type: Type) -> bool>,
pub take_request_with_info: Option<unsafe extern "C" fn(server: *const rmw_service_t, request: *mut rmw_mut_byte_span_t, seq_out: *mut i64, taken: *mut bool, service_info: *mut rmw_service_info_t) -> rmw_ret_t>,
pub take_response_with_info: Option<unsafe extern "C" fn(client: *const rmw_client_t, reply: *mut rmw_mut_byte_span_t, seq_out: *mut i64, taken: *mut bool, service_info: *mut rmw_service_info_t) -> rmw_ret_t>,
pub readiness_source: Option<unsafe extern "C" fn(session: *const rmw_session_t, out_kind: *mut u8, out_handle: *mut i32) -> rmw_ret_t>,
}Fields§
§create_session: Option<unsafe extern "C" fn(locator: *const c_char, mode: u8, domain_id: u32, node_name: *const c_char, options: *const rmw_session_options_t, out: *mut rmw_session_t) -> rmw_ret_t>Create a session (phase-301: renamed from open to the table’s
own create_* convention). The runtime supplies a
zero-initialised rmw_session_t via @p out with
node_name / namespace_ already filled. The backend writes
out->backend_data.
@param mode One of nros_rmw_session_mode_t. Passed as uint8_t
rather than the enum to keep the slot’s width fixed
across compilers. A backend with no peer/client
distinction must IGNORE it, not reject it.
@param options NULLable; NULL means every default. Issue 0808 — the
home for init-time context this flat list cannot grow
without another break. mode is NOT moved into it: doing
so would be a second break for no gain, and it is already
a named argument every backend reads. What moved in are the
two fields issue 0785 measured as GAPS, localhost_only
and enclave.
destroy_session: Option<unsafe extern "C" fn(session: *mut rmw_session_t) -> rmw_ret_t>§drive_io: Option<unsafe extern "C" fn(session: *mut rmw_session_t, timeout_ms: i32) -> rmw_ret_t>§create_publisher: Option<unsafe extern "C" fn(node: *const rmw_node_t, type_support: *const rmw_message_type_support_t, topic_name: *const c_char, domain_id: u32, qos: *const rmw_qos_profile_t, options: *const rmw_publisher_options_t, out: *mut rmw_publisher_t) -> rmw_ret_t>Create a publisher. The runtime fills out->topic_name,
out->type_name, out->qos before this call; the backend
writes out->backend_data. out->can_loan_messages is DERIVED
by the runtime, not written here (issue 0814).
options carries transport hints (phase-301: moved out of the
QoS struct); NULL = all defaults.
destroy_publisher: Option<unsafe extern "C" fn(publisher: *mut rmw_publisher_t) -> rmw_ret_t>§publish: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, payload: rmw_byte_span_t) -> rmw_ret_t>§create_subscription: Option<unsafe extern "C" fn(node: *const rmw_node_t, type_support: *const rmw_message_type_support_t, topic_name: *const c_char, domain_id: u32, qos: *const rmw_qos_profile_t, options: *const rmw_subscription_options_t, out: *mut rmw_subscription_t) -> rmw_ret_t>options carries transport hints (phase-301: moved out of the
QoS struct); NULL = all defaults.
destroy_subscription: Option<unsafe extern "C" fn(subscription: *mut rmw_subscription_t) -> rmw_ret_t>§take: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, out: *mut rmw_mut_byte_span_t, taken: *mut bool) -> rmw_ret_t>Upstream rmw_take. Phase 376 W3.b/W3.d step A.
*taken says whether a message was copied; *out_len is
how many bytes, meaningful only when taken. Both are
written only on NROS_RMW_RET_OK.
Second slot to retire NROS_RMW_RET_NO_DATA: an empty
subscription is taken = false with OK, which is what
upstream’s taken out-parameter means.
Deviations from upstream, declared:
buf/buf_len/*out_lenreplace upstream’s typedvoid *ros_message. There is no typesupport indirection on target — the payload is bytes and the caller owns the buffer, so it needs the length back.- no
rmw_subscription_allocation_t *: it is an OPAQUE per-implementation handle ({const char *implementation_identifier; void *data;}in Humble’srmw/types.h— no allocator in it), and the only thing that produces one isrmw_init_subscription_allocation, whose other parameters are a typesupport pointer and a sequence bound, both declined ABI-wide. Nothing can make one, so the argument has nothing to point at. Two earlier reasons here were wrong: “pools are baked” (issue 0777 — cyclonedds callsddsrt_callocon this very path) and then “upstream pre-sizes anrcutils_allocator_t” (there is none).
Phase 403 W1 — buf_len is AUTHORITATIVE on every call, and a
sample that does not fit it is a FAILURE (*taken = false with
NROS_RMW_RET_BUFFER_TOO_SMALL), never a truncated success.
That holds however the backend treated
rmw_subscription_options_t.rx_buffer_hint, whose doc writes the
rule out in full.
Issue 1612 — on that failure out->len is the size the sample
NEEDED, so the caller can say how big the dropped sample was rather
than only that it did not fit. A backend that cannot know leaves it at
NROS_RMW_TAKE_LEN_UNKNOWN, which is what the caller pre-sets.
has_data: Option<unsafe extern "C" fn(subscription: *mut rmw_subscription_t, out_has_data: *mut bool) -> rmw_ret_t>Phase 376 W3.d step A — status in the return, answer in the
out-parameter, so no slot multiplexes a flag with a status.
*out_has_data is written only on NROS_RMW_RET_OK.
RTOS addition: upstream has no equivalent, because a hosted caller reaches for a wait-set. This is the poll a loop with no wait-set needs, and it allocates nothing.
Logically read-only, and that is weaker than it sounds: zenoh’s
implementation fires deadline and liveliness callbacks from inside this
probe and writes their cells, and cyclonedds’ peeks its reader (which
marks samples READ). The rule a backend must actually keep is that a
probe may not CONSUME a message — the sample a has_data reports must
still be there for the take that follows. The stronger “must not
mutate subscription state” was recorded here and true of nobody
(issue 0780).
create_service: Option<unsafe extern "C" fn(node: *const rmw_node_t, type_support: *const rmw_service_type_support_t, service_name: *const c_char, domain_id: u32, qos: *const rmw_qos_profile_t, out: *mut rmw_service_t) -> rmw_ret_t>§destroy_service: Option<unsafe extern "C" fn(server: *mut rmw_service_t) -> rmw_ret_t>§take_request: Option<unsafe extern "C" fn(server: *const rmw_service_t, request: *mut rmw_mut_byte_span_t, seq_out: *mut i64, taken: *mut bool) -> rmw_ret_t>Upstream rmw_take_request. Phase 376 W3.b/W3.d step A.
*taken says whether a request was copied, *out_len how
many bytes, *seq_out the sequence number to reply against.
All three are written only on NROS_RMW_RET_OK.
Deviations from upstream, declared: the payload is bytes
(buf / buf_len / *out_len) rather than a typed
void *ros_request, and *seq_out stands in for
rmw_service_info_t * — an RTOS reply needs the sequence
and nothing else in that struct. A backend that has the rest
(who sent the request, when) reports it through
take_request_with_info (issue 2081).
taken = false with NROS_RMW_RET_OK means NOTHING WAS
PENDING, and only that (upstream rmw.h). A backend that
holds a request it cannot accept right now — every
reply-correlation slot held by a request not yet answered
with send_response — returns NROS_RMW_RET_WOULD_BLOCK,
consumes nothing, and leaves the request for a later take.
Folding that into taken = false makes a saturated server
indistinguishable from an idle one (issue 1088).
Issue 1632 — a request bigger than request->capacity is refused with
NROS_RMW_RET_BUFFER_TOO_SMALL, and request->len then carries the
size it NEEDED (NROS_RMW_TAKE_LEN_UNKNOWN, which the caller pre-sets,
when the backend cannot know) — the take slot’s rule, widened.
has_request: Option<unsafe extern "C" fn(server: *mut rmw_service_t, out_has_request: *mut bool) -> rmw_ret_t>Phase 376 W3.d step A — the service-side sibling of
has_data; same contract, same reason.
send_response: Option<unsafe extern "C" fn(server: *const rmw_service_t, seq: i64, response: rmw_byte_span_t) -> rmw_ret_t>§create_client: Option<unsafe extern "C" fn(node: *const rmw_node_t, type_support: *const rmw_service_type_support_t, service_name: *const c_char, domain_id: u32, qos: *const rmw_qos_profile_t, out: *mut rmw_client_t) -> rmw_ret_t>§destroy_client: Option<unsafe extern "C" fn(client: *mut rmw_client_t) -> rmw_ret_t>§send_request: Option<unsafe extern "C" fn(client: *const rmw_client_t, request: rmw_byte_span_t, sequence_id: *mut i64) -> rmw_ret_t>Phase 130.4 — non-blocking send_request_raw. Phase-301: the
deprecated blocking call_raw slot is DELETED (rmw has no
blocking call); this + take_response is the ONE
request/reply path and both slots are now REQUIRED for a backend
that supports services.
Sends the request to the backend without blocking for a reply. Returns immediately.
*sequence_id is the id the backend assigned, written only on
NROS_RMW_RET_OK. Upstream returns it for one reason and it is the
same reason here: a client with two calls outstanding has nothing else
to match a reply against.
Issue 0778 — this out-parameter was ABSENT until 2026-08-25, and every
backend computed the id and threw it away (cyclonedds a
RequestId{guid, seq}, zenoh a fetch_add into the rmw attachment,
xrce uxr_buffer_request’s id). With nothing to correlate BY, each
invented a policy: cyclonedds ABANDONED the first request when a second
was sent, zenoh took FIRST REPLY WINS on the grounds that “a queryable
is idempotent at the application layer” — which this ABI cannot
enforce and which is false for send_goal and SetParameters, both of
which travel this path. Same application code, different behaviour per
transport. The id is what deletes both policies.
Issue 2009 — the caller pre-sets *sequence_id to
NROS_RMW_SEQUENCE_ID_UNKNOWN; a backend that cannot assign ids
leaves it there, and the runtime then keeps one call in flight on that
client instead of matching replies it cannot tell apart.
take_response: Option<unsafe extern "C" fn(client: *const rmw_client_t, reply: *mut rmw_mut_byte_span_t, seq_out: *mut i64, taken: *mut bool) -> rmw_ret_t>Upstream rmw_take_response. Same shape and the same
declared deviations as take_request.
*seq_out is the sequence_id of the request this reply answers,
written only when *taken is true. It is the other half of issue
0778: handing the id out at send time is useless if it does not come
back. Mirrors take_request’s seq_out, which the SERVER side has
always had — the asymmetry was the tell.
Issue 2009 — the runtime delivers a reply ONLY to the request whose id
it carries, and drops a reply whose request was cancelled or is not
pending. *seq_out is pre-set to NROS_RMW_SEQUENCE_ID_UNKNOWN; a
reply that comes back with it is delivered only when one request is
pending on the client.
(The paragraph that used to sit here described >= 0 = bytes and
“other negative = backend error”, the pre-W3.d shape, three phases
after step A moved the count to an out-parameter and step B made the
errors positive.)
Issue 1632 — on NROS_RMW_RET_BUFFER_TOO_SMALL, reply->len is the
size the reply NEEDED, as for take_request.
A backend that has the rest of upstream’s rmw_service_info_t (the
reply’s timestamps, the request id it answers) reports it through
take_response_with_info (issue 2136).
subscription_event_init: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, kind: Type, deadline_ms: u32, cb: rmw_status_event_callback_t, user_context: *mut c_void) -> rmw_ret_t>Register a callback for a subscription-side event. NULL function
pointer = backend doesn’t generate any subscription events.
Specific kind unsupported on a backend that supports some
events = NROS_RMW_RET_UNSUPPORTED return.
deadline_ms is consulted for REQUESTED_DEADLINE_MISSED
only; ignored otherwise.
subscription_take_event: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, kind: Type, out: *mut rmw_event_payload_t, taken: *mut bool) -> rmw_ret_t>Upstream rmw_take_event, subscription side.
*taken says whether an event was copied into *out; both are written
only on NROS_RMW_RET_OK. kind selects which event to drain, and
which member of the payload union is valid.
Deviations from upstream, declared: no rmw_event_t * — that handle is
declined, so the entity plus the kind identifies the event — and the
payload is our rmw_event_payload_t union rather than a void * the
caller must know the shape of.
NULL is the normal answer for a backend that delivers status events
through the *_event_init callback and has a safe context to do it
from. It is NOT the right answer for a backend whose notifications
arrive on a thread of its own.
publisher_take_event: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, kind: Type, out: *mut rmw_event_payload_t, taken: *mut bool) -> rmw_ret_t>Upstream rmw_take_event, publisher side. Same contract as
subscription_take_event; rmw_take_event is recorded as GROUPED onto
that one, since upstream has a single name for both.
publisher_event_init: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, kind: Type, deadline_ms: u32, cb: rmw_status_event_callback_t, user_context: *mut c_void) -> rmw_ret_t>§publisher_assert_liveliness: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t) -> rmw_ret_t>Phase 108.B — manually assert this publisher’s liveliness.
Required for MANUAL_BY_TOPIC / MANUAL_BY_NODE liveliness
kinds; no-op (return NROS_RMW_RET_OK) for other kinds.
NULL function pointer = backend doesn’t support manual
liveliness; runtime returns NROS_RMW_RET_OK for AUTOMATIC /
NONE callers and NROS_RMW_RET_UNSUPPORTED for MANUAL_*.
next_deadline_ms: Option<unsafe extern "C" fn(session: *const rmw_session_t, out_ms: *mut u32, has_deadline: *mut bool) -> rmw_ret_t>Phase 110.0 — backend’s next internal-event deadline in
milliseconds from now (lease keepalive, heartbeat, reader
ACK-NACK timeout, etc.). The runtime caps its drive_io
timeout against min(user_timeout, timer_deadline, this) so
quiet links don’t wake early, see no user-visible work, and
round-trip back into drive_io.
Phase 376 W3.d step A — *out_ms carries the value and
*has_deadline whether there is one; both are written only
on NROS_RMW_RET_OK.
This slot was the ONE member of the eleven that step B’s
renumbering did not force: its old negative return was a
“no deadline” SENTINEL, not an error code, so nothing would
have collided. It is converted anyway because it had the
shape every other conversion found a silent failure in — a
backend that FAILED to compute its deadline returned -1
and was read as “quiet link”, which is exactly the reading
that makes the executor sleep longer. It now has an error
channel it never had.
NULL function pointer is permitted — the runtime treats it
the same as *has_deadline = false.
set_wake_callback: Option<unsafe extern "C" fn(session: *mut rmw_session_t, cb: Option<unsafe extern "C" fn(ctx: *mut c_void)>, ctx: *mut c_void) -> rmw_ret_t>Phase 124.B.1 — executor wake callback.
The runtime calls this once per session right after open
with cb pointing at a runtime-supplied function and ctx
pointing at the executor’s wake state. The backend stores
both in its per-session state and calls cb(ctx) whenever
its transport-notification path fires — datagram arrival,
condvar wake-up, select-fd ready, etc. The runtime cb does
flag-write + condvar-signal atomically so a spin_once
blocked on the wake condvar resumes immediately.
cb == NULL clears any previously installed callback; the
backend must drop the stored (cb, ctx) and never invoke
again after this returns.
NULL slot = backend has no asynchronous wake path (purely poll-driven: bare-metal). The runtime still drains the session on its deadline-bound cv-wait boundary.
phase-521 W4 — a backend that also fills readiness_source
is believed on THAT: the runtime treats the session as woken
by this callback only when it reports
NROS_RMW_READINESS_NOTIFY. So a backend may fill this slot
for every session and install the callback only where its
transport can call it — XRCE hands it to the UDP stack’s
receive notification on lwIP and NetX, and installs nothing
over a descriptor or a board’s custom transport.
borrow_loaned_message: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, requested_len: usize, out_slot: *mut rmw_mut_byte_span_t, out_token: *mut *mut rmw_loan_token_t) -> rmw_ret_t>Phase 124.A — zero-copy publisher loan.
Reserve a writable slot of at least requested_len bytes inside
the backend’s outbound buffer. Returns:
NROS_RMW_RET_OK+ writes*out_buf/*out_cap/*out_token.NROS_RMW_RET_WOULD_BLOCKif the backend has no slot available (caller may retry or fall back to a copy path). This line saidTRY_AGAINuntil issue 1126 — a spellingrmw_ret.hhas never defined, in the header that IS the ABI contract a third-party backend writes against. The runtime readsWOULD_BLOCK(andNO_DATA) as “no slot, not an error”; every other code propagates as a failure.NROS_RMW_RET_INVALID_ARGUMENTon bad pointers / size.
*out_cap may exceed requested_len. The slot’s bytes are
valid until the matching publish_loaned_message or
return_loaned_message_from_publisher runs.
*out_token is an opaque per-loan handle the backend uses to
match commit / discard back to the right slot.
NULL function pointer = backend doesn’t natively lend. The
fallback then depends on whether the image has an allocator:
with alloc, try_lend_slot allocates a staging Vec PER
LOAN, hands its pointer back as the slot, and commit memcpys
it into publish; without alloc there is no fallback at
all and the loan is refused PERMANENTLY, not “try again”
(issue 0814 — both facts are about the image and the vtable,
neither can change while the publisher lives).
This said “a per-publisher staging arena” until 2026-09-11.
There is no per-publisher buffer anywhere in this tree — the
same fiction issue 0782 corrected 200 lines below, on
publish_streamed, and left standing here.
publish_loaned_message: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, token: *mut rmw_loan_token_t, actual_len: usize) -> rmw_ret_t>Phase 124.A — commit a previously loaned slot.
token MUST be a value returned from a prior pub_loan on the
same publisher. actual_len is the byte count actually
written into the slot (≤ the loan’s out_cap). Triggers the
wire send.
NULL = paired NULL with pub_loan.
return_loaned_message_from_publisher: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, token: *mut rmw_loan_token_t) -> rmw_ret_t>Phase 124.A — abandon a previously loaned slot.
Releases the slot without sending. token MUST be a value
returned from a prior pub_loan on the same publisher.
NULL = paired NULL with pub_loan.
take_loaned_message: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, out_view: *mut rmw_byte_span_t, out_token: *mut *mut rmw_loan_token_t, taken: *mut bool) -> rmw_ret_t>Upstream rmw_take_loaned_message. Phase 376 W3.b/W3.d step A.
*taken says whether a view was handed out; *out_buf,
*out_len and *out_token describe it and are meaningful
only when taken. All are written only on
NROS_RMW_RET_OK.
Before this, the length was returned AND written to
*out_len, and the runtime used the return — so a backend
that disagreed with itself had one of its two answers
silently ignored. There is now one length.
Deviation from upstream, declared: upstream loans a typed
void **loaned_message; ours is a byte view plus an opaque
token to release, because there is no typesupport on target
and the backend owns the buffer until sub_release.
No backend fills this slot today (Cyclone NULL, XRCE NULL, and the
Rust adapter leaves it at EMPTY_VTABLE), so every try_borrow through
the C ABI takes the copy fallback. The slot is carried, not deleted,
because it is the only shape that can hand a view to a caller which
outlives the call — nros-c / nros-cpp try_borrow — where
process_raw_in_place’s scoped callback cannot reach. Zenoh’s native
zero-copy receive is live but arrives through Rust
SlotBorrowing for ZenohSubscriber, not through here. Recorded so
“the slot exists” is not read as “the capability works”: issue 0781.
return_loaned_message_from_subscription: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, token: *mut rmw_loan_token_t) -> rmw_ret_t>Phase 124.A — release a previously borrowed view.
token MUST be a value returned from a prior sub_borrow
on the same subscription. Lets the next message advance into
the buffer.
NULL = paired NULL with sub_borrow.
service_server_is_available: Option<unsafe extern "C" fn(client: *const rmw_client_t, out_available: *mut bool) -> rmw_ret_t>Phase 124.C.1 — service-server availability probe.
*out_available says whether ≥ 1 matching server has been
discovered on the RMW graph; the return is a plain status.
(This opened by describing the answer as the RETURN — 1,
0, or a negative code — the pre-W3.d shape that the rest of
this same block goes on to explain was retired. Corrected
2026-09-11, phase-428 W12.) The runtime exposes this to user
code as rclc_client_server_available() /
Client<S>::server_available() — clients use it to gate the
first request so a startup-ordering race doesn’t surface as
a request-side timeout.
Implementation notes per backend:
- Zenoh:
z_sessiontracks matched queryables via interest declarations. - Cyclone DDS / dust-DDS: built-in topic readers expose matched-pub counts.
- XRCE: agent has no participant enumeration; return
NROS_RMW_RET_UNSUPPORTED.
NULL function pointer = backend cannot answer; the runtime
surfaces NROS_RMW_RET_UNSUPPORTED to the caller.
Phase 376 W3.d step A — upstream’s shape: the STATUS is the
return value and the answer is an out-parameter. Previously
this slot multiplexed both through one int32_t (1 = yes,
0 = no, negative = error), which is what makes upstream’s
positive RMW_RET_ERROR = 1 unadoptable — 1 would mean
both “available” and “failed”. Splitting them is what lets
step B renumber at all.
A backend writes *out_available only on
NROS_RMW_RET_OK; on any error the caller’s value is
untouched. The old contract’s tolerance for “any positive
value other than 1 means available” is gone with the int:
a bool has no non-spec value to be lenient about.
Deviation from upstream, declared: no node parameter.
rmw_service_server_is_available takes both a node and a
client; an image has no node object to pass — the client
reaches its session directly.
take_sequence: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, buf: *mut u8, per_msg_cap: usize, max_msgs: usize, out_lens: *mut usize, taken: *mut usize) -> rmw_ret_t>Upstream rmw_take_sequence. Phase 376 W3.b/W3.d step A —
the COUNT moves to *taken, matching upstream’s
size_t *taken, and the return carries only a status.
*taken is written only on NROS_RMW_RET_OK; a partial
drain reports what it got rather than erroring.
Drains up to max_msgs queued messages into a contiguous
caller buffer in a single backend call, avoiding N × vtable
dispatch when a burst-sensor subscription catches up on a
backlog (e.g. a 100 Hz IMU feed polled at 10 Hz).
Storage contract:
bufis a contiguousmax_msgs * per_msg_capblock.- The i-th delivered message lives at
buf + i * per_msg_capand has byte lengthout_lens[i]. out_lensis at leastmax_msgsentries long.
NULL slot = the backend does not natively batch; the runtime
loops the single take instead, which gives identical
observable behaviour (each message still costs one vtable hop)
while letting user code commit to the batched API.
Issue 0971 — which leaves a question the count alone cannot
answer: WHY the drain stopped. A batch that ends because a
message did not fit per_msg_cap returns the same shape as
one that drained the reader, and the message that stopped it
is consumed — deliberately, for the reason the single take
consumes it too: a sample left behind that no caller can
ever take is a stuck subscription
(nros-verification’s take_post_fix /
no_silent_truncation).
So a backend that stops a drain for a reason the caller must
hear PARKS that status on the subscription and returns it
from the NEXT take or take_sequence, which takes nothing
else that call. That rule is what makes the fallback note
above true rather than aspirational: without it the runtime’s
take loop and a native batch answer the same
condition differently — the loop erroring out and discarding
the count it had already earned, the native path reporting a
count and no reason.
publish_streamed: Option<unsafe extern "C" fn(publisher: *mut rmw_publisher_t, size_cb: Option<unsafe extern "C" fn(out_total_len: *mut usize, user_ctx: *mut c_void)>, chunk_cb: Option<unsafe extern "C" fn(out_buf: *mut u8, cap: usize, out_written: *mut usize, user_ctx: *mut c_void)>, user_ctx: *mut c_void) -> rmw_ret_t>Phase 124.E.1 — streamed publish.
Caller hands the backend two callbacks. The backend invokes
size_cb once to learn the total payload length, allocates
a single slot of that size in its outbound buffer, then
invokes chunk_cb repeatedly to fill the slot in chunks
until the buffer is full. What it saves is the CALLER-side
buffer holding a whole serialised message.
That justification used to read “saves the per-publisher
staging buffer … where the staging buffer dominates .bss”,
which described a design this tree does not have (issue 0782).
There is no per-publisher buffer and none of it is in .bss:
EmbeddedPublisher::publish serialises into a per-CALL STACK
array of DEFAULT_TX_BUF (= NROS_SUBSCRIPTION_BUFFER_SIZE,
1024 by default), and the runtime’s own NULL-slot fallback
stages into a 4 KiB stack array and refuses anything larger.
The saving is real and it is STACK — which on an MCU with
small per-task stacks is the tighter budget of the two — but a
reason has to name the thing it is about.
Callback contract:
size_cb(*out_total_len, user_ctx)— write the exact total payload length, in bytes, to*out_total_len. Called exactly once perpublish_streamedinvocation.chunk_cb(out_buf, cap, *out_written, user_ctx)— write up tocapbytes starting atout_buf, then report the count written via*out_written. The backend may callchunk_cbrepeatedly until the total promised bysize_cbhas been delivered.*out_written == 0means EOF; the backend tears down the slot.
Lesson from micro-ROS’s
rmw_uros_set_continous_serialization_callbacks: pass the
callbacks per-call rather than binding them to publisher
state, so different messages on the same publisher can use
different serialisation strategies.
NULL function pointer = backend doesn’t stream; the runtime
stages into a 4 KiB buffer on the CALLER’s stack and then
publishes it, refusing a total larger than that with
NROS_RMW_RET_BUFFER_TOO_SMALL. The cap is a constant in
CffiPublisher::publish_streamed, not a knob: this said
“the configured NROS_MAX_STREAM_CHUNK” until 2026-09-11 and
no such setting has ever existed, so a reader looking for
the knob to raise found nothing and had no way to know the
limit was 4096 (phase-428 W12).
ping_session: Option<unsafe extern "C" fn(session: *mut rmw_session_t, timeout_ms: i32) -> rmw_ret_t>Phase 124.F.1 — session-level connectivity probe.
Sends a wire-level round-trip probe (“is the peer / agent /
router still reachable?”) and waits up to timeout_ms for
a reply. No discovery state required — cheaper than the
service-availability probe (which needs matched-publication
bookkeeping). Lesson from micro-ROS’s
rmw_uros_ping_agent.
Returns:
NROS_RMW_RET_OK— peer responded within budget.NROS_RMW_RET_TIMEOUT— no reply beforetimeout_ms.NROS_RMW_RET_UNSUPPORTED— backend can’t probe (DDS with no participant introspection).- any other named constant — backend error. (This read “other negative” until 2026-09-11; W3.d step B made every error positive.)
Implementation notes per backend:
- Zenoh: one keep-alive frame (
zpico_send_keep_alive); zenoh-pico has no round-trip ping, so OK means only that the local link accepted the frame. - XRCE:
uxr_ping_agent_session. - DDS: built-in participant ping if available, else
NROS_RMW_RET_UNSUPPORTED.
NULL function pointer = runtime surfaces
NROS_RMW_RET_UNSUPPORTED to the caller.
subscription_supports_in_place: Option<unsafe extern "C" fn(subscription: *mut rmw_subscription_t, out_supports: *mut bool) -> rmw_ret_t>Capability query: does this subscription support process_raw_in_place()? The runtime consults it once at subscription registration to choose in-place dispatch over the buffered (copying) path.
*out_supports is written only on NROS_RMW_RET_OK (Phase 376 W3.d
step A — capability out, status returned).
The capability is the CONJUNCTION of this probe answering true and
process_raw_in_place being non-NULL. Either alone is unsupported.
Why the probe is not redundant with that nullity (issue 0781 proposed
deleting it, and this is the counterexample that stopped it):
RustBackendAdapter::<R>::VTABLE is a const, so it installs
process_raw_in_place for EVERY R: RustBackend — the slot is non-NULL
whatever the backend can do. The Rust-side answer is a runtime &self
method (Subscription::supports_process_in_place) because
CffiSubscription multiplexes over whichever backend registered, so it
cannot become an associated const the vtable initializer could branch
on. Today nros-rmw-zenoh says true and nros-rmw-metadata takes the
false default behind an identically-shaped vtable: two backends, same
nullity, different capability. Deriving from nullity would route
metadata’s subscriptions into in-place dispatch and every take would
return MessageTooLarge from the trait default.
A C backend that knows its answer at compile time (XRCE says true, Cyclone leaves both NULL) may express it with nullity alone; the conjunction makes that spelling correct without making it the only one.
NULL function pointer = treated as unsupported (buffered path).
process_raw_in_place: Option<unsafe extern "C" fn(subscription: *mut rmw_subscription_t, ctx: *mut c_void, cb: Option<unsafe extern "C" fn(ctx: *mut c_void, message: rmw_byte_span_t)>, out_processed: *mut bool) -> rmw_ret_t>Borrow one ready message in place: hand its raw CDR bytes to cb (with
the opaque ctx) for the duration of the call, then release the slot.
cb MUST NOT re-enter this subscription’s receive.
Deviation from upstream, declared. Avoiding the copy is NOT the reason —
upstream already has a name for that, rmw_take_loaned_message, and we
carry it (take_loaned_message). What this shape buys is that the
borrow is SCOPED: it ends when cb returns, so there is no release
token to hold and nothing a caller can forget. Upstream’s loan is
unscoped — a caller who misses
rmw_return_loaned_message_from_subscription retires one entry of a
fixed-depth receive ring for good, and a target with no reclaim and no
swap does not get that entry back. That is the RTOS constraint; “no
copy” is a property both shapes share.
Both are carried because they answer different callers: this one serves
dispatch from inside the executor, where the callback frame is the
natural scope, and the loan pair serves nros-c / nros-cpp, whose
try_borrow hands a view back to a caller that outlives the call.
NULL function pointer = unsupported (the runtime uses the buffered
path); see subscription_supports_in_place for the conjunction. /
/* Phase 376 W3.d step A — “did it process one” moves to an
out-parameter and the return is a plain status.
This retires NROS_RMW_RET_NO_DATA from this slot: an empty
subscription is *out_processed = false with
NROS_RMW_RET_OK, which is upstream’s taken = false
semantics. A sentinel that means “fine, but nothing” is
exactly the shape that makes a status enum ambiguous.
*out_processed is written only on OK.
get_implementation_identifier: Option<unsafe extern "C" fn() -> *const c_char>Upstream rmw_get_implementation_identifier.
The backend’s name, static for the life of the image. A gid is only
comparable with another carrying the same identifier, which matters here
because nros_rmw_cffi_register_named admits several backends at once.
RETURN THE STRING THE BACKEND STAMPS INTO rmw_gid_t. That is the
identity that is load-bearing: rmw_compare_gids_equal compares it
before the bytes, so a backend has ONE spelling of its name and this
slot hands it out. Cyclone’s kImplementationIdentifier is that shape.
THE NANO-ROS NAME, NOT A STOCK rmw_*_cpp NAME. In-tree answers are the
registry names: "zenoh", "cyclonedds", "xrce", "uorb". Upstream’s
identifier is a process-local type tag — each rmw checks a handle’s
implementation_identifier against its own, and the string never
crosses the wire — so answering "rmw_zenoh_cpp" would buy no interop
(wire compatibility is the serialization format and the gid derivation,
which are answered elsewhere) and would tell a tool or ported code that
branches on the name that it runs on an implementation it does not.
READ by CffiSession::implementation_identifier_cstr, which the
per-node accessors in every language reach
(Node::rmw_implementation_identifier,
rclc_node_get_rmw_implementation_identifier,
rclcpp::Node::rmw_implementation_identifier) — per SESSION, because
an image linking two backends has two answers.
NULL slot: FALLBACK class — the runtime answers with the name the vtable
was registered under (nros_rmw_cffi_register_named; the first such
name if one vtable is registered twice). That name is a real fact about
the backend, not a guess; it is what a foreign vtable that predates the
slot is known by. (Until 2026-10-27 this slot had no reader and this doc
had once promised that fallback with no code behind it — corrected
phase-393 W2, implemented with the reader.)
get_serialization_format: Option<unsafe extern "C" fn() -> *const c_char>Upstream rmw_get_serialization_format.
The backend’s wire encoding, as its cross-image identity STRING
(RFC-0088 D2: the u8 discriminant is assigned per image and means
nothing outside it; the name is what two images can agree on). Static
for the life of the image, like the identifier above.
PRODUCED by every backend since phase-421 W2. It was reserved until
then for a stated reason — “every backend here speaks CDR, nothing
asks, so a body would be parity shape with no reader” — and both halves
of that reason have since stopped being true. uORB’s wire is the PX4
struct verbatim (RFC-0011), so it answers "uorb" where every other
backend answers "cdr"; and nros_rmw_cffi_register_named admits
several backends in one image, so a bridge has two sessions whose
formats differ and one compile-time constant cannot describe both.
CffiSession::serialization_format is the reader.
This is the ONLY per-session answer. nros_node::IMAGE_SERIALIZATION_ FORMAT and the generated NROS_SERIALIZATION_FORMAT macro are
compile-time constants and are meaningful only in a single-backend
image; ask the slot whenever the image links more than one.
NULL slot: the runtime answers NULL — it does NOT guess "cdr". A
backend that does not declare its format has not told anyone what it
speaks, and inventing an answer is how the identifier slot’s doc came
to promise a fallback nobody had written (corrected phase-393 W2).
feature_supported: Option<unsafe extern "C" fn(feature: Type) -> bool>Upstream rmw_feature_supported.
Whether the backend populates an optional piece of CONTENT — upstream’s two values both concern message-info sequence numbers. Deliberately not expressed as slot nullity: a NULL pointer says the backend cannot perform an OPERATION, which is a different question from whether the data an implemented operation returns is populated.
NULL slot: there is NOTHING TO ANSWER — no code in this tree
reads this slot, so a NULL one is not “answered elsewhere as
false”, it is unreachable. This doc promised the false
fallback until 2026-09-11 and no such code was ever written:
the same fiction corrected 60 lines above on
get_implementation_identifier, and left standing here
(phase-428 W12). It earns a dispatch site when a caller
exists.
get_gid_for_publisher: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, gid: *mut rmw_gid_t) -> rmw_ret_t>Upstream rmw_get_gid_for_publisher. Exact parity.
The backend zero-pads to the full width; see rmw_gid_t.
publisher_count_matched_subscriptions: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, subscription_count: *mut usize) -> rmw_ret_t>Upstream rmw_publisher_count_matched_subscriptions. Exact parity.
Every backend already tracks this to implement liveliness events — see
service_server_is_available, which is the same question one entity
over. NULL where a backend has no discovery at all (XRCE).
subscription_count_matched_publishers: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, publisher_count: *mut usize) -> rmw_ret_t>Upstream rmw_subscription_count_matched_publishers. Exact parity.
publisher_get_actual_qos: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, qos: *mut rmw_qos_profile_t) -> rmw_ret_t>Upstream rmw_publisher_get_actual_qos. Exact parity.
We bake the REQUESTED profile and, until now, never read back the GRANTED one. On DDS the two differ whenever a writer and reader negotiate, and the difference is exactly what answers “why is nothing arriving” — so a consumer that cannot ask has to guess.
PARTIAL ANSWERS ARE ALLOWED (W5/B2). A backend that can determine four
policies and not the fifth writes the four it knows and
*_UNKNOWN for the fifth, then returns NROS_RMW_RET_OK. Until the
policy values took upstream’s numbering there was no UNKNOWN to write,
so the contract had to be all-or-nothing — a partial answer would have
been indistinguishable from a confident one, and W4 chose
NROS_RMW_RET_UNSUPPORTED over lying.
NROS_RMW_RET_UNSUPPORTED now means what it says: this backend has no
read-back at all. It is NOT the answer for “I know some of it”.
A caller that treats UNKNOWN as a value rather than as an absence gets
a wrong comparison, so rmw_qos_profile_check_compatible reports it as
a WARNING rather than an incompatibility — upstream’s
RMW_QOS_COMPATIBILITY_WARNING, which was unreachable here until there
was a sentinel to trigger it.
Six upstream entry points, six slots, deliberately: the name rule is mechanical so that no alias table has to be authored and kept true. Backends share ONE helper and write six one-line thunks — sharing an implementation is free, sharing an ABI slot is not.
subscription_get_actual_qos: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, qos: *mut rmw_qos_profile_t) -> rmw_ret_t>Upstream rmw_subscription_get_actual_qos. Exact parity.
client_request_publisher_get_actual_qos: Option<unsafe extern "C" fn(client: *const rmw_client_t, qos: *mut rmw_qos_profile_t) -> rmw_ret_t>Upstream rmw_client_request_publisher_get_actual_qos. Exact parity.
The four service/client read-backs carry information available NOWHERE
else: rmw_client_t and rmw_service_t have no qos field, and
create_client / create_service take ONE profile for both
directions, so the granted per-direction profile is otherwise
unobservable.
client_response_subscription_get_actual_qos: Option<unsafe extern "C" fn(client: *const rmw_client_t, qos: *mut rmw_qos_profile_t) -> rmw_ret_t>Upstream rmw_client_response_subscription_get_actual_qos.
service_request_subscription_get_actual_qos: Option<unsafe extern "C" fn(service: *const rmw_service_t, qos: *mut rmw_qos_profile_t) -> rmw_ret_t>Upstream rmw_service_request_subscription_get_actual_qos.
service_response_publisher_get_actual_qos: Option<unsafe extern "C" fn(service: *const rmw_service_t, qos: *mut rmw_qos_profile_t) -> rmw_ret_t>Upstream rmw_service_response_publisher_get_actual_qos.
publisher_wait_for_all_acked: Option<unsafe extern "C" fn(publisher: *const rmw_publisher_t, timeout_ms: u32) -> rmw_ret_t>Upstream rmw_publisher_wait_for_all_acked.
Blocks until every sample this publisher sent has been acknowledged, or the timeout elapses. Without it an image that publishes and then halts cannot know whether anything left the box.
Deviation from upstream, declared: uint32_t timeout_ms for upstream’s
by-value rmw_time_t. Every duration in this ABI is u32 milliseconds
(issue 0241) — one width, one unit, no per-call struct.
Best-effort backends (zenoh best-effort, XRCE) leave this NULL.
take_with_info: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, message: *mut rmw_mut_byte_span_t, taken: *mut bool, message_info: *mut rmw_message_info_t) -> rmw_ret_t>Upstream rmw_take_with_info.
take plus the sample’s metadata, written to caller-owned storage. See
rmw_message_info_t for why this is a pointer parameter rather than the
side table the runtime uses today.
Deviations from upstream, declared: the same two take declares —
bytes (buf/buf_len/*out_len) instead of a typed void *, because
there is no typesupport on target; and no allocation argument, because
nothing in this ABI can produce upstream’s opaque
rmw_subscription_allocation_t — see take, which carries the full
reason and the two wrong ones that preceded it.
DISPATCHED since issue 1495: a with-info take on the runtime
calls this slot when it is non-NULL and converts the struct to
MessageInfo. A NULL slot falls back to take plus the Rust
adapter’s side table, which is metadata for a Rust backend and
none for a C/C++ one. (Before 1495 nothing called this slot at
all, so filling it changed no behaviour — phase-428 W12 found
the fallback prose describing a mechanism that did not exist.)
take_loaned_message_with_info: Option<unsafe extern "C" fn(subscription: *const rmw_subscription_t, out_view: *mut rmw_byte_span_t, out_token: *mut *mut rmw_loan_token_t, taken: *mut bool, message_info: *mut rmw_message_info_t) -> rmw_ret_t>Upstream rmw_take_loaned_message_with_info.
take_loaned_message plus metadata; same deviations as that slot (a
byte view and an opaque release token rather than a typed loan).
get_node_names: Option<unsafe extern "C" fn(session: *const rmw_session_t, visitor: rmw_node_visitor_t) -> rmw_ret_t>Upstream rmw_get_node_names AND rmw_get_node_names_with_enclaves.
One slot, two upstream names: upstream split them only because appending to a fixed out-parameter list would have broken its ABI. A visitor has no such list, so the enclave is simply a fourth argument, NULL where untracked. Recorded in the checker’s grouping table.
get_topic_names_and_types: Option<unsafe extern "C" fn(session: *const rmw_session_t, no_demangle: bool, visitor: rmw_names_and_types_visitor_t) -> rmw_ret_t>Upstream rmw_get_topic_names_and_types.
get_service_names_and_types: Option<unsafe extern "C" fn(session: *const rmw_session_t, visitor: rmw_names_and_types_visitor_t) -> rmw_ret_t>Upstream rmw_get_service_names_and_types.
get_publisher_names_and_types_by_node: Option<unsafe extern "C" fn(session: *const rmw_session_t, node_name: *const c_char, node_namespace: *const c_char, no_demangle: bool, visitor: rmw_names_and_types_visitor_t) -> rmw_ret_t>Upstream rmw_get_publisher_names_and_types_by_node.
get_subscriber_names_and_types_by_node: Option<unsafe extern "C" fn(session: *const rmw_session_t, node_name: *const c_char, node_namespace: *const c_char, no_demangle: bool, visitor: rmw_names_and_types_visitor_t) -> rmw_ret_t>Upstream rmw_get_subscriber_names_and_types_by_node.
get_service_names_and_types_by_node: Option<unsafe extern "C" fn(session: *const rmw_session_t, node_name: *const c_char, node_namespace: *const c_char, visitor: rmw_names_and_types_visitor_t) -> rmw_ret_t>Upstream rmw_get_service_names_and_types_by_node.
get_client_names_and_types_by_node: Option<unsafe extern "C" fn(session: *const rmw_session_t, node_name: *const c_char, node_namespace: *const c_char, visitor: rmw_names_and_types_visitor_t) -> rmw_ret_t>Upstream rmw_get_client_names_and_types_by_node.
get_publishers_info_by_topic: Option<unsafe extern "C" fn(session: *const rmw_session_t, topic_name: *const c_char, no_mangle: bool, visitor: rmw_topic_endpoint_info_visitor_t) -> rmw_ret_t>Upstream rmw_get_publishers_info_by_topic.
get_subscriptions_info_by_topic: Option<unsafe extern "C" fn(session: *const rmw_session_t, topic_name: *const c_char, no_mangle: bool, visitor: rmw_topic_endpoint_info_visitor_t) -> rmw_ret_t>Upstream rmw_get_subscriptions_info_by_topic.
count_publishers: Option<unsafe extern "C" fn(session: *const rmw_session_t, topic_name: *const c_char, count: *mut usize) -> rmw_ret_t>Upstream rmw_count_publishers.
count_subscribers: Option<unsafe extern "C" fn(session: *const rmw_session_t, topic_name: *const c_char, count: *mut usize) -> rmw_ret_t>Upstream rmw_count_subscribers.
node_get_graph_guard_condition: Option<unsafe extern "C" fn(session: *mut rmw_session_t, callback: rmw_event_callback_t, user_data: *const c_void) -> rmw_ret_t>Upstream rmw_node_get_graph_guard_condition.
Registers a callback fired when the graph CHANGES. Upstream returns a
guard condition the caller adds to a wait set; we have no wait set to
add it to, and guard conditions are an executor concept here, so this is
the set_wake_callback shape instead — the one guard condition whose
trigger is genuinely backend knowledge.
The callback is an EDGE, carrying no payload: delivering WHAT changed would mean buffering it, which is the graph cache a small target cannot afford.
Named after upstream mechanically, per the campaign’s rule, but the honest name for this shape would be set-on-graph-change-callback — flagged for W5 rather than decided quietly here. (Unticked: no such slot exists, and a backtick in this header means it does.)
callback == NULL clears any previously installed edge; the backend
must drop the stored (callback, user_data) and never invoke it again
after this returns. A clear is also the runtime’s CAPABILITY PROBE, so
it must be cheap and must answer the way a real install would:
NROS_RMW_RET_OK from a backend that has a graph,
NROS_RMW_RET_UNSUPPORTED from one that does not. Nullity of this
slot cannot serve as that probe on the Rust road, where the adapter
fills every slot for every backend.
NULL slot = backend has no graph-change edge (uORB, and any
backend that discovers nothing). The runtime surfaces that as
UNSUPPORTED and declines to create the guard condition: handing back
one nothing will ever trigger is the silent difference RFC-0089 Part I
refuses. phase-467 Row 8 wrote this paragraph, because the slot had
carried no NULL sentence at all — unlike set_wake_callback two
hundred lines above — and check-rmw-slot-producers.py reads a
REACHABLE slot with neither a producer nor a documented NULL as
unimplemented.
create_node: Option<unsafe extern "C" fn(session: *mut rmw_session_t, name: *const c_char, namespace_: *const c_char, out: *mut rmw_node_t) -> rmw_ret_t>Upstream rmw_create_node.
Declares a node on the graph. NULL slot is the expected implementation in a static image: the runtime still tracks the node, the backend simply has nothing to declare.
Deviations from upstream, declared: no rmw_context_t * (an image has
one session and reaches it directly), and the node is an OUT parameter
rather than a returned pointer — no runtime allocation, the caller owns
the storage, exactly as create_publisher does.
The runtime calls this once per distinct (name, namespace_).
destroy_node: Option<unsafe extern "C" fn(node: *mut rmw_node_t) -> rmw_ret_t>Upstream rmw_destroy_node.
Releases the backend’s backend_data; the shell stays valid until its
owner drops it. Called for every node the session created, from
close(), BEFORE destroy_session — a backend’s node state hangs off
its session state, so the order is not free.
NULL slot: nothing is called and the runtime simply forgets the node.
Correct only for a backend that allocates nothing in create_node;
a backend that fills backend_data there must fill this too, or it
leaks one node’s state per session close (issue 0800 — this slot had a
producer in no backend and a consumer nowhere, so the leak was
indistinguishable from an optional slot nobody needed).
set_log_severity: Option<unsafe extern "C" fn(severity: Type) -> rmw_ret_t>Upstream rmw_set_log_severity. Exact parity.
Sets the verbosity of the BACKEND’s own logging — Cyclone’s dds_log,
zenoh-pico’s log, the XRCE client’s. Not nros_log: that is the
runtime’s logger, already runtime-settable through
nros_log::Logger::set_level, and it needs no ABI to reach.
This IS a slot rather than a plain ABI function, and the distinction is
the one the pure functions turn on: “what does this middleware print”
genuinely varies by middleware, so a per-backend answer is correct here
rather than a defect. All three reference implementations
(librmw_{cyclonedds,fastrtps,zenoh}_cpp.so) implement it with real
bodies, which is the opposite of what they do for
rmw_get_serialized_message_size.
Phase 376 W5 — this was DECLINED, on the reasoning that “log level is a
build-time constant (nros_log); a runtime setter implies a mutable
global”. Both clauses were false: Logger::level is an AtomicU8 with a
public set_level already used by tests, and the compile-time part is a
CEILING that defaults open. The decline described a design we do not
have.
NULL slot: the backend has no adjustable logging, and the runtime
surfaces UNSUPPORTED.
supported_qos_policies: Option<unsafe extern "C" fn(session: *const rmw_session_t, out_mask: *mut u32) -> rmw_ret_t>The NROS_RMW_QOS_POLICY_* bits this backend honours, for the session
behind @p session. RTOS addition; upstream has no counterpart, because
upstream links ONE rmw per process and its equivalent knowledge is
compiled into rmw_qos_profile_check_compatible.
The runtime validates a requested profile against this mask at entity
create and answers NROS_RMW_RET_INCOMPATIBLE_QOS for a policy the
backend cannot enforce — the no-silent-downgrade contract, decided
where it can still be reported.
WHY IT IS A SLOT AND NOT A CONSTANT. The cffi route multiplexes: which
backend it is talking to is decided at run time by
nros_rmw_cffi_register_named, and until this slot existed the route
had no way to ask. It therefore answered the UNION of what any
nano-ros-supported RMW honours — issue 1329, measured — so an
application asking cyclonedds for avoid_ros_namespace_conventions, or
XRCE for a deadline, was admitted and then ignored downstream. A union
is an over-claim for every member of it.
A BIT IS EARNED, NOT DECLARED. Setting one asserts that the backend
READS the mapped profile field and either applies the request or
refuses a value it cannot serve. Forwarding the field to somebody else
— a discovery keyexpr, an Agent that may ignore it — is not honouring
it. check-qos-mask-derivation holds each backend’s mask equal to the
set of nros-qos-honours: claims sited in its own sources, in both
directions, so this slot cannot drift away from the code under it.
NULL SLOT: the backend has NOT SAID, and the runtime assumes
NROS_RMW_QOS_POLICY_NONE. The same is true of a non-OK return; @p
out_mask is then not read.
That choice is the one thing here worth arguing, so: the two
alternatives are both worse. “NULL means the union” reinstates issue
1329 with the ABI as its author — a route that cannot ask answering as
though every backend honoured everything. “NULL means do not validate”
is worse still: it admits even the bits no backend implements, and it
switches off the no-silent-downgrade contract at the one seam that
enforces it, invisibly. NONE is the same answer
nros_rmw::Session::supported_qos_policies gives an implementation
that has written no code (phase-428 W9 moved that default from CORE
to nothing for exactly this reason), so the ABI and the trait give one
answer to one question.
NONE is loud rather than silent: the affected creates fail with
INCOMPATIBLE_QOS at once, naming a policy, and the route logs the
missing slot once per session. An application cannot mistake it for
working, which is what the union allowed for as long as it stood.
So a backend that honours ANY policy must fill this slot — not as an
ABI requirement (first_missing_vtable_slot does not demand it, and
check-rmw-required-slots holds that set equal to what the runtime
.expect()s), but because leaving it NULL is a declaration that it
honours nothing. check-qos-mask-derivation refuses a backend that
carries claims and no slot.
@p session is BORROWED for the call. A backend whose answer does not
depend on the session ignores it; one that configures policies per
session (a connection-parameter-driven Agent, say) reads its own state
off session->backend_data.
subscription_set_on_new_message_callback: Option<unsafe extern "C" fn(subscription: *mut rmw_subscription_t, callback: rmw_event_callback_t, user_data: *const c_void) -> rmw_ret_t>Upstream rmw_subscription_set_on_new_message_callback. Exact parity
in name and signature.
set_wake_callback above wakes the session’s executor on ANY arrival;
this names the entity. The backend stores (callback, user_data)
against @p subscription and calls callback(user_data, n) from its
arrival path when samples become takeable on it, with n >= 1.
n is a LOWER BOUND, not upstream’s exact count: a backend that
cannot count calls once per arrival with 1, and none is required to
track per-entity pending depth for it (issue 0960’s RAM argument, kept).
Nor is a backend required to replay samples that arrived BEFORE the
registration, as upstream’s backends do; a caller registers first and
then checks has_data, which closes the gap.
A second call REPLACES the stored registration; callback == NULL
clears it. After either returns, the backend never calls the previous
pair again. destroy_subscription clears it as well.
callback may run on a backend thread, concurrently with any other
slot, so it must be short and non-blocking (set a flag, signal a
condition variable).
@p subscription is BORROWED for the call; the backend keys the
registration on its own backend_data, not on the struct’s address.
NULL SLOT, or NROS_RMW_RET_UNSUPPORTED: the backend cannot name the
entity whose data arrived (uORB, a poll-only bare-metal link).
The runtime then reports the registration as unsupported rather than
OK, and an executor falls back to the session wake.
service_set_on_new_request_callback: Option<unsafe extern "C" fn(service: *mut rmw_service_t, callback: rmw_event_callback_t, user_data: *const c_void) -> rmw_ret_t>Upstream rmw_service_set_on_new_request_callback: callback fires
when a request becomes takeable. Same contract as the subscription
slot above.
client_set_on_new_response_callback: Option<unsafe extern "C" fn(client: *mut rmw_client_t, callback: rmw_event_callback_t, user_data: *const c_void) -> rmw_ret_t>Upstream rmw_client_set_on_new_response_callback: callback fires
when a response becomes takeable. Same contract as the subscription
slot above.
count_clients: Option<unsafe extern "C" fn(session: *const rmw_session_t, service_name: *const c_char, count: *mut usize) -> rmw_ret_t>Upstream rmw_count_clients (Jazzy). The service twin of
count_publishers: how many service CLIENTS this session can see on
@p service_name, a ROS name (/add_two_ints) the backend mangles as it
does for its own clients. Same warm-up caveat as every graph slot: a
count reflects what has already been discovered.
NULL slot, or NROS_RMW_RET_UNSUPPORTED: the backend has no graph
(XRCE, uORB) or it is not running — never a fabricated 0, which would
claim there are none.
count_services: Option<unsafe extern "C" fn(session: *const rmw_session_t, service_name: *const c_char, count: *mut usize) -> rmw_ret_t>Upstream rmw_count_services (Jazzy). As count_clients, counting
service SERVERS.
get_gid_for_client: Option<unsafe extern "C" fn(client: *const rmw_client_t, gid: *mut rmw_gid_t) -> rmw_ret_t>Upstream rmw_get_gid_for_client (Jazzy). Exact parity. The client
twin of get_gid_for_publisher: this client’s own identity, the value
a server keys the request id on (zenoh: the gid stamped into the request
attachment; Cyclone: the request writer’s GUID). The backend zero-pads to
the full width; see rmw_gid_t.
event_type_is_supported: Option<unsafe extern "C" fn(event_type: Type) -> bool>Upstream rmw_event_type_is_supported (Jazzy). Exact parity.
A STATIC per-backend answer: whether ANY entity of this backend can
carry status event @p event_type, through either event shape
(*_event_init callback or *_take_event poll). It is the question a
port asks at configuration so it fails there rather than at entity
creation; registration stays the per-entity authority (a kind the
backend supports can still be refused for one entity).
A true is only meaningful where the backend also fills an event path
for that kind’s side; the runtime requires both.
NULL slot: the backend states nothing per kind, and the runtime falls
back to slot granularity — a kind is supported iff its side has an
event path. A backend with neither (uORB) therefore answers
false for every kind. The runtime reads this slot through ONE helper
(cffi’s backend_event_type_is_supported), never a per-layer guess.
take_request_with_info: Option<unsafe extern "C" fn(server: *const rmw_service_t, request: *mut rmw_mut_byte_span_t, seq_out: *mut i64, taken: *mut bool, service_info: *mut rmw_service_info_t) -> rmw_ret_t>Upstream rmw_take_request, with its rmw_service_info_t.
Issue 2081 — optional, appended. take_request plus the request’s metadata — who sent it
(request_id: the client’s writer GUID and its sequence number) and the
timestamps the backend has — written to caller-owned storage. The
service twin of take_with_info.
Deviations from upstream, declared: the payload is bytes, as for
take_request; and *seq_out, the backend’s reply-correlation token
that send_response takes back, is reported BESIDE the struct rather
than as its request_id.sequence_number, because a backend’s token
(a reply-slot index on Cyclone, XRCE and zenoh) is not the client’s
sequence number, and both are needed.
Every other rule is take_request’s: *taken, request->len,
*seq_out and *service_info are written only on NROS_RMW_RET_OK
with *taken = true; a field the backend cannot report is zero (see
rmw_service_info_t).
DISPATCHED when non-NULL: the runtime takes every request through this
slot and converts the struct to its ServiceInfo. A NULL slot falls
back to take_request plus the Rust adapter’s side table, which is
metadata for a Rust backend and none for a C/C++ one.
take_response_with_info: Option<unsafe extern "C" fn(client: *const rmw_client_t, reply: *mut rmw_mut_byte_span_t, seq_out: *mut i64, taken: *mut bool, service_info: *mut rmw_service_info_t) -> rmw_ret_t>Upstream rmw_take_response, with its rmw_service_info_t.
Issue 2136 — optional, appended. The client twin of
take_request_with_info: take_response plus the reply’s metadata,
written to caller-owned storage —
request_id: the request this reply answers, as upstream’s client sees it — the CLIENT’s own request-writer GUID and the sequence number of its request;source_timestamp: the SERVER’s clock when it sent the reply;received_timestamp: this client’s clock when the reply arrived.
Deviations from upstream, declared: the payload is bytes, as for
take_response; and *seq_out stays beside the struct, because it is
what the runtime matches a reply against (issue 2009) whether or not
the backend can report the rest.
Every other rule is take_response’s: *taken, reply->len,
*seq_out and *service_info are written only on NROS_RMW_RET_OK
with *taken = true; a field the backend cannot report is zero (see
rmw_service_info_t), and a request id whose writer GUID is all zero
is “not reported” however its sequence number reads.
DISPATCHED when non-NULL: the runtime takes every reply through this
slot and keeps the struct as the client’s response info. A NULL slot
falls back to take_response, and the reply reports nothing.
readiness_source: Option<unsafe extern "C" fn(session: *const rmw_session_t, out_kind: *mut u8, out_handle: *mut i32) -> rmw_ret_t>phase-521 W3 (RFC-0113 D4) — how this session reports an arrival.
Optional, appended. The executor’s park ends on exactly two things: the tier’s deadline, or readiness. This slot says which form of readiness the session has, so the park can be ended by data rather than the executor blocking inside the transport’s own receive:
NROS_RMW_READINESS_NOTIFY: the backend calls the callbackset_wake_callbackinstalled when data arrives.*out_handleis not written.NROS_RMW_READINESS_WAITABLE: the backend has an OS handle that becomes readable when data arrives — a file descriptor on POSIX and NuttX — written to*out_handle. The executor multiplexes it with its own wake object (nros_platform_wake_park_handles_us) and drives the session non-blockingly after the park. The handle stays valid until the session closes.NROS_RMW_READINESS_NONE: neither. RFC-0113 D5: the runtime refuses such a session at open (phase-521 W6); the executor never blocks insidedrive_io.
*out_kind and *out_handle are written only on NROS_RMW_RET_OK.
NULL slot: the session is NROS_RMW_READINESS_NOTIFY when
set_wake_callback is non-NULL and NROS_RMW_READINESS_NONE
otherwise — what the runtime inferred before this slot existed, so
a backend that delivers a wake callback needs no change.
Trait Implementations§
Source§impl Clone for nros_rmw_vtable_t
impl Clone for nros_rmw_vtable_t
Source§fn clone(&self) -> nros_rmw_vtable_t
fn clone(&self) -> nros_rmw_vtable_t
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more