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
5 changes: 5 additions & 0 deletions .github/workflows/ros2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ jobs:
- name: Lint
run: source /root/.cargo/env && cargo fmt --all -- --check

- name: Install wstring interoperability test messages
run: |
apt-get update
apt-get install -y ros-${{ matrix.distro }}-test-msgs

- name: Start rosbridge services
run: |
source /opt/ros/${{ matrix.distro }}/setup.bash
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,32 @@ If you want to see what the generated code looks like checkout [our generated me
While the macro is useful for getting started, we recommend using `roslibrust_codegen` with a `build.rs` as shown in [example_package](https://github.com/RosLibRust/roslibrust/tree/master/example_package).
This allows cargo to know when message files are edited and automatically re-generate the code.

## ROS 2 `wstring` compatibility

ROS 2 `wstring` fields are generated as `roslibrust::codegen::WString`. The
Rust value contains a normal UTF-8 `String` and supports conversion from `&str`
and `String` with `.into()`.

The wire representation is selected automatically by the backend:

- Native ROS 2 CDR backends, including ros-z, convert the Rust string to a DDS
`wstring`: a sequence of UTF-16 code units. Received UTF-16 is validated and
converted back to UTF-8; invalid UTF-16 produces a serialization error.
- Rosbridge represents `wstring` as a normal JSON string. JSON handles scalar,
array, and sequence fields without a special wrapper representation.
- ROS 1 has no native `wstring` type. The TCPROS and ROS 1 Zenoh backends
therefore encode each `WString` as an ordinary ROS 1 UTF-8 `string` and
convert it back automatically when receiving. This compatibility conversion
also applies to `wstring` arrays and sequences.

The ROS 1 conversion does not add `wstring` to the ROS 1 type system. For topic
connections, the TCPROS and ROS 1 Zenoh backends automatically advertise a
compatible definition and MD5 using `string`, `string[N]`, or `string[]` in the
corresponding fields. The ROS 1 peer must have that compatible definition under
the same package and message name. String and sequence bounds remain in the
generated ROS 2 metadata but, like the existing bounded collection support,
are not enforced at runtime.

## Getting Started / Examples

- Checkout the [Quick Getting Started Guide](https://roslibrust.github.io/roslibrust/quick_getting_started.html) for a brief guide on how to get started with RosLibRust.
Expand Down
10 changes: 10 additions & 0 deletions assets/ros2_test_msgs/msg/WStrings.msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
wstring wstring_value
wstring wstring_value_default1 "Hello world!"
wstring wstring_value_default2 "Hellö wörld!"
wstring wstring_value_default3 "ハローワールド"
#wstring WSTRING_CONST="Hello world!"
#wstring<=22 bounded_wstring_value
#wstring<=22 bounded_wstring_value_default1 "Hello world!"
wstring[3] array_of_wstrings
wstring[<=3] bounded_sequence_of_wstrings
wstring[] unbounded_sequence_of_wstrings
10 changes: 10 additions & 0 deletions assets/ros2_wstring_test_msgs/test_msgs/msg/WStrings.msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
wstring wstring_value
wstring wstring_value_default1 "Hello world!"
wstring wstring_value_default2 "Hellö wörld!"
wstring wstring_value_default3 "ハローワールド"
#wstring WSTRING_CONST="Hello world!"
#wstring<=22 bounded_wstring_value
#wstring<=22 bounded_wstring_value_default1 "Hello world!"
wstring[3] array_of_wstrings
wstring[<=3] bounded_sequence_of_wstrings
wstring[] unbounded_sequence_of_wstrings
10 changes: 10 additions & 0 deletions assets/ros2_wstring_test_msgs/test_msgs/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<package format="3">
<name>test_msgs</name>
<version>2.0.0</version>
<description>Minimal ROS 2 test_msgs fixture for wstring interoperability tests.</description>
<maintainer email="roslibrust@example.com">RosLibRust</maintainer>
<license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<build_depend>rosidl_default_generators</build_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
</package>
2 changes: 1 addition & 1 deletion docker/galactic/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ LABEL maintainer="Carter Schultz <carterjschultz@gmail.com>"
# Required by github CI for submodule support
RUN apt update && apt install -y git

RUN apt update && apt install -y ros-galactic-rosbridge-suite
RUN apt update && apt install -y ros-galactic-rosbridge-suite ros-galactic-test-msgs

# Install rosbag2 and MCAP storage plugin for MCAP integration testing
# Note: MCAP is not the default storage format in Galactic, so we need the plugin
Expand Down
2 changes: 1 addition & 1 deletion docker/humble/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ LABEL maintainer="Carter Schultz <carterjschultz@gmail.com>"
# Required by github CI for submodule support
RUN apt update && apt install -y git

RUN apt update && apt install -y ros-humble-rosbridge-suite
RUN apt update && apt install -y ros-humble-rosbridge-suite ros-humble-test-msgs

# Install rosbag2 and MCAP storage plugin for MCAP integration testing
# Note: MCAP is not the default storage format in Humble, so we need the plugin
Expand Down
2 changes: 1 addition & 1 deletion docker/iron/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ LABEL maintainer="Carter Schultz <carterjschultz@gmail.com>"
# Required by github CI for submodule support
RUN apt update && apt install -y git

RUN apt update && apt install -y ros-iron-rosbridge-suite
RUN apt update && apt install -y ros-iron-rosbridge-suite ros-iron-test-msgs

# Install rosbag2 and MCAP storage plugin for MCAP integration testing
# Note: MCAP is not the default storage format in Iron, so we need the plugin
Expand Down
2 changes: 1 addition & 1 deletion docker/kilted/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ LABEL maintainer="Carter Schultz <carterjschultz@gmail.com>"
RUN apt update && apt install -y git

# Install rosbridge suite for rosbridge backend testing
RUN apt update && apt install -y ros-kilted-rosbridge-suite
RUN apt update && apt install -y ros-kilted-rosbridge-suite ros-kilted-test-msgs

# Install rosbag2 for MCAP integration testing
RUN apt update && apt install -y ros-kilted-rosbag2
Expand Down
3 changes: 1 addition & 2 deletions docker/rolling/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ LABEL maintainer="Carter Schultz <carterjschultz@gmail.com>"
RUN apt update && apt install -y git

# Install rosbridge suite for rosbridge backend testing
RUN apt update && apt install -y ros-rolling-rosbridge-suite
RUN apt update && apt install -y ros-rolling-rosbridge-suite ros-rolling-test-msgs

# Install rosbag2 for MCAP integration testing
RUN apt update && apt install -y ros-rolling-rosbag2
Expand All @@ -24,4 +24,3 @@ WORKDIR /
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

1 change: 1 addition & 0 deletions roslibrust_codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ anyhow = "1.0"
tokio = { workspace = true }
env_logger = "0.10"
test-log = { workspace = true }
cdr = "0.2"

[features]
# For use with CI environment or any environment with ROS1 installed
Expand Down
15 changes: 12 additions & 3 deletions roslibrust_codegen/src/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,10 @@ fn convert_ros_constant_type_to_rust_type(

convert_ros_type_to_rust_type(version, &constant.constant_type)
.map(|rust_type| {
if rust_type == "::std::string::String" {
if matches!(
rust_type,
"::std::string::String" | "::roslibrust::codegen::WString"
) {
"&'static str"
} else {
rust_type
Expand Down Expand Up @@ -457,14 +460,20 @@ fn parse_ros_value(
"int32" => generic_parse_value::<i32>(value, is_list),
"uint64" => generic_parse_value::<u64>(value, is_list),
"int64" => generic_parse_value::<i64>(value, is_list),
"string" => {
"string" | "wstring" => {
// String is a special case because of quotes and to_string()
if is_list {
// TODO there is a bug here, no idea how I should be attempting to convert / escape single quotes here...
let parsed: Vec<String> = serde_json::from_str(value).map_err(|e|
Error::with(format!("Failed to parse a literal value in a message file to the corresponding rust type: {value} to Vec<String>").as_str(), e)
)?;
let vec_str = format!("{parsed:?}.iter().map(|x| x.to_string()).collect()");
let vec_str = if ros_type == "wstring" {
format!(
"{parsed:?}.iter().map(|x| ::roslibrust::codegen::WString::from(*x)).collect()"
)
} else {
format!("{parsed:?}.iter().map(|x| x.to_string()).collect()")
};
Ok(quote! { #vec_str })
} else {
match version {
Expand Down
2 changes: 2 additions & 0 deletions roslibrust_codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ mod ros2_builtin_interfaces;

pub mod integral_types;
pub use integral_types::*;
mod wstring;
pub use wstring::WString;

// Custom serde module for Vec<u8> that handles both base64 (rosbridge) and arrays (other formats)
pub mod serde_rosmsg_bytes;
Expand Down
34 changes: 29 additions & 5 deletions roslibrust_codegen/src/parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ lazy_static::lazy_static! {
("float32", "f32"),
("float64", "f64"),
("string", "::std::string::String"),
("wstring", "::roslibrust::codegen::WString"),
("builtin_interfaces/Time", "::roslibrust::codegen::integral_types::Time"),
("builtin_interfaces/Duration", "::roslibrust::codegen::integral_types::Duration"),
// ("wstring", TODO),
].into_iter().collect();
}

Expand Down Expand Up @@ -216,17 +216,26 @@ fn parse_field_type(
}
}

/// Specifically handles bounded string types, e.g. "string<=10"
/// Specifically handles bounded string types, e.g. "string<=10" or "wstring<=10".
/// Returns the field_type and the string_capacity if it is a bounded string
/// Otherwise returns the original type and None for the capacity
fn parse_bounded_string(type_str: &str) -> Result<(String, Option<usize>), Error> {
if let Some(stripped) = type_str.strip_prefix("string<=") {
let capacity = stripped.parse::<usize>().map_err(|err| {
let bounded_type = type_str
.strip_prefix("string<=")
.map(|capacity| ("string", capacity))
.or_else(|| {
type_str
.strip_prefix("wstring<=")
.map(|capacity| ("wstring", capacity))
});

if let Some((field_type, capacity)) = bounded_type {
let capacity = capacity.parse::<usize>().map_err(|err| {
Error::new(format!(
"Unable to parse capacity of bounded string: {type_str}: {err}"
))
})?;
Ok(("string".to_string(), Some(capacity)))
Ok((field_type.to_string(), Some(capacity)))
} else {
Ok((type_str.to_string(), None))
}
Expand Down Expand Up @@ -326,6 +335,21 @@ mod test {
assert_eq!(parsed.array_info, ArrayType::Unbounded);
}

#[test_log::test]
fn parse_type_handles_bounded_wstring_correctly() {
let pkg = Package {
name: "test_pkg".to_string(),
path: "./not_a_path".into(),
version: Some(RosVersion::ROS2),
};
let parsed = parse_type("wstring<=32[<=4]", &pkg).unwrap();

assert_eq!(parsed.field_type, "wstring");
assert_eq!(parsed.string_capacity, Some(32));
assert_eq!(parsed.array_info, ArrayType::Bounded(4));
assert_eq!(parsed.package_name, None);
}

#[test_log::test]
fn parse_constant_with_hash_in_value() {
use crate::parse::parse_constant_field;
Expand Down
Loading
Loading