Skip to content

feat(parameters): support common Rust types as parameters - #691

Open
azerupi wants to merge 1 commit into
azerupi/params/descriptor-constraintsfrom
azerupi/params/std-types
Open

feat(parameters): support common Rust types as parameters#691
azerupi wants to merge 1 commit into
azerupi/params/descriptor-constraintsfrom
azerupi/params/std-types

Conversation

@azerupi

@azerupi azerupi commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Problem

A parameter could only be declared with the Rust type that most directly represents one of the nine ROS 2 parameter types. A list of names had to be Arc<[Arc<str>]> and a port number an i64, even where the application would naturally use Vec<String> and u16.

let wheels: MandatoryParameter<Arc<[Arc<str>]>> = node
    .declare_parameter("wheels")
    .default_string_array(["left_wheel", "right_wheel"])
    .mandatory()?;

let port: MandatoryParameter<i64> = node
    .declare_parameter("port")
    .default(8080)
    .mandatory()?;
// nothing stops this being set to 70000, and nothing says so in the descriptor

Solution

Implement ParameterVariant for the Rust types an application would reach for anyway, so the declaration says what the value is and the narrowing that implies is enforced and reported.

let wheels: MandatoryParameter<Vec<String>> = node
    .declare_parameter("wheels")
    .default(vec!["left_wheel".to_string(), "right_wheel".to_string()])
    .mandatory()?;

let port: MandatoryParameter<u16> = node
    .declare_parameter("port")
    .default(8080)
    .range(1024..)
    .mandatory()?;

Types added

Strings and paths String, PathBuf
Narrow integers i8, i16, i32, u8, u16, u32
Narrow float f32
Arrays Vec<T> for every scalar above, plus Vec<i64>, Vec<f64>, Vec<bool>, Vec<u8>

Vec<u8> is the one exception to array mapping: a sequence of bytes is a ROS 2 byte array, not an integer array. u64, usize, i128 and u128 are deliberately unsupported because a parameter value is stored as an i64

Narrowing is enforced

A conversion back from a stored value can be partial, not every i64 is a u16. The type check added in the previous PR rejects those on both write paths, so a narrow parameter can never hold a value that goes out of bounds of the Rust type, including over SetParameters:

$ ros2 param set /node port 70000
Setting parameter failed: Parameter value is not valid for this parameter's type:
70000 is out of range for u16, which accepts 0..=65535

Arrays are checked element-wise and the rejection says which element was at fault:

element 2: 70000 is out of range for u16, which accepts 0..=65535

Reading an element goes through the scalar type's own conversion, so what a u16 may be is decided in exactly one place.

Narrowing

A u16 is bounded by what it can hold, so the limits ride on the type's ParameterConversion and combine with ranges:

.range(1024..)     // declared: lower 1024, upper open
                   // reported: 1024..=65535
$ ros2 param describe /node port
  Constraints: 0..=65535
  Integer range: 1024..65535

rqt_reconfigure and anything else reading the descriptor now sees the real bound, instead of an operator discovering it by having a value rejected. Constraints replace (the declaration's text wins), ranges narrow (a value must satisfy both).

A parameter could only be declared with the Rust type that most directly
represents one of the nine ROS 2 parameter types, so a list of names had
to be handled as Arc<[Arc<str>]> and a port number as i64 even where the
application would otherwise use Vec<String> and u16.

Add ParameterVariant for String, PathBuf, Vec<String>, Vec<i64>,
Vec<f64>, Vec<bool>, Vec<u8>, f32 and the integer types that fit in an
i64 (i8, i16, i32, u8, u16, u32). Conversions back from a stored value
can be partial -- not every i64 is a u16 -- which the type check on the
write paths rejects before it can be stored, so such a parameter can
never hold a value of the wrong type.

Every scalar type also has a Vec form, using whichever ROS 2 array type
holds the scalar's representation, so a Vec<u16> is an integer array whose
elements are each checked against the range of a u16. Reading an element
goes through the scalar type's own conversion, so what an element may be
is decided in one place, and a rejection says which element was at fault.
Vec<u8> is the one exception: a sequence of bytes is a ROS 2 byte array
rather than an integer array.

Ranges are expressed in the parameter's own type, so range = 1024..=49151
on a u16 parameter means what it appears to and a bound the type cannot
hold is a compile error. Each of these types also names the value just
below an exclusive end, the previous integer or the previous
representable f32, so range = 1024..49152 admits up to 49151.

The limits a type imposes are carried by its ParameterConversion and
narrow whatever the declaration asks for, so a bound left open is filled
in and the result reaches the descriptor's IntegerRange where
`ros2 param describe` and rqt_reconfigure can read it, rather than only
being discovered by having a value rejected.

Duration is not among them, and needs no wrapper type to be one. A
declaration says which unit it is stored in through its own conversion,
for instance ParameterConversion::double(Duration::as_secs_f64,
Duration::try_from_secs_f64), which puts the unit at the declaration
where it belongs rather than in a type that exists only to carry it.

u64, usize, i128 and u128 are deliberately unsupported: every way of
storing a value above i64::MAX in a ROS 2 parameter silently produces a
number the application did not ask for. Declaring one now says so, along
with what the parameter types are and how to use a type of your own,
through a diagnostic::on_unimplemented on ParameterVariant, rather than
reporting a bare unsatisfied trait bound.

Assisted-by: Claude:claude-opus-5 [Claude Code]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant