diff --git a/.github/workflows/ros2.yml b/.github/workflows/ros2.yml
index 2ce1de81..9f24e483 100644
--- a/.github/workflows/ros2.yml
+++ b/.github/workflows/ros2.yml
@@ -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
diff --git a/Cargo.lock b/Cargo.lock
index 63decad9..273c4cc4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3469,6 +3469,7 @@ version = "0.21.0"
dependencies = [
"anyhow",
"base64 0.22.1",
+ "cdr",
"chrono",
"env_logger 0.10.2",
"hex",
@@ -3663,7 +3664,9 @@ dependencies = [
"roslibrust",
"roslibrust_ros2",
"roslibrust_rosbridge",
+ "roslibrust_serde_rosmsg",
"roslibrust_zenoh",
+ "serde_json",
"test-log",
"tokio",
"zenoh",
diff --git a/README.md b/README.md
index 2e354561..cdd52a40 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/assets/ros2_test_msgs/msg/WStrings.msg b/assets/ros2_test_msgs/msg/WStrings.msg
new file mode 100644
index 00000000..666d0ec6
--- /dev/null
+++ b/assets/ros2_test_msgs/msg/WStrings.msg
@@ -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
diff --git a/assets/ros2_wstring_test_msgs/test_msgs/msg/WStrings.msg b/assets/ros2_wstring_test_msgs/test_msgs/msg/WStrings.msg
new file mode 100644
index 00000000..666d0ec6
--- /dev/null
+++ b/assets/ros2_wstring_test_msgs/test_msgs/msg/WStrings.msg
@@ -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
diff --git a/assets/ros2_wstring_test_msgs/test_msgs/package.xml b/assets/ros2_wstring_test_msgs/test_msgs/package.xml
new file mode 100644
index 00000000..b564533d
--- /dev/null
+++ b/assets/ros2_wstring_test_msgs/test_msgs/package.xml
@@ -0,0 +1,10 @@
+
+ test_msgs
+ 2.0.0
+ Minimal ROS 2 test_msgs fixture for wstring interoperability tests.
+ RosLibRust
+ Apache-2.0
+ ament_cmake
+ rosidl_default_generators
+ rosidl_interface_packages
+
diff --git a/docker/galactic/Dockerfile b/docker/galactic/Dockerfile
index b4e51ac1..86f313e3 100644
--- a/docker/galactic/Dockerfile
+++ b/docker/galactic/Dockerfile
@@ -4,7 +4,7 @@ LABEL maintainer="Carter Schultz "
# 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
diff --git a/docker/humble/Dockerfile b/docker/humble/Dockerfile
index aad4ad88..c87dd968 100644
--- a/docker/humble/Dockerfile
+++ b/docker/humble/Dockerfile
@@ -4,7 +4,7 @@ LABEL maintainer="Carter Schultz "
# 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
diff --git a/docker/iron/Dockerfile b/docker/iron/Dockerfile
index 2dec3c31..2039cc04 100644
--- a/docker/iron/Dockerfile
+++ b/docker/iron/Dockerfile
@@ -4,7 +4,7 @@ LABEL maintainer="Carter Schultz "
# 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
diff --git a/docker/kilted/Dockerfile b/docker/kilted/Dockerfile
index c375c62b..aad36b78 100644
--- a/docker/kilted/Dockerfile
+++ b/docker/kilted/Dockerfile
@@ -5,7 +5,7 @@ LABEL maintainer="Carter Schultz "
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
diff --git a/docker/rolling/Dockerfile b/docker/rolling/Dockerfile
index 159c08bd..825664d8 100644
--- a/docker/rolling/Dockerfile
+++ b/docker/rolling/Dockerfile
@@ -5,7 +5,7 @@ LABEL maintainer="Carter Schultz "
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
@@ -24,4 +24,3 @@ WORKDIR /
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
-
diff --git a/roslibrust_codegen/Cargo.toml b/roslibrust_codegen/Cargo.toml
index c007e192..7a8abaee 100644
--- a/roslibrust_codegen/Cargo.toml
+++ b/roslibrust_codegen/Cargo.toml
@@ -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
diff --git a/roslibrust_codegen/src/gen.rs b/roslibrust_codegen/src/gen.rs
index 1aa5c12e..32a49e29 100644
--- a/roslibrust_codegen/src/gen.rs
+++ b/roslibrust_codegen/src/gen.rs
@@ -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
@@ -457,14 +460,20 @@ fn parse_ros_value(
"int32" => generic_parse_value::(value, is_list),
"uint64" => generic_parse_value::(value, is_list),
"int64" => generic_parse_value::(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 = 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").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 {
diff --git a/roslibrust_codegen/src/lib.rs b/roslibrust_codegen/src/lib.rs
index a4f35210..9366f2fa 100644
--- a/roslibrust_codegen/src/lib.rs
+++ b/roslibrust_codegen/src/lib.rs
@@ -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 that handles both base64 (rosbridge) and arrays (other formats)
pub mod serde_rosmsg_bytes;
diff --git a/roslibrust_codegen/src/parse/mod.rs b/roslibrust_codegen/src/parse/mod.rs
index e3555c34..b2079bd5 100644
--- a/roslibrust_codegen/src/parse/mod.rs
+++ b/roslibrust_codegen/src/parse/mod.rs
@@ -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();
}
@@ -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), Error> {
- if let Some(stripped) = type_str.strip_prefix("string<=") {
- let capacity = stripped.parse::().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::().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))
}
@@ -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;
diff --git a/roslibrust_codegen/src/wstring.rs b/roslibrust_codegen/src/wstring.rs
new file mode 100644
index 00000000..44bed5b5
--- /dev/null
+++ b/roslibrust_codegen/src/wstring.rs
@@ -0,0 +1,159 @@
+use serde::{
+ de::Error as DeError, ser::SerializeSeq, Deserialize, Deserializer, Serialize, Serializer,
+};
+use std::{borrow::Borrow, fmt, ops::Deref, str::FromStr};
+
+/// A ROS 2 `wstring`.
+///
+/// ROS 2 represents this type as a [`std::u16string`](https://en.cppreference.com/w/cpp/string/basic_string)
+/// in C++ and as a DDS `wstring` on the wire. Rust strings are UTF-8, so this
+/// wrapper converts to and from UTF-16 for ROS 2 CDR. Human-readable serializers
+/// use a normal JSON string, and ROS 1 backends enable a compatibility scope
+/// that represents the value as an ordinary UTF-8 ROS string.
+#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub struct WString(String);
+
+impl WString {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+
+ pub fn into_string(self) -> String {
+ self.0
+ }
+}
+
+impl Serialize for WString {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: Serializer,
+ {
+ if serializer.is_human_readable() || roslibrust_common::ros1_wstring_compatibility_enabled()
+ {
+ serializer.serialize_str(&self.0)
+ } else {
+ let code_units = self.0.encode_utf16();
+ let mut sequence = serializer.serialize_seq(Some(code_units.clone().count()))?;
+ for code_unit in code_units {
+ sequence.serialize_element(&code_unit)?;
+ }
+ sequence.end()
+ }
+ }
+}
+
+impl<'de> Deserialize<'de> for WString {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: Deserializer<'de>,
+ {
+ if deserializer.is_human_readable()
+ || roslibrust_common::ros1_wstring_compatibility_enabled()
+ {
+ String::deserialize(deserializer).map(Self)
+ } else {
+ let code_units = Vec::::deserialize(deserializer)?;
+ String::from_utf16(&code_units)
+ .map(Self)
+ .map_err(D::Error::custom)
+ }
+ }
+}
+
+impl AsRef for WString {
+ fn as_ref(&self) -> &str {
+ self.as_str()
+ }
+}
+
+impl Borrow for WString {
+ fn borrow(&self) -> &str {
+ self.as_str()
+ }
+}
+
+impl Deref for WString {
+ type Target = str;
+
+ fn deref(&self) -> &Self::Target {
+ self.as_str()
+ }
+}
+
+impl fmt::Display for WString {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(formatter)
+ }
+}
+
+impl From for WString {
+ fn from(value: String) -> Self {
+ Self(value)
+ }
+}
+
+impl From<&str> for WString {
+ fn from(value: &str) -> Self {
+ Self(value.to_owned())
+ }
+}
+
+impl From for String {
+ fn from(value: WString) -> Self {
+ value.0
+ }
+}
+
+impl FromStr for WString {
+ type Err = std::convert::Infallible;
+
+ fn from_str(value: &str) -> Result {
+ Ok(Self::from(value))
+ }
+}
+
+impl PartialEq for WString {
+ fn eq(&self, other: &str) -> bool {
+ self.0 == other
+ }
+}
+
+impl PartialEq<&str> for WString {
+ fn eq(&self, other: &&str) -> bool {
+ self.0 == *other
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::WString;
+
+ #[test]
+ fn json_uses_a_normal_string() {
+ let value = WString::from("ハロー 🌍");
+ let encoded = serde_json::to_string(&value).unwrap();
+ assert_eq!(encoded, "\"ハロー 🌍\"");
+ assert_eq!(serde_json::from_str::(&encoded).unwrap(), value);
+ }
+
+ #[test]
+ fn cdr_uses_a_utf16_sequence_without_a_null_terminator() {
+ let value = WString::from("A🌍");
+ let encoded = cdr::serialize::<_, _, cdr::CdrLe>(&value, cdr::Infinite).unwrap();
+
+ assert_eq!(
+ encoded,
+ [
+ 0x00, 0x01, 0x00, 0x00, // CDR little-endian encapsulation
+ 0x03, 0x00, 0x00, 0x00, // three UTF-16 code units
+ 0x41, 0x00, // A
+ 0x3c, 0xd8, 0x0d, 0xdf, // U+1F30D surrogate pair
+ ]
+ );
+ assert_eq!(cdr::deserialize::(&encoded).unwrap(), value);
+ }
+}
diff --git a/roslibrust_common/src/lib.rs b/roslibrust_common/src/lib.rs
index d9331dd3..2b1d0b03 100644
--- a/roslibrust_common/src/lib.rs
+++ b/roslibrust_common/src/lib.rs
@@ -67,6 +67,12 @@ impl RosMessageType for ShapeShifter {
/// These functions are needed both in roslibrust_ros1 and roslibrust_codegen so they're in this crate
/// for the moment.
pub mod md5sum;
+mod serialization_context;
+#[doc(hidden)]
+pub use serialization_context::{
+ ros1_message_description, ros1_wstring_compatibility_enabled, with_ros1_wstring_compatibility,
+ Ros1MessageDescription,
+};
/// Contains the generic traits represent a pubsub system and service system.
/// These traits will be implemented for specific backends to provides access to "ROS Like" functionality.
diff --git a/roslibrust_common/src/serialization_context.rs b/roslibrust_common/src/serialization_context.rs
new file mode 100644
index 00000000..a3f00d1c
--- /dev/null
+++ b/roslibrust_common/src/serialization_context.rs
@@ -0,0 +1,131 @@
+use std::cell::Cell;
+
+use crate::RosMessageType;
+
+thread_local! {
+ static ROS1_WSTRING_COMPATIBILITY_DEPTH: Cell = const { Cell::new(0) };
+}
+
+/// Runs a ROS 1 serialization or deserialization operation with `wstring`
+/// compatibility enabled.
+///
+/// This is used internally by ROS 1 backends. ROS 1 has no native `wstring`,
+/// so generated ROS 2 `wstring` values are represented as ordinary UTF-8 ROS
+/// strings while this scope is active.
+#[doc(hidden)]
+pub fn with_ros1_wstring_compatibility(operation: impl FnOnce() -> T) -> T {
+ struct ScopeGuard;
+
+ impl Drop for ScopeGuard {
+ fn drop(&mut self) {
+ ROS1_WSTRING_COMPATIBILITY_DEPTH.with(|depth| depth.set(depth.get() - 1));
+ }
+ }
+
+ ROS1_WSTRING_COMPATIBILITY_DEPTH.with(|depth| depth.set(depth.get() + 1));
+ let _guard = ScopeGuard;
+ operation()
+}
+
+/// Reports whether the current synchronous serde operation is being performed
+/// by a ROS 1 backend.
+#[doc(hidden)]
+pub fn ros1_wstring_compatibility_enabled() -> bool {
+ ROS1_WSTRING_COMPATIBILITY_DEPTH.with(|depth| depth.get() > 0)
+}
+
+/// ROS 1-compatible connection metadata for a message type.
+#[doc(hidden)]
+pub struct Ros1MessageDescription {
+ pub definition: String,
+ pub md5sum: String,
+}
+
+/// Converts ROS 2 `wstring` field declarations to their closest ROS 1 `string`
+/// equivalents and calculates the MD5 advertised to ROS 1 peers.
+#[doc(hidden)]
+pub fn ros1_message_description() -> Ros1MessageDescription {
+ let definition = ros1_compatible_definition(T::DEFINITION);
+ let md5sum = crate::md5sum::from_message_definition(T::ROS_TYPE_NAME, &definition)
+ .unwrap_or_else(|_| T::MD5SUM.to_owned());
+ Ros1MessageDescription { definition, md5sum }
+}
+
+fn ros1_compatible_definition(definition: &str) -> String {
+ definition
+ .lines()
+ .map(|line| {
+ let trimmed = line.trim_start();
+ if trimmed.starts_with('#') {
+ return line.to_owned();
+ }
+
+ let Some(type_end) = trimmed.find(char::is_whitespace) else {
+ return line.to_owned();
+ };
+ let field_type = &trimmed[..type_end];
+ if !field_type.starts_with("wstring") {
+ return line.to_owned();
+ }
+
+ let array = field_type.find('[').map(|start| &field_type[start..]);
+ let ros1_array = match array {
+ Some(array) if array.starts_with("[<=") => "[]",
+ Some(array) => array,
+ None => "",
+ };
+ let replacement = format!("string{ros1_array}");
+ let indentation = &line[..line.len() - trimmed.len()];
+ let remainder = trimmed[type_end..].trim_start();
+ let field_name_end = remainder
+ .find(char::is_whitespace)
+ .unwrap_or(remainder.len());
+ let field_name = &remainder[..field_name_end];
+ if field_name.contains('=') {
+ format!("{indentation}{replacement} {remainder}")
+ } else {
+ // ROS 2 permits field defaults, but ROS 1 definitions do not.
+ format!("{indentation}{replacement} {field_name}")
+ }
+ })
+ .collect::>()
+ .join("\n")
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn compatibility_scope_is_nested_and_restored() {
+ assert!(!ros1_wstring_compatibility_enabled());
+ with_ros1_wstring_compatibility(|| {
+ assert!(ros1_wstring_compatibility_enabled());
+ with_ros1_wstring_compatibility(|| {
+ assert!(ros1_wstring_compatibility_enabled());
+ });
+ assert!(ros1_wstring_compatibility_enabled());
+ });
+ assert!(!ros1_wstring_compatibility_enabled());
+ }
+
+ #[test]
+ fn definition_conversion_only_changes_wstring_type_tokens() {
+ let definition = r#"# wstring in a comment
+wstring wstring_value
+wstring<=22 bounded_wstring_value "default"
+wstring[3] array_of_wstrings
+wstring[<=3] bounded_sequence_of_wstrings
+wstring[] unbounded_sequence_of_wstrings"#;
+
+ assert_eq!(
+ ros1_compatible_definition(definition),
+ r#"# wstring in a comment
+string wstring_value
+string bounded_wstring_value
+string[3] array_of_wstrings
+string[] bounded_sequence_of_wstrings
+string[] unbounded_sequence_of_wstrings"#
+ );
+ }
+}
diff --git a/roslibrust_ros1/src/node/actor.rs b/roslibrust_ros1/src/node/actor.rs
index ba0a2082..b10698b7 100644
--- a/roslibrust_ros1/src/node/actor.rs
+++ b/roslibrust_ros1/src/node/actor.rs
@@ -150,13 +150,14 @@ impl NodeServerHandle {
) -> Result<(broadcast::Sender, mpsc::Sender<()>), NodeError> {
// Create a weak reference to pass to the publication
let weak_node = self.downgrade();
+ let description = roslibrust_common::ros1_message_description::();
let mut node = self.node.lock().await;
node.register_publisher(
topic.to_owned(),
T::ROS_TYPE_NAME,
queue_size,
- T::DEFINITION.to_owned(),
- T::MD5SUM.to_owned(),
+ description.definition,
+ description.md5sum,
latching,
weak_node,
)
@@ -245,11 +246,15 @@ impl NodeServerHandle {
// Uses Bytes for efficient handling of incoming request data
let server_typeless =
move |message: Bytes| -> Result, Box> {
- let request = roslibrust_serde_rosmsg::from_slice::(&message)
- .map_err(|err| Error::SerializationError(err.to_string()))?;
+ let request = roslibrust_common::with_ros1_wstring_compatibility(|| {
+ roslibrust_serde_rosmsg::from_slice::(&message)
+ })
+ .map_err(|err| Error::SerializationError(err.to_string()))?;
let response = server(request)?;
- Ok(roslibrust_serde_rosmsg::to_vec(&response)
- .map_err(|err| Error::SerializationError(err.to_string()))?)
+ Ok(roslibrust_common::with_ros1_wstring_compatibility(|| {
+ roslibrust_serde_rosmsg::to_vec(&response)
+ })
+ .map_err(|err| Error::SerializationError(err.to_string()))?)
};
let server_typeless = Box::new(server_typeless);
@@ -281,13 +286,14 @@ impl NodeServerHandle {
topic: &str,
queue_size: usize,
) -> Result, NodeError> {
+ let description = roslibrust_common::ros1_message_description::();
let mut node = self.node.lock().await;
node.register_subscriber(
topic,
T::ROS_TYPE_NAME,
queue_size,
- T::DEFINITION,
- T::MD5SUM,
+ &description.definition,
+ &description.md5sum,
)
.await
.map_err(|err| {
diff --git a/roslibrust_ros1/src/publisher.rs b/roslibrust_ros1/src/publisher.rs
index 25e983f5..12d7c2b6 100644
--- a/roslibrust_ros1/src/publisher.rs
+++ b/roslibrust_ros1/src/publisher.rs
@@ -72,7 +72,9 @@ impl Publisher {
let mut writer = buffer.writer();
// Write empty u32 for size
writer.write(&[0, 0, 0, 0]).unwrap();
- roslibrust_serde_rosmsg::to_writer_skip_length(&mut writer, data)?;
+ roslibrust_common::with_ros1_wstring_compatibility(|| {
+ roslibrust_serde_rosmsg::to_writer_skip_length(&mut writer, data)
+ })?;
let mut buffer = writer.into_inner();
// Patch size back to front of buffer
let size = buffer.len() as u32 - 4;
@@ -491,3 +493,41 @@ impl From for PublisherError {
Self::SerializingError(value.to_string())
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use roslibrust_test::ros2::test_msgs::WStrings;
+
+ #[tokio::test]
+ async fn publisher_encodes_wstring_as_an_ordinary_ros1_string() {
+ let (sender, mut receiver) = broadcast::channel(1);
+ let (shutdown_sender, _shutdown_receiver) = tokio::sync::mpsc::channel(1);
+ let publisher = Publisher::new("/wstring_test", sender, shutdown_sender);
+ let mut message = WStrings::default();
+ message.wstring_value = "ハロー 🌍".into();
+ let description = roslibrust_common::ros1_message_description::();
+
+ assert!(description.definition.contains("string wstring_value"));
+ assert!(description
+ .definition
+ .contains("string[] bounded_sequence_of_wstrings"));
+ assert!(!description.definition.contains("wstring wstring_value"));
+ assert!(description
+ .definition
+ .contains("string wstring_value_default1\n"));
+ assert!(!description
+ .definition
+ .contains("string wstring_value_default1 \"Hello world!\""));
+
+ publisher.publish(&message).await.unwrap();
+ let bytes = receiver.recv().await.unwrap();
+
+ let encoded_length = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize;
+ assert_eq!(encoded_length, message.wstring_value.len());
+ assert_eq!(
+ &bytes[8..8 + encoded_length],
+ message.wstring_value.as_bytes()
+ );
+ }
+}
diff --git a/roslibrust_ros1/src/service_client.rs b/roslibrust_ros1/src/service_client.rs
index 0a1edd7d..319b3558 100644
--- a/roslibrust_ros1/src/service_client.rs
+++ b/roslibrust_ros1/src/service_client.rs
@@ -52,8 +52,10 @@ impl ServiceClient {
}
pub async fn call(&self, request: &T::Request) -> std::result::Result {
- let request_payload = roslibrust_serde_rosmsg::to_vec(request)
- .map_err(|err| Error::SerializationError(err.to_string()))?;
+ let request_payload = roslibrust_common::with_ros1_wstring_compatibility(|| {
+ roslibrust_serde_rosmsg::to_vec(request)
+ })
+ .map_err(|err| Error::SerializationError(err.to_string()))?;
let (response_tx, response_rx) = oneshot::channel();
self.sender
@@ -67,7 +69,10 @@ impl ServiceClient {
self.service_name,
result_payload
);
- let response: T::Response = roslibrust_serde_rosmsg::from_slice(&result_payload)
+ let response: T::Response =
+ roslibrust_common::with_ros1_wstring_compatibility(|| {
+ roslibrust_serde_rosmsg::from_slice(&result_payload)
+ })
.map_err(|err| Error::SerializationError(err.to_string()))?;
Ok(response)
}
diff --git a/roslibrust_ros1/src/subscriber.rs b/roslibrust_ros1/src/subscriber.rs
index 3c97ebad..24be2882 100644
--- a/roslibrust_ros1/src/subscriber.rs
+++ b/roslibrust_ros1/src/subscriber.rs
@@ -53,7 +53,9 @@ impl Subscriber {
T::ROS_TYPE_NAME
);
let tick = tokio::time::Instant::now();
- match roslibrust_serde_rosmsg::from_slice::(&data[..]) {
+ match roslibrust_common::with_ros1_wstring_compatibility(|| {
+ roslibrust_serde_rosmsg::from_slice::(&data[..])
+ }) {
Ok(p) => {
let duration = tick.elapsed();
trace!(
diff --git a/roslibrust_ros2/tests/test_wstring_interop.rs b/roslibrust_ros2/tests/test_wstring_interop.rs
new file mode 100644
index 00000000..783a41e4
--- /dev/null
+++ b/roslibrust_ros2/tests/test_wstring_interop.rs
@@ -0,0 +1,78 @@
+#![cfg(feature = "ros2_zenoh_test")]
+
+mod common;
+
+use roslibrust_common::traits::*;
+use roslibrust_ros2::ZenohClient;
+use roslibrust_test::ros2::test_msgs::WStrings;
+use std::io::{BufRead, BufReader};
+use std::process::{Child, Command, Stdio};
+use tokio::time::{sleep, timeout, Duration};
+
+struct ChildGuard(Child);
+
+impl Drop for ChildGuard {
+ fn drop(&mut self) {
+ let _ = self.0.kill();
+ let _ = self.0.wait();
+ }
+}
+
+fn spawn_ros2_relay(input_topic: &str, output_topic: &str) -> ChildGuard {
+ let mut child = Command::new("python3")
+ .arg(concat!(
+ env!("CARGO_MANIFEST_DIR"),
+ "/../roslibrust_test/tests/ros2_wstring_relay.py"
+ ))
+ .args([input_topic, output_topic])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::inherit())
+ .spawn()
+ .expect("failed to start the rclpy wstring relay");
+ let mut ready = String::new();
+ BufReader::new(child.stdout.take().unwrap())
+ .read_line(&mut ready)
+ .expect("failed to read readiness from the rclpy wstring relay");
+ assert_eq!(
+ ready.trim(),
+ "READY",
+ "rclpy wstring relay exited before becoming ready"
+ );
+ ChildGuard(child)
+}
+
+fn test_message() -> WStrings {
+ WStrings {
+ wstring_value: "ハローワールド 🌍".into(),
+ wstring_value_default1: "Hello world!".into(),
+ wstring_value_default2: "Hellö wörld!".into(),
+ wstring_value_default3: "ハローワールド".into(),
+ array_of_wstrings: ["one".into(), "二".into(), "🌍".into()],
+ bounded_sequence_of_wstrings: vec!["bounded".into(), "文字列".into()],
+ unbounded_sequence_of_wstrings: vec!["".into(), "ascii".into(), "四".into()],
+ }
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn wstring_round_trips_through_an_rclpy_node() {
+ const INPUT_TOPIC: &str = "/roslibrust_ros_z_wstring_input";
+ const OUTPUT_TOPIC: &str = "/roslibrust_ros_z_wstring_output";
+
+ let context = common::make_test_context();
+ let client = ZenohClient::new(&context, "roslibrust_wstring_interop")
+ .await
+ .unwrap();
+ let publisher = client.advertise::(INPUT_TOPIC).await.unwrap();
+ let mut subscriber = client.subscribe::(OUTPUT_TOPIC).await.unwrap();
+ let _relay = spawn_ros2_relay(INPUT_TOPIC, OUTPUT_TOPIC);
+
+ sleep(Duration::from_secs(2)).await;
+ let expected = test_message();
+ publisher.publish(&expected).await.unwrap();
+
+ let received = timeout(Duration::from_secs(5), subscriber.next())
+ .await
+ .expect("timed out waiting for the rclpy relay")
+ .unwrap();
+ assert_eq!(received, expected);
+}
diff --git a/roslibrust_rosbridge/src/integration_tests.rs b/roslibrust_rosbridge/src/integration_tests.rs
index 181c82df..c2734822 100644
--- a/roslibrust_rosbridge/src/integration_tests.rs
+++ b/roslibrust_rosbridge/src/integration_tests.rs
@@ -528,4 +528,76 @@ mod integration_tests {
assert_eq!(received, msg, "Messages do not match");
}
+
+ #[cfg(feature = "ros2_test")]
+ #[test_log::test(tokio::test)]
+ async fn test_wstring_roundtrip_through_rclpy_node() {
+ use std::io::{BufRead, BufReader};
+ use std::process::{Child, Command, Stdio};
+ use test_msgs::WStrings;
+
+ struct ChildGuard(Child);
+ impl Drop for ChildGuard {
+ fn drop(&mut self) {
+ let _ = self.0.kill();
+ let _ = self.0.wait();
+ }
+ }
+
+ const INPUT_TOPIC: &str = "/roslibrust_rosbridge_wstring_input";
+ const OUTPUT_TOPIC: &str = "/roslibrust_rosbridge_wstring_output";
+
+ let client =
+ ClientHandle::new_with_options(ClientHandleOptions::new(LOCAL_WS).timeout(TIMEOUT))
+ .await
+ .expect("Failed to construct client");
+ let publisher = client
+ .advertise::(INPUT_TOPIC)
+ .await
+ .expect("Failed to advertise");
+ let subscriber = client
+ .subscribe::(OUTPUT_TOPIC)
+ .await
+ .expect("Failed to subscribe");
+ let mut relay = Command::new("python3")
+ .arg(concat!(
+ env!("CARGO_MANIFEST_DIR"),
+ "/../roslibrust_test/tests/ros2_wstring_relay.py"
+ ))
+ .args([INPUT_TOPIC, OUTPUT_TOPIC])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::inherit())
+ .spawn()
+ .expect("Failed to start the rclpy wstring relay");
+ let mut ready = std::string::String::new();
+ BufReader::new(relay.stdout.take().unwrap())
+ .read_line(&mut ready)
+ .expect("Failed to read readiness from the rclpy wstring relay");
+ assert_eq!(
+ ready.trim(),
+ "READY",
+ "rclpy wstring relay exited before becoming ready"
+ );
+ let _relay = ChildGuard(relay);
+
+ tokio::time::sleep(Duration::from_secs(2)).await;
+ let expected = WStrings {
+ wstring_value: "ハローワールド 🌍".into(),
+ wstring_value_default1: "Hello world!".into(),
+ wstring_value_default2: "Hellö wörld!".into(),
+ wstring_value_default3: "ハローワールド".into(),
+ array_of_wstrings: ["one".into(), "二".into(), "🌍".into()],
+ bounded_sequence_of_wstrings: vec!["bounded".into(), "文字列".into()],
+ unbounded_sequence_of_wstrings: vec!["".into(), "ascii".into(), "四".into()],
+ };
+ publisher
+ .publish(&expected)
+ .await
+ .expect("Failed to publish");
+
+ let received = timeout(Duration::from_secs(5), subscriber.next())
+ .await
+ .expect("Timed out waiting for the rclpy relay");
+ assert_eq!(received, expected);
+ }
}
diff --git a/roslibrust_test/Cargo.toml b/roslibrust_test/Cargo.toml
index 46bad858..c986613a 100644
--- a/roslibrust_test/Cargo.toml
+++ b/roslibrust_test/Cargo.toml
@@ -22,6 +22,8 @@ roslibrust_zenoh = { path = "../roslibrust_zenoh" }
roslibrust_ros2 = { path = "../roslibrust_ros2" }
roslibrust_rosbridge = { path = "../roslibrust_rosbridge" }
ros-z = { git = "https://github.com/ZettaScaleLabs/ros-z.git", rev = "9bb6305" }
+roslibrust_serde_rosmsg = { workspace = true }
+serde_json = "1.0"
[[bin]]
path = "src/performance_ramp.rs"
diff --git a/roslibrust_test/src/main.rs b/roslibrust_test/src/main.rs
index 44eec6c6..0fbf1a57 100644
--- a/roslibrust_test/src/main.rs
+++ b/roslibrust_test/src/main.rs
@@ -26,12 +26,17 @@ const ROS_2_SERVICE_MSGS_PATH: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../assets/ros2_required_msgs/rcl_interfaces/service_msgs"
);
+const ROS_2_WSTRING_TEST_MSGS_PATH: &str = concat!(
+ env!("CARGO_MANIFEST_DIR"),
+ "/../assets/ros2_wstring_test_msgs"
+);
lazy_static! {
static ref ROS_2_PATHS: Vec = vec![
ROS_2_PATH.into(),
ROS_2_TEST_PATH.into(),
ROS_2_REQUIRED_PATH.into(),
- ROS_2_SERVICE_MSGS_PATH.into()
+ ROS_2_SERVICE_MSGS_PATH.into(),
+ ROS_2_WSTRING_TEST_MSGS_PATH.into()
];
}
diff --git a/roslibrust_test/src/ros2.rs b/roslibrust_test/src/ros2.rs
index 725741b3..3c69234d 100644
--- a/roslibrust_test/src/ros2.rs
+++ b/roslibrust_test/src/ros2.rs
@@ -11,6 +11,7 @@ pub mod actionlib_msgs {
use super::std_msgs;
use super::std_srvs;
use super::stereo_msgs;
+ use super::test_msgs;
use super::trajectory_msgs;
use super::visualization_msgs;
#[allow(non_snake_case)]
@@ -325,6 +326,7 @@ pub mod builtin_interfaces {
use super::std_msgs;
use super::std_srvs;
use super::stereo_msgs;
+ use super::test_msgs;
use super::trajectory_msgs;
use super::visualization_msgs;
#[allow(non_snake_case)]
@@ -414,6 +416,7 @@ pub mod diagnostic_msgs {
use super::std_msgs;
use super::std_srvs;
use super::stereo_msgs;
+ use super::test_msgs;
use super::trajectory_msgs;
use super::visualization_msgs;
#[allow(non_snake_case)]
@@ -801,6 +804,7 @@ pub mod geometry_msgs {
use super::std_msgs;
use super::std_srvs;
use super::stereo_msgs;
+ use super::test_msgs;
use super::trajectory_msgs;
use super::visualization_msgs;
#[allow(non_snake_case)]
@@ -2943,6 +2947,7 @@ pub mod nav_msgs {
use super::std_msgs;
use super::std_srvs;
use super::stereo_msgs;
+ use super::test_msgs;
use super::trajectory_msgs;
use super::visualization_msgs;
#[allow(non_snake_case)]
@@ -5498,6 +5503,7 @@ pub mod ros2_test_msgs {
use super::std_msgs;
use super::std_srvs;
use super::stereo_msgs;
+ use super::test_msgs;
use super::trajectory_msgs;
use super::visualization_msgs;
#[allow(non_snake_case)]
@@ -5759,6 +5765,49 @@ uint32 nanosec"####;
PartialEq,
)]
#[serde(crate = "::roslibrust::codegen::serde")]
+ pub struct WStrings {
+ pub r#wstring_value: ::roslibrust::codegen::WString,
+ #[default("Hello world!")]
+ pub r#wstring_value_default1: ::roslibrust::codegen::WString,
+ #[default("Hellö wörld!")]
+ pub r#wstring_value_default2: ::roslibrust::codegen::WString,
+ #[default("ハローワールド")]
+ pub r#wstring_value_default3: ::roslibrust::codegen::WString,
+ pub r#array_of_wstrings: [::roslibrust::codegen::WString; 3],
+ pub r#bounded_sequence_of_wstrings: ::std::vec::Vec<::roslibrust::codegen::WString>,
+ pub r#unbounded_sequence_of_wstrings: ::std::vec::Vec<::roslibrust::codegen::WString>,
+ }
+ impl ::roslibrust::RosMessageType for WStrings {
+ const ROS_TYPE_NAME: &'static str = "ros2_test_msgs/WStrings";
+ const MD5SUM: &'static str = "036bafa324da8e3fa41a7cb28fb90abe";
+ const DEFINITION: &'static str = r####"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"####;
+ const ROS2_HASH: &'static [u8; 32] = &[
+ 0x97, 0x46, 0xec, 0x29, 0xfa, 0x17, 0x7c, 0xc8, 0x91, 0x64, 0xe1, 0x88, 0xe0, 0xa7,
+ 0xcd, 0xae, 0x9f, 0xfb, 0x9f, 0xd0, 0xf3, 0x03, 0x5b, 0x8a, 0xa7, 0x71, 0x1f, 0x79,
+ 0x5d, 0x36, 0xca, 0x00,
+ ];
+ const ROS2_TYPE_NAME: &'static str = "ros2_test_msgs::msg::dds_::WStrings_";
+ }
+ #[allow(non_snake_case)]
+ #[allow(dead_code)]
+ #[derive(
+ :: roslibrust :: codegen :: Deserialize,
+ :: roslibrust :: codegen :: Serialize,
+ :: roslibrust :: codegen :: SmartDefault,
+ Debug,
+ Clone,
+ PartialEq,
+ )]
+ #[serde(crate = "::roslibrust::codegen::serde")]
pub struct AddTwoIntsRequest {
pub r#a: i64,
pub r#b: i64,
@@ -5828,6 +5877,7 @@ pub mod sensor_msgs {
use super::std_msgs;
use super::std_srvs;
use super::stereo_msgs;
+ use super::test_msgs;
use super::trajectory_msgs;
use super::visualization_msgs;
#[allow(non_snake_case)]
@@ -8645,6 +8695,7 @@ pub mod service_msgs {
use super::std_msgs;
use super::std_srvs;
use super::stereo_msgs;
+ use super::test_msgs;
use super::trajectory_msgs;
use super::visualization_msgs;
#[allow(non_snake_case)]
@@ -8729,6 +8780,7 @@ pub mod shape_msgs {
use super::std_msgs;
use super::std_srvs;
use super::stereo_msgs;
+ use super::test_msgs;
use super::trajectory_msgs;
use super::visualization_msgs;
#[allow(non_snake_case)]
@@ -8970,6 +9022,7 @@ pub mod std_msgs {
use super::shape_msgs;
use super::std_srvs;
use super::stereo_msgs;
+ use super::test_msgs;
use super::trajectory_msgs;
use super::visualization_msgs;
#[allow(non_snake_case)]
@@ -10577,6 +10630,7 @@ pub mod std_srvs {
use super::shape_msgs;
use super::std_msgs;
use super::stereo_msgs;
+ use super::test_msgs;
use super::trajectory_msgs;
use super::visualization_msgs;
#[allow(non_snake_case)]
@@ -10784,6 +10838,7 @@ pub mod stereo_msgs {
use super::shape_msgs;
use super::std_msgs;
use super::std_srvs;
+ use super::test_msgs;
use super::trajectory_msgs;
use super::visualization_msgs;
#[allow(non_snake_case)]
@@ -10971,6 +11026,66 @@ uint32 nanosec"####;
}
}
#[allow(unused_imports)]
+pub mod test_msgs {
+ use super::actionlib_msgs;
+ use super::builtin_interfaces;
+ use super::diagnostic_msgs;
+ use super::geometry_msgs;
+ use super::nav_msgs;
+ use super::ros2_test_msgs;
+ use super::sensor_msgs;
+ use super::service_msgs;
+ use super::shape_msgs;
+ use super::std_msgs;
+ use super::std_srvs;
+ use super::stereo_msgs;
+ use super::trajectory_msgs;
+ use super::visualization_msgs;
+ #[allow(non_snake_case)]
+ #[allow(dead_code)]
+ #[derive(
+ :: roslibrust :: codegen :: Deserialize,
+ :: roslibrust :: codegen :: Serialize,
+ :: roslibrust :: codegen :: SmartDefault,
+ Debug,
+ Clone,
+ PartialEq,
+ )]
+ #[serde(crate = "::roslibrust::codegen::serde")]
+ pub struct WStrings {
+ pub r#wstring_value: ::roslibrust::codegen::WString,
+ #[default("Hello world!")]
+ pub r#wstring_value_default1: ::roslibrust::codegen::WString,
+ #[default("Hellö wörld!")]
+ pub r#wstring_value_default2: ::roslibrust::codegen::WString,
+ #[default("ハローワールド")]
+ pub r#wstring_value_default3: ::roslibrust::codegen::WString,
+ pub r#array_of_wstrings: [::roslibrust::codegen::WString; 3],
+ pub r#bounded_sequence_of_wstrings: ::std::vec::Vec<::roslibrust::codegen::WString>,
+ pub r#unbounded_sequence_of_wstrings: ::std::vec::Vec<::roslibrust::codegen::WString>,
+ }
+ impl ::roslibrust::RosMessageType for WStrings {
+ const ROS_TYPE_NAME: &'static str = "test_msgs/WStrings";
+ const MD5SUM: &'static str = "036bafa324da8e3fa41a7cb28fb90abe";
+ const DEFINITION: &'static str = r####"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"####;
+ const ROS2_HASH: &'static [u8; 32] = &[
+ 0x5a, 0x2c, 0x8d, 0xdb, 0x20, 0x54, 0x60, 0x00, 0x23, 0xb9, 0xa1, 0x80, 0x65, 0x2f,
+ 0x2b, 0xa8, 0x95, 0x37, 0x32, 0x17, 0xca, 0xd4, 0xb1, 0xf0, 0xfc, 0x12, 0x58, 0xc9,
+ 0xe4, 0x42, 0x4a, 0x79,
+ ];
+ const ROS2_TYPE_NAME: &'static str = "test_msgs::msg::dds_::WStrings_";
+ }
+}
+#[allow(unused_imports)]
pub mod trajectory_msgs {
use super::actionlib_msgs;
use super::builtin_interfaces;
@@ -10984,6 +11099,7 @@ pub mod trajectory_msgs {
use super::std_msgs;
use super::std_srvs;
use super::stereo_msgs;
+ use super::test_msgs;
use super::visualization_msgs;
#[allow(non_snake_case)]
#[allow(dead_code)]
@@ -11548,6 +11664,7 @@ pub mod visualization_msgs {
use super::std_msgs;
use super::std_srvs;
use super::stereo_msgs;
+ use super::test_msgs;
use super::trajectory_msgs;
#[allow(non_snake_case)]
#[allow(dead_code)]
diff --git a/roslibrust_test/tests/ros1_wstring_backend_tests.rs b/roslibrust_test/tests/ros1_wstring_backend_tests.rs
new file mode 100644
index 00000000..66f92683
--- /dev/null
+++ b/roslibrust_test/tests/ros1_wstring_backend_tests.rs
@@ -0,0 +1,68 @@
+use roslibrust::traits::{Publish, Subscribe, TopicProvider};
+use roslibrust_test::ros2::ros2_test_msgs::WStrings;
+
+fn wstrings_message() -> WStrings {
+ WStrings {
+ wstring_value: "ハローワールド 🌍".into(),
+ wstring_value_default1: "Hello world!".into(),
+ wstring_value_default2: "Hellö wörld!".into(),
+ wstring_value_default3: "ハローワールド".into(),
+ array_of_wstrings: ["one".into(), "二".into(), "🌍".into()],
+ bounded_sequence_of_wstrings: vec!["bounded".into(), "文字列".into()],
+ unbounded_sequence_of_wstrings: vec!["".into(), "ascii".into()],
+ }
+}
+
+#[test]
+fn tcpros_codec_uses_utf8_ros_strings_and_round_trips_wstrings() {
+ let message = wstrings_message();
+
+ // roslibrust_ros1 and roslibrust_zenoh both use this codec when publishing.
+ let bytes = roslibrust::with_ros1_wstring_compatibility(|| {
+ roslibrust_serde_rosmsg::ser::to_vec(&message)
+ })
+ .unwrap();
+
+ // The first field has ordinary ROS 1 string encoding: its UTF-8 byte length
+ // followed by its UTF-8 bytes.
+ let encoded_string_bytes = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
+ assert_eq!(encoded_string_bytes as usize, message.wstring_value.len());
+ assert_eq!(
+ &bytes[8..8 + encoded_string_bytes as usize],
+ message.wstring_value.as_bytes()
+ );
+
+ let decoded: WStrings = roslibrust::with_ros1_wstring_compatibility(|| {
+ roslibrust_serde_rosmsg::de::from_slice(&bytes)
+ })
+ .unwrap();
+
+ assert_eq!(decoded, message);
+}
+
+#[test]
+fn rosbridge_json_represents_wstrings_as_json_strings() {
+ let json = serde_json::to_value(wstrings_message()).unwrap();
+
+ assert_eq!(json["wstring_value"], "ハローワールド 🌍");
+ assert_eq!(json["array_of_wstrings"][1], "二");
+ assert_eq!(json["bounded_sequence_of_wstrings"][1], "文字列");
+}
+
+#[tokio::test]
+async fn mock_backend_round_trips_wstrings() {
+ let backend = roslibrust::mock::MockRos::new();
+ let publisher = backend
+ .advertise::("/wstring_test")
+ .await
+ .unwrap();
+ let mut subscriber = backend
+ .subscribe::("/wstring_test")
+ .await
+ .unwrap();
+ let message = wstrings_message();
+
+ publisher.publish(&message).await.unwrap();
+
+ assert_eq!(subscriber.next().await.unwrap(), message);
+}
diff --git a/roslibrust_test/tests/ros2_codegen_tests.rs b/roslibrust_test/tests/ros2_codegen_tests.rs
index 74c098d6..146bfcd5 100644
--- a/roslibrust_test/tests/ros2_codegen_tests.rs
+++ b/roslibrust_test/tests/ros2_codegen_tests.rs
@@ -11,6 +11,30 @@ fn test_defaults() {
assert_eq!(x.f_samples, vec![-200.0, -1.0, 0.0]);
}
+#[test]
+fn wstrings_generate_for_scalar_array_and_sequence_fields() {
+ use roslibrust::codegen::WString;
+
+ let message = ros2_test_msgs::WStrings {
+ wstring_value: "ハローワールド".into(),
+ wstring_value_default1: "Hello world!".into(),
+ wstring_value_default2: "Hellö wörld!".into(),
+ wstring_value_default3: "ハローワールド".into(),
+ array_of_wstrings: ["one".into(), "二".into(), "🌍".into()],
+ bounded_sequence_of_wstrings: vec!["bounded".into(), "文字列".into()],
+ unbounded_sequence_of_wstrings: vec!["".into(), "ascii".into()],
+ };
+
+ assert_eq!(message.wstring_value, "ハローワールド");
+ assert_eq!(message.array_of_wstrings[2], WString::from("🌍"));
+ assert_eq!(message.bounded_sequence_of_wstrings[1], "文字列");
+
+ let defaults = ros2_test_msgs::WStrings::default();
+ assert_eq!(defaults.wstring_value_default1, "Hello world!");
+ assert_eq!(defaults.wstring_value_default2, "Hellö wörld!");
+ assert_eq!(defaults.wstring_value_default3, "ハローワールド");
+}
+
#[test]
fn fixed_sized_arrays() {
// Prove the default works, compiler failure here is the test
diff --git a/roslibrust_test/tests/ros2_wstring_relay.py b/roslibrust_test/tests/ros2_wstring_relay.py
new file mode 100644
index 00000000..a9ca22ff
--- /dev/null
+++ b/roslibrust_test/tests/ros2_wstring_relay.py
@@ -0,0 +1,31 @@
+"""ROS 2 node used by the gated wstring backend interoperability tests."""
+
+import sys
+
+import rclpy
+from rclpy.node import Node
+from test_msgs.msg import WStrings
+
+
+class WStringRelay(Node):
+ def __init__(self, input_topic: str, output_topic: str) -> None:
+ super().__init__("roslibrust_wstring_relay")
+ self.publisher = self.create_publisher(WStrings, output_topic, 10)
+ self.subscription = self.create_subscription(
+ WStrings, input_topic, self.publisher.publish, 10
+ )
+
+
+def main() -> None:
+ rclpy.init()
+ node = WStringRelay(sys.argv[1], sys.argv[2])
+ print("READY", flush=True)
+ try:
+ rclpy.spin(node)
+ finally:
+ node.destroy_node()
+ rclpy.shutdown()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/roslibrust_zenoh/src/lib.rs b/roslibrust_zenoh/src/lib.rs
index ee248313..c605dda6 100644
--- a/roslibrust_zenoh/src/lib.rs
+++ b/roslibrust_zenoh/src/lib.rs
@@ -243,9 +243,10 @@ impl Publish for ZenohPublisher {
let size_hint = self.capacity_hint.load(Ordering::Relaxed);
let mut bytes = Vec::with_capacity(size_hint);
- roslibrust_serde_rosmsg::to_writer_skip_length(&mut bytes, data).map_err(|e| {
- Error::SerializationError(format!("Failed to serialize message: {e:?}"))
- })?;
+ roslibrust_common::with_ros1_wstring_compatibility(|| {
+ roslibrust_serde_rosmsg::to_writer_skip_length(&mut bytes, data)
+ })
+ .map_err(|e| Error::SerializationError(format!("Failed to serialize message: {e:?}")))?;
if bytes.len() > size_hint {
self.capacity_hint.store(bytes.len(), Ordering::Relaxed);
@@ -296,8 +297,10 @@ impl Subscribe for ZenohSubscriber {
fn deserialize_payload(payload: &ZBytes, context: &str) -> Result {
// Note: Zenoh decided to not make the 4 byte length header part of the payload.
let mut reader = payload.reader();
- roslibrust_serde_rosmsg::from_reader_known_length(&mut reader, payload.len() as u32)
- .map_err(|e| Error::SerializationError(format!("Failed to deserialize {context}: {e:?}")))
+ roslibrust_common::with_ros1_wstring_compatibility(|| {
+ roslibrust_serde_rosmsg::from_reader_known_length(&mut reader, payload.len() as u32)
+ })
+ .map_err(|e| Error::SerializationError(format!("Failed to deserialize {context}: {e:?}")))
}
impl TopicProvider for ZenohClient {
@@ -310,7 +313,9 @@ impl TopicProvider for ZenohClient {
topic: impl ToGlobalTopicName,
) -> Result> {
let topic: GlobalTopicName = topic.to_global_name()?;
- let mangled_topic = mangle_topic(topic.as_ref(), MsgType::ROS_TYPE_NAME, MsgType::MD5SUM);
+ let description = roslibrust_common::ros1_message_description::();
+ let mangled_topic =
+ mangle_topic(topic.as_ref(), MsgType::ROS_TYPE_NAME, &description.md5sum);
let publisher = match self.session.declare_publisher(mangled_topic).await {
Ok(publisher) => publisher,
Err(e) => {
@@ -325,7 +330,7 @@ impl TopicProvider for ZenohClient {
DiscoveryClass::Publisher,
topic.as_ref(),
MsgType::ROS_TYPE_NAME,
- MsgType::MD5SUM,
+ &description.md5sum,
)
.await?;
@@ -342,7 +347,9 @@ impl TopicProvider for ZenohClient {
topic: impl ToGlobalTopicName,
) -> Result> {
let topic: GlobalTopicName = topic.to_global_name()?;
- let mangled_topic = mangle_topic(topic.as_ref(), MsgType::ROS_TYPE_NAME, MsgType::MD5SUM);
+ let description = roslibrust_common::ros1_message_description::();
+ let mangled_topic =
+ mangle_topic(topic.as_ref(), MsgType::ROS_TYPE_NAME, &description.md5sum);
let sub = match self.session.declare_subscriber(mangled_topic).await {
Ok(sub) => sub,
Err(e) => {
@@ -357,7 +364,7 @@ impl TopicProvider for ZenohClient {
DiscoveryClass::Subscriber,
topic.as_ref(),
MsgType::ROS_TYPE_NAME,
- MsgType::MD5SUM,
+ &description.md5sum,
)
.await?;
Ok(ZenohSubscriber {
@@ -485,9 +492,10 @@ pub struct ZenohServiceClient {
impl Service for ZenohServiceClient {
async fn call(&self, request: &T::Request) -> Result {
// Note: Zenoh decided the 4 byte length header is not part of the payload
- let request_bytes = roslibrust_serde_rosmsg::to_vec_skip_length(request).map_err(|e| {
- Error::SerializationError(format!("Failed to serialize message: {e:?}"))
- })?;
+ let request_bytes = roslibrust_common::with_ros1_wstring_compatibility(|| {
+ roslibrust_serde_rosmsg::to_vec_skip_length(request)
+ })
+ .map_err(|e| Error::SerializationError(format!("Failed to serialize message: {e:?}")))?;
let query = match self
.session
@@ -640,11 +648,12 @@ impl ServiceProvider for ZenohClient {
}
};
- let Ok(response_bytes) = roslibrust_serde_rosmsg::to_vec_skip_length(&response)
- .map_err(|e| {
- error!("Failed to serialize response: {e:?}");
- })
- else {
+ let Ok(response_bytes) = roslibrust_common::with_ros1_wstring_compatibility(|| {
+ roslibrust_serde_rosmsg::to_vec_skip_length(&response)
+ })
+ .map_err(|e| {
+ error!("Failed to serialize response: {e:?}");
+ }) else {
continue;
};