pub fn honour(
type_name: &str,
topic: &str,
qos: QoSProfile,
) -> Result<QoSProfile, NodeError>Expand description
The QoS a Rust subscription actually registers with, once the contract has had its say — phase-454 W13, the Rust half of W10, widened to the two policies by issue 1256.
§Why Rust TAKES the declared value where C and C++ ASSERT it
C++ settles this at the call site, by arity: NROS_SUBSCRIBE(Msg, m, topic)
states no QoS, so qos_from_declared BUILDS one from the declaration
(“nothing to assert — there is only ever one statement”), and the
four-argument form, where the call site did state one, static_asserts that
the two agree. C does the same with NROS_ASSERT_DECLARED_*.
Rust has neither half of that seam: there is no nros::subscribe!, and the
topic is a runtime &str, so nothing at the call site can be told apart
from anything else at compile time — and by the time a QoS reaches a
registration, “the caller wrote this” and “a constructor defaulted it” are
the same QoSProfile. Node::create_subscription hands
QoSProfile::default() — rmw_qos_profile_default, KEEP_LAST(10),
RELIABLE, VOLATILE — to the same _with_qos entry point a caller with an
opinion reaches, and the declarative road folds both into one
EntityMetadata::qos field long before this point.
So the distinction C++ makes is not available here, and the two candidate rules that remain each fail on their own:
- Check only. Every
QoSProfile::default()call site in the tree would be refused the moment its endpoint is declared at anything but the default — which is the entire point of declaring one. A declaration would become a way to stop an image booting. - Take, always. A caller who narrowed a queue on purpose
(
create_subscription_viewablerequires KEEP_LAST(1)) or chosebest_effortfor a sensor would be silently widened by a contract that over-declared.
§The rule, and why the asymmetry is real
The contract states what the BUILD RESERVED. W12 made that literal for
the depth: the executor arena, the zenoh receive ring and both payload pools
on a declared image are all sized from the declared depth. The policies are
the same kind of statement: XRCE drops both reliable stream buffers to the
protocol floor when every endpoint declares best_effort (RFC-0100 D3), and
a transient_local endpoint is what the build counts a late-joiner slot
for. So for each column, ordered by how much it ASKS FOR (deeper; reliable
over best_effort; transient_local over volatile):
asked > declared— the registration wants more than was bought, and asking for it would ride storage the build did not reserve. TAKEN, and reported. For the two policies it is also the direction RxO tolerates: abest_effortorvolatilereader still matches areliableortransient_localwriter.asked < declared— taking would WIDEN what the code narrowed, and the image paid for something nothing wants. Two statements about one fact disagreeing, with no default that explains either (RFC-0100 D8). REFUSED, naming both.- equal, or nothing declared — returned unchanged. An image with no contract is byte-identical to every build before this wave.
- a policy asked as
SystemDefault— the code stated no opinion, so the declaration is TAKEN, silently: there is nothing to report a divergence from.
§And it says so
A take is REPORTED, once per endpoint that diverges — the C++ surface’s
value is visible in the source at the call site and Rust’s is not, so a
registration that quietly stopped being what lib.rs reads would be the lie
this whole model exists to remove. Every take here narrows what the code
asked for (a shallower queue, no retransmission, no late-joiner history),
any of which can drop a sample, so each is WARN.
Called instead of check at every Rust subscription registration. The C
and C++ FFI seams keep check — they have already taken the declared QoS
at their own call site, so a disagreement reaching THEM is a real refusal
and not a default nobody wrote.
§#[inline] is load-bearing, and it was MEASURED
An image with no contract must be byte-identical to every build before this
wave, and “the table is empty so the work folds away” is a claim about the
optimiser, not a fact. It was FALSE as first written: with the table
threaded in as an unwrap_or(&[]) argument, honour stayed out of line —
a real T nros_node::declared_qos::honour symbol, +144 bytes of text and
+8 of data on the no-contract listener against origin/main, because a
40-byte QoSProfile moving in and out by value is past LLVM’s inlining
budget. Matching the Option HERE, behind #[inline], is what lets a
None table fold the call to Ok(qos) at every one of the fourteen
registration sites and leave honour_in unreferenced. Verified by
rebuilding both trees and comparing the linked binary byte for byte.