Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"rclrs",
"rclrs-macros",
]
resolver = "2"
14 changes: 14 additions & 0 deletions rclrs-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "rclrs-macros"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
description = "Procedural macros for the rclrs ROS 2 client library"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full", "extra-traits"] }
14 changes: 14 additions & 0 deletions rclrs-macros/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<?xml-model
href="http://download.ros.org/schema/package_format3.xsd"
schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>rclrs-macros</name>
<version>0.5.0</version>
<description>Procedural macros for the rclrs ROS 2 client library.</description>
<maintainer email="esteve@apache.org">Esteve Fernandez</maintainer>
<license>Apache License 2.0</license>
<export>
<build_type>ament_cargo</build_type>
</export>
</package>
15 changes: 15 additions & 0 deletions rclrs-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use proc_macro::TokenStream;

/// Derive macro for declaring ROS 2 parameters from struct definitions.
///
/// See `rclrs::ParameterSet` for full documentation.
#[proc_macro_derive(ParameterSet, attributes(parameters, param))]
pub fn derive_parameter_set(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as syn::DeriveInput);
match parameter_set::expand(input) {
Ok(tokens) => tokens.into(),
Err(e) => e.to_compile_error().into(),
}
}

mod parameter_set;
110 changes: 110 additions & 0 deletions rclrs-macros/src/parameter_set/codegen.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//! Code generation for individual fields in a `ParameterSet` struct.

use proc_macro2::TokenStream;
use quote::quote;

use super::field_attrs::{FieldAttrs, ParameterType, RangeAttr};
use super::field_info::FieldInfo;

/// Generates the field initializer expression for one struct field.
pub(crate) fn generate_field(field: &FieldInfo) -> TokenStream {
match field {
FieldInfo::Leaf {
ident,
attrs,
param_type,
} => {
let name_str = ident.to_string();
let builder_chain = generate_builder_chain(attrs);
let terminal_call = generate_terminal(*param_type);
quote! {
#ident: node
.declare_parameter(
rclrs::__private::param_name(prefix, #name_str)
)
#builder_chain
#terminal_call?
}
}
FieldInfo::Nested { ident, flatten } => generate_nested(ident, *flatten),
}
}

/// Generates the chained builder method calls from field attributes.
fn generate_builder_chain(attrs: &FieldAttrs) -> TokenStream {
let mut calls = TokenStream::new();

if let Some(default) = &attrs.default {
calls.extend(quote! { .default(#default) });
}
if let Some(description) = &attrs.description {
calls.extend(quote! { .description(#description) });
}
if let Some(constraints) = &attrs.constraints {
calls.extend(quote! { .constraints(#constraints) });
}
if let Some(range) = &attrs.range {
let range_expr = generate_range(range);
calls.extend(quote! { .range(#range_expr) });
}
if attrs.ignore_override {
calls.extend(quote! { .ignore_override() });
}
if attrs.discard_mismatching_prior_value {
calls.extend(quote! { .discard_mismatching_prior_value() });
}
if let Some(discriminate) = &attrs.discriminate {
calls.extend(quote! { .discriminate(#discriminate) });
}

calls
}

/// Generates the terminal builder call (`.mandatory()`, `.optional()`, `.read_only()`).
fn generate_terminal(param_type: ParameterType) -> TokenStream {
match param_type {
ParameterType::Mandatory => quote! { .mandatory() },
ParameterType::Optional => quote! { .optional() },
ParameterType::ReadOnly => quote! { .read_only() },
}
}

/// Generates a `ParameterRange { ... }` expression from parsed range attributes.
fn generate_range(range: &RangeAttr) -> TokenStream {
let lower = match &range.lower {
Some(expr) => quote! { Some(#expr) },
None => quote! { None },
};
let upper = match &range.upper {
Some(expr) => quote! { Some(#expr) },
None => quote! { None },
};
let step = match &range.step {
Some(expr) => quote! { Some(#expr) },
None => quote! { None },
};
quote! {
rclrs::ParameterRange {
lower: #lower,
upper: #upper,
step: #step,
}
}
}

/// Generates the field initializer for a nested `ParameterSet` field.
fn generate_nested(ident: &syn::Ident, flatten: bool) -> TokenStream {
let name_str = ident.to_string();
if flatten {
quote! {
#ident: rclrs::ParameterSet::declare(node, prefix)?
}
} else {
quote! {
#ident: rclrs::ParameterSet::declare(
node,
&rclrs::__private::param_name(prefix, #name_str),
)?
}
}
}
154 changes: 154 additions & 0 deletions rclrs-macros/src/parameter_set/field_attrs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//! Parsing of field-level `#[param(...)]` attributes.

use syn::{Expr, LitStr};

/// The kind of parameter: mandatory, optional, or read-only.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ParameterType {
Mandatory,
Optional,
ReadOnly,
}

/// Parsed range attribute: `range(lower = x, upper = y, step = z)`.
/// All fields are optional.
#[derive(Debug, Default)]
pub(crate) struct RangeAttr {
pub lower: Option<Expr>,
pub upper: Option<Expr>,
pub step: Option<Expr>,
}

/// Parsed field-level configuration from `#[param(...)]`.
#[derive(Debug)]
pub(crate) struct FieldAttrs {
/// Explicit parameter type (escape hatch for type aliases).
pub param_type: Option<ParameterType>,
/// Default value expression.
pub default: Option<Expr>,
/// Parameter description string.
pub description: Option<LitStr>,
/// Parameter constraints string.
pub constraints: Option<LitStr>,
/// Parameter range.
pub range: Option<RangeAttr>,
/// Whether to ignore parameter overrides from command line.
pub ignore_override: bool,
/// Whether to discard prior values with mismatching types.
pub discard_mismatching_prior_value: bool,
/// Path to a custom discriminator function.
pub discriminate: Option<Expr>,
/// Whether this nested field should be flattened (no extra namespace).
pub flatten: bool,
}

impl FieldAttrs {
/// Parses all `#[param(...)]` attributes on a field.
pub fn parse(field: &syn::Field) -> syn::Result<Self> {
let mut attrs = Self {
param_type: None,
default: None,
description: None,
constraints: None,
range: None,
ignore_override: false,
discard_mismatching_prior_value: false,
discriminate: None,
flatten: false,
};

for attr in &field.attrs {
if !attr.path().is_ident("param") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("mandatory") {
attrs.set_param_type(ParameterType::Mandatory, &meta)?;
Ok(())
} else if meta.path.is_ident("optional") {
attrs.set_param_type(ParameterType::Optional, &meta)?;
Ok(())
} else if meta.path.is_ident("read_only") {
attrs.set_param_type(ParameterType::ReadOnly, &meta)?;
Ok(())
} else if meta.path.is_ident("default") {
attrs.default = Some(meta.value()?.parse()?);
Ok(())
} else if meta.path.is_ident("description") {
attrs.description = Some(meta.value()?.parse()?);
Ok(())
} else if meta.path.is_ident("constraints") {
attrs.constraints = Some(meta.value()?.parse()?);
Ok(())
} else if meta.path.is_ident("range") {
attrs.range = Some(parse_range(&meta)?);
Ok(())
} else if meta.path.is_ident("ignore_override") {
attrs.ignore_override = true;
Ok(())
} else if meta.path.is_ident("discard_mismatching_prior_value") {
attrs.discard_mismatching_prior_value = true;
Ok(())
} else if meta.path.is_ident("discriminate") {
attrs.discriminate = Some(meta.value()?.parse()?);
Ok(())
} else if meta.path.is_ident("flatten") {
attrs.flatten = true;
Ok(())
} else {
Err(meta.error(format!(
"unknown param attribute: '{}'",
meta.path.get_ident().map_or("?".into(), |i| i.to_string())
)))
}
})?;
}

Ok(attrs)
}

/// Sets the parameter type, erroring if one was already set.
fn set_param_type(
&mut self,
param_type: ParameterType,
meta: &syn::meta::ParseNestedMeta<'_>,
) -> syn::Result<()> {
if self.param_type.is_some() {
return Err(meta.error("only one of 'mandatory', 'optional', 'read_only' is allowed"));
}
self.param_type = Some(param_type);
Ok(())
}

/// Returns true if any leaf-only attribute was set.
pub fn has_leaf_attrs(&self) -> bool {
self.default.is_some()
|| self.description.is_some()
|| self.constraints.is_some()
|| self.range.is_some()
|| self.ignore_override
|| self.discard_mismatching_prior_value
|| self.discriminate.is_some()
|| self.param_type.is_some()
}
}

/// Parses `range(lower = x, upper = y, step = z)`.
fn parse_range(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<RangeAttr> {
let mut range = RangeAttr::default();
meta.parse_nested_meta(|nested| {
if nested.path.is_ident("lower") {
range.lower = Some(nested.value()?.parse()?);
Ok(())
} else if nested.path.is_ident("upper") {
range.upper = Some(nested.value()?.parse()?);
Ok(())
} else if nested.path.is_ident("step") {
range.step = Some(nested.value()?.parse()?);
Ok(())
} else {
Err(nested.error("expected 'lower', 'upper', or 'step'"))
}
})?;
Ok(range)
}
Loading