Several improvements around parameters - #663
Open
azerupi wants to merge 3 commits into
Open
Conversation
A ParameterKind does not uniquely identify a Rust type. Several types can
share one, and a value of the right kind can still be unrepresentable in
the type a parameter was declared with. validate_parameter_setting only
compared kind discriminants and Parameters::set only compared T::kind(),
while every getter assumes the stored value converts back:
self.value.read().unwrap().clone().try_into().ok().unwrap()
So a remote SetParameters call could store a value that passes validation
and then panic the node on the next read. This is reachable today by
anyone implementing the public ParameterVariant trait for a type whose
conversion from ParameterValue is partial, and becomes unavoidable once
narrow integer and enum parameter types exist.
Capture the declared type's check as a fn pointer at declaration time and
run it on both write paths, before the range check and before the custom
validate callback (whose type-erasure wrapper depends on the value being
convertible). The check is the type's own TryFrom, so there is nothing
extra for a type to implement and no way for it to accept a value that a
later read would then reject. ParameterValueError::Invalid carries the
reason the conversion gave back to the caller, which the parameter
service puts in the response so the operator who made the call can see
why the value was refused.
additional_constraints in a parameter descriptor was only ever what the declaration passed to .constraints(). For a type with a closed set of valid values, such as a string-backed enum, that meant every declaration site restating the type's rules, with nothing keeping the text in sync as variants are added -- and an empty descriptor field when the call was forgotten, leaving operators no way to discover what a value may be. ParameterVariant::type_constraints() lets a type describe itself once. A declaration that sets no constraints of its own inherits it, so `ros2 param describe` reports the valid values for free. An explicit .constraints() still takes precedence for rules that belong to one declaration rather than to the type.
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 added in the previous commit 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: range = 1024..=49151 on a u16 parameter means what it appears to. A bound the declaration leaves open is filled in from the type's own limits, so the narrowing 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. DurationSecs and DurationMillis carry a Duration as a double or an integer. Duration itself is not a parameter type, since the unit it would be stored in is not something to leave implicit in a robotics configuration. Both wrappers deref to Duration and report their unit in the descriptor's constraints. 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR has 3 changes that improve parameter handling.
1. Prevent panic for parameter values that can't be represented in the Rust type
Problem
Right now on main, users of rclrs can implement the public
ParameterVarianttrait for arbitrary Rust types. This is for example very useful to implement string-backed enums.However the code in rclrs that writes the parameter only checks that the kind is correct before storing the value. However the functions that read the value (
MandatoryParameter::get,OptionalParameter::get,ReadOnlyParameter::get, bothsubscribe()closures, and bothon_changewrappers) assume it can infallibly convert to the Rust type.So let's imagine we have this user code
It will only work if the string value is
"on"or"off", anything else will result in a panic since we are only validating the kind. Any string can be stored in the parameter.This is pretty bad because any remote client on the ROS graph could crash a node that implements this.
Fix 7c9cac1
To fix this, we are now checking the conversion before storing the value. Any value that doesn't pass the conversion is does not get stored. In the same way other built-in checks run (e.g. ranges).
and
param.get()still returns the previous value because the new value was never stored.2. Define constraints at
ParameterVariantimplementationGap
ROS 2 parameter descriptors carry constraints in
IntegerRange,FloatingPointRangeoradditional_constraints(free text).Right now, for custom fields, for example the previously defined
Switchenum the constraints have to be passed at each declaration of a parameter using this type. There is also nothing that keeps those multiple constraint declarations in sync with the type, adding a new enum variant will silently make the declared constraints drift unless all of them are updated. And if you forget to declare the constraints,ros2 param describegives you no information.Fix 3f27dcc
We add a new method on
ParameterVariantthat allows the the constraints to be declared where the type is defined as a ROS parameter. It has a default implementation that returnsNone.This allows a type to specify its constraints. If the user declaring the parameter adds explicit constraints they will be used otherwise we default to the type's constraints.
3. Support common Rust types as parameters
Gap
A parameter could only be declared with the Rust type that most directly represents one of the nine ROS 2 types: bool, i64, f64, Arc, Arc<[u8]>, Arc<[bool]>, Arc<[i64]>, Arc<[f64]>, Arc<[Arc]>. This is convenient for the parameter machinery (rclrs) but inconvenient for the application (user).
Fix 4f098d4
We implement
ParameterVariantfor many common Rust types to allow them to be used as parameters.String,PathBufi8,i16,i32,u8,u16,u32f32DurationSecs,DurationMillis(new types aroundDuration)Vec<T>of every scalar above, plusVec<i64>,Vec<f64>,Vec<bool>,Vec<u8>,Vec<String>Note:
Vec<u8>is a byte array, not an integer array. ROS 2 has a dedicated byte array type. It's the one Rust array type that doesn't follow its scalar (u8 alone is an Integer).u64,usize,i128andu128are deliberately absent. ROS 2's integer type is i64 and conversions from those types could silently produce values that did not match the intent.Durationto be able to differentiate between seconds (Float) and milliseconds (Integer).This was only possible after fix n°1 because now we have many Rust types that can't represent all values that could be stored in the ROS type. But since we now check the conversion before storing, this is totally fine!
The narrow integers define an
IntegerRangethat allow to describe the parameter constraints.Before:
After:
Beyond the ergonomics provided here, this is the groundwork for future PRs that provide a derive proc-macro based API similar to clap.