diff --git a/Cargo.toml b/Cargo.toml index afac85b2a..ad2071805 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ "rclrs", + "rclrs-macros", ] resolver = "2" diff --git a/Dockerfile b/Dockerfile index 07cb6970d..a15aab292 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,8 +22,10 @@ COPY src/ros2_rust/docker/rosidl_rust_setup.sh / RUN ./rosidl_rust_setup.sh RUN mkdir -p /workspace && echo "Did you forget to mount the repository into the Docker container?" > /workspace/HELLO.txt +RUN echo "\nsource /opt/ros/${ROS_DISTRO}/setup.sh" >> /root/.bashrc WORKDIR /workspace + COPY src/ros2_rust/docker/rosidl_rust_entrypoint.sh / ENTRYPOINT ["/rosidl_rust_entrypoint.sh"] CMD ["/bin/bash"] diff --git a/rclrs-macros/Cargo.toml b/rclrs-macros/Cargo.toml new file mode 100644 index 000000000..dbf19b006 --- /dev/null +++ b/rclrs-macros/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name= "rclrs-macros" +version = "0.0.1" +authors = ["Balthasar Schüss "] +edition = "2021" +license = "Apache-2.0" +description = "A rust library providing proc macros for rclrs" +rust-version = "1.75" + + +[lib] +path = "src/lib.rs" +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = "2.0" diff --git a/rclrs-macros/src/impl.rs b/rclrs-macros/src/impl.rs new file mode 100644 index 000000000..34d3828be --- /dev/null +++ b/rclrs-macros/src/impl.rs @@ -0,0 +1,162 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{DeriveInput, Expr}; + +pub(crate) fn derive_structured_parameters(input: DeriveInput) -> syn::Result { + let ident = input.ident; + + match input.data { + syn::Data::Struct(ref s) => derive_structured_parameters_struct(ident, s), + _ => { + return syn::Result::Err(syn::Error::new_spanned( + ident, + "StructuredParameters trait can only be derived for structs", + )); + } + } +} + +fn derive_structured_parameters_struct( + ident: proc_macro2::Ident, + struct_: &syn::DataStruct, +) -> syn::Result { + let fields = &struct_.fields; + + let field_types: Vec<_> = fields.iter().map(|f| &f.ty).collect(); + let mut args = Vec::new(); + for f in fields { + let ident = f.ident.as_ref().unwrap(); + let ident_str = syn::LitStr::new(&f.ident.as_ref().unwrap().to_string(), ident.span()); + + let mut default: Option = None; + let mut description: Option = None; + let mut constraints: Option = None; + let mut ignore_override = false; + let mut discard_mismatching_prior_value = false; + let mut discriminate: Option = None; + let mut range: Option = None; + + for attr in &f.attrs { + if attr.path().is_ident("param") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("default") { + default = Some(meta.value()?.parse()?); + Ok(()) + } else if meta.path.is_ident("description") { + description = Some(meta.value()?.parse()?); + Ok(()) + } else if meta.path.is_ident("constraints") { + constraints = Some(meta.value()?.parse()?); + Ok(()) + } else if meta.path.is_ident("ignore_override") { + ignore_override = true; + Ok(()) + } else if meta.path.is_ident("discard_mismatching_prior_value") { + discard_mismatching_prior_value = true; + Ok(()) + } else if meta.path.is_ident("discriminate") { + discriminate = Some(meta.value()?.parse()?); + Ok(()) + } else if meta.path.is_ident("range") { + range = Some(meta.value()?.parse()?); + Ok(()) + } else { + let err = format!("Unknown key: {:?}", &meta.path.get_ident()); + syn::Result::Err(syn::Error::new_spanned(meta.path, err)) + } + })?; + } + } + + let default = match default { + Some(expr) => quote! {Some(#expr)}, + None => quote! {None}, + }; + let description = match description { + Some(expr) => quote! {#expr}, + None => quote! {""}, + }; + let constraints = match constraints { + Some(expr) => quote! {#expr}, + None => quote! {""}, + }; + let discriminate = match discriminate { + Some(expr) => quote! { + core::option::Option::Some(Box::new(#expr)) + }, + None => quote! {core::option::Option::None}, + }; + let range = match range { + Some(expr) => quote! { + core::option::Option::Some(#expr) + }, + None => quote! {core::option::Option::None}, + }; + + let field_type = match &f.ty { + syn::Type::Path(p) => { + let mut p = p.path.clone(); + for segment in &mut p.segments { + segment.arguments = syn::PathArguments::None; + } + p + } + e => { + return syn::Result::Err(syn::Error::new_spanned( + e, + "Only PathType attributes are supported", + )); + } + }; + let r = quote! { + #ident : #field_type::declare_structured( + node, + &{match name { + "" => #ident_str.to_string(), + prefix => [prefix, ".", #ident_str].concat(), + }}, + #default, + #description, + #constraints, + #ignore_override, + #discard_mismatching_prior_value, + #discriminate, + #range, + )?, + }; + args.push(r); + } + + let result = quote!( + impl #ident { + const _ASSERT_PARAMETER: fn() = || { + fn assert_parameter() {} + #( + assert_parameter::<#field_types>(); + )* + }; + } + impl rclrs::parameter::structured::StructuredParametersMeta for #ident { + fn declare_structured_( + node: &rclrs::NodeState, + name: &str, + default: core::option::Option, + description: impl core::convert::Into>, + constraints: impl core::convert::Into>, + ignore_override: bool, + discard_mismatching_prior_value: bool, + discriminate: core::option::Option) + -> core::option::Option>>, + range: core::option::Option<::Range>, + ) + -> core::result::Result { + core::result::Result::Ok(Self{ #(#args)*}) + } + + } + impl rclrs::StructuredParameters for #ident {} + ); + syn::Result::Ok(result) +} diff --git a/rclrs-macros/src/lib.rs b/rclrs-macros/src/lib.rs new file mode 100644 index 000000000..7daf128dd --- /dev/null +++ b/rclrs-macros/src/lib.rs @@ -0,0 +1,11 @@ +mod r#impl; +use proc_macro::TokenStream; +use syn::{parse_macro_input, DeriveInput}; + +#[proc_macro_derive(StructuredParameters, attributes(param))] +pub fn derive_structured_parameters(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + r#impl::derive_structured_parameters(input) + .unwrap_or_else(|e| e.to_compile_error()) + .into() +} diff --git a/rclrs/Cargo.toml b/rclrs/Cargo.toml index 7f4e61903..4d62337b9 100644 --- a/rclrs/Cargo.toml +++ b/rclrs/Cargo.toml @@ -36,11 +36,13 @@ rosidl_runtime_rs = "0.4" serde = { version = "1", optional = true, features = ["derive"] } serde-big-array = { version = "0.5.1", optional = true } + [dev-dependencies] # Needed for e.g. writing yaml files in tests tempfile = "3.3.0" # Needed for parameter service tests tokio = { version = "1", features = ["rt", "time", "macros"] } +rclrs-macros = {path = "../rclrs-macros"} [build-dependencies] # Needed for uploading documentation to docs.rs diff --git a/rclrs/src/lib.rs b/rclrs/src/lib.rs index 3952682a1..96a0a5663 100644 --- a/rclrs/src/lib.rs +++ b/rclrs/src/lib.rs @@ -213,7 +213,10 @@ pub use error::*; pub use executor::*; pub use logging::*; pub use node::*; -pub use parameter::*; +pub use parameter::{ + structured::{StructuredParameters, StructuredParametersMeta}, + *, +}; pub use publisher::*; pub use qos::*; pub use rcl_bindings::rmw_request_id_t; diff --git a/rclrs/src/parameter.rs b/rclrs/src/parameter.rs index fe9fa0918..6905944fa 100644 --- a/rclrs/src/parameter.rs +++ b/rclrs/src/parameter.rs @@ -1,6 +1,7 @@ mod override_map; mod range; mod service; +pub mod structured; mod value; pub(crate) use override_map::*; diff --git a/rclrs/src/parameter/structured.rs b/rclrs/src/parameter/structured.rs new file mode 100644 index 000000000..189bfc1b6 --- /dev/null +++ b/rclrs/src/parameter/structured.rs @@ -0,0 +1,385 @@ +//! This module provides the trait [`StructuredParameters`] default implementations for declaring parameters in structured fashion. +//! [`rclrs_proc_macros::StructuredParameters`] provides a macro to derive the trait for structs automatically. +//! + +use crate::NodeState; + +use super::ParameterVariant; + +/// Marker trait for implementing [`StructuredParameters`] where a default value cannot be specified. +/// This is usually the case for container types, that are not represented by any actual parameter. + +#[derive(Clone, Copy)] +pub enum DefaultForbidden {} +impl crate::ParameterVariant for DefaultForbidden { + type Range = (); + + fn kind() -> crate::ParameterKind { + // cannot be instantiated cannot be called + // let's satisfy the type checker + unreachable!() + } +} +impl From for crate::ParameterValue { + fn from(_value: DefaultForbidden) -> Self { + // cannot be instantiated cannot be called + // let's satisfy the type checker + unreachable!() + } +} +impl From for DefaultForbidden { + fn from(_value: crate::ParameterValue) -> Self { + // cannot be instantiated cannot be called + // let's satisfy the type checker + unreachable!() + } +} + +/// Types implementing this trait can delcare their parameters with[`NodeState::declare_parameters`]. +/// The trait can be automatically derived using [`rclrs_proc_macros`] if: +/// - if the type is a struct +/// - all attributes implement [`StructuredParameters`] +pub trait StructuredParameters: Sized { + /// Declares all parameters in ros node. + /// + /// # Parameters + /// + /// - `node`: The ros node to declare parameters for. + /// - `name`: The name of the parameter. Nested parameters are recursively declared with "{name}.{field_name}" if the name is not empty else "{field_name}". + /// - `default`: The default value for the paramter. + /// - `description` The description of the parameter + /// + /// # Returns + /// + /// [`Result`] containing the declared structured parameters or [`crate::DeclarationError`] + fn declare_structured( + node: &NodeState, + name: &str, + default: Option, + description: impl Into>, + constraints: impl Into>, + ignore_override: bool, + discard_mismatching_prior_value: bool, + discriminate: Option) -> Option>>, + range: Option<::Range>, + ) -> core::result::Result + where + Self: StructuredParametersMeta, + { + Self::declare_structured_( + node, + name, + default, + description, + constraints, + ignore_override, + discard_mismatching_prior_value, + discriminate, + range, + ) + } +} + +/// Helper trait to unify the default value type with generic container types like +/// - [`crate::MandatoryParameter`] +/// - [`crate::OptionalParameter`] +/// - [`crate::ReadOnlyParameter`] +/// +/// In these cases [`Self`] != [`T`] and forces the usage of a generic trait. +/// However, a generic trait also requires annotating this default value in the derive macro. +/// For the container based structured parameters [`T`] is always [`DefaultForbidden`] +/// and therefore we can hide this from the trait + macro by using this helper trait. +/// The previously mentioned leaf types that actually hold parameters, are to be implemented manually anyway. +/// +pub trait StructuredParametersMeta: Sized { + /// See [`StructuredParameters::declare_structured`] + fn declare_structured_( + node: &NodeState, + name: &str, + default: Option, + description: impl Into>, + constraints: impl Into>, + ignore_override: bool, + discard_mismatching_prior_value: bool, + discriminate: Option) -> Option>>, + range: Option<::Range>, + ) -> core::result::Result; +} + +impl NodeState { + /// Declares all nested parameters of the [`StructuredParameters`]. + /// Parameter naming recursively follows "`name`.`field_name`" or "`field_name`" if the initial `name` is empty. + pub fn declare_parameters( + &self, + name: &str, + ) -> core::result::Result + where + T: StructuredParameters + StructuredParametersMeta, + { + T::declare_structured(self, name, None, "", "", false, false, None, None) + } +} + +impl StructuredParameters for crate::MandatoryParameter {} +impl StructuredParametersMeta for crate::MandatoryParameter { + fn declare_structured_( + node: &NodeState, + name: &str, + default: Option, + description: impl Into>, + constraints: impl Into>, + ignore_override: bool, + discard_mismatching_prior_value: bool, + discriminate: Option) -> Option>>, + range: Option<::Range>, + ) -> core::result::Result { + let builder = node.declare_parameter(name); + let builder = match default { + Some(default) => builder.default(default), + None => builder, + }; + let builder = match ignore_override { + true => builder.ignore_override(), + false => builder, + }; + let builder = match discard_mismatching_prior_value { + true => builder.discard_mismatching_prior_value(), + false => builder, + }; + let builder = match discriminate { + Some(f) => builder.discriminate(f), + None => builder, + }; + let builder = match range { + Some(range) => builder.range(range), + None => builder, + }; + builder + .description(description) + .constraints(constraints) + .mandatory() + } +} +impl StructuredParameters for crate::ReadOnlyParameter {} +impl StructuredParametersMeta for crate::ReadOnlyParameter { + fn declare_structured_( + node: &NodeState, + name: &str, + default: Option, + description: impl Into>, + constraints: impl Into>, + ignore_override: bool, + discard_mismatching_prior_value: bool, + discriminate: Option) -> Option>>, + range: Option<::Range>, + ) -> core::result::Result { + let builder = node.declare_parameter(name); + let builder = match default { + Some(default) => builder.default(default), + None => builder, + }; + let builder = match ignore_override { + true => builder.ignore_override(), + false => builder, + }; + let builder = match discard_mismatching_prior_value { + true => builder.discard_mismatching_prior_value(), + false => builder, + }; + let builder = match discriminate { + Some(f) => builder.discriminate(f), + None => builder, + }; + let builder = match range { + Some(range) => builder.range(range), + None => builder, + }; + builder + .description(description) + .constraints(constraints) + .read_only() + } +} +impl StructuredParameters for crate::OptionalParameter {} +impl StructuredParametersMeta for crate::OptionalParameter { + fn declare_structured_( + node: &NodeState, + name: &str, + default: Option, + description: impl Into>, + constraints: impl Into>, + ignore_override: bool, + discard_mismatching_prior_value: bool, + discriminate: Option) -> Option>>, + range: Option<::Range>, + ) -> core::result::Result { + let builder = node.declare_parameter(name); + let builder = match default { + Some(default) => builder.default(default), + None => builder, + }; + let builder = match ignore_override { + true => builder.ignore_override(), + false => builder, + }; + let builder = match discard_mismatching_prior_value { + true => builder.discard_mismatching_prior_value(), + false => builder, + }; + let builder = match discriminate { + Some(f) => builder.discriminate(f), + None => builder, + }; + let builder = match range { + Some(range) => builder.range(range), + None => builder, + }; + builder + .description(description) + .constraints(constraints) + .optional() + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use crate as rclrs; + use rclrs::{parameter::structured::*, CreateBasicExecutor}; + use rclrs_macros::StructuredParameters; + + #[derive(StructuredParameters, Debug)] + struct SimpleStructuredParameters { + _mandatory: rclrs::MandatoryParameter, + _optional: rclrs::OptionalParameter, + _readonly: rclrs::ReadOnlyParameter, + } + + #[test] + fn test_simple_structured_parameters() { + let args: Vec = [ + "test", + "--ros-args", + "-p", + "_mandatory:=1.0", + "-p", + "_optional:=1.0", + "-p", + "_readonly:=1.0", + ] + .into_iter() + .map(str::to_string) + .collect(); + + let context = crate::Context::new(args, rclrs::InitOptions::default()).unwrap(); + let exec = context.create_basic_executor(); + let node = exec.create_node(rclrs::NodeOptions::new("test")).unwrap(); + let _params: SimpleStructuredParameters = node.declare_parameters("").unwrap(); + println!("{:?}", _params); + } + + #[derive(StructuredParameters, Debug)] + struct NestedStructuredParameters { + _simple: SimpleStructuredParameters, + _mandatory: rclrs::MandatoryParameter>, + } + + #[test] + fn test_nested_structured_parameters() { + let args: Vec = [ + "test", + "--ros-args", + "-p", + "nested._simple._mandatory:=1.0", + "-p", + "nested._simple._optional:=1.0", + "-p", + "nested._simple._readonly:=1.0", + "-p", + "nested._mandatory:=foo", + ] + .into_iter() + .map(str::to_string) + .collect(); + + let context = crate::Context::new(args, rclrs::InitOptions::default()).unwrap(); + let exec = context.create_basic_executor(); + let node = exec.create_node(rclrs::NodeOptions::new("test")).unwrap(); + + let _params: NestedStructuredParameters = node.declare_parameters("nested").unwrap(); + println!("{:?}", _params); + } + + #[derive(Debug, StructuredParameters)] + struct SimpleStructuredParametersWithDefaults { + #[param(default = 42.0)] + _mandatory: rclrs::MandatoryParameter, + #[param(default = 42.0)] + _optional: rclrs::OptionalParameter, + #[param(default = Arc::from("test"))] + _readonly: rclrs::ReadOnlyParameter>, + } + + #[test] + fn test_simple_structured_parameters_with_defaults() { + let args: Vec = ["test", "--ros-args"] + .into_iter() + .map(str::to_string) + .collect(); + let context = crate::Context::new(args, rclrs::InitOptions::default()).unwrap(); + let exec = context.create_basic_executor(); + let node = exec.create_node(rclrs::NodeOptions::new("test")).unwrap(); + let _params: SimpleStructuredParametersWithDefaults = node.declare_parameters("").unwrap(); + println!("{:?}", _params); + } + #[derive(Debug, StructuredParameters)] + struct SimpleStructuredParametersWithDefaultsAndDescriptions { + #[param(default = 42.0, ignore_override, description = "_mandatory")] + _mandatory: rclrs::MandatoryParameter, + #[param(default = 42.0, description = "_optional")] + _optional: rclrs::OptionalParameter, + #[param(default = Arc::from("test"), description = "_readonly")] + _readonly: rclrs::ReadOnlyParameter>, + } + + #[test] + fn test_simple_structured_parameters_with_defaults_and_descriptions() { + let args: Vec = ["test", "--ros-args"] + .into_iter() + .map(str::to_string) + .collect(); + let context = crate::Context::new(args, rclrs::InitOptions::default()).unwrap(); + let exec = context.create_basic_executor(); + let node = exec.create_node(rclrs::NodeOptions::new("test")).unwrap(); + let _params: SimpleStructuredParametersWithDefaultsAndDescriptions = + node.declare_parameters("").unwrap(); + println!("{:?}", _params); + } + #[derive(Debug, StructuredParameters)] + struct AllMacroOptions { + #[param( + default = 42.0, + ignore_override, + description = "_mandatory", + constraints = "some_constraints", + discard_mismatching_prior_value, + discriminate = |av| av.default_value, + range = rclrs::ParameterRange { lower: Some(1.0), ..Default::default()}, + )] + _mandatory: rclrs::MandatoryParameter, + } + + #[test] + fn test_all_macro_options() { + let args: Vec = ["test", "--ros-args"] + .into_iter() + .map(str::to_string) + .collect(); + let context = crate::Context::new(args, rclrs::InitOptions::default()).unwrap(); + let exec = context.create_basic_executor(); + let node = exec.create_node(rclrs::NodeOptions::new("test")).unwrap(); + let _params: AllMacroOptions = node.declare_parameters("").unwrap(); + println!("{:?}", _params); + } +}