-
Notifications
You must be signed in to change notification settings - Fork 127
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
Action message support #417
Conversation
This follows generally the same pattern as the service server. It required adding a typesupport function to the Action trait and pulling in some more rcl_action bindings.
This is incomplete, since the service types aren't yet being generated.
This results in the exact same file being produced for services, except for some whitespace changes. However, it enables actions to invoke the respective service template for its generation, similar to how the it works for services and their underlying messages.
C++ uses duck typing for this, knowing that for any `Action`, the type `Action::Impl::SendGoalService::Request` will always have a `goal_id` field of type `unique_identifier_msgs::msg::UUID` without having to prove this to the compiler. Rust's generics are more strict, requiring that this be proven using type bounds. The `Request` type is also action-specific as it contains a `goal` field containing the `Goal` message type of the action. We therefore cannot enforce that all `Request`s are a specific type in `rclrs`. This seems most easily represented using associated type bounds on the `SendGoalService` associated type within `ActionImpl`. To avoid introducing to `rosidl_runtime_rs` a circular dependency on message packages like `unique_identifier_msgs`, the `ExtractUuid` trait only operates on a byte array rather than a more nicely typed `UUID` message type. I'll likely revisit this as we introduce more similar bounds on the generated types.
Rather than having a bunch of standalone traits implementing various message functions like `ExtractUuid` and `SetAccepted`, with the trait bounds on each associated type in `ActionImpl`, we'll instead add these functions directly to the `ActionImpl` trait. This is simpler on both the rosidl_runtime_rs and the rclrs side.
Adds a trait method to create a feedback message given the goal ID and the user-facing feedback message type. Depending on how many times we do this, it may end up valuable to define a GoalUuid type in rosidl_runtime_rs itself. We wouldn't be able to utilize the `RCL_ACTION_UUID_SIZE` constant imported from `rcl_action`, but this is pretty much guaranteed to be 16 forever. Defining this method signature also required inverting the super-trait relationship between Action and ActionImpl. Now ActionImpl is the sub-trait as this gives it access to all of Action's associated types. Action doesn't need to care about anything from ActionImpl (hopefully).
These still don't build without errors, but it's close.
rclrs needs to be able to generically construct result responses, including the user-defined result field.
This adds methods to ActionImpl for creating and accessing action-specific message types. These are needed by the rclrs ActionClient to generically read and write RMW messages. Due to issues with qualified paths in certain places (rust-lang/rust#86935), the generator now refers directly to its service and message types rather than going through associated types of the various traits. This also makes the generated code a little easier to read, with the trait method signatures from rosidl_runtime_rs still enforcing type-safety.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be a good idea to create Rust packages on a test basis and check whether they generate warnings. I would also like to see the generated rust structures include a few derivable traits. I have attached a list. It all looks great in itself.
Common Derivable Rust Traits
In Rust, the derive attribute allows for automatic implementation of certain traits for structs and enums, simplifying the process of writing boilerplate code. Here are some of the most common derivable traits:
-
Clone: This trait allows for creating a duplicate of a value. When you derive
Clone
, it enables the use of theclone
method to create a copy of the instance. -
Copy: This trait provides "copy semantics" instead of "move semantics." Types that implement
Copy
can be duplicated simply by copying bits, which is useful for simple data types. -
Debug: This trait enables formatting a value using the
{:?}
formatter, which is particularly useful for debugging purposes. -
Default: The
Default
trait allows you to create a default value for a type. When derived, it implements thedefault
function, which calls the default function on each field of the type. -
PartialEq: This trait allows for equality comparisons between instances of a type. Deriving
PartialEq
enables the use of the==
and!=
operators. -
Eq: This trait is a marker trait that indicates that a type's equality comparison is reflexive, symmetric, and transitive. It is typically derived alongside
PartialEq
. -
PartialOrd: This trait allows for partial ordering of instances, enabling the use of comparison operators like
<
,>
,<=
, and>=
. -
Ord: This trait provides a total ordering for instances of a type. It is derived from
PartialOrd
and allows for the use of thesort
method. -
Hash: This trait allows for computing a hash from an instance, which is essential for types that will be used in hash-based collections like
HashMap
.
I think one of the difficulties of deriving traits for message types is that we have very little information about the individual messages before compile time. We cannot be sure that any of these traits are derivable before the generators create the code. Additionally, these messages are interacted with through the FFI boundary, which can potentially cause some weirdness at the best of times. I would be cautious about the automatic derivation of any traits unless we are 100% certain that we aren't going to have a problem introduced due to the FFI interactions. Admittedly this is a rare issue, but I feel it's best to be cautious when dealing with FFI. Is there a specific reason that you would like to see these traits automatically derived? |
I'm not so sure this is true. We know three important things:
So I believe we could trivially derive all the popular traits besides Copy automatically for all message definitions. |
I'm strongly in favor of providing the implementations for the sake of developer quality of life. For better or worse, ROS message data structures play a big role in the internal mechanics of most ROS nodes.
|
All that said, I might suggest we save traits for a follow up PR. |
It would definitely be nice if we can clean up that situation a bit. I don't love what I'm about to suggest, but here's a stab at having something a bit more pleasant:
Let me know if anyone has thoughts on that. I can take a stab at drafting it if there are no strong objections. |
In my opinion, this makes handling the structs feel more idiomatic. When I program other things with rust, I expect these standard traits from the imported structs. It also makes the handling of the structs simpler, as certain things like comparisons or the output of the structs in the standard output then already work. You get a better feel for what the struct is doing. It's also a bit awkward every time you have to implement it yourself. In many cases it works with the newtype pattern. In some cases you have to dig deeper into the material. It doesn't matter if it doesn't work. I thought it would be a nice idea. |
@Guelakais I assume we're only talking about the message types, since the top-level action type is an empty struct that's only useful as a generic type parameter. It's not useful to create an actual instance of this type, so I don't see a need for any trait derivations there. For the message types, I agree that it's useful to have certain common traits implemented on them. Regarding the list you gave:
So the traits that are trivially derivable are already done. In any case, I'd say this should be discussed in a separate issue since the message generation isn't actually touched in this PR. |
This at least makes the template easier to read, but also helps with the generated code. In general, the generated code could actually fit on one line for the function signatures, but it's not a big deal to split it across multiple.
An alternative we could consider would be this:
Without trait aliases, this might be a bit cumbersome to work with on the |
OH, I didn't know that. If I didn't know that - I should make a pull request to improve the documentation of the codegenerator. :) I didn't want to cause so much confusion. all in all, the integration of actions is great. A small note: The communication system of ros2 has strong dependencies on the DDS interface. Depending on which DDS is used here, this can lead to strange effects. How does the rust api handle this? |
This is a very interesting idea for how to break the circular dependency. One nitpick I might have is that if a trait has generic parameters, that implies that users should expect it to be possible for the same data structure to implement the same trait with different generic arguments. I think a slightly better arrangement might be to use associated types:
then if
Then
as a trait bound when needed. That being said, I don't see anything wrong with special-casing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for splitting this out as its own PR. I wound up spending most of my reviewing time by looking at #410 anyway because it's difficult to understand the traits without the context of how they will be used, but it's nice that we can move along the message generation and enable rosidl_generator_rs
to get onto the build farm.
I just left one inline comment about a fix that's needed, and I opened this PR to make a style refactoring recommendation. Besides those items, I think this is good to go.
On the topic of UUID and timestamps, after looking over the actual usage I think the current approach is fine, and it's not worth bringing in generic parameters or associated types. But I will point out that rclrs::GoalUuid
could be moved into rosidl_runtime_rs
and then publicly re-exported in rclrs
. Then it could be used in the API of ActionImpl
. I don't think this is important either way (the current approach is perfectly fine; end users won't touch that interface anyway), but I thought I'd point out that it's another option to consider.
This is user-facing and so should use the friendly message types.
Signed-off-by: Michael X. Grey <grey@openrobotics.org>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR looks good to me. I believe this is ready for a maintainer review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for picking up this work @nwn! We are closer than we've ever been to having action support thanks to your contributions.
I know this may be asking a lot for such complex message generation, but can you add some high level docs? It doesn't need to be in depth, that's a completely different task to fully document our message generation pipeline.
But, something that would help a new developer approach this code could go a long way.
|
||
fn get_type_support() -> *const std::os::raw::c_void { | ||
// SAFETY: No preconditions for this function. | ||
unsafe { rosidl_typesupport_c__get_action_type_support_handle__@(package_name)__@(subfolder)__@(type_name)() } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a little confusing to me. Looking at (what I believe to be) the impl for this typesupport function (link)
-
Why are we actually returning a void pointer here? This function is not marked as unsafe, but any handling of the return value may well need to use unsafe code. Could we not return a well formed type, such as
rosidl_service_type_support_t
? -
This function seems to initialize the request and response members, but I don't actually see where those members are. The
@(type_name)
struct appears empty. How is this working?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- You're right, we could have this return a more specific type than just
c_void
. However, doing so hasn't been necessary yet, and this is following the same pattern as the existingget_type_support
functions for messages and services. To make this happen, we would have to create a Rust binding for therosidl_{message,service,action}_type_support_t
structs, probably inrosidl_runtime_rs
. I would be inclined to leave this for a separate PR to avoid growing the scope of this one. - I'm not sure what you mean here. The
@(type_name)
struct is empty since it's not really meant to be instantiated. It would only be used as a genericimpl Action
argument and to access the specificGoal
,Feedback
, andResult
associated message types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
Sure, I'm fine leaving this to another PR. Just took a look at our usages (1) of this function historically and we do have
as
casts everywhere.as
casts can be problematic. So it would be nice to eventually not need this. However, maybe there is some other reason I'm missing as to why we returnvoid*
here. -
I was referring to the data that function actually mutates in its implementation. I was misunderstanding and assumed that we actually held a handle to that data, but apparently we do not. It is a global variable managed by I guess the rosidl_runtime(?) that will be generated for each service.
static rosidl_service_type_support_t @(function_prefix)__@(spec.srv_name)_service_type_support_handle = {
0,
&@(function_prefix)__@(spec.srv_name)_service_members,
get_service_typesupport_handle_function,
};
This is outside the scope of this PR though. Just calling attention to it because we've been bitten by global variables we directly interact with via FFI before, see #386
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Yes, you're right, it would be good to avoid those
as
casts. I think the only thing preventing that would be defining the appropriate binding type in therosidl_runtime_rs
package. I can make an attempt at that in a follow-up PR. - The typesupport structs are global statics that are managed by the various typesupport implementations. Some of them seem to handle it a bit more dubiously than others, so that's a good thing to watch out for.
goal: crate::@(subfolder)::rmw::@(type_name)@(ACTION_GOAL_SUFFIX), | ||
) -> crate::@(subfolder)::rmw::@(type_name)@(ACTION_GOAL_SERVICE_SUFFIX)@(SERVICE_REQUEST_MESSAGE_SUFFIX) { | ||
crate::@(subfolder)::rmw::@(type_name)@(ACTION_GOAL_SERVICE_SUFFIX)@(SERVICE_REQUEST_MESSAGE_SUFFIX) { | ||
goal_id: unique_identifier_msgs::msg::rmw::UUID { uuid: *goal_id }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we didn't want to have a dependency on unique_identifier_msgs
here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to avoid creating a dependency on unique_identifier_msgs
from the rosidl_runtime_rs
package, since the latter is a dependency of all message packages. This would cause a cyclic dependency.
However, the crates generated by the rosidl_generator_rs
do have dependencies on any message packages they use as fields. In this case, unique_identifier_msgs/msg/UUID
is a field of the underlying goal service request type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, I see. So the function signature of create_goal_request
can't reference unique_identifier_msgs
but the impl of that function actually can.
Yeah, we should explore some of the ideas discussed earlier to simplify this. Not needed for this PR though.
rosidl_runtime_rs/src/traits.rs
Outdated
type Feedback: Message; | ||
|
||
/// Get a pointer to the correct `rosidl_action_type_support_t` structure. | ||
fn get_type_support() -> *const std::os::raw::c_void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: This can be done in another PR because this is not the only place in ros2_rust 😅 but this std::os::raw::c_void
type is an alias to std::ffi::c_void which is the more appropriate type to use.
std::os::raw::c_void
only exists to maintain backwards compatibility with the Rust compiler <1.30 (until 1.1). Here is the RFC where this was introduced, RFC 2521.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I've updated all the uses in rosidl_runtime_rs
and rosidl_generator_rs
to use std::ffi::c_void
instead. I'll leave rclrs
as-is for this PR.
While these are aliases of each other, we might as well use the more appropriate std::ffi version, as requested by reviewers.
Some of the variables are present but no longer used. Others were not updated with the action changes.
This should help newcomers orient themselves around the rosidl_*_rs packages.
@maspe36 I've added a short doc page under |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The high level doc looks great! Thanks for all your hard work @nwn
goal: crate::@(subfolder)::rmw::@(type_name)@(ACTION_GOAL_SUFFIX), | ||
) -> crate::@(subfolder)::rmw::@(type_name)@(ACTION_GOAL_SERVICE_SUFFIX)@(SERVICE_REQUEST_MESSAGE_SUFFIX) { | ||
crate::@(subfolder)::rmw::@(type_name)@(ACTION_GOAL_SERVICE_SUFFIX)@(SERVICE_REQUEST_MESSAGE_SUFFIX) { | ||
goal_id: unique_identifier_msgs::msg::rmw::UUID { uuid: *goal_id }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, I see. So the function signature of create_goal_request
can't reference unique_identifier_msgs
but the impl of that function actually can.
Yeah, we should explore some of the ideas discussed earlier to simplify this. Not needed for this PR though.
This LGTM, we can address anything more in follow up PRs. Thanks @nwn! |
This implements action support in
rosidl_generator_rs
androsidl_runtime_rs
. It's a selective rebase of the original changes made in #295 and #410, applying only changes to the tworosidl
packages, without any modification torclrs
. The intention here is to merge these changes sooner so that we can split the message support packages into a separate repo and add them to the ROS buildfarm.The general approach follows the same pattern as the existing message and service generation. However, the additional message wrapping and introspection done on actions means that we need generic access to certain message internals. In
rclcpp
, this is done using duck typing on theAction
andAction::Impl
template parameters. However, Rust is stricter about this and requires trait bounds to ensure that generic type parameters can be accessed in a given way. As a result, a series of trait methods are defined inActionImpl
that enable a client library likerclrs
to generically create and access the various message types.Certain of these methods involve accessing timestamps and UUIDs in messages. To avoid adding a dependency in
rosidl_runtime_rs
onbuiltin_interfaces
andunique_identifier_msgs
, these are represented in a more primitive form as(i32, u32)
and[u8; 16]
in the signatures, respectively. This is rather ugly, so ideas are welcome.The existing
srv.rs.em
file is split into separate "idiomatic" and "RMW" implementations, similarly to how messages are already handled. This makes embedding the service template into theaction.rs.em
easier.