Skip to content

Add ROS2 timers #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft

Add ROS2 timers #4

wants to merge 19 commits into from

Conversation

imdexsam
Copy link
Collaborator

This PR adds ROS2 timers and enables them to be triggered by waitsets.

Copy link

@imdex-matthew imdex-matthew left a comment

Choose a reason for hiding this comment

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

Nice work on this it's looking real clean. There wasn't much to comment on, but I've left some thoughts and questions.

// Therefore, this type can be safely sent to another thread.
unsafe impl Send for rcl_timer_t {}

/// Manage the lifecycle of an `rcl_timer_t`, including managing its dependencies

Choose a reason for hiding this comment

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

Suggested change
/// Manage the lifecycle of an `rcl_timer_t`, including managing its dependencies
/// Manage the lifecycle of an `rcl_timer_t`, and its dependencies

Comment on lines 92 to 99
pub fn new<F>(
context: &Context,
clock: Clock,
period: Duration,
callback: F,
) -> Result<Self, RclrsError>
where
F: FnMut(&mut Timer) + 'static + Send + Sync,

Choose a reason for hiding this comment

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

Could we get away with a lifetime guarantee something like the suggested? What is the benefit of a 'static lifetime here?

Suggested change
pub fn new<F>(
context: &Context,
clock: Clock,
period: Duration,
callback: F,
) -> Result<Self, RclrsError>
where
F: FnMut(&mut Timer) + 'static + Send + Sync,
pub fn new<'a, F>(
context: &'a Context,
clock: Clock,
period: Duration,
callback: F,
) -> Result<Self, RclrsError>
where
F: FnMut(&mut Timer) + 'a+ Send + Sync,

Copy link
Collaborator Author

@imdexsam imdexsam Nov 1, 2024

Choose a reason for hiding this comment

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

This one often gets me. Keep in mind the 'static trait bound is subtly different from the 'static reference lifetime (as per rust by example).

By my understanding the practical implication of a 'static trait bound (as seen here) is that Timer needs to own the callback, since that fulfills the requirement that "the receiver can hold on to the type for as long as they want and it will never become invalid until they drop it". This includes owning anything captured by a closure being used as the callback.

If we wanted to switch to lifetimes it would probably need to be more like:

    pub fn new<'a, F>(
        context: &Context,
        clock: Clock,
        period: Duration,
        callback: F,
    ) -> Result<Timer<'a>, RclrsError>
    where
        F: FnMut(&mut Timer) + 'a + Send + Sync,

Since the callback needs to live at least as long as the Timer class, so they're what need to share the lifetime (the &Context just contains an Arc that can be cloned).

This works and does mean you can pass in a &mut to closures used for the callback, but there's a trade-off in that we can no longer store the Timer class inside an Arc.

The trade-off comes from Rusts subtyping and variance system. I've only got a basic understanding of it, but it seems Arc<T> creates a new lifetime that is independent of the object it's storing, so Arc<T> itself needs a 'static trait bound on T (as Arc is invariant over T), so the compiler won't allow storing a Timer<'a> that isn't 'static.

With that in mind, the benefits of explicitly tracking the lifetime doesn't seem to outweigh the costs. Closures can easily "own" any data they reference just by using Arc<Mutex> types, working around an inability to use &mut, but there doesn't seem to be a neat workaround to not being able to put Timer classes inside Arcs.

All of that is to say I think we're best served by sticking with 'static here.

Choose a reason for hiding this comment

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

That makes good sense, thanks for digging into it and providing a nice succinct answer!

let new_period = Duration::from_millis(100);

// Calling set_period will trigger the debug_assert check on the rcl return value.
timer.set_period(new_period.clone());

Choose a reason for hiding this comment

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

As per: https://github.com/geoff-imdex/ros2_rust/actions/runs/11564633206/job/32190192780#step:11:395

Suggested change
timer.set_period(new_period.clone());
timer.set_period(new_period);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, fixed.

// SAFETY:
// * The timer is initialized, which is guaranteed by the constructor.
// * The elapsed_time pointer is allocated on the stack and is valid for the duration of this function.
let ret = unsafe { rcl_timer_get_time_since_last_call(&mut *timer, &mut elapsed_time) };

Choose a reason for hiding this comment

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

As per: https://github.com/geoff-imdex/ros2_rust/actions/runs/11564633206/job/32190192780#step:11:395

Suggested change
let ret = unsafe { rcl_timer_get_time_since_last_call(&mut *timer, &mut elapsed_time) };
let ret = unsafe { rcl_timer_get_time_since_last_call(& *timer, &mut elapsed_time) };

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, fixed.

// * The timer is initialized, which is guaranteed by the constructor.
// * The remaining_time pointer is allocated on the stack and is valid for the duration of this function.
unsafe {
rcl_timer_get_time_until_next_call(&mut *timer, &mut remaining_time).ok()?;

Choose a reason for hiding this comment

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

As per: https://github.com/geoff-imdex/ros2_rust/actions/runs/11564633206/job/32190192780#step:11:395

Suggested change
rcl_timer_get_time_until_next_call(&mut *timer, &mut remaining_time).ok()?;
rcl_timer_get_time_until_next_call(& *timer, &mut remaining_time).ok()?;

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, fixed.

// SAFETY:
// * The timer is initialized, which is guaranteed by the constructor.
// * The is_ready pointer is allocated on the stack and is valid for the duration of this function.
let ret = unsafe { rcl_timer_is_ready(&mut *timer, &mut is_ready) };

Choose a reason for hiding this comment

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

As Per: https://github.com/geoff-imdex/ros2_rust/actions/runs/11564633206/job/32190192780#step:11:395

Suggested change
let ret = unsafe { rcl_timer_is_ready(&mut *timer, &mut is_ready) };
let ret = unsafe { rcl_timer_is_ready(& *timer, &mut is_ready) };

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, fixed.

/// [5]: crate::Timer::execute
/// [6]: crate::WaitSet
pub struct Timer {
callback: Arc<Mutex<dyn FnMut(&mut Timer) + Send + Sync>>,

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, fixed.

@mxgrey
Copy link

mxgrey commented Nov 2, 2024

Note that the upstream rclrs repo has an open PR that introduces async executors, and it will make some significant changes to how wait sets work: ros2-rust#421

Be ready for some merge conflicts down the road. I do have a plan to introduce timers on top of the async execution PR, but I'm currently waiting until that PR gets merged first. If you have a need for timers and are interested in jumping forward to the async execution implementation (at the risk of some API instability), then I'd be happy to make a branch that includes timers.

@imdexsam
Copy link
Collaborator Author

imdexsam commented Nov 4, 2024

@mxgrey I appreciate the heads up, that's very useful to know. It looks like you've done a lot of great work on the async executor, that's a nice improvement.

We'll go ahead with the sync version in the short term, as that unblocks our immediate need for timers, but the less time we spend diverged from upstream the better.

If you're willing to do an async timer implementation that would be amazing. You're more than welcome to copy anything here, I would assume the Timer struct will be largely transferrable. Once the async execution is merged into main I might be able to help port timers across too.

Copy link
Owner

@geoff-imdex geoff-imdex left a comment

Choose a reason for hiding this comment

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

Looks really good. Only one extremely pedantic comment/question - the rcl_bindings:: in the timer.rs explicitly includes all the used functions (nice), however this a long list and many other places seem to use the rcl_bindings::* approach unless they're pulling in a trivially small amount of functions. Not sure it's worth changing, but I had to mention

@imdexsam imdexsam marked this pull request as draft November 5, 2024 05:35
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.

4 participants