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
4 changes: 2 additions & 2 deletions docs/writing-your-first-rclrs-node.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Instead, you can store the node as a regular member. Let's add a struct that con

```rust
use std::sync::Arc;
use std_msgs::msg::String as StringMsg;
use rclrs::std_msgs::msg::String as StringMsg;

struct RepublisherNode {
node: Arc<rclrs::Node>,
Expand Down Expand Up @@ -111,7 +111,7 @@ So, to store the received data in the struct, the following things have to chang

```rust
use std::sync::{Arc, Mutex}; // (1)
use std_msgs::msg::String as StringMsg;
use rclrs::std_msgs::msg::String as StringMsg;

struct RepublisherNode {
node: Arc<rclrs::Node>,
Expand Down
8 changes: 4 additions & 4 deletions docs/writing_a_simple_publisher_and_subscriber.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ To construct a node, replace the code in your `main.rs` file with the following:
/// methods to publish a simple "Hello World" message on a loop in separate threads.
use rclrs::{create_node, Context, Node, Publisher, RclrsError, QOS_PROFILE_DEFAULT};
use std::{env, sync::Arc, thread, time::Duration};
use std_msgs::msg::String as StringMsg;
use rclrs::std_msgs::msg::String as StringMsg;
/// SimplePublisherNode struct contains node and publisher members.
/// Used to initialize a ROS 2 node and publisher, and publish messages.
struct SimplePublisherNode {
Expand Down Expand Up @@ -138,7 +138,7 @@ handling, iteration, threading, ROS 2 communication, and string message publishi
```rust
use rclrs::{create_node, Context, Node, Publisher, RclrsError, QOS_PROFILE_DEFAULT};
use std::{env, sync::Arc, thread, time::Duration};
use std_msgs::msg::String as StringMsg;
use rclrs::std_msgs::msg::String as StringMsg;
```
* `use std::{sync::Arc, time::Duration, iter, thread};`: Imports specific features from the standard library:
- `Arc` is for thread-safe shared ownership of data.
Expand All @@ -149,7 +149,7 @@ use std_msgs::msg::String as StringMsg;
- `RclrsError` for handling errors.
- `QOS_PROFILE_DEFAULT` for default Quality of Service settings.
- `Context, create_node, Node, Publisher` are for ROS 2 node creation and publishing.
* `use std_msgs::msg::String as StringMsg;`: Imports the `StringMsg` type for publishing string messages.
* `use rclrs::std_msgs::msg::String as StringMsg;`: Imports the `StringMsg` type for publishing string messages.

#### `SimplePublisherNode`
Next, this structure defines a `SimplePublisherNode` which holds references to a ROS 2 node and a publisher for string messages.
Expand Down Expand Up @@ -291,7 +291,7 @@ use std::{
thread,
time::Duration,
};
use std_msgs::msg::String as StringMsg;
use rclrs::std_msgs::msg::String as StringMsg;
pub struct SimpleSubscriptionNode {
node: Arc<Node>,
_subscriber: Arc<Subscription<StringMsg>>,
Expand Down
3 changes: 3 additions & 0 deletions rclrs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ tokio = { version = "1", features = ["rt", "time", "macros"] }
cfg-if = "1.0.0"
rustflags = "0.1"

# Used to read Cargo.toml dependencies when re-exporting generated crates
cargo_toml = "0.14"

[features]
default = []
dyn_msg = ["ament_rs", "libloading"]
Expand Down
104 changes: 103 additions & 1 deletion rclrs/build.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use std::{env, path::Path};
use std::{env, fs, path::Path};
use std::path::PathBuf;

use cargo_toml::Manifest;

const AMENT_PREFIX_PATH: &str = "AMENT_PREFIX_PATH";
const ROS_DISTRO: &str = "ROS_DISTRO";

Expand All @@ -13,6 +17,19 @@ fn get_env_var_or_abort(env_var: &'static str) -> String {
}
}

fn marked_reexport(cargo_toml: String) -> bool {
cargo_toml.contains("[package.metadata.rclrs]")
&& cargo_toml.contains("reexport = true")
}

fn star_deps_to_use(manifest: &Manifest) -> String {
manifest.dependencies
.iter()
.filter(|(_, version)| version.req() == "*")
.map(|(name, _)| format!("use crate::{name};\n"))
.collect::<String>()
}

fn main() {
println!(
"cargo:rustc-check-cfg=cfg(ros_distro, values(\"{}\"))",
Expand Down Expand Up @@ -54,6 +71,91 @@ fn main() {
println!("cargo:rustc-link-search=native={}", library_path.display());
}

// Re-export any generated interface crates that we find
let ament_prefix_paths = env!("AMENT_PREFIX_PATH", "AMENT_PREFIX_PATH environment variable not set - please source ROS 2 installation first.");
let export_crate_tomls = ament_prefix_paths
.split(':')
.map(PathBuf::from)
.flat_map(|base_path| {
// 1. Try to read share/ directory
fs::read_dir(base_path.join("share")).into_iter().flatten()
})
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().is_dir())
.flat_map(|package_dir| {
// 2. Try to read <package>/rust/ directory
fs::read_dir(package_dir.path().join("rust"))
.into_iter()
.flatten()
})
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| path.file_name() == Some(std::ffi::OsStr::new("Cargo.toml")))
.filter(|path| {
fs::read_to_string(path)
.map(marked_reexport)
.unwrap_or(false)
});

let content: String = export_crate_tomls
.filter_map(|path| path.parent().map(|p| p.to_path_buf()))
.map(|package_dir| {
let package = package_dir
.parent()
.unwrap()
.file_name()
.unwrap()
.to_str()
.unwrap();

// Find all dependencies for this crate that have a `*` version requirement.
// We will assume that these are other exported dependencies that need symbols
// exposed in their module.
let dependencies: String = Manifest::from_path(package_dir.clone().join("Cargo.toml"))
.iter()
.map(star_deps_to_use)
.collect();

let internal_mods: String = fs::read_dir(package_dir.join("src"))
.into_iter()
.flatten()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().is_file())
// Ignore lib.rs and any rmw.rs. lib.rs is only used if the crate is consumed
// independently, and rmw.rs files need their top level module
// (i.e. msg, srv, action) to exist to be re-exported.
.filter(|entry| {
let name = entry.file_name();
name != "lib.rs" && name != "rmw.rs"
})
// Wrap the inclusion of each file in a module matching the file stem
// so that the generated code can be imported like `rclrs::std_msgs::msgs::Bool`
.filter_map(|e| {
e.path()
.file_stem()
.and_then(|stem| stem.to_str())
.map(|stem| {
let idiomatic_path = e.path().display().to_string();
let sep = std::path::MAIN_SEPARATOR;
let rmw_path = idiomatic_path
.rsplit_once(std::path::MAIN_SEPARATOR)
.map(|(dir, _)| format!("{dir}{sep}{stem}{sep}rmw.rs"))
.unwrap_or_else(|| "rmw.rs".to_string());
format!("pub mod {stem} {{ {dependencies} include!(\"{idiomatic_path}\"); pub mod rmw {{ {dependencies} include!(\"{rmw_path}\");}} }}")
})
})
.collect();

format!("pub mod {package} {{ {internal_mods} }}")
})
.collect();

let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set ");
let dest_path = PathBuf::from(out_dir).join("interfaces.rs");

// TODO I would like to run rustfmt on this generated code, similar to how bindgen does it
fs::write(dest_path, content.clone()).expect("Failed to write interfaces.rs");
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interface.rs ends up looking like this

pub mod builtin_interfaces { ... };

pub mod std_msgs {
  pub mod msg {
    use crate::builtin_interfaces;
    // use crate::...; etc
    
    include!("path/to/generated/code");
    
    pub mod rmw {
      use crate::builtin_interfaces;
      // use crate::...; etc
    
      include!("path/to/generated/rmw/code");
    }
  }
}

Where all generated interface code is placed into a module based on the paths found in the AMENT_PREFIX_PATH share directories.


Interestingly enough, I also learned that the #[path = "..."] attribute for modules works with absolute paths. So that also could work, but I didn't have proper intellisense with that approach so I stuck with include!()


println!("cargo:rustc-link-lib=dylib=rcl");
println!("cargo:rustc-link-lib=dylib=rcl_action");
println!("cargo:rustc-link-lib=dylib=rcl_yaml_param_parser");
Expand Down
4 changes: 2 additions & 2 deletions rclrs/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub use action_goal_receiver::*;
pub(crate) mod action_server;
pub use action_server::*;

use crate::{log_error, rcl_bindings::*, vendor::builtin_interfaces::msg::Time, DropGuard};
use crate::{log_error, rcl_bindings::*, builtin_interfaces::msg::Time, DropGuard};
use std::fmt;

#[cfg(feature = "serde")]
Expand Down Expand Up @@ -256,7 +256,7 @@ fn empty_goal_status_array() -> DropGuard<rcl_action_goal_status_array_t> {
#[cfg(test)]
mod tests {
use crate::{
vendor::example_interfaces::action::{
example_interfaces::action::{
Fibonacci, Fibonacci_Feedback, Fibonacci_Goal, Fibonacci_Result,
},
*,
Expand Down
2 changes: 1 addition & 1 deletion rclrs/src/action/action_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::empty_goal_status_array;
use crate::{
log_warn,
rcl_bindings::*,
vendor::{action_msgs::srv::CancelGoal_Response, builtin_interfaces::msg::Time},
{action_msgs::srv::CancelGoal_Response, builtin_interfaces::msg::Time},
CancelResponse, CancelResponseCode, DropGuard, GoalStatus, GoalStatusCode, GoalUuid,
MultiCancelResponse, Node, NodeHandle, QoSProfile, RclPrimitive, RclPrimitiveHandle,
RclPrimitiveKind, RclrsError, ReadyKind, TakeFailedAsNone, ToResult, Waitable,
Expand Down
4 changes: 2 additions & 2 deletions rclrs/src/action/action_client/goal_client.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
vendor::builtin_interfaces::msg::Time, CancellationClient, FeedbackClient, GoalStatus,
builtin_interfaces::msg::Time, CancellationClient, FeedbackClient, GoalStatus,
GoalStatusCode, ResultClient, StatusWatcher,
};
use rosidl_runtime_rs::Action;
Expand Down Expand Up @@ -96,7 +96,7 @@ impl<A: Action> GoalClient<A> {
///
/// ```
/// use rclrs::*;
/// use crate::rclrs::vendor::example_interfaces::action::Fibonacci;
/// use crate::rclrs::example_interfaces::action::Fibonacci;
/// use futures::StreamExt;
///
/// async fn process_goal_client_stream(
Expand Down
2 changes: 1 addition & 1 deletion rclrs/src/action/action_server.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::empty_goal_status_array;
use crate::{
action::GoalUuid, error::ToResult, rcl_bindings::*,
vendor::action_msgs::srv::CancelGoal_Response, ActionGoalReceiver, CancelResponseCode,
action_msgs::srv::CancelGoal_Response, ActionGoalReceiver, CancelResponseCode,
DropGuard, GoalStatusCode, Node, NodeHandle, QoSProfile, RclPrimitive, RclPrimitiveHandle,
RclPrimitiveKind, RclrsError, ReadyKind, TakeFailedAsNone, Waitable, WaitableLifecycle,
ENTITY_LIFECYCLE_MUTEX,
Expand Down
2 changes: 1 addition & 1 deletion rclrs/src/action/action_server/cancellation_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::ActionServerHandle;
use crate::{
log_error,
rcl_bindings::*,
vendor::{
{
action_msgs::{msg::GoalInfo, srv::CancelGoal_Response},
unique_identifier_msgs::msg::UUID,
},
Expand Down
8 changes: 4 additions & 4 deletions rclrs/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ where
/// signatures and which returns a `()` (a.k.a. nothing).
/// ```
/// # use rclrs::*;
/// # use crate::rclrs::vendor::test_msgs;
/// # use crate::rclrs::test_msgs;
/// # let node = Context::default()
/// # .create_basic_executor()
/// # .create_node("test_node")?;
Expand Down Expand Up @@ -187,7 +187,7 @@ where
///
/// ```
/// # use rclrs::*;
/// # use crate::rclrs::vendor::test_msgs;
/// # use crate::rclrs::test_msgs;
/// # use std::future::Future;
/// # let node = Context::default()
/// # .create_basic_executor()
Expand Down Expand Up @@ -216,7 +216,7 @@ where
///
/// ```
/// # use rclrs::*;
/// # use crate::rclrs::vendor::test_msgs;
/// # use crate::rclrs::test_msgs;
/// # let node = Context::default()
/// # .create_basic_executor()
/// # .create_node("test_node")?;
Expand Down Expand Up @@ -568,7 +568,7 @@ unsafe impl Send for rcl_client_t {}
#[cfg(test)]
mod tests {
use super::*;
use crate::{test_helpers::*, vendor::test_msgs};
use crate::{test_helpers::*, test_msgs};

#[test]
fn traits() {
Expand Down
11 changes: 6 additions & 5 deletions rclrs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
//!
//! ```no_run
//! use rclrs::*;
//! # use crate::rclrs::vendor::example_interfaces;
//! # use crate::rclrs::example_interfaces;
//!
//! let context = Context::default_from_env()?;
//! let mut executor = context.create_basic_executor();
Expand All @@ -59,7 +59,7 @@
//! # let context = Context::default_from_env()?;
//! # let mut executor = context.create_basic_executor();
//! # let node = executor.create_node("example_node")?;
//! # use crate::rclrs::vendor::example_interfaces;
//! # use crate::rclrs::example_interfaces;
//! #
//! // This worker will manage the data for us.
//! // The worker's data is called its payload.
Expand Down Expand Up @@ -99,7 +99,7 @@
//! The following is a simple example of using a mandatory parameter:
//! ```no_run
//! use rclrs::*;
//! # use crate::rclrs::vendor::example_interfaces;
//! # use crate::rclrs::example_interfaces;
//! use std::sync::Arc;
//!
//! let mut executor = Context::default_from_env()?.create_basic_executor();
Expand Down Expand Up @@ -129,7 +129,7 @@
//!
//! ```no_run
//! use rclrs::*;
//! # use crate::rclrs::vendor::example_interfaces;
//! # use crate::rclrs::example_interfaces;
//! use std::time::Duration;
//!
//! let mut executor = Context::default_from_env()?.create_basic_executor();
Expand Down Expand Up @@ -196,7 +196,6 @@ mod subscription;
mod time;
mod time_source;
mod timer;
pub mod vendor;
mod wait_set;
mod worker;

Expand All @@ -205,6 +204,8 @@ mod test_helpers;

mod rcl_bindings;

include!(concat!(env!("OUT_DIR"), "/interfaces.rs"));
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generates a lot of warnings right now. Will need to look at how to hygienically do that (or move to another crate and suppress there)


#[cfg(feature = "dyn_msg")]
pub mod dynamic_message;

Expand Down
Loading
Loading