Skip to content

Commit 3cdcadc

Browse files
rosterlohazerupi
andauthored
fix!: match rosidl_buffer primitive-sequence ABI on Lyrical+ (#22)
* fix: match rosidl_buffer primitive-sequence ABI on newer distros (ros2/rosidl#942) ros2/rosidl#942 (rosidl_buffer, from Lyrical on) adds two trailing fields to *primitive* C sequence structs: <T>* data; size_t size; size_t capacity; // 0 / 8 / 16 bool is_rosidl_buffer; bool owns_rosidl_buffer; // -> struct is 32 bytes `rosidl_runtime_rs::Sequence<T>` is a single 24-byte struct for all T, so on those distros every primitive-sequence field (float64[], uint8[], ...) is at the wrong offset: silent corruption, or a segfault when a misread length is non-zero (e.g. visualization_msgs/Marker::from_rmw_message, sensor_msgs/JointState, trajectory_msgs/JointTrajectoryPoint). Message-element sequences are NOT extended by #942, so the layout is asymmetric (primitive 32, message 24) and a plain distro cfg on the generic struct would wrongly grow message sequences too. Fix: a per-element `LayoutTail` on a dedicated `SequenceLayout` trait (a supertrait of `SequenceAlloc`, so that trait stays about the C alloc functions), carried as a trailing field on `Sequence<T>`. Primitive element types set it to `BufferFlags` on distros that have the feature and `()` otherwise, gated on `ros_distro` the way rclrs handles distro ABI differences; String and message element types are always `()`. Verified on robostack-lyrical: ROS_DISTRO=lyrical gives size_of::<Sequence<u8>>() == 32 (matches C) and message-element Sequence == 24; ROS_DISTRO=jazzy gives 24 for both. Pairs with the rosidl_rust generator change that emits the `SequenceLayout` impl for generated message types. Note: `SequenceAlloc` now has `SequenceLayout` as a supertrait; hand-written impls need a matching `SequenceLayout` impl (generated code is covered by the generator template). Refs ros2-rust/ros2_rust#659, ros2/rosidl#942 Assisted-by: Claude:claude-opus-5 [Claude Code] * fix: string sequences carry the buffer flags too rosidl_runtime_c/string.h declares String__Sequence and U16String__Sequence through the same ROSIDL_RUNTIME_C__PRIMITIVE_SEQUENCE macro as the numeric primitives, so from Lyrical on they carry the two trailing rosidl_buffer flags as well. Measured on a Lyrical install: double__Sequence and String__Sequence are both 32 bytes, while a bare String stays 24 and message element sequences stay 24. Leaving String, WString, BoundedString and BoundedWString on an empty LayoutTail therefore kept every string[] field 8 bytes short, which shifts the fields after it (for example sensor_msgs/JointState.name ahead of position, velocity and effort). Gate their tail on the distro exactly like the primitives. Add a layout test that compares against repr(C) mirrors of the two C shapes, so the padding comes from the compiler rather than a hand-computed size. Assisted-by: Claude:claude-opus-5 [Claude Code] * test: check the layout against the installed C headers Compile a small C file against the real headers and let it export the sizes and offsets of the C structsas globals, then compare against those in the tests. The expected values now come from the C compiler reading primitives_sequence.h. The probe sits behind the abi_check feature, which is turned on by adding a dependency to ourselves in the dev-dependencies. This builds cc only for test targets and a plain build doesn't pull it in. Assisted-by: Claude:claude-opus-5 [Claude Code] * fix: add comment that users should not use abi_check feature * fix: pass ros_distro cfg so docs.rs shim builds can run docs.rs has no ROS_DISTRO; build.rs must read --cfg ros_distro from rustc-args the same way rclrs does. Also document that String sequences share the primitive LayoutTail. Assisted-by: Cursor:auto [Cursor Agent] * fix: use env::split_paths in the ABI probe The probe compiled under abi_check could not see Path from the cfg_if import, which broke cargo test. Match main's path-separator fix so AMENT_PREFIX_PATH works on Windows too. Assisted-by: Cursor:auto [Cursor Agent] * fix: give RmwMessage types an empty SequenceLayout by default Message-element sequences stay 24 bytes on Lyrical+, so generated RMW structs do not need a generator-emitted SequenceLayout impl. The ABI probe now also measures a C message sequence and asserts it differs from primitive sequences on Lyrical+. Assisted-by: Cursor:Grok-4.6 [Cursor Agent] --------- Co-authored-by: Mathieu David <mathieudavid@mathieudavid.org>
1 parent c443e1e commit 3cdcadc

7 files changed

Lines changed: 311 additions & 4 deletions

File tree

rosidl_runtime_rs/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,27 @@ default = []
2222
# This feature is solely for the purpose of being able to generate documentation without a ROS installation
2323
# The only intended usage of this feature is for docs.rs builders to work, and is not intended to be used by end users
2424
use_ros_shim = []
25+
# Adds the C compiler to build and link the C probe to test size/layout against the installed ROS 2 C headers
26+
# This is only intended to be used for testing, and is not intended to be used by end users
27+
abi_check = ["dep:cc"]
2528

2629
[dev-dependencies]
30+
# Trick to enable the abi_check feature for the tests, without enabling it for the library itself
31+
rosidl_runtime_rs = { path = ".", features = ["abi_check"] }
2732
# Needed for writing property tests
2833
quickcheck = "1"
2934
# Needed for testing serde support
3035
serde_json = "1"
3136

3237
[build-dependencies]
38+
# Compiles the probe that reports the layout of the installed C sequence structs
39+
cc = { version = "1", optional = true }
3340
# Needed for uploading documentation to docs.rs
3441
cfg-if = "1.0.0"
42+
# Reads the --cfg ros_distro flag under the ROS shim (matches rclrs)
43+
rustflags = "0.1"
3544

3645
[package.metadata.docs.rs]
3746
features = ["use_ros_shim"]
47+
# build.rs reads this via rustflags under use_ros_shim (same pattern as rclrs)
48+
rustc-args = ["--cfg", "ros_distro=\"humble\""]

rosidl_runtime_rs/build.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,75 @@ cfg_if::cfg_if! {
1414
);
1515
}
1616
}
17+
18+
/// Compiles the probe that reports the layout of the installed C
19+
/// sequence structs, so that the tests can check `Sequence<T>` against
20+
/// this ROS installation instead of against a Rust recreation of those
21+
/// structs.
22+
#[cfg(feature = "abi_check")]
23+
fn compile_sequence_abi_probe(ament_prefix_path_list: &str) {
24+
let mut probe = cc::Build::new();
25+
for ament_prefix_path in env::split_paths(ament_prefix_path_list) {
26+
// Iron and later nest the headers of a package one level
27+
// deeper, so offer both conventions and let the compiler pick.
28+
let include_path = ament_prefix_path.join("include");
29+
probe.include(include_path.join("rosidl_runtime_c"));
30+
probe.include(include_path.join("builtin_interfaces"));
31+
probe.include(include_path);
32+
}
33+
probe
34+
.file("src/sequence_abi.c")
35+
.compile("rosidl_rs_sequence_abi");
36+
37+
println!("cargo:rustc-cfg=has_c_abi_probe");
38+
}
1739
}
1840
}
1941

42+
// Gate the primitive-sequence layout on the ROS distro, like other distro
43+
// differences in rclrs (starting with Lyrical, primitive sequences carry extra
44+
// ABI flags; see `Sequence`/`BufferFlags`).
45+
const ROS_DISTRO: &str = "ROS_DISTRO";
46+
const KNOWN_DISTROS: &[&str] = &["humble", "jazzy", "kilted", "lyrical", "rolling"];
47+
48+
fn get_ros_distro() -> String {
49+
std::env::var(ROS_DISTRO)
50+
.or_else(|_| {
51+
if std::env::var("CARGO_FEATURE_USE_ROS_SHIM").is_ok() {
52+
rustflags::from_env()
53+
.find_map(|f| match f {
54+
rustflags::Flag::Cfg { name, value } if name.as_str() == "ros_distro" => {
55+
value
56+
}
57+
_ => None,
58+
})
59+
.ok_or_else(|| "Missing --cfg ros_distro in RUSTFLAGS".to_string())
60+
} else {
61+
Err(format!("Set {ROS_DISTRO} or use ROS shim"))
62+
}
63+
})
64+
.expect("Failed to determine ROS distro")
65+
}
66+
2067
fn main() {
68+
println!(
69+
"cargo:rustc-check-cfg=cfg(ros_distro, values(\"{}\"))",
70+
KNOWN_DISTROS.join("\", \"")
71+
);
72+
println!("cargo:rustc-cfg=ros_distro=\"{}\"", get_ros_distro());
73+
println!("cargo:rerun-if-env-changed={ROS_DISTRO}");
74+
println!("cargo:rustc-check-cfg=cfg(has_c_abi_probe)");
75+
2176
#[cfg(not(feature = "use_ros_shim"))]
2277
{
2378
let ament_prefix_path_list = get_env_var_or_abort(AMENT_PREFIX_PATH);
2479
for ament_prefix_path in env::split_paths(&ament_prefix_path_list) {
2580
let library_path = ament_prefix_path.join("lib");
2681
println!("cargo:rustc-link-search=native={}", library_path.display());
2782
}
83+
84+
#[cfg(feature = "abi_check")]
85+
compile_sequence_abi_probe(&ament_prefix_path_list);
2886
}
2987

3088
// Invalidate the built crate whenever this script changes

rosidl_runtime_rs/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
#[macro_use]
55
mod sequence;
6-
pub use sequence::{BoundedSequence, Sequence, SequenceExceedsBoundsError};
6+
pub use sequence::{BoundedSequence, BufferFlags, Sequence, SequenceExceedsBoundsError};
77

88
mod string;
99
pub use string::{BoundedString, BoundedWString, String, StringExceedsBoundsError, WString};

rosidl_runtime_rs/src/sequence.rs

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::{
99
#[cfg(feature = "serde")]
1010
mod serde;
1111

12-
use crate::traits::SequenceAlloc;
12+
use crate::traits::{SequenceAlloc, SequenceLayout};
1313

1414
/// An unbounded sequence.
1515
///
@@ -41,6 +41,21 @@ pub struct Sequence<T: SequenceAlloc> {
4141
data: *mut T,
4242
size: usize,
4343
capacity: usize,
44+
_tail: T::LayoutTail,
45+
}
46+
47+
/// Mirrors the two trailing fields of `rosidl_runtime_c__<T>__Sequence` for
48+
/// primitive element types. Starting with Lyrical, primitive sequences have
49+
/// these extra flags in their ABI; they are set/read only by the C++ side, so
50+
/// on the CPU backend both are `false` and the sequence behaves as before. See
51+
/// <https://github.com/ros2/rosidl/blob/rolling/rosidl_runtime_c/include/rosidl_runtime_c/primitives_sequence.h>.
52+
#[repr(C)]
53+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
54+
pub struct BufferFlags {
55+
/// `true` when `data` points to an `rosidl::Buffer<T>*` rather than a plain array.
56+
_is_rosidl_buffer: bool,
57+
/// `true` when the sequence's fini must destroy that buffer.
58+
_owns_rosidl_buffer: bool,
4459
}
4560

4661
/// A bounded sequence.
@@ -114,6 +129,7 @@ impl<T: SequenceAlloc> Default for Sequence<T> {
114129
data: std::ptr::null_mut(),
115130
size: 0,
116131
capacity: 0,
132+
_tail: Default::default(),
117133
}
118134
}
119135
}
@@ -312,6 +328,7 @@ impl<T: SequenceAlloc, const N: usize> Default for BoundedSequence<T, N> {
312328
data: std::ptr::null_mut(),
313329
size: 0,
314330
capacity: 0,
331+
_tail: Default::default(),
315332
},
316333
}
317334
}
@@ -398,6 +415,7 @@ impl<T: SequenceAlloc, const N: usize> IntoIterator for BoundedSequence<T, N> {
398415
data: std::ptr::null_mut(),
399416
size: 0,
400417
capacity: 0,
418+
_tail: Default::default(),
401419
},
402420
);
403421
SequenceIterator { seq, idx: 0 }
@@ -525,6 +543,16 @@ macro_rules! impl_sequence_alloc_for_primitive_type {
525543
) -> bool;
526544
}
527545

546+
// Primitive sequences carry the layout flags from Lyrical on.
547+
#[cfg(not(any(ros_distro = "humble", ros_distro = "jazzy", ros_distro = "kilted")))]
548+
impl SequenceLayout for $rust_type {
549+
type LayoutTail = crate::BufferFlags;
550+
}
551+
#[cfg(any(ros_distro = "humble", ros_distro = "jazzy", ros_distro = "kilted"))]
552+
impl SequenceLayout for $rust_type {
553+
type LayoutTail = ();
554+
}
555+
528556
impl SequenceAlloc for $rust_type {
529557
fn sequence_init(seq: &mut Sequence<Self>, size: usize) -> bool {
530558
// SAFETY: There are no special preconditions to the sequence_init function.
@@ -687,6 +715,94 @@ mod tests {
687715
}
688716
}
689717

718+
// The layout of the C structs, as reported by src/sequence_abi.c after the
719+
// C compiler read the headers of this ROS installation.
720+
#[cfg(has_c_abi_probe)]
721+
extern "C" {
722+
static rosidl_rs_primitive_sequence_size: usize;
723+
static rosidl_rs_string_sequence_size: usize;
724+
static rosidl_rs_u16string_sequence_size: usize;
725+
static rosidl_rs_message_sequence_size: usize;
726+
static rosidl_rs_sequence_data_offset: usize;
727+
static rosidl_rs_sequence_size_offset: usize;
728+
static rosidl_rs_sequence_capacity_offset: usize;
729+
static rosidl_rs_sequence_align: usize;
730+
}
731+
732+
/// `Sequence<T>` is handed to C as `rosidl_runtime_c__<T>__Sequence`, so it
733+
/// has to be the size of that struct in the installed headers. From Lyrical
734+
/// on, every type declared through ROSIDL_RUNTIME_C__PRIMITIVE_SEQUENCE
735+
/// carries two trailing flags, and that macro covers `String` and
736+
/// `U16String` as well as the numeric primitives.
737+
#[test]
738+
#[cfg(has_c_abi_probe)]
739+
fn test_sequence_size_matches_c() {
740+
// SAFETY: These are `const size_t` objects with external linkage.
741+
let (primitive, string, u16string, message) = unsafe {
742+
(
743+
rosidl_rs_primitive_sequence_size,
744+
rosidl_rs_string_sequence_size,
745+
rosidl_rs_u16string_sequence_size,
746+
rosidl_rs_message_sequence_size,
747+
)
748+
};
749+
750+
#[cfg(any(ros_distro = "humble", ros_distro = "jazzy", ros_distro = "kilted"))]
751+
assert_eq!(primitive, message);
752+
#[cfg(not(any(ros_distro = "humble", ros_distro = "jazzy", ros_distro = "kilted")))]
753+
assert_ne!(
754+
primitive, message,
755+
"Lyrical+ primitive sequences carry buffer flags; message sequences do not"
756+
);
757+
758+
assert_eq!(std::mem::size_of::<Sequence<f64>>(), primitive);
759+
assert_eq!(std::mem::size_of::<Sequence<u8>>(), primitive);
760+
assert_eq!(std::mem::size_of::<Sequence<i16>>(), primitive);
761+
assert_eq!(std::mem::size_of::<Sequence<bool>>(), primitive);
762+
assert_eq!(std::mem::size_of::<BoundedSequence<f64, 4>>(), primitive);
763+
764+
assert_eq!(std::mem::size_of::<Sequence<crate::String>>(), string);
765+
assert_eq!(
766+
std::mem::size_of::<BoundedSequence<crate::String, 4>>(),
767+
string
768+
);
769+
assert_eq!(
770+
std::mem::size_of::<Sequence<crate::BoundedString<4>>>(),
771+
string
772+
);
773+
774+
assert_eq!(std::mem::size_of::<Sequence<crate::WString>>(), u16string);
775+
assert_eq!(
776+
std::mem::size_of::<BoundedSequence<crate::WString, 4>>(),
777+
u16string
778+
);
779+
assert_eq!(
780+
std::mem::size_of::<Sequence<crate::BoundedWString<4>>>(),
781+
u16string
782+
);
783+
}
784+
785+
/// A matching size is not enough on its own, because it says nothing about
786+
/// the order of the fields C reads and writes.
787+
#[test]
788+
#[cfg(has_c_abi_probe)]
789+
fn test_sequence_field_offsets_match_c() {
790+
// SAFETY: These are `const size_t` objects with external linkage.
791+
let (data, size, capacity, align) = unsafe {
792+
(
793+
rosidl_rs_sequence_data_offset,
794+
rosidl_rs_sequence_size_offset,
795+
rosidl_rs_sequence_capacity_offset,
796+
rosidl_rs_sequence_align,
797+
)
798+
};
799+
800+
assert_eq!(std::mem::offset_of!(Sequence<f64>, data), data);
801+
assert_eq!(std::mem::offset_of!(Sequence<f64>, size), size);
802+
assert_eq!(std::mem::offset_of!(Sequence<f64>, capacity), capacity);
803+
assert_eq!(std::mem::align_of::<Sequence<f64>>(), align);
804+
}
805+
690806
#[test]
691807
fn test_empty_sequence() {
692808
assert!(Sequence::<i32>::default().is_empty());
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Reports the layout of the C structs in header files installed with the ROS 2 distribution.
2+
// rosidl_runtime_rs casts its types to those C structs so we need to make sure the sizes and
3+
// alignments match.
4+
//
5+
// Every sequence type, `String` and `U16String` included, is declared through
6+
// the ROSIDL_RUNTIME_C__PRIMITIVE_SEQUENCE macro, so one primitive sequence
7+
// stands in for the whole family, and only the string sequences need measuring
8+
// separately because their element structs have a size of their own.
9+
10+
#include <stddef.h>
11+
12+
#include <rosidl_runtime_c/primitives_sequence.h>
13+
#include <rosidl_runtime_c/string.h>
14+
#include <rosidl_runtime_c/u16string.h>
15+
#include <builtin_interfaces/msg/detail/time__struct.h>
16+
17+
typedef rosidl_runtime_c__double__Sequence primitive_sequence;
18+
19+
// A member placed after a char sits at that member's alignment, which reports
20+
// the alignment without _Alignof and the C11 that it needs.
21+
struct alignment_probe
22+
{
23+
char before;
24+
primitive_sequence sequence;
25+
};
26+
27+
// The sizes of the structs a Sequence<T> is cast to.
28+
const size_t rosidl_rs_primitive_sequence_size = sizeof(primitive_sequence);
29+
const size_t rosidl_rs_string_sequence_size = sizeof(rosidl_runtime_c__String__Sequence);
30+
const size_t rosidl_rs_u16string_sequence_size = sizeof(rosidl_runtime_c__U16String__Sequence);
31+
// Message-element sequences were not extended in rosidl#942; they stay
32+
// pointer/size/capacity on every distro, including Lyrical+.
33+
const size_t rosidl_rs_message_sequence_size = sizeof(builtin_interfaces__msg__Time__Sequence);
34+
35+
// Where C keeps the three fields both sides read and write, and how it aligns
36+
// the struct that holds them.
37+
const size_t rosidl_rs_sequence_data_offset = offsetof(primitive_sequence, data);
38+
const size_t rosidl_rs_sequence_size_offset = offsetof(primitive_sequence, size);
39+
const size_t rosidl_rs_sequence_capacity_offset = offsetof(primitive_sequence, capacity);
40+
const size_t rosidl_rs_sequence_align = offsetof(struct alignment_probe, sequence);
41+
42+
// The flags belong to the sequence structs, so the string structs themselves
43+
// keep the same size on every distro.
44+
const size_t rosidl_rs_string_size = sizeof(rosidl_runtime_c__String);
45+
const size_t rosidl_rs_u16string_size = sizeof(rosidl_runtime_c__U16String);

0 commit comments

Comments
 (0)