Skip to content
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

Message generation refactoring #80

Merged
merged 8 commits into from
Mar 18, 2022
Merged

Message generation refactoring #80

merged 8 commits into from
Mar 18, 2022

Conversation

nnmm
Copy link
Contributor

@nnmm nnmm commented Mar 5, 2022

Previously, only messages consisting of basic types and strings were supported. Now, all message types will work, including those that have fields of nested types, bounded types, or arrays.

Changes

  • The "rsext" library is deleted
  • Unused messages in "rosidl_generator_rs" are deleted
  • There is a new package, "rosidl_runtime_rs", see below
  • The RMW-compatible messages from C, which do not require an extra conversion step, are exposed in addition to the "idiomatic" messages
  • Publisher and subscription are changed to work with both idiomatic and rmw types, through the unifying Message trait

On rosidl_runtime_rs: This package is the successor of rclrs_msg_utilities package, but doesn't have much in common with it anymore.
It provides common types and functionality for messages. The String and Sequence types and their variants in that package essentially wrap C types from the rosidl_runtime_c package and C messages generated by the "rosidl_generator_c" package.
A number of functions and traits are implemented on these types, so that they feel as ergonomic as possible, for instance, a seq! macro for creating a sequence. There is also some documentation and doctests.

The memory for the (non-pretty) message types is managed by the C allocator.

Not yet implemented

  • long double
  • constants
  • Services/clients
  • @verbatim comments
  • ndarray for sequences/arrays of numeric types
  • implementing Eq, Ord and Hash when a message contains no floats

How to review

I know that this is a really large PR. If desired, I can split it up for reviewing. Here are some steps I suggest to get familiar with the changes:

  • Run the message_demo and read the code
  • Generate cargo docs for the rosidl_runtime_rs crate and read them
  • Dive into msg.rs.em and __init__.py files

@nnmm
Copy link
Contributor Author

nnmm commented Mar 5, 2022

There is currently a limitaiton where I'm using copy functions (ros2/rosidl#650) that are not yet in Foxy and Galactic. Jacob wanted to take a look at backporting those. However, I could also try to do a polyfill for them if we don't want to wait that long.

@jhdcs
Copy link
Collaborator

jhdcs commented Mar 7, 2022

There is currently a limitaiton where I'm using copy functions (ros2/rosidl#650) that are not yet in Foxy and Galactic. Jacob wanted to take a look at backporting those. However, I could also try to do a polyfill for them if we don't want to wait that long.

What would be involved in the creation of a "polyfill"? I'm not familiar with the process.

@nnmm
Copy link
Contributor Author

nnmm commented Mar 7, 2022

There is currently a limitaiton where I'm using copy functions (ros2/rosidl#650) that are not yet in Foxy and Galactic. Jacob wanted to take a look at backporting those. However, I could also try to do a polyfill for them if we don't want to wait that long.

What would be involved in the creation of a "polyfill"? I'm not familiar with the process.

I'd try implementing the functions from that PR in Rust, using rcutils_get_default_allocator(). So rosidl_runtime_rs would need to link against rcutils for the time being.

@esteve
Copy link
Collaborator

esteve commented Mar 7, 2022

@nnmm awesome work, thanks! CI seems to be failing because of a missing message, the file that's causing the problem is NestedType.msg, any reference to it should be removed if we're no longer using it.

@nnmm
Copy link
Contributor Author

nnmm commented Mar 7, 2022

@nnmm awesome work, thanks! CI seems to be failing because of a missing message, the file that's causing the problem is NestedType.msg, any reference to it should be removed if we're no longer using it.

Oh, forgot to add it, fixed now. CI will still fail because of the issue mentioned above (the copy functions being Rolling-only for now). I still opened this PR for review to parallelize reviewing + work on that issue.

@jhdcs
Copy link
Collaborator

jhdcs commented Mar 7, 2022

There is currently a limitaiton where I'm using copy functions (ros2/rosidl#650) that are not yet in Foxy and Galactic. Jacob wanted to take a look at backporting those. However, I could also try to do a polyfill for them if we don't want to wait that long.

What would be involved in the creation of a "polyfill"? I'm not familiar with the process.

I'd try implementing the functions from that PR in Rust, using rcutils_get_default_allocator(). So rosidl_runtime_rs would need to link against rcutils for the time being.

I think implementing the functions in Rust for now might be good, provided the work isn't too difficult. That way, if I get pulled away to work on other projects you won't be stuck waiting on me for this to get merged/working.

@@ -96,7 +97,7 @@ impl Node {
qos: QoSProfile,
) -> Result<Publisher<T>, RclReturnCode>
where
T: MessageDefinition<T>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why was the generic type parameter dropped?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just didn't see any reason why it should be there.

@@ -0,0 +1,30 @@
cmake_minimum_required(VERSION 3.5)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of adding rclrs_example_msgs, I'd prefer using the example_interfaces or test_interface_files packages.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test_interface_files looks good, thanks for the tip.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need ros2/test_interface_files#18 to make that work, I believe

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd propose to merge the version with the rclrs_example_msgs if the PR I linked isn't approved by the time the rest of the review is finished, and then later remove it once it is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems like bringing in test_interface_files is more complicated than we thought. Would you be fine with leaving in rclrs_example_msgs for now?

@esteve
Copy link
Collaborator

esteve commented Mar 7, 2022

@nnmm I just did a first pass. This is really awesome, you truly outdid yourself dude! I'll do a more extensive review later, but this looks great.

rclrs_examples/src/minimal_publisher.rs Outdated Show resolved Hide resolved
Copy link
Collaborator

@jhdcs jhdcs left a comment

Choose a reason for hiding this comment

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

Mostly a few comments/clarifications that I'd like to know about. Looks excellent, though!

rclrs_example_msgs/msg/VariousTypes.msg Show resolved Hide resolved
rosidl_generator_rs/resource/msg.rs.em Show resolved Hide resolved
rosidl_generator_rs/resource/msg.rs.em Show resolved Hide resolved
rosidl_runtime_rs/build.rs Outdated Show resolved Hide resolved
// If there is no more capacity for the next element, resize to the
// next power of two.
//
// A pedantic implementation would check for usize overflow here, but
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we be pedantic, though? If ros2_rust is being used on safety-critical systems, they may want this sort of check. Or is there a better value to test against? What would the performance impact be?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, good question. I'm not worried about performance impact, but about code complexity/overengineering.

I think ROS 2 messages will never reach this limit – certainly not on 64-bit machines, and I don't know if anyone is building ROS 2 for 32-bit systems. There is also a 1 GB message size limit, so even if we have a 32-bit system, there won't be any sequences with more than usize/2 elements. Besides, there are probably plenty of unchecked additions in rcl and rmw that would cause trouble too.

So I propose to not be pedantic about usize overflow, but to just leave comments like this one when such an issue is encountered. If anyone ever really wants to use ros2_rust in a safety-critical context, they'll need to go through the code and refactor a whole lot of stuff anyway, and then at least there's a comment to point to potential issues.

rosidl_runtime_rs/src/traits.rs Show resolved Hide resolved
@nnmm nnmm force-pushed the no_c_glue branch 2 times, most recently from 54e662c to f26454c Compare March 8, 2022 10:58
jhdcs
jhdcs previously approved these changes Mar 8, 2022
Copy link
Collaborator

@jhdcs jhdcs left a comment

Choose a reason for hiding this comment

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

Just another comment, mostly a stylistic thing. Otherwise, this looks good to me!

Thank you very much for your work!

impl Clone for $string {
fn clone(&self) -> Self {
let mut msg = Self::default();
// SAFETY: This is doing the same thing as rosidl_runtime_c__String__copy.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure this comment addresses why this is safe. I feel like these safety comments should be written so that someone won't need to dive into an external C library to verify, unless they really wanted to.

Something like: "This is safe because neither self.data nor self.size are mutated, and self.data is guaranteed to be pointing to valid memory by this point, and self.size is guaranteed to be correct"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I rephrased it to be a little clearer. The safety argument is basically: The rosidl_runtime_c__String__copy function is implemented exactly the same way as this Clone instance: https://github.com/ros2/rosidl/blob/caef9b8f820dfcb16e6a2df9118c5669cc816642/rosidl_runtime_c/src/string_functions.c#L135-L143

We may assume that the libraries we're binding to are correct, so when we do the same thing as that library, it's safe.

@esteve
Copy link
Collaborator

esteve commented Mar 8, 2022

@nnmm given RFC344 is there a need for BoundedSequence and BoundedString? Checking for length is done in setters in other ROS2 bindings instead of using a separate structure.

@nnmm
Copy link
Contributor Author

nnmm commented Mar 8, 2022

@nnmm given RFC344 is there a need for BoundedSequence and BoundedString? Checking for length is done in setters in other ROS2 bindings instead of using a separate structure.

I'd say that direct access to a field is the most flexible and ergonomic option, and only when that for some reason is not allowed (e.g. because it would allow violating some invariant) are getters and setters needed.

This PR also has TryFrom<&str> for BoundedString and I will also add TryFrom<&[T]> for bounded sequences. With that, the pattern of building up a string/sequence outside and checking the length when it's inserted into the message is also covered.

@nnmm
Copy link
Contributor Author

nnmm commented Mar 8, 2022

@esteve Do you know how to turn off these cpplint_rosidl_generated_rs, cppcheck_rosidl_generated_rs, uncrustify_rosidl_generated_rs tests in CI?

@@ -94,17 +95,16 @@ where
})
}

pub fn publish(&self, message: &T) -> Result<(), RclReturnCode> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Re-add reference to message here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, in a sense.

Copy link
Collaborator

Choose a reason for hiding this comment

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

What do you mean by "in a sense", are there any caveats?

Copy link
Contributor Author

@nnmm nnmm Mar 11, 2022

Choose a reason for hiding this comment

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

It's not message: &T now, it's message: impl MessageCow, which accepts T and &T (aka theConvert trait from the Matrix chat).

Suggestions for a better name welcome. In case you dislike this design, we could also split it up into two functions, publish_owned() and publish_ref().

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't get it, you seemed to be very keen on not having a separate create_subscription function for RMW-compatible types, but publish_owned and publish_ref are ok. Could you elaborate?

Let's keep it as is now, though, and we could refine the APIs later if needed, though it'll be more painful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure. The distinction between publish_owned() and publish_ref() is of course orthogonal to the question of which message type is published. But you're right that both are a "have two functions for doing the thing vs. having one more general function" question.

In this case here, I'm more on the fence between splitting vs unifying, since the more general function comes at the cost of a more complicated signature: The MessageCow trait. In the case of the extra create_subscription function, the create_subscription function would look the same whether or not we also have a create_rmw_subscription, so there's no extra complexity in the unified version. Being simpler is not the only argument in favor of one subscription function, but I'll keep that discussion in the other thread.

README.md Show resolved Hide resolved
@@ -109,7 +110,7 @@ impl Node {
callback: F,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Given that the RmwMessage API is specific to ros2-rust, I prefer if there are two separate functions here. One for the expected normal ROS2 API (i.e. idiomatic), and another (e.g. called create_subscription_rmw_type) for RmwMessage. That way it's more explicit and the latter can be marked as an optimization for Rust.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why would we have a redundant function like this? The current Subscription API can receive Messages, and both idiomatic and RMW-compatible messages are instances of that. Adding a second API that only supports the latter type is redundant and confuses only, imo. Uniformity is nice.

Copy link
Collaborator

@esteve esteve Mar 11, 2022

Choose a reason for hiding this comment

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

Precisely for uniformity with the other bindings, no other bindings accept these so-call rmw types, all of them support idiomatic types only. Adding a second API clears that confusion by explicitly signaling that it's specific to ros2-rust. Rust, very much like Python, and unlike C++, encourages explicitness vs implicitness. Explicitness is nice too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't want the more efficient message type to become a second-class citizen by only being accessible through less-than-canonical APIs. There shouldn't be any confusion with the two message types, since (1) I tried to document it really well and (2) even if someone skips the documentation, they will not be confused, they simply won't know that there's also the RMW-compatible message type. For such a user, the API will arguably appear more uniform with other languages than if there was an create_subscription_rmw_type function they need to look up.

Besides, there is also lots of stuff that is specific to rclcpp, and none of it is marked as such (topic statistics, generic pub/sub, intraprocess, etc.).

Copy link
Collaborator

Choose a reason for hiding this comment

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

through less-than-canonical APIs

The RMW-compatible message API is already non-canonical, because it does not have any counterpart in the other bindings. APIs are not only about names, but also semantics, the arguments they accept and the values they return. Having a distinctly different name emphasizes that it behaves differently from the other bindings, and that users should expect differences.

Besides, there is also lots of stuff that is specific to rclcpp, and none of it is marked as such (topic statistics, generic pub/sub, intraprocess, etc.).

Because the goal is that eventually that functionality might be incorporated into other bindings. For example, rclc could potentially get support for intraprocess.

Anyway, I rather not waste more time on this, but it's important to get APIs right early on, because changing them later is far more difficult

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As for counterparts in other bindings, here's my perspective for posterity:

  • Python is slow anyway, and usability of a raw C message (= our "RMW-compatible" message) would be absymal from a Python perspective, so of course the messages in that language are an idiomatic version that are converted to/from the C message.
  • C and C++ have the privilege of being natively RMW-compatible, so they can be both idiomatic and maximally efficient. They are the counterpart to the RMW-compatible and the idiomatic messages.
  • I assume other language bindings also use their own idiomatic message types that are converted to/from C messages. However, I believe none of the other languages are quite as performance-focused as Rust/C/C++.

Hence, Rust is kind of unique, in that its users expect maximum performance but it can't achieve that (yet) by serializing/deserializing its standard library types to/from CDR directly. So you could argue that it would be canonical to use RMW-compatible messages because that's what the other performance-focused language bindings do. Or you could argue that it would be canonical to use idiomatic messages because that's what all language bindings do.

I think the solution in this PR, to offer both, isn't too bad. And I placed the idiomatic messages in the <pkg>::msg namespace and the RMW-compatible ones in <pkg>::msg::rmw, because they can't be both in the same namespace, but I believe we shouldn't hide the RMW-compatible messages more than that if it's not technically necessary.

I don't think the time spent discussing on this is wasted. Hope I'm not too stubborn! If you insist, I'll add the separate function. Or I can also poll the ROS + Rust people I know to get more input. But since you approved, I take it that it's okay to merge as it is now and potentially discuss it again later.

Copy link

Choose a reason for hiding this comment

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

  • I assume other language bindings also use their own idiomatic message types that are converted to/from C messages. However, I believe none of the other languages are quite as performance-focused as Rust/C/C++.

Ada reuses the C data layout in memory, so there is no overhead involved. The price is somewhat un-idiomatic use when sequences are involved that I hope to alleviate at some point in the future.

rclrs_examples/src/minimal_publisher.rs Outdated Show resolved Hide resolved
rclrs_example_msgs/msg/VariousTypes.msg Show resolved Hide resolved
@esteve
Copy link
Collaborator

esteve commented Mar 10, 2022

@esteve Do you know how to turn off these cpplint_rosidl_generated_rs, cppcheck_rosidl_generated_rs, uncrustify_rosidl_generated_rs tests in CI?

I'll look into it, neither of those tests should run on Rust files and there are no more C files to be checked.

@nnmm
Copy link
Contributor Author

nnmm commented Mar 10, 2022

Thanks for fixing the linter @esteve. Now this PR is ready to merge from my side: I added the workaround for the copy_sequence functions in Foxy and Galactic until the backports are shipped, and changed the publishing + conversion logic to allow publishing of both borrowed and owned messages, for maximum efficiency.

@esteve
Copy link
Collaborator

esteve commented Mar 11, 2022

Thanks for fixing the linter @esteve. Now this PR is ready to merge from my side: I added the workaround for the copy_sequence functions in Foxy and Galactic until the backports are shipped, and changed the publishing + conversion logic to allow publishing of both borrowed and owned messages, for maximum efficiency.

Thanks, there is still some unanswered feedback, could you address it? CI is not passing either, do you know why?

@nnmm nnmm force-pushed the no_c_glue branch 4 times, most recently from 4c0f6ae to 3aecf1f Compare March 13, 2022 15:39
esteve
esteve previously approved these changes Mar 17, 2022
@esteve
Copy link
Collaborator

esteve commented Mar 17, 2022

@nnmm awesome job! I've approved these changes, but there are conflicts with main because of #81 I think removing rclrs_msg_utilities should suffice.

nnmm and others added 7 commits March 18, 2022 10:54
Previously, only messages consisting of basic types and strings were supported. Now, all message types will work, including those that have fields of nested types, bounded types, or arrays.

Changes:
- The "rsext" library is deleted
- Unused messages in "rosidl_generator_rs" are deleted
- There is a new package, "rosidl_runtime_rs", see below
- The RMW-compatible messages from C, which do not require an extra conversion step, are exposed in addition to the "idiomatic" messages
- Publisher and subscription are changed to work with both idiomatic and rmw types, through the unifying `Message` trait

On `rosidl_runtime_rs`: This package is the successor of `rclrs_msg_utilities` package, but doesn't have much in common with it anymore.
It provides common types and functionality for messages. The `String` and `Sequence` types and their variants in that package essentially wrap C types from the `rosidl_runtime_c` package and C messages generated by the "rosidl_generator_c" package.
A number of functions and traits are implemented on these types, so that they feel as ergonomic as possible, for instance, a `seq!` macro for creating a sequence. There is also some documentation and doctests.

The memory for the (non-pretty) message types is managed by the C allocator.

Not yet implemented:
- long double
- constants
- Services/clients
- @verbatim comments
- ndarray for sequences/arrays of numeric types
- implementing `Eq`, `Ord` and `Hash` when a message contains no floats
This requires a cfg for conditionally compiling the code depending on the ROS distro.
I'm setting that cfg in the build script instead of colcon-ros-cargo so that building with cargo doesn't become too annoying.
@nnmm nnmm merged commit df971f1 into master Mar 18, 2022
@nnmm
Copy link
Contributor Author

nnmm commented Mar 18, 2022

Huge thanks for reviewing @esteve and also @jhdcs! 🎉

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