#[repr(C)]pub struct nros_rmw_vtable_t {Show 68 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 required_rx_bytes: Option<unsafe extern "C" fn(type_name: *const c_char, type_hash: *const c_char, hint: usize, out_bytes: *mut usize) -> 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.
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.
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 + try_recv_reply_raw 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.
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.
(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.)
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: XRCE, bare-metal). The runtime still drains the session on its deadline-bound cv-wait boundary.
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_TRY_AGAINif the backend has no slot available (caller may retry or fall back to a copy path).NROS_RMW_RET_INVALID_ARGUMENTon bad pointers / size.
*out_cap may exceed requested_len. The slot’s bytes are
valid until the matching pub_commit or pub_discard 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 runtime falls back to a per-publisher staging arena and emits a single memcpy on commit.
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>Phase 124.A — zero-copy subscription borrow.
Borrow a read-only view of the next available message in
place, without copying into a caller buffer. Returns:
* >= 0 — message length; writes *out_buf / *out_token.
* 0 — no message ready (subscription empty).
* < 0 — error (see rmw_ret_t codes negated).
The view is valid until the matching sub_release runs.
Only one borrow may be outstanding per subscription at a time —
callers MUST release before requesting another borrow.
NULL function pointer = backend doesn’t natively borrow; the
runtime falls back to try_recv_raw into a staging buffer. /
/* 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.
Returns 1 if ≥ 1 matching server has been discovered on the
RMW graph, 0 if none yet, or a negative rmw_ret_t
constant on backend error. The runtime exposes this to user
code as nros_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>Phase 124.D.1 — burst-take.
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:
* buf is a contiguous max_msgs * per_msg_cap block.
* The i-th delivered message lives at buf + i * per_msg_cap
and has byte length out_lens[i].
* out_lens is at least max_msgs entries long.
Returns:
* >= 0 — count of messages taken (0..=max_msgs).
* < 0 — rmw_ret_t error code; partial drains MUST
use the count form, not error-out.
NULL function pointer = backend doesn’t natively batch; the
runtime emits a try_recv_raw loop fallback in
CffiSubscriber::try_recv_sequence. The fallback gives
identical observable behaviour (each call still costs N
vtable hops) but lets user code commit to the batched API. /
/* 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.
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 try_recv_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
try_recv_raw 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
falls back to a one-shot staging buffer (capped at the
configured NROS_MAX_STREAM_CHUNK) + publish_raw.
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).- other negative — backend error.
Implementation notes per backend:
- Zenoh:
z_send_ping(or session keep-alive piggyback). - XRCE:
uxr_ping_agent_session_until_timeout. - DDS: built-in participant ping if available, else
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.
RESERVED, and NULL in every backend. There is no runtime fallback: this doc said the runtime answered with the registry name, and no such code was ever written — nothing in the tree calls this slot, so a NULL one is not “answered elsewhere”, it is unanswerable. Corrected 2026-08-29 (phase-393 W2) after grepping for the promised fallback and finding only this sentence.
Filling it is NOT what makes the identity correct. The identity that is
load-bearing today is the one a backend stamps into rmw_gid_t, because
rmw_compare_gids_equal compares that string before the bytes — so what
matters is that a backend has ONE spelling of its name, not that it can
be asked for it. Cyclone’s kImplementationIdentifier is that shape.
Do not fill this slot to move a counter: check-rmw-slot-producers
classifies any slot with a producer as produced whether or not
anything reads it, which is exactly the overstatement issue 0800 exists
to catch. It earns a body when a CALLER exists — a bridge image asking
which of two linked backends it is on.
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: the runtime answers false for every feature.
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.
NULL slot: the runtime falls back to take, and the caller gets no
metadata — which is exactly today’s behaviour for every C backend.
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 is set_on_graph_change_callback — flagged
for W5 rather than decided quietly here.
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.
required_rx_bytes: Option<unsafe extern "C" fn(type_name: *const c_char, type_hash: *const c_char, hint: usize, out_bytes: *mut usize) -> rmw_ret_t>How many bytes of TAKE buffer this type actually needs, so the runtime
can stop sizing take’s buf from one global constant.
RTOS addition; upstream has no counterpart. Upstream’s nearest thing is
rmw_get_serialized_message_size, which is about a MESSAGE and which
all three reference implementations leave unimplemented; this is about
the buffer the CALLER must present, and the answer is the backend’s
because only the backend knows what its framing, size classes and
attachment handling add on top of the payload.
type_name and type_hash name the type (both BORROWED for the call;
type_hash may be NULL exactly as in create_subscription). hint is
the same number the runtime would put in
rmw_subscription_options_t.rx_buffer_hint, and carries the same
meaning, including that 0 says the CALLER stated nothing rather than
that the type is unbounded — every message type has a derived bound or
the build fails. On NROS_RMW_RET_OK, *out_bytes is the MINIMUM
take-buffer length that is sufficient for this type at this hint: a
take given that many bytes must not fail for want of room, and no
smaller number has that property. It may exceed hint — a backend that
frames or pads says so here rather than discovering it at take time —
and it may be smaller.
MINIMUM, tightened in phase-403 W4, and the word carries the whole value of the slot. A backend that keeps size CLASSES may not answer with the class it would round this type up to. Doing so is arithmetically safe and useless: a 68-byte type and a 1000-byte type sharing a class come back with one number, which is the global-constant answer the runtime already had without asking, and the runtime would spend the difference on every subscription in the image. Report what the type needs; the rounding is the backend’s own business and stays there. If a backend genuinely cannot separate the two — its framing really does make the class size the floor — then the class size IS the minimum and answering it is correct; what is forbidden is reporting a rounding as though it were a requirement.
This is a QUERY about a type, not about an entity: it is answerable before any subscription exists, which is the point — the runtime has to size the buffer in order to create one.
NULL slot: FALLBACK class (RFC-0035’s NULL-slot contract) — THE HINT IS
THE ANSWER, never UNSUPPORTED to the caller. The runtime uses hint,
and since a type without a derived bound is a build error, that is a
real number rather than a fallback to a configured default. A backend
that cannot answer for a PARTICULAR type returns
NROS_RMW_RET_UNSUPPORTED and leaves *out_bytes untouched; the
runtime falls back to the hint exactly as for a NULL slot.
OPTIONAL on the merits, not for compatibility. nano-ros is unreleased and this ABI may be broken, so “a mandatory slot breaks every out-of-tree backend” is not the reason and must not be cited as one. Three reasons that survive without it:
- A SLOT CANNOT BE REQUIRED BEFORE SOMETHING DISPATCHES IT. Required
here means
first_missing_vtable_slotREFUSES to register a backend that leaves it NULL, andcheck-rmw-required-slots.shholds that set equal to the set the runtime.expect()s. Nothing calls this yet — phase-403 W3/W5 own the dispatch site — so requiring it now would refuse working backends over a function no caller reaches. That is issue 0349 exactly, and it cost three backends their registration once. (W4 filled the slot for zenoh-pico, which makes the slot PRODUCED; it is the CONSUMER that decides whether required is reachable, and there is still none.) - “NO OPINION” IS A REAL ANSWER, and mandatory does not delete it, only
relocates it: five in-tree backends would each carry the same
*out_bytes = hint; return OK;body, and the Rust ones would get it from a defaultedRustBackendtrait method — the same special case, one layer up and less visible. - IT IS SLOT 75, AND TWO BACKENDS INITIALISE POSITIONALLY. uORB’s C++14 initialiser stops at slot 17 and positional initialisation cannot skip, so reaching a mandatory slot 75 means writing 58 meaningless entries to get there.
Promotion stays cheap and stays open: making this required later is a change to the registration check, not to the struct.
DECIDED 2026-08-31: this slot stays OPTIONAL permanently. Phase-403 W4 recommended it after filling the slot for zenoh-pico, and that is now a ruling rather than a recommendation, so promotion is off the table unless the reason below stops being true. The first argument above dissolves once a dispatch site exists, but the other two do not, and they are the load-bearing ones: cyclonedds and XRCE keep ONE receive buffer, so “no opinion” is their true answer and requiring the slot only relocates it into identical bodies; and slot 75 is out of reach of uORB’s positional C++14 initialiser whatever the registration check says. A slot that most backends must fill with a restatement of the default is worse than a NULL whose meaning the header pins down, which this one does.
It does not weaken take’s obligation. The runtime may pass a buf_len
smaller than what this returned — it has its own memory to answer to —
and a sample that does not fit is still a reported failure rather than
a truncated success. Answering here is how a backend avoids that
outcome, not how it licenses one.
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