Skip to main content

rx_buffer_for

Macro rx_buffer_for 

Source
macro_rules! rx_buffer_for {
    ($msg:ty) => { ... };
}
Expand description

Route this image’s panics to its platform — phase-366 W5.c / RFC-0077.

Emits the #[panic_handler] for an embedded image, forwarding the message to nros_platform_panic so a Rust panic ends the same way a C precondition failure or a C++ terminate does. What that ending IS belongs to the port: k_panic() on Zephyr, esp_system_abort() on ESP-IDF, UART-then-exit-QEMU on the ThreadX RV64 board.

§Why this is a macro you INVOKE, not something nros::main! emits

#[panic_handler] is a singleton of the final artifact, and the image owns it. Emitting one silently from nros::main!() would collide with every image that already declares its own — examples/qemu-esp32-baremetal writes use esp_backtrace as _;, and logging-smoke-freertos-mps2 uses panic-semihosting with features = ["exit"] so a panic exits QEMU instead of hanging the test harness. Those images are RIGHT, and an invisible default would fight them.

So the line is written in the entry, where it can be read, swapped for use panic_halt as _;, or replaced by a hand-written handler that logs to NVM and reboots. A default you cannot see is a constraint, not a default.

§Use — main!(panic = …) is the normal way; this is the escape hatch

phase-366 R3. An entry that goes through nros::main!() should say panic = "platform" (or nothing — that is the default since M5) and let the macro emit this body. One line, and the build can check it.

#![no_std]
nros::main!();            // ends through `nros_platform_panic`

Invoke this macro directly only where no main!() expansion can carry the item — a hand-rolled no_std binary, or the lib side of a crate whose crate-type includes staticlib and whose entry macro is zephyr_component_main! / a board app_main!. It is kept for exactly those: deleting it would strand the images that cannot use the macro replacing it.

// src/app_main.rs — the .a is a final artifact to rustc, and `main!()` in
// the bin target never reaches it.
nros::panic_to_platform!();

Do NOT invoke it in a std image: libstd supplies the lang item there and a second one does not compile. Do not invoke it alongside use panic_halt as _ or any other provider, for the same reason — that is the duplicate check-archive-lang-items exists to catch. Park the core on panic — the halt value of nros::main!(panic = …).

For an image that must not print: no formatting, no allocation, no call out to the platform. Interrupts are masked first so the parked core cannot be woken back into a half-dead system by a timer or a driver ISR still armed from before the panic.

This is a body rather than a re-export of the panic-halt crate so that choosing it costs the entry no new dependency — main!(panic = "halt") is a word in a macro the crate already calls, which is the point of the surface. The behaviour is the same: mask, then spin forever.

Prefer panic = "platform". Halting discards the diagnosis, and every port implements nros_platform_panic precisely so a dying image can say why. Phase 392 W3b — the receive-buffer size for a message type, as a constant that cannot drift from the type.

node.subscription::<PointCloud2>("points")
    .rx_buffer::<{ nros::rx_buffer_for!(PointCloud2) }>()
    .build(on_cloud)?;

.rx_buffer::<N>() has always accepted a number. The problem is where the number comes from: a literal is correct until someone appends a field to the message, and then it is silently too small — the sample is received, ACKed and dropped at the transport, which needs a packet capture to attribute (report_dropped_take, and the 13.4 KiB Autoware trajectory case). This expands to the type’s own bound, computed from its schema by phase 380, so appending a field moves the buffer with it.

Why a macro and not a method. The builder cannot do this for you: inside impl<M> TypedSubscriptionBuilder<M> the type is a generic parameter, and on stable Rust a generic parameter may not appear in a const operation — error: generic parameters may not be used in const operations. At a call site the type is CONCRETE, which is legal, so the size has to be named where the type is. That constraint is also why phase-392’s size classes were “decoupled from codegen” in the first place.

Unbounded types are a BUILD ERROR (phase-403 W0). A type with an unbounded string/wstring/sequence has no bound, and this refuses to invent one. It used to expand to DEFAULT_RX_BUF_SIZE; it now fails to compile, naming both remedies.

Every message type is REQUIRED to carry a derived upper bound, stated in the .msg (string<=64) or capped in nros-codegen.toml. Phase 380 is explicit that None means “no bound EXISTS”, never “unknown”, and that a buffer must not be sized from a fallback. Substituting the configured default was the violation of that rule; refusing is what it licenses. report_dropped_take is a backstop for a buffer that is too small, not a licence to pick one.

use nros_serdes::schema::{Field, FieldType, Message};
struct Unbounded;
impl Message for Unbounded {
    const TYPE_NAME: &'static str = "test/msg/Unbounded";
    const FIELDS: &'static [Field] = &[Field {
        name: "s",
        ty: FieldType::String,
        offset: 0,
    }];
}
let _n: usize = nros::rx_buffer_for!(Unbounded);

The positive control for that compile_fail, so it cannot pass because the fixture stopped compiling for an unrelated reason:

use nros_serdes::schema::{Field, FieldType, Message};
struct Bounded;
impl Message for Bounded {
    const TYPE_NAME: &'static str = "test/msg/Bounded";
    const FIELDS: &'static [Field] = &[Field {
        name: "a",
        ty: FieldType::Uint64,
        offset: 0,
    }];
}
let n: usize = nros::rx_buffer_for!(Bounded);
assert!(n > 0);

Why the const block. The macro is used in two positions: as a const-generic argument (.rx_buffer::<{ rx_buffer_for!(M) }>()) and as a plain expression (let n = rx_buffer_for!(M);). A bare panic! is a build error in the first and a RUNTIME panic in the second, which would make the rule depend on where the macro appears. Wrapping the whole match in an inline const block forces compile-time evaluation in BOTH, so an unbounded type can never reach a running image.

What the error names. rustc points at this macro invocation, where the type is written literally, so the TYPE is named. The MEMBER that costs the bound cannot be in the message: const evaluation does not format, so a panic! there takes a literal only. The member is named by the codegen diagnostic for the same type – unbounded_reason in the generated C header (packs/c/message.h.jinja), which names EVERY member that costs the bound in one build (phase-403 W0), or nros_serdes::size::visit_unbounded over its FIELDS.