-
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
Changes from all commits
110d965
047c8f1
556a606
dfdcbd3
67c3b8b
b197155
13474d1
3e70087
17cd980
fb9b0e4
5f3373f
562132b
dc90b21
1ed2981
568bb7c
23e9c94
6d7021a
7a39794
c5bd258
2748b5f
507a7af
7d30fb1
3c1663f
0493705
8bedfe0
5dece63
db575ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# `ros2_rust` Message Generation | ||
|
||
The `ros2_rust` project strives to maintain consistency with the upstream ROS 2 message generation | ||
system. To this end, it provides two main packages: `rosidl_generator_rs` and `rosidl_runtime_rs`. | ||
These packages provide the infrastructure required for defining and using ROS 2 messages, services, | ||
and actions in Rust. | ||
|
||
At a high level, the `rosidl_generator_rs` package handles generation of interface crates from the | ||
`.msg`, `.srv`, and `.action` files defined by a user. The `rosidl_runtime_rs` package provides | ||
common functionality shared across message packages, such as support types and traits. Each of these | ||
packages is described in more detail below. | ||
|
||
## `rosidl_generator_rs` | ||
|
||
`rosidl_generator_rs` follows a very similar pattern to the other message generation packages for | ||
ROS 2. To tie into this pipeline, it registers itself as a `"rosidl_generate_idl_interfaces"` | ||
extension with `ament`. Doing so ensures that message packages calling `rosidl_generate_interfaces` | ||
will invoke the Rust language generator in addition to any others. This is accomplished using the | ||
various CMake scripts under the `cmake` directory. When this happens, the input interface files will | ||
be converted into IDL files which, along with additional metadata, are fed into the `generate_rs` | ||
function of `rosidl_generator_rs/__init__.py`. | ||
|
||
From here, the IDL files are parsed into an internal representation using the upstream | ||
[`rosidl_parser`](https://github.com/ros2/rosidl/tree/rolling/rosidl_parser) package. This abstract | ||
representation is then used to instantiate the various template files under the `resource` | ||
subdirectory, producing a full Rust crate for each package. | ||
|
||
For each input message type, two `struct`s are generated: | ||
|
||
- An ergonomic representation of the message, using more idiomatic `std` types like `Vec` and | ||
`String` for sequence and string fields. These are placed directly in the `msg` submodule of the | ||
crate. | ||
- A FFI-suitable `struct` that is ABI-compatible with the layout expected by the RMW layer. While | ||
less ergonomic, these avoid the conversion overhead when passed to RMW. These `struct`s are placed | ||
under the `msg::rmw` submodule. | ||
|
||
All the produced `struct`s implement the standard traits from `std` when possible, such as `Clone`, | ||
`PartialEq`, and `Debug`. Additionally, when the generated crate's `serde` feature is enabled, these | ||
structs implement the `Serialize` and `Deserialize` traits. | ||
|
||
## `rosidl_runtime_rs` | ||
|
||
`rosidl_runtime_rs` is a runtime support package, providing `Message`, `Service`, and `Action` | ||
traits that are implemented by the corresponding structs generated by `rosidl_generator_rs`. These | ||
allow for generic interaction with these various interface types, both in client libraries like | ||
`rclrs` and in user code. | ||
|
||
The package also provides a number of string and sequence types that are ABI-compatible with their | ||
`rosidl_runtime_c` equivalents. This allows for more ergonomic use of the RMW-native message types. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
@{ | ||
from rosidl_parser.definition import ( | ||
ACTION_FEEDBACK_MESSAGE_SUFFIX, | ||
ACTION_FEEDBACK_SUFFIX, | ||
ACTION_GOAL_SERVICE_SUFFIX, | ||
ACTION_GOAL_SUFFIX, | ||
ACTION_RESULT_SERVICE_SUFFIX, | ||
ACTION_RESULT_SUFFIX, | ||
SERVICE_REQUEST_MESSAGE_SUFFIX, | ||
SERVICE_RESPONSE_MESSAGE_SUFFIX, | ||
) | ||
|
||
action_msg_specs = [] | ||
|
||
for subfolder, action in action_specs: | ||
action_msg_specs.append((subfolder, action.goal)) | ||
action_msg_specs.append((subfolder, action.result)) | ||
action_msg_specs.append((subfolder, action.feedback)) | ||
action_msg_specs.append((subfolder, action.feedback_message)) | ||
|
||
action_srv_specs = [] | ||
|
||
for subfolder, action in action_specs: | ||
action_srv_specs.append((subfolder, action.send_goal_service)) | ||
action_srv_specs.append((subfolder, action.get_result_service)) | ||
}@ | ||
|
||
pub mod rmw { | ||
@{ | ||
TEMPLATE( | ||
'msg_rmw.rs.em', | ||
package_name=package_name, interface_path=interface_path, | ||
msg_specs=action_msg_specs, | ||
get_rs_name=get_rs_name, get_rmw_rs_type=get_rmw_rs_type, | ||
pre_field_serde=pre_field_serde, | ||
get_idiomatic_rs_type=get_idiomatic_rs_type, | ||
constant_value_to_rs=constant_value_to_rs) | ||
|
||
TEMPLATE( | ||
'srv_rmw.rs.em', | ||
package_name=package_name, interface_path=interface_path, | ||
srv_specs=action_srv_specs, | ||
get_rs_name=get_rs_name, get_rmw_rs_type=get_rmw_rs_type, | ||
pre_field_serde=pre_field_serde, | ||
get_idiomatic_rs_type=get_idiomatic_rs_type, | ||
constant_value_to_rs=constant_value_to_rs) | ||
}@ | ||
} // mod rmw | ||
|
||
@{ | ||
TEMPLATE( | ||
'msg_idiomatic.rs.em', | ||
package_name=package_name, interface_path=interface_path, | ||
msg_specs=action_msg_specs, | ||
get_rs_name=get_rs_name, get_rmw_rs_type=get_rmw_rs_type, | ||
pre_field_serde=pre_field_serde, | ||
get_idiomatic_rs_type=get_idiomatic_rs_type, | ||
constant_value_to_rs=constant_value_to_rs) | ||
}@ | ||
|
||
@{ | ||
TEMPLATE( | ||
'srv_idiomatic.rs.em', | ||
package_name=package_name, interface_path=interface_path, | ||
srv_specs=action_srv_specs, | ||
get_rs_name=get_rs_name, get_rmw_rs_type=get_rmw_rs_type, | ||
pre_field_serde=pre_field_serde, | ||
get_idiomatic_rs_type=get_idiomatic_rs_type, | ||
constant_value_to_rs=constant_value_to_rs) | ||
}@ | ||
|
||
@[for subfolder, action_spec in action_specs] | ||
|
||
@{ | ||
type_name = action_spec.namespaced_type.name | ||
}@ | ||
|
||
#[link(name = "@(package_name)__rosidl_typesupport_c")] | ||
extern "C" { | ||
fn rosidl_typesupport_c__get_action_type_support_handle__@(package_name)__@(subfolder)__@(type_name)() -> *const std::ffi::c_void; | ||
} | ||
|
||
// Corresponds to @(package_name)__@(subfolder)__@(type_name) | ||
pub struct @(type_name); | ||
|
||
impl rosidl_runtime_rs::Action for @(type_name) { | ||
type Goal = crate::@(subfolder)::@(type_name)@(ACTION_GOAL_SUFFIX); | ||
type Result = crate::@(subfolder)::@(type_name)@(ACTION_RESULT_SUFFIX); | ||
type Feedback = crate::@(subfolder)::@(type_name)@(ACTION_FEEDBACK_SUFFIX); | ||
|
||
fn get_type_support() -> *const std::ffi::c_void { | ||
// SAFETY: No preconditions for this function. | ||
unsafe { rosidl_typesupport_c__get_action_type_support_handle__@(package_name)__@(subfolder)__@(type_name)() } | ||
} | ||
} | ||
|
||
impl rosidl_runtime_rs::ActionImpl for @(type_name) { | ||
type GoalStatusMessage = action_msgs::msg::rmw::GoalStatusArray; | ||
type FeedbackMessage = crate::@(subfolder)::rmw::@(type_name)@(ACTION_FEEDBACK_MESSAGE_SUFFIX); | ||
|
||
type SendGoalService = crate::@(subfolder)::rmw::@(type_name)@(ACTION_GOAL_SERVICE_SUFFIX); | ||
type CancelGoalService = action_msgs::srv::rmw::CancelGoal; | ||
type GetResultService = crate::@(subfolder)::rmw::@(type_name)@(ACTION_RESULT_SERVICE_SUFFIX); | ||
|
||
fn create_goal_request( | ||
goal_id: &[u8; 16], | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. I thought we didn't want to have a dependency on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We want to avoid creating a dependency on However, the crates generated by the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahh, I see. So the function signature of Yeah, we should explore some of the ideas discussed earlier to simplify this. Not needed for this PR though. |
||
goal, | ||
} | ||
} | ||
|
||
fn get_goal_request_uuid( | ||
request: &crate::@(subfolder)::rmw::@(type_name)@(ACTION_GOAL_SERVICE_SUFFIX)@(SERVICE_REQUEST_MESSAGE_SUFFIX), | ||
) -> &[u8; 16] { | ||
&request.goal_id.uuid | ||
} | ||
|
||
fn create_goal_response( | ||
accepted: bool, | ||
stamp: (i32, u32), | ||
) -> crate::@(subfolder)::rmw::@(type_name)@(ACTION_GOAL_SERVICE_SUFFIX)@(SERVICE_RESPONSE_MESSAGE_SUFFIX) { | ||
crate::@(subfolder)::rmw::@(type_name)@(ACTION_GOAL_SERVICE_SUFFIX)@(SERVICE_RESPONSE_MESSAGE_SUFFIX) { | ||
accepted, | ||
stamp: builtin_interfaces::msg::rmw::Time { | ||
sec: stamp.0, | ||
nanosec: stamp.1, | ||
}, | ||
} | ||
} | ||
|
||
fn get_goal_response_accepted( | ||
response: &crate::@(subfolder)::rmw::@(type_name)@(ACTION_GOAL_SERVICE_SUFFIX)@(SERVICE_RESPONSE_MESSAGE_SUFFIX), | ||
) -> bool { | ||
response.accepted | ||
} | ||
|
||
fn get_goal_response_stamp( | ||
response: &crate::@(subfolder)::rmw::@(type_name)@(ACTION_GOAL_SERVICE_SUFFIX)@(SERVICE_RESPONSE_MESSAGE_SUFFIX), | ||
) -> (i32, u32) { | ||
(response.stamp.sec, response.stamp.nanosec) | ||
} | ||
|
||
fn create_feedback_message( | ||
goal_id: &[u8; 16], | ||
feedback: crate::@(subfolder)::rmw::@(type_name)@(ACTION_FEEDBACK_SUFFIX), | ||
) -> crate::@(subfolder)::rmw::@(type_name)@(ACTION_FEEDBACK_MESSAGE_SUFFIX) { | ||
let mut message = crate::@(subfolder)::rmw::@(type_name)@(ACTION_FEEDBACK_MESSAGE_SUFFIX)::default(); | ||
message.goal_id.uuid = *goal_id; | ||
message.feedback = feedback; | ||
message | ||
} | ||
|
||
fn get_feedback_message_uuid( | ||
feedback: &crate::@(subfolder)::rmw::@(type_name)@(ACTION_FEEDBACK_MESSAGE_SUFFIX), | ||
) -> &[u8; 16] { | ||
&feedback.goal_id.uuid | ||
} | ||
|
||
fn get_feedback_message_feedback( | ||
feedback: &crate::@(subfolder)::rmw::@(type_name)@(ACTION_FEEDBACK_MESSAGE_SUFFIX), | ||
) -> &crate::@(subfolder)::rmw::@(type_name)@(ACTION_FEEDBACK_SUFFIX) { | ||
&feedback.feedback | ||
} | ||
|
||
fn create_result_request( | ||
goal_id: &[u8; 16], | ||
) -> crate::@(subfolder)::rmw::@(type_name)@(ACTION_RESULT_SERVICE_SUFFIX)@(SERVICE_REQUEST_MESSAGE_SUFFIX) { | ||
crate::@(subfolder)::rmw::@(type_name)@(ACTION_RESULT_SERVICE_SUFFIX)@(SERVICE_REQUEST_MESSAGE_SUFFIX) { | ||
goal_id: unique_identifier_msgs::msg::rmw::UUID { uuid: *goal_id }, | ||
} | ||
} | ||
|
||
fn get_result_request_uuid( | ||
request: &crate::@(subfolder)::rmw::@(type_name)@(ACTION_RESULT_SERVICE_SUFFIX)@(SERVICE_REQUEST_MESSAGE_SUFFIX), | ||
) -> &[u8; 16] { | ||
&request.goal_id.uuid | ||
} | ||
|
||
fn create_result_response( | ||
status: i8, | ||
result: crate::@(subfolder)::rmw::@(type_name)@(ACTION_RESULT_SUFFIX), | ||
) -> crate::@(subfolder)::rmw::@(type_name)@(ACTION_RESULT_SERVICE_SUFFIX)@(SERVICE_RESPONSE_MESSAGE_SUFFIX) { | ||
crate::@(subfolder)::rmw::@(type_name)@(ACTION_RESULT_SERVICE_SUFFIX)@(SERVICE_RESPONSE_MESSAGE_SUFFIX) { | ||
status, | ||
result, | ||
} | ||
} | ||
|
||
fn get_result_response_result( | ||
response: &crate::@(subfolder)::rmw::@(type_name)@(ACTION_RESULT_SERVICE_SUFFIX)@(SERVICE_RESPONSE_MESSAGE_SUFFIX), | ||
) -> &crate::@(subfolder)::rmw::@(type_name)@(ACTION_RESULT_SUFFIX) { | ||
&response.result | ||
} | ||
|
||
fn get_result_response_status( | ||
response: &crate::@(subfolder)::rmw::@(type_name)@(ACTION_RESULT_SERVICE_SUFFIX)@(SERVICE_RESPONSE_MESSAGE_SUFFIX), | ||
) -> i8 { | ||
response.status | ||
} | ||
} | ||
|
||
@[end for] |
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.
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.@(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.
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.
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.