Skip to content

Add structured parameters#612

Open
azerupi wants to merge 1 commit intoros2-rust:mainfrom
azerupi:structured-params
Open

Add structured parameters#612
azerupi wants to merge 1 commit intoros2-rust:mainfrom
azerupi:structured-params

Conversation

@azerupi
Copy link

@azerupi azerupi commented Mar 9, 2026

This is an alternative implementation of #516.

The goal of this PR is not to dismiss or negate the work done in #516. But while reviewing that PR, because I'm excited about this feature, I felt like there was a better approach and it felt easier to explore an alternative design by trying another implementation from scratch (with AI assistance) rather than posting a bunch of review comments on the initial PR.

Anyone working on or reviewing #516 should feel free to dismiss this PR entirely or incorporate any ideas from it into the original PR if desired.

Key differences from #516

#516 (StructuredParameters) This PR (ParameterSet)
Code generation approach Trait-based indirection — the macro generates a StructuredParametersMeta<DefaultForbidden> trait impl that calls declare_structured_() passing all builder options as function arguments. The actual builder logic lives in hand-written trait impls on MandatoryParameter<T>, OptionalParameter<T>, etc. Direct code generation — the macro emits ParameterBuilder method chains (.default().description().mandatory()) directly, with no intermediate traits.
Intermediate types Requires a DefaultForbidden marker type with unreachable!() impls to satisfy the type system for nested structs. No intermediate types — nested structs are detected by type name and generate recursive ParameterSet::declare() calls.
Leaf vs nested detection No distinction — all fields (leaf and nested) use the same declare_structured() call with all builder options passed as arguments. For nested structs, options like default, description, range are silently ignored. Leaf-only attributes on nested fields are not caught at compile time. The macro distinguishes leaf from nested at expansion time and generates different code for each. Leaf-only attributes on nested fields produce compile errors.
Namespacing Parameter names are composed from field names and an optional user-provided prefix. E.g. node.declare_parameters("") on a Robot struct with a nested sensors: SensorConfig field produces speed, sensors.rate. With node.declare_parameters("robot"): robot._speed, robot._sensors._rate. The top-level struct name is converted to snake_case and used as namespace prefix by default. node.declare_parameter_set::<Robot>() produces robot.speed, robot.sensors.rate. Overridable with #[parameters(namespace = "...")] or #[parameters(flatten)].
Flatten Not supported. #[param(flatten)] on fields skips the field name in the namespace; #[parameters(flatten)] on structs skips the struct name. Flatten is not recursive — it only affects the level it's applied to.
Range syntax range = rclrs::ParameterRange { lower: Some(1.0), ..Default::default() } range(lower = 1.0) — parsed into ParameterRange by the macro.
Macro crate structure Single impl.rs file (~160 lines). Modular: struct_attrs.rs, field_attrs.rs, field_info.rs, codegen.rs + mod.rs (~550 lines).
rclrs-macros dependency Added as dev-dependencies in rclrs and not re-exported, the derive macro is only usable by adding the proc-macro crate as a dependency in downstream crates. Added as dependencies and re-exported via pub use rclrs_macros::ParameterSet — downstream crates use #[derive(ParameterSet)] from rclrs directly

What this PR adds

  • rclrs-macros crate — proc-macro crate with #[derive(ParameterSet)]
  • ParameterSet trait in rclrs with default_namespace() and declare() methods
  • NodeState convenience methodsdeclare_parameter_set() and declare_parameter_set_with_prefix()
  • Struct-level attributes#[parameters(flatten)], #[parameters(namespace = "...")]
  • Field-level attributes#[param(default, description, constraints, range(...), ignore_override, discard_mismatching_prior_value, discriminate, mandatory, optional, read_only, flatten)]
  • 10 integration tests covering all features

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