ZeroDDS SHM Transport 1.0 — Spec Coverage
Source: this document is also the spec. For shared memory OMG
standardizes nothing beyond the vendor-reserved locator kind
(DDSI-RTPS 2.5 §9.4.5); segment layout, synchronization and the cleanup
protocol are vendor-specific. The normative requirements live in the
## sections §2 through §7; every ### item below carries a spec
citation, a repo anchor, a test and a status.
Implementation: crates/transport-shm/src/posix.rs — segment layout,
SpSc ring buffer, crash recovery. The zero-copy loan path of §7 lives in
crates/flatdata/ and crates/zerodds-c-api/src/shm_loan_ffi.rs; see
§7.2 for the delineation.
Open items: zerodds-shm-transport-1.0.open.en.md.
§1 Scope and spec status
§1.1 Vendor-reserved locator kind
Spec: DDSI-RTPS 2.5 §9.3.2.4 — 0x00000003 to 0x01ffffff are reserved
for vendor-specific locator kinds. ZeroDDS takes 0x01F0_0001 for shared memory.
The port field is meaningless for SHM and MUST be 0; the 16 address
bytes carry the segment id.
Repo: crates/rtps/src/wire_types.rs (LocatorKind::Shm,
Locator::shm, Locator::SHM_ANY); bound in
crates/transport-shm/src/posix.rs (PosixShmTransport::local_locator,
peer_locator, the kind check in Transport::send).
Tests: crates/transport-shm/src/posix.rs::tests::locator_kind_is_the_vendor_reserved_shm_value
(asserts the numeric value 0x01F0_0001, the position in the range 0x00000003 to 0x01ffffff and
port == 0),
crates/transport-shm/src/posix.rs::tests::local_locator_is_shm_with_own_id,
crates/transport-shm/src/posix.rs::tests::send_rejects_non_shm_locator,
crates/rtps/src/wire_types.rs::tests::locator_kind_roundtrip.
Status: done
§1.2 What OMG does not standardize for shared memory
Spec: DDSI-RTPS 2.5 knows no SHM wire format, no segment layout, no synchronization model and no cleanup protocol. Cyclone DDS integrates iceoryx as a third-party component, FastDDS and RTI each have their own SHM transports. The section records the scope and states no implementation requirement.
Repo: —
Tests: —
Status: n/a (informative) — scope statement; the requirements are the items from §2 on.
§2 Segment layout
offset 0: magic: u32 BE "ZSHM"
offset 4: version: u32 LE
offset 8: capacity: u64 LE (data region, header excluded)
offset 16: head: AtomicU64 (next write offset, writer-owned)
offset 24: tail: AtomicU64 (next read offset, reader-owned)
offset 32: shutdown: AtomicU32 (0=active, 1=owner-gone)
offset 36: reserved (padding to the 64-byte cache line)
offset 64: data region [capacity bytes]
§2.1 Header fields
Spec: §2 — the creator MUST write the magic "ZSHM" big-endian at
offset 0, the layout version little-endian at offset 4 and the capacity
of the data region little-endian at offset 8. The header is 64 bytes long
so head and tail do not share a cache line with the data region.
Repo: crates/transport-shm/src/posix.rs (SHM_MAGIC,
SHM_VERSION, HEADER_BYTES = 64, initialization in the is_creator
branch of PosixShmTransport::open).
Tests: crates/transport-shm/src/posix.rs::tests::owner_writes_magic_and_version_into_the_header
(reads magic, version and capacity straight out of the mapping and
compares the magic against b"ZSHM"),
crates/transport-shm/src/posix.rs::tests::invalid_config_capacity_too_small_for_max_datagram.
Status: done
§2.2 Refusing foreign segments
Spec: §2 — on attach the opener MUST check magic and version and refuse a foreign or newer segment instead of interpreting its bytes as a ring. A layout change bumps the version.
Repo: crates/transport-shm/src/posix.rs (the else branch of
PosixShmTransport::open, PosixShmError::InvalidHeader).
Tests: crates/transport-shm/src/posix.rs::tests::attach_rejects_foreign_magic
(a segment with a foreign magic, built through the same shared_memory
primitives, is refused with InvalidHeader),
crates/transport-shm/src/posix.rs::tests::attach_rejects_unknown_version
(SHM_VERSION + 1 is refused).
Status: done
§2.3 Memory ordering of head and tail
Spec: §2 — the writer MUST store head with Release after writing
the frame bytes; the reader MUST load head with Acquire before
reading them. A frame becomes visible exactly when the reader sees the
new head value — across process boundaries too.
Repo: crates/transport-shm/src/posix.rs (SegmentLayout::head,
SegmentLayout::tail, the Ordering::Release and Ordering::Acquire
sites in push_frame and pop_frame).
Tests: crates/transport-shm/tests/l1_cross_process.rs::l1_shm_datagram_crosses_process_boundary
(two OS processes, one segment; the receiver sees the sender’s bytes),
crates/transport-shm/src/posix.rs::tests::open_concurrent_two_threads_both_bound,
crates/transport-shm/src/posix.rs::tests::many_frames_roundtrip_with_wraparound.
Status: done
§3 Frame format
Length-prefix format inside the data region:
+---------+---------+---------+---------+----- ...
| len: u32 LE | bytes [len]
+---------+---------+---------+---------+----- ...
§3.1 Length-prefix frame in the ring
Spec: §3 — every frame starts with its length as a u32
little-endian, followed by exactly that many bytes. head and tail are
monotonically growing counters; the ring position is head % capacity.
Occupancy is head - tail.
Repo: crates/transport-shm/src/posix.rs
(SegmentLayout::write_u32, read_u32, push_frame, pop_frame,
occupied_bytes).
Tests: crates/transport-shm/src/posix.rs::tests::open_owner_then_consumer_roundtrip_single_frame,
crates/transport-shm/src/posix.rs::tests::occupied_bytes_reflects_send_state,
crates/transport-shm/src/posix.rs::tests::recv_after_send_drains_occupied_bytes.
Status: done
§3.2 Padding frame at the ring end
Spec: §3 — a frame is never split across the ring end. If the
contiguous space at the end does not suffice, the writer MUST place a
padding frame with len = 0xFFFF_FFFE and jump to the start; the reader
skips it and counts it for diagnosis. If fewer than 4 bytes remain, the
rest is skipped without a marker.
Repo: crates/transport-shm/src/posix.rs (PADDING_FRAME_LEN, the
wrap branch in push_frame with the padding and the skip of a rest under
4 bytes, the padding and tail_space < 4 branches in pop_frame,
padding_frames_seen).
Tests: crates/transport-shm/src/posix.rs::tests::fill_ring_triggers_wraparound_padding
(300-byte ring, five 60-byte frames — the wrap is forced and every frame
arrives unchanged),
crates/transport-shm/src/posix.rs::tests::a_remainder_too_small_for_a_padding_header_is_skipped
(a frame ends 2 bytes before the ring end; the next one goes through),
crates/transport-shm/src/posix.rs::tests::frames_of_every_length_cross_the_ring_end_whole_and_in_order
(5 000 rounds of lengths 1–200 bytes; without the skip of the rest the run
stopped in round 16 with “shm ring full near wraparound”),
crates/transport-shm/src/posix.rs::tests::many_frames_roundtrip_with_wraparound,
crates/transport-shm/src/posix.rs::tests::corrupt_frame_counter_default_zero.
Status: done
§3.3 Length sanity check with a counter
Spec: §3 — the reader MUST discard a length above half the ring
instead of allocating the announced size, and MUST count the event. The
bound is the ring’s, not the reader’s own max_datagram: an owner that
configured a larger one sends valid frames up to it. A
silent drop is not acceptable: the counter is the ops signal for a broken
or hostile owner.
Repo: crates/transport-shm/src/posix.rs (the DoS branch in
pop_frame, corrupt_frame_counter, corrupt_frames_seen).
Tests: crates/transport-shm/src/posix.rs::tests::corrupt_length_is_dropped_and_counted
(writes half the ring + 1 as the length header into the ring; pop_frame
returns nothing and corrupt_frames_seen() reads 1),
crates/transport-shm/src/posix.rs::tests::corrupt_frame_counter_default_zero,
crates/transport-shm/src/posix.rs::tests::send_rejects_oversize_payload.
Status: done
§4 Synchronization model
§4.1 One segment per (owner, consumer) pair
Spec: §4 — a segment belongs to exactly one pair, not to one owner with multi-reader fan-out. Lock-free SpSc scales linearly with the reader count and has no global contention; SpmC as in iceoryx would slow the writer down to the slowest reader. Price: N segments for N readers.
Repo: crates/transport-shm/src/posix.rs (segment_os_id and
segment_flink are functions of (owner_id, consumer_id); ShmRole).
Tests: crates/transport-shm/src/posix.rs::tests::open_owner_then_consumer_roundtrip_single_frame,
crates/transport-shm/src/posix.rs::tests::send_rejects_wrong_peer
(a locator that is not this segment’s peer is refused),
crates/transport-shm/tests/l1_cross_process.rs::l1_shm_datagram_crosses_process_boundary.
Status: done
§4.2 Role separation of send and recv
Spec: §4 — the owner writes, the consumer reads. A send on the
consumer side and a recv on the owner side MUST fail with an error;
otherwise a second write direction would break the ring’s SpSc
assumption.
Repo: crates/transport-shm/src/posix.rs (the role check in
Transport::send and Transport::recv, ShmRole).
Tests: crates/transport-shm/src/posix.rs::tests::consumer_cannot_send,
crates/transport-shm/src/posix.rs::tests::owner_cannot_recv.
Status: done
§4.3 Idempotent open regardless of role
Spec: §4 — the SEDP match order is not predictable: the reader side may open before the writer side. Whoever comes first creates the segment; whoever comes later attaches. The role determines only send/recv, not who creates.
Repo: crates/transport-shm/src/posix.rs (open — flock, attach
attempt, otherwise create; open_owner and open_consumer share that
path).
Tests: crates/transport-shm/src/posix.rs::tests::open_consumer_first_then_owner_roundtrip,
crates/transport-shm/src/posix.rs::tests::consumer_open_succeeds_without_owner_creates_segment,
crates/transport-shm/src/posix.rs::tests::open_concurrent_two_threads_both_bound.
Status: done
§4.4 Event-driven waiting instead of a poll loop
Spec: §4 — a waiting consumer MUST be woken by the writer rather than
spinning in a loop. The head value is read before pop_frame so a
frame published in between makes the park fail immediately (no lost
wakeup).
Repo: crates/transport-shm/src/posix.rs (ring_park / ring_wake
on the low 32 bits of head, called from wait_parked and
push_frame: ring_futex on Linux, ring_os_sync on macOS from 14.4
with os_sync_wait_on_address and OS_SYNC_WAIT_ON_ADDRESS_SHARED,
resolved at run time through dlsym; wait_polling otherwise).
Tests: crates/transport-shm/src/posix.rs::tests::the_consumer_parks_instead_of_polling
(Linux and macOS),
crates/transport-shm/src/posix.rs::tests::owner_drop_signals_consumer_via_shutdown_flag
(the waiting consumer returns in under 200 ms although recv_timeout is
10 s),
crates/transport-shm/src/posix.rs::tests::interrupt_recv_returns_a_waiting_recv_at_once,
crates/transport-shm/src/posix.rs::tests::recv_times_out_when_idle,
crates/flatdata/src/posix.rs::tests::futex_notify_wakes_consumer_across_mappings.
Measured on macOS 26 (a Mac under load, load average 97), three runs:
100 waiting consumers 0.3 % of a core; wake latency p50 22 to 24 µs
against 63 to 69 µs over UDP loopback in the same run. With the
sleep-poll a measurement on the same Mac gave 4.45 % of a core per
consumer and p50 177 µs.
Big-endian Linux parks on the same low 32 bits of head, at byte 20
there (head_word); green on s390x under qemu-user (every
transport-shm test, Linux test host). Windows waits on a named auto-reset event
per segment (RingEvent) the owner sets after every frame; in nightly
6555 (windows-smoke, 81d8500d495)
interrupt_recv_returns_a_waiting_recv_at_once and
bell::tests::a_ring_wakes_the_parked_receiver_and_none_is_lost ran
green on Windows.
Status: partial — macOS before 14.4 waits by sleep-poll.
§4.5 Backpressure on a full ring
Spec: §4 — if the ring is full because the reader cannot keep up,
send MUST return a named error after a bounded spin phase. Blocking
forever and discarding quietly are both unacceptable.
Repo: crates/transport-shm/src/posix.rs (SPIN_LIMIT = 1024, the
two SendError::Io branches in push_frame).
Tests: crates/transport-shm/src/posix.rs::tests::full_ring_returns_an_error_instead_of_dropping
(512-byte ring, 128-byte frames, no consumer drain; the error text names
“ring full”),
crates/transport-shm/src/posix.rs::tests::send_rejects_oversize_payload.
Status: done
§5 Cleanup semantics
§5.1 Predictable os_id
Spec: §5 — the segment name MUST be a pure function of the pair so a
zombie segment can be found again after a crash. The name starts with /
and stays under the platform limit (macOS PSHMNAMLEN = 31); the
direction (A,B) differs from (B,A). The name MUST depend on every
byte of both ids: where it has to be shorter than the ids in hex (macOS),
it carries a fixed hash over both. A slice of the ids is not enough, since
the entity ids repeat between participants: every participant numbers its
endpoints from the same start.
Repo: crates/transport-shm/src/posix.rs (segment_os_id,
segment_os_id_in, fnv1a_128, segment_flink, DEFAULT_FLINK_DIR).
Tests: crates/transport-shm/src/posix.rs::tests::segment_os_id_is_deterministic_and_within_platform_limit
(the same pair yields the same name, the reversed pair a different one,
length under 31 on macOS),
crates/transport-shm/src/posix.rs::tests::short_names_of_pairs_that_differ_only_in_the_prefix_differ
(the short form, on every platform: two pairs with the same entity ids and
other participants get different names),
crates/transport-shm/src/posix.rs::tests::open_owner_then_consumer_roundtrip_single_frame.
Status: done
§5.2 Crash recovery
Spec: §5 — before creating a segment a zombie of the same name MUST
be removed (shm_unlink, idempotent — ENOENT is ignored), otherwise
/dev/shm leaks on the Nth restart. A leftover flink file must not make
the open fail.
Repo: crates/transport-shm/src/posix.rs (shm_unlink_by_os_id, the
create branch in open with a preceding remove_file on the flink).
Tests: crates/transport-shm/src/posix.rs::tests::open_recovers_from_stale_flink_of_crashed_owner
(a crashed owner’s flink file points at a segment that no longer exists;
open_owner cleans up and a frame passes through afterwards),
crates/transport-shm/src/posix.rs::tests::consumer_open_succeeds_without_owner_creates_segment.
Status: done
§5.3 Shutdown flag
Spec: §5 — the owner MUST set shutdown = 1 on drop (release store).
The consumer checks the flag after every empty poll and returns a
targeted error instead of waiting out recv_timeout. Frames published
before the drop still arrive — the check sits behind pop_frame.
Repo: crates/transport-shm/src/posix.rs (SegmentLayout::shutdown,
Drop for PosixShmTransport, the shutdown check in wait_for_frame).
Tests: crates/transport-shm/src/posix.rs::tests::owner_drop_signals_consumer_via_shutdown_flag
(measures the return time against a 10 s timeout and checks the error
text for “owner”),
crates/transport-shm/src/posix.rs::tests::posix_shm_error_display_covers_all_variants.
Status: done
§5.4 Race protection on create
Spec: §5 — concurrent openers of the same os_id MUST be serialized
across threads and processes, otherwise one removes the segment the other
just created. The lock is released automatically when the handle closes
or the process dies (the same crash resilience as the segment itself).
Repo: crates/transport-shm/src/posix.rs (acquire_flock_excl —
flock(LOCK_EX) on Unix, LockFileEx with LOCKFILE_EXCLUSIVE_LOCK on
Windows —, FlockGuard, the sentinel file <flink>.lock).
Tests: crates/transport-shm/src/posix.rs::tests::open_concurrent_two_threads_both_bound
(two threads open the same os_id concurrently; both end up validly
bound),
crates/transport-shm/src/posix.rs::tests::users_in_different_flink_directories_share_the_segment
(owner and consumer look the segment up in different flink directories,
under another TMPDIR say; the second attaches to the held segment
instead of removing it and making its own).
Status: done
§5.5 Liveness lock and orphaned segments
Spec: §5 — every user of a segment, owner and consumer alike, MUST
hold a shared flock on the segment for as long as it uses it. The kernel
releases it when the process ends, also by SIGKILL; a segment without a
lock holder is orphaned. An opener that finds an orphaned segment under
its pair’s name MUST remove it and create it anew instead of taking it
over: its ring holds the frames of the ended session. Orphaned segments
nobody opens again are removed by a sweep over /dev/shm. It leaves
alone segments younger than a minimum age (which covers the gap between
the creation and the creator’s lock) and segments of another layout
version. Version 2 marks this protocol: a version-1 user holds no lock,
so its live segments would look orphaned.
Repo: crates/transport-shm/src/posix.rs (hold_segment,
SegmentHold, sweep_orphaned_segments, SweepReport,
sweep_stale_pair_files for the .lock files and, on Linux, the flinks
of pairs whose segment is gone — they stayed after every process that did
not end cleanly (a test chain on a MacBook Air: 3 504 .lock files in one domain
directory); lock_pair checks after locking that the path still names
the locked file, SHM_VERSION = 2; a ring with a blank header, whose creator ended before
it wrote the header, counts as removable, since no attacher accepts it),
crates/transport-shm/src/bell.rs (a participant’s doorbell: a shared
flock on its file in /dev/shm, sweep_orphaned_doorbells);
crates/dcps/src/same_host_shm.rs (sweep_orphaned_segments_once: once
per process when a participant with transport.same_host_shm starts,
rings and doorbells, then per flink directory the pair files, minimum age
60 s, event shm.orphaned_segments_removed).
Tests: crates/transport-shm/src/posix.rs::tests::an_orphaned_segment_is_made_anew_not_attached
(a process of its own opens the owner, publishes a frame and ends without
a drop; the consumer then finds an empty ring and receives only the frame
of the new session),
crates/transport-shm/src/posix.rs::tests::the_sweep_removes_orphaned_segments_and_keeps_held_ones
(the orphaned segment stays below the minimum age and is removed above
it; a held segment stays, and a frame passes afterwards; a segment with a
blank header is removed),
crates/transport-shm/src/bell.rs::tests::the_sweep_removes_orphaned_bells_and_keeps_held_ones,
crates/transport-shm/src/posix.rs::tests::the_pair_file_sweep_removes_lone_lock_files_and_keeps_the_rest,
crates/transport-shm/src/posix.rs::tests::a_lock_taken_on_a_removed_lock_file_is_taken_anew.
Measured on the Linux test host: two participants ended by kill -9; the start of a
third after 60 s removed their 2 rings and 2 doorbells (removed: 4).
Before, the doorbell of every process that did not end cleanly stayed
(138 after a day of probes).
On macOS and the other Unixes without shared memory in the file system
the lock is on the pair’s flink, and sweep_orphaned_flinks finds
orphaned segments through the flink directory (dcps: once per directory,
that is per host and domain). A doorbell leaves a marker <name>.bell
there on which its participant holds the lock; the sweep removes bell and
marker when nobody holds it. Tests
the_flink_sweep_removes_orphaned_segments_and_keeps_held_ones and
bell.rs::tests::the_flink_sweep_removes_orphaned_bells_and_keeps_held_ones,
green on macOS 26; an_orphaned_segment_is_made_anew_not_attached runs on every
Unix. Windows: §6.3.
Status: done
§6 Platform support
§6.1 Linux and macOS
Spec: §6 — Linux is the primary platform, macOS is supported. On
macOS the PSHMNAMLEN limit of 31 characters applies to the segment
name; the name is truncated accordingly there.
Repo: crates/transport-shm/src/posix.rs
(the #[cfg(target_os = "macos")] branch in segment_os_id,
#[cfg(any(target_os = "linux", target_os = "macos"))] for
shm_unlink_by_os_id, #[cfg(unix)] for acquire_flock_excl),
crates/transport-shm/src/mapping.rs (a mapping holds no descriptor:
shm_open, ftruncate, mmap, then close. With shared_memory one
stayed open per mapping, two rings and a doorbell per peer; a process of
a multi-process test chain ran out at macOS’ default limit of 256 descriptors, 62 of
them shared-memory objects. What stays per peer is the liveness lock of
each of the two rings).
Tests: crates/transport-shm/src/mapping.rs::tests::a_mapping_holds_no_file_descriptor
(100 mappings, the number of open descriptors unchanged in a process of
its own; macOS 26),
crates/transport-shm/src/mapping.rs::tests::create_open_share_and_the_owner_unlinks,
crates/transport-shm/src/posix.rs::tests::segment_os_id_is_deterministic_and_within_platform_limit
(checks the 31-character limit under cfg(target_os = "macos")),
crates/transport-shm/tests/l1_cross_process.rs::l1_shm_datagram_crosses_process_boundary,
crates/transport-shm/src/posix.rs::tests::open_owner_then_consumer_roundtrip_single_frame.
Status: done
§6.2 Windows
Spec: §6 — on Windows CreateFileMapping supplies the mappings, the
create race is serialized via LockFileEx. The transport MUST run there
with the same test suite as on Unix.
Repo: crates/transport-shm/src/posix.rs (the #[cfg(windows)]
variant of acquire_flock_excl with
LockFileEx/LOCKFILE_EXCLUSIVE_LOCK, the
#[cfg(not(any(target_os = "linux", target_os = "macos")))] no-op of
shm_unlink_by_os_id); the CI job windows-smoke in .gitlab-ci.yml
calls cargo test -p zerodds-transport-shm on the Windows runner.
Tests: crates/transport-shm/src/posix.rs::tests::open_concurrent_two_threads_both_bound
(the test that drives the LockFileEx path on Windows),
crates/transport-shm/src/posix.rs::tests::open_recovers_from_stale_flink_of_crashed_owner.
Status: partial — in nightly 6555 (windows-smoke, 81d8500d495)
39 of 40 transport-shm lib tests ran green on Windows; the failing one
is §6.3.
§6.3 Evidence for the Windows cleanup semantics
Spec: §6 — that a crashed owner leaves no mapping with its session’s frames on Windows MUST be shown by a test on the Windows runner, not merely asserted by the documentation.
Repo: crates/transport-shm/src/posix.rs (the #[cfg(windows)]
variant of hold_segment with LockFileEx on the flink, as the macOS
lock; discard_orphaned_segment removes the backing file). The earlier
assumption that Windows releases the mapping with its last handle does
not hold for shared_memory: it puts every mapping on a file
%TEMP%\shared_memory-rs\<name> that outlives the process (the
crate’s file windows.rs, function new_map). Measured in nightly 6555: the test found 29 bytes of the
ended session in the ring.
Tests: crates/transport-shm/src/posix.rs::tests::an_orphaned_segment_is_made_anew_not_attached
(a child process opens the owner, publishes a frame and ends without a
drop; the same name then opens with an empty ring). Runs on every
platform, on Windows in the windows-smoke job.
Status: partial — the lock is built and clippy-checked for
x86_64-pc-windows-gnu; it has not run, the next windows-smoke run
measures it.
§6.4 no_std
Spec: §6 — the SHM transport is std-only. shm_open, mmap,
flock and the flink files are OS calls; a no_std target without an
operating system has no equivalent. The crate is therefore gated behind
#[cfg(feature = "std")].
Repo: —
Tests: —
Status: open — the transport runs with std only
(crates/transport-shm/src/lib.rs: #[cfg(feature = "std")] pub mod posix):
shm_open, mmap and flock are POSIX calls. A target without POSIX would need a slot store
over the memory its platform shares between processes or cores; there is
none.
§7 Zero-copy loan (same host)
§7.1 Loan path writer → reader
Spec: §7 — for samples on the same host a path MUST exist that avoids a staging copy: the writer receives a pointer into a shared-memory slot, fills it, and the commit finalizes the slot in place. A reader on the same host maps the same segment and reads the slot through its own pointer — the bytes never cross the network and are never re-copied.
Repo: crates/flatdata/src/posix.rs (PosixSlotAllocator,
reserve_slot, slot_data_ptr, commit_in_place, slot_read_ptr,
next_unread_slot, mark_read);
crates/zerodds-c-api/src/shm_loan_ffi.rs (try_loan, try_commit,
try_take, zerodds_dw_enable_shm_loan, zerodds_dr_enable_shm,
zerodds_dr_take_shm, zerodds_dr_release_shm).
Tests: crates/zerodds-c-api/tests/shm_loan_e2e.rs::shm_loan_writer_to_reader_zero_copy
(measures the loan path itself, not its setup: the test writes the
payload straight into the loaned pointer, commits in place and asserts on
the reader side with assert_ne! that its read pointer carries a
different address than the writer pointer — its own mapping of the same
segment — and that the bytes are equal; then release, NoData on the
next take and a second sample through the same ring),
crates/zerodds-c-api/tests/shm_loan_e2e.rs::shm_loan_runtime_path_writer_to_reader_zero_copy
(the same path over the runtime FFI the ROS 2 RMW uses),
crates/zerodds-c-api/tests/shm_loan_e2e.rs::loan_without_shm_falls_back_to_heap
(without an enabled loan the heap path stays),
crates/flatdata/src/posix.rs::tests::write_read_through_shm
(slot backend without the FFI). The flatdata-loan feature is in
default of crates/zerodds-c-api/Cargo.toml; the tests run in the
default run, not behind a switch.
Status: done
§7.2 Delineation: crates/transport-shm copies
Spec: §7 — the ring of §2 through §5 is not a zero-copy path:
push_frame copies the payload into the ring region, pop_frame copies
it out into a Vec. The ring carries RTPS datagrams between processes;
the loan path of §7.1 carries sample bytes without a copy. The two
mechanisms are separate and are not substituted for each other. The
section records the delineation and states no implementation requirement
of its own.
Repo: —
Tests: —
Status: n/a (informative) — boundary between ring and loan path; the loan path is item §7.1.
Audit-Status
18 done / 3 partial / 1 open / 0 blocked / 2 n/a (informative).
Test run: cargo test -p zerodds-transport-shm — 27 tests (lib) + 1 test
(tests/l1_cross_process.rs) green, 0 failed;
cargo test -p zerodds-c-api --test shm_loan_e2e — 5 tests green, 0
failed. Measured on macOS 15 (Rust host aarch64-apple-darwin); the
Windows run of §6.2 and the Linux futex path of §4.4 are not included.
Open items: zerodds-shm-transport-1.0.open.en.md.
ZeroDDS-SHM-Transport 1.0 — Spec-Coverage
Quelle: dieses Dokument ist zugleich die Spec. Für Shared Memory
normiert OMG nichts außer dem vendor-reservierten Locator-Kind
(DDSI-RTPS 2.5 §9.4.5); Segment-Layout, Synchronisation und
Cleanup-Protokoll sind vendor-spezifisch. Die normativen Anforderungen
stehen in den ##-Sektionen §2 bis §7; jedes ###-Item darunter trägt
Spec-Zitat, Repo-Anker, Test und Status.
Implementation: crates/transport-shm/src/posix.rs — Segment-Layout,
SpSc-Ringbuffer, Crash-Recovery. Der Zero-Copy-Loan-Pfad aus §7 liegt in
crates/flatdata/ und crates/zerodds-c-api/src/shm_loan_ffi.rs; siehe
§7.2 zur Abgrenzung.
Offene Punkte: zerodds-shm-transport-1.0.open.md.
§1 Scope und Spec-Status
§1.1 Vendor-reservierter Locator-Kind
Spec: DDSI-RTPS 2.5 §9.3.2.4 — 0x00000003 bis 0x01ffffff sind für
vendor-spezifische Locator-Kinds reserviert. ZeroDDS belegt 0x01F0_0001 für
Shared Memory. Das Port-Feld ist für SHM bedeutungslos und MUSS 0 sein;
die 16 Adress-Bytes tragen die Segment-ID.
Repo: crates/rtps/src/wire_types.rs (LocatorKind::Shm,
Locator::shm, Locator::SHM_ANY); gebunden in
crates/transport-shm/src/posix.rs (PosixShmTransport::local_locator,
peer_locator, Kind-Prüfung in Transport::send).
Tests: crates/transport-shm/src/posix.rs::tests::locator_kind_is_the_vendor_reserved_shm_value
(prüft den Zahlwert 0x01F0_0001, die Lage im Bereich 0x00000003 bis 0x01ffffff und port == 0),
crates/transport-shm/src/posix.rs::tests::local_locator_is_shm_with_own_id,
crates/transport-shm/src/posix.rs::tests::send_rejects_non_shm_locator,
crates/rtps/src/wire_types.rs::tests::locator_kind_roundtrip.
Status: done
§1.2 Was OMG für Shared Memory nicht normiert
Spec: DDSI-RTPS 2.5 kennt kein SHM-Wire-Format, kein Segment-Layout, kein Synchronisations-Modell und kein Cleanup-Protokoll. Cyclone DDS bindet iceoryx als Fremdkomponente ein, FastDDS und RTI haben je eigene SHM-Transporte. Die Sektion hält den Scope fest und stellt keine Implementierungs-Anforderung.
Repo: —
Tests: —
Status: n/a (informative) — Abgrenzung des Geltungsbereichs; die Anforderungen stehen in den Items ab §2.
§2 Segment-Layout
offset 0: magic: u32 BE "ZSHM"
offset 4: version: u32 LE
offset 8: capacity: u64 LE (Daten-Region, ohne Header)
offset 16: head: AtomicU64 (nächster Schreib-Offset, Writer-owned)
offset 24: tail: AtomicU64 (nächster Lese-Offset, Reader-owned)
offset 32: shutdown: AtomicU32 (0=active, 1=owner-gone)
offset 36: reserved (Padding zur 64-Byte-Cache-Line)
offset 64: data-region [capacity bytes]
§2.1 Header-Felder
Spec: §2 — der Ersteller MUSS Magic "ZSHM" big-endian auf Offset 0,
die Layout-Version little-endian auf Offset 4 und die Kapazität der
Daten-Region little-endian auf Offset 8 schreiben. Der Header ist 64 Byte
lang, damit head und tail nicht mit der Daten-Region in dieselbe
Cache-Line fallen.
Repo: crates/transport-shm/src/posix.rs (SHM_MAGIC,
SHM_VERSION, HEADER_BYTES = 64, Initialisierung im is_creator-Zweig
von PosixShmTransport::open).
Tests: crates/transport-shm/src/posix.rs::tests::owner_writes_magic_and_version_into_the_header
(liest Magic, Version und Kapazität direkt aus dem Mapping und vergleicht
das Magic gegen b"ZSHM"),
crates/transport-shm/src/posix.rs::tests::invalid_config_capacity_too_small_for_max_datagram.
Status: done
§2.2 Zurückweisung fremder Segmente
Spec: §2 — beim Attach MUSS der Opener Magic und Version prüfen und ein fremdes oder neueres Segment ablehnen, statt seine Bytes als Ring zu interpretieren. Eine Layout-Änderung erhöht die Version.
Repo: crates/transport-shm/src/posix.rs (else-Zweig von
PosixShmTransport::open, PosixShmError::InvalidHeader).
Tests: crates/transport-shm/src/posix.rs::tests::attach_rejects_foreign_magic
(Segment mit fremdem Magic, angelegt über dieselben
shared_memory-Primitiven, wird mit InvalidHeader abgelehnt),
crates/transport-shm/src/posix.rs::tests::attach_rejects_unknown_version
(SHM_VERSION + 1 wird abgelehnt).
Status: done
§2.3 Memory-Ordering von head und tail
Spec: §2 — der Writer MUSS head mit Release speichern, nachdem er
die Frame-Bytes geschrieben hat; der Reader MUSS head mit Acquire
laden, bevor er sie liest. Sichtbar wird ein Frame damit genau dann, wenn
der Reader den neuen head-Wert sieht — auch über Prozessgrenzen hinweg.
Repo: crates/transport-shm/src/posix.rs
(SegmentLayout::head, SegmentLayout::tail, die Ordering::Release-
und Ordering::Acquire-Stellen in push_frame und pop_frame).
Tests: crates/transport-shm/tests/l1_cross_process.rs::l1_shm_datagram_crosses_process_boundary
(zwei OS-Prozesse, ein Segment; der Empfänger sieht die Bytes des
Senders),
crates/transport-shm/src/posix.rs::tests::open_concurrent_two_threads_both_bound,
crates/transport-shm/src/posix.rs::tests::many_frames_roundtrip_with_wraparound.
Status: done
§3 Frame-Format
Length-Prefix-Format innerhalb der Daten-Region:
+---------+---------+---------+---------+----- ...
| len: u32 LE | bytes [len]
+---------+---------+---------+---------+----- ...
§3.1 Length-Prefix-Frame im Ring
Spec: §3 — jeder Frame beginnt mit seiner Länge als u32 little-endian,
gefolgt von genau so vielen Bytes. head und tail sind monoton
wachsende Zähler; die Position im Ring ist head % capacity. Die
Belegung ist head - tail.
Repo: crates/transport-shm/src/posix.rs
(SegmentLayout::write_u32, read_u32, push_frame, pop_frame,
occupied_bytes).
Tests: crates/transport-shm/src/posix.rs::tests::open_owner_then_consumer_roundtrip_single_frame,
crates/transport-shm/src/posix.rs::tests::occupied_bytes_reflects_send_state,
crates/transport-shm/src/posix.rs::tests::recv_after_send_drains_occupied_bytes.
Status: done
§3.2 Padding-Frame am Ring-Ende
Spec: §3 — ein Frame wird nie über das Ring-Ende hinweg gesplittet.
Reicht der zusammenhängende Platz am Ende nicht, MUSS der Writer einen
Padding-Frame mit len = 0xFFFF_FFFE setzen und an den Anfang springen;
der Reader überspringt ihn und zählt ihn zur Diagnose mit. Bleiben unter
4 Byte übrig, wird der Rest ohne Marker übersprungen.
Repo: crates/transport-shm/src/posix.rs (PADDING_FRAME_LEN,
Wrap-Zweig in push_frame mit Padding und Überspringen eines Rests unter
4 Byte, Padding- und tail_space < 4-Zweig in pop_frame,
padding_frames_seen).
Tests: crates/transport-shm/src/posix.rs::tests::fill_ring_triggers_wraparound_padding
(300-Byte-Ring, fünf 60-Byte-Frames — der Wrap ist erzwungen und jeder
Frame kommt unverändert an),
crates/transport-shm/src/posix.rs::tests::a_remainder_too_small_for_a_padding_header_is_skipped
(ein Frame endet 2 Byte vor dem Ring-Ende; der nächste geht durch),
crates/transport-shm/src/posix.rs::tests::frames_of_every_length_cross_the_ring_end_whole_and_in_order
(5 000 Runden mit Längen 1–200 Byte; ohne das Überspringen des Rests
brach der Lauf in Runde 16 mit „shm ring full near wraparound“ ab),
crates/transport-shm/src/posix.rs::tests::many_frames_roundtrip_with_wraparound,
crates/transport-shm/src/posix.rs::tests::corrupt_frame_counter_default_zero.
Status: done
§3.3 Längen-Sanity-Check mit Zähler
Spec: §3 — der Reader MUSS eine Länge oberhalb der halben Ringgröße
verwerfen, statt die angekündigte Größe zu allokieren, und den Vorgang
zählen. Die Schranke ist die des Rings, nicht das eigene max_datagram:
ein Owner, der ein größeres konfiguriert hat, schickt gültige Frames bis
dorthin. Ein stiller Drop ist nicht zulässig: der Zähler ist das
Ops-Signal für einen defekten oder bösartigen Owner.
Repo: crates/transport-shm/src/posix.rs (DoS-Zweig in pop_frame,
corrupt_frame_counter, corrupt_frames_seen).
Tests: crates/transport-shm/src/posix.rs::tests::corrupt_length_is_dropped_and_counted
(schreibt halbe Ringgröße + 1 als Längen-Header in den Ring; pop_frame
liefert nichts und corrupt_frames_seen() steht auf 1),
crates/transport-shm/src/posix.rs::tests::corrupt_frame_counter_default_zero,
crates/transport-shm/src/posix.rs::tests::send_rejects_oversize_payload.
Status: done
§4 Synchronisations-Modell
§4.1 Ein Segment pro (Owner, Consumer)-Paar
Spec: §4 — ein Segment gehört genau einem Paar, nicht einem Owner mit Multi-Reader-Fan-out. Lock-freies SpSc skaliert linear mit der Reader-Zahl und hat keine globale Contention; SpmC wie bei iceoryx würde den Writer am langsamsten Reader bremsen. Preis: N Segmente bei N Readern.
Repo: crates/transport-shm/src/posix.rs (segment_os_id und
segment_flink sind Funktionen von (owner_id, consumer_id);
ShmRole).
Tests: crates/transport-shm/src/posix.rs::tests::open_owner_then_consumer_roundtrip_single_frame,
crates/transport-shm/src/posix.rs::tests::send_rejects_wrong_peer
(ein Locator, der nicht der Peer dieses Segments ist, wird abgewiesen),
crates/transport-shm/tests/l1_cross_process.rs::l1_shm_datagram_crosses_process_boundary.
Status: done
§4.2 Rollen-Trennung von send und recv
Spec: §4 — der Owner schreibt, der Consumer liest. Ein send auf der
Consumer-Seite und ein recv auf der Owner-Seite MÜSSEN mit einem Fehler
scheitern; sonst würde eine zweite Schreibrichtung die SpSc-Annahme des
Rings brechen.
Repo: crates/transport-shm/src/posix.rs (Rollen-Prüfung in
Transport::send und Transport::recv, ShmRole).
Tests: crates/transport-shm/src/posix.rs::tests::consumer_cannot_send,
crates/transport-shm/src/posix.rs::tests::owner_cannot_recv.
Status: done
§4.3 Idempotentes Öffnen unabhängig von der Rolle
Spec: §4 — die SEDP-Match-Reihenfolge ist nicht vorhersagbar: die Reader-Seite kann vor der Writer-Seite öffnen. Wer zuerst kommt, legt das Segment an; wer später kommt, hängt sich an. Die Rolle bestimmt nur send/recv, nicht wer erzeugt.
Repo: crates/transport-shm/src/posix.rs (open — flock, Attach-
Versuch, sonst Create; open_owner und open_consumer teilen sich
denselben Pfad).
Tests: crates/transport-shm/src/posix.rs::tests::open_consumer_first_then_owner_roundtrip,
crates/transport-shm/src/posix.rs::tests::consumer_open_succeeds_without_owner_creates_segment,
crates/transport-shm/src/posix.rs::tests::open_concurrent_two_threads_both_bound.
Status: done
§4.4 Ereignisgetriebenes Warten statt Poll-Schleife
Spec: §4 — ein wartender Consumer MUSS vom Writer geweckt werden,
statt in einer Schleife zu pollen. Der head-Wert wird vor pop_frame
gelesen, damit ein zwischenzeitlich publizierter Frame das Parken sofort
scheitern lässt (kein verlorener Wakeup).
Repo: crates/transport-shm/src/posix.rs (ring_park / ring_wake
auf den unteren 32 Bit von head, aufgerufen in wait_parked und
push_frame: auf Linux ring_futex, auf macOS ab 14.4 ring_os_sync
mit os_sync_wait_on_address und OS_SYNC_WAIT_ON_ADDRESS_SHARED, zur
Laufzeit über dlsym aufgelöst; sonst wait_polling).
Tests: crates/transport-shm/src/posix.rs::tests::the_consumer_parks_instead_of_polling
(Linux und macOS),
crates/transport-shm/src/posix.rs::tests::owner_drop_signals_consumer_via_shutdown_flag
(der wartende Consumer kehrt in unter 200 ms zurück, obwohl das
recv_timeout 10 s beträgt),
crates/transport-shm/src/posix.rs::tests::interrupt_recv_returns_a_waiting_recv_at_once,
crates/transport-shm/src/posix.rs::tests::recv_times_out_when_idle,
crates/flatdata/src/posix.rs::tests::futex_notify_wakes_consumer_across_mappings.
Gemessen auf macOS 26 (Mac unter Last, Lastmittel 97), drei Läufe: 100
wartende Consumer 0,3 % eines Kerns; Wecklatenz p50 22 bis 24 µs gegen
63 bis 69 µs über UDP-Loopback im selben Lauf. Mit dem Sleep-Poll hatte
eine Messung am selben Mac 4,45 % eines Kerns je Consumer und p50 177 µs
ergeben.
Big-Endian-Linux parkt auf denselben unteren 32 Bit von head, die dort
auf Byte 20 liegen (head_word); auf s390x unter qemu-user grün (alle
transport-shm-Tests, Linux-Testhost). Windows wartet auf einem benannten
Auto-Reset-Event je Segment (RingEvent), das der Owner nach jedem Frame
setzt; im Nightly 6555 (windows-smoke, 81d8500d495) liefen
interrupt_recv_returns_a_waiting_recv_at_once und
bell::tests::a_ring_wakes_the_parked_receiver_and_none_is_lost auf
Windows grün.
Status: partial — macOS vor 14.4 wartet per Sleep-Poll.
§4.5 Backpressure bei vollem Ring
Spec: §4 — ist der Ring voll, weil der Reader nicht nachkommt, MUSS
send nach einer begrenzten Spin-Phase mit einem benannten Fehler
zurückkehren. Blockieren ohne Ende und stilles Verwerfen sind beide nicht
zulässig.
Repo: crates/transport-shm/src/posix.rs (SPIN_LIMIT = 1024, die
beiden SendError::Io-Zweige in push_frame).
Tests: crates/transport-shm/src/posix.rs::tests::full_ring_returns_an_error_instead_of_dropping
(512-Byte-Ring, 128-Byte-Frames, kein Consumer-Drain; der Fehlertext
nennt „ring full”),
crates/transport-shm/src/posix.rs::tests::send_rejects_oversize_payload.
Status: done
§5 Cleanup-Semantik
§5.1 Vorhersagbare os_id
Spec: §5 — der Segment-Name MUSS eine reine Funktion des Paares sein,
damit ein Zombie-Segment nach einem Crash wiedergefunden werden kann. Der
Name beginnt mit / und bleibt unter dem Plattform-Limit (macOS
PSHMNAMLEN = 31); die Richtung (A,B) ist von (B,A) verschieden.
Der Name MUSS von jedem Byte beider IDs abhängen: wo er kürzer sein muss
als die IDs in Hex (macOS), trägt er einen festen Hash über beide. Ein
Ausschnitt der IDs reicht nicht, denn die Entity-IDs wiederholen sich
zwischen Teilnehmern: jeder Teilnehmer zählt seine Endpunkte vom selben
Anfang.
Repo: crates/transport-shm/src/posix.rs (segment_os_id,
segment_os_id_in, fnv1a_128, segment_flink, DEFAULT_FLINK_DIR).
Tests: crates/transport-shm/src/posix.rs::tests::segment_os_id_is_deterministic_and_within_platform_limit
(gleiches Paar liefert denselben Namen, umgekehrtes Paar einen anderen,
Länge unter 31 auf macOS),
crates/transport-shm/src/posix.rs::tests::short_names_of_pairs_that_differ_only_in_the_prefix_differ
(die kurze Form, auf jeder Plattform: zwei Paare mit gleichen Entity-IDs
und anderen Teilnehmern bekommen verschiedene Namen),
crates/transport-shm/src/posix.rs::tests::open_owner_then_consumer_roundtrip_single_frame.
Status: done
§5.2 Crash-Recovery
Spec: §5 — vor dem Anlegen eines Segments MUSS ein Zombie desselben
Namens entfernt werden (shm_unlink, idempotent — ENOENT wird
ignoriert), sonst leckt /dev/shm beim N-ten Neustart. Eine
zurückgebliebene Flink-Datei darf das Öffnen nicht scheitern lassen.
Repo: crates/transport-shm/src/posix.rs (shm_unlink_by_os_id,
Create-Zweig in open mit vorherigem remove_file auf den Flink).
Tests: crates/transport-shm/src/posix.rs::tests::open_recovers_from_stale_flink_of_crashed_owner
(Flink-Datei eines abgestürzten Owners zeigt auf ein nicht mehr
existentes Segment; open_owner räumt auf, und ein Frame läuft danach
durch),
crates/transport-shm/src/posix.rs::tests::consumer_open_succeeds_without_owner_creates_segment.
Status: done
§5.3 Shutdown-Flag
Spec: §5 — der Owner MUSS beim Drop shutdown = 1 setzen
(Release-Store). Der Consumer prüft das Flag nach jedem leeren Poll und
kehrt mit einem gezielten Fehler zurück, statt bis zum recv_timeout zu
warten. Frames, die vor dem Drop publiziert wurden, kommen noch an — die
Prüfung steht hinter pop_frame.
Repo: crates/transport-shm/src/posix.rs (SegmentLayout::shutdown,
Drop for PosixShmTransport, Shutdown-Prüfung in wait_for_frame).
Tests: crates/transport-shm/src/posix.rs::tests::owner_drop_signals_consumer_via_shutdown_flag
(misst die Rückkehrzeit gegen ein 10-s-Timeout und prüft den
Fehlertext auf „owner”),
crates/transport-shm/src/posix.rs::tests::posix_shm_error_display_covers_all_variants.
Status: done
§5.4 Race-Schutz beim Anlegen
Spec: §5 — parallele Öffner derselben os_id MÜSSEN über Threads und
Prozesse hinweg serialisiert werden, sonst entfernt der eine das gerade
angelegte Segment des anderen. Der Lock wird beim Schließen des Handles
oder beim Prozess-Tod automatisch frei (gleiche Crash-Resilienz wie das
Segment selbst).
Repo: crates/transport-shm/src/posix.rs (acquire_flock_excl —
flock(LOCK_EX) auf Unix, LockFileEx mit LOCKFILE_EXCLUSIVE_LOCK auf
Windows —, FlockGuard, Sentinel-Datei <flink>.lock).
Tests: crates/transport-shm/src/posix.rs::tests::open_concurrent_two_threads_both_bound
(zwei Threads öffnen dieselbe os_id gleichzeitig; beide sind danach
gültig gebunden),
crates/transport-shm/src/posix.rs::tests::users_in_different_flink_directories_share_the_segment
(Owner und Consumer suchen das Segment in verschiedenen Flink-Verzeichnissen,
etwa unter verschiedenem TMPDIR; der zweite hängt sich an das gehaltene
Segment, statt es zu entfernen und ein eigenes anzulegen).
Status: done
§5.5 Liveness-Lock und verwaiste Segmente
Spec: §5 — jeder Nutzer eines Segments, Owner wie Consumer, MUSS für
die Dauer der Nutzung einen geteilten flock auf dem Segment halten. Der
Kernel gibt ihn frei, wenn der Prozess endet, auch durch SIGKILL; ein
Segment ohne Lock-Halter ist verwaist. Ein Opener, der unter dem Namen
seines Paares ein verwaistes Segment findet, MUSS es entfernen und neu
anlegen, statt es zu übernehmen: sein Ring enthält die Frames der
beendeten Sitzung. Verwaiste Segmente, die niemand wieder öffnet, entfernt
ein Sweep über /dev/shm. Er lässt Segmente stehen, die jünger als ein
Mindestalter sind (das deckt den Abstand zwischen dem Anlegen und dem Lock
des Erstellers) oder eine andere Layout-Version tragen. Version 2
kennzeichnet dieses Protokoll: ein Nutzer der Version 1 hält keinen Lock,
seine lebenden Segmente sähen verwaist aus.
Repo: crates/transport-shm/src/posix.rs (hold_segment,
SegmentHold, sweep_orphaned_segments, SweepReport,
sweep_stale_pair_files für die .lock-Dateien und, auf Linux, die
Flinks der Paare, deren Segment fort ist — sie lagen nach jedem nicht
sauber beendeten Prozess (eine Testkette auf einem MacBook Air: 3 504 .lock in einem
Domain-Verzeichnis); lock_pair prüft nach dem Sperren, dass der Pfad
noch die gesperrte Datei nennt, SHM_VERSION = 2; ein Ring mit leerem Kopf, dessen Ersteller vor dem
Schreiben des Kopfs endete, gilt als entfernbar, weil kein Anhänger ihn
annimmt), crates/transport-shm/src/bell.rs (die Türglocke eines
Teilnehmers: geteilter flock auf ihrer Datei in /dev/shm,
sweep_orphaned_doorbells); crates/dcps/src/same_host_shm.rs
(sweep_orphaned_segments_once: einmal pro Prozess beim Start eines
Teilnehmers mit transport.same_host_shm, Ringe und Türglocken, danach
je Flink-Verzeichnis die Paar-Dateien, Mindestalter 60 s, Ereignis
shm.orphaned_segments_removed).
Tests: crates/transport-shm/src/posix.rs::tests::an_orphaned_segment_is_made_anew_not_attached
(ein eigener Prozess öffnet den Owner, publiziert einen Frame und endet
ohne Drop; der Consumer findet danach einen leeren Ring und empfängt nur
den Frame der neuen Sitzung),
crates/transport-shm/src/posix.rs::tests::the_sweep_removes_orphaned_segments_and_keeps_held_ones
(das verwaiste Segment bleibt unter dem Mindestalter stehen und wird
darüber entfernt; ein gehaltenes Segment bleibt, und ein Frame läuft
danach durch; ein Segment mit leerem Kopf wird entfernt),
crates/transport-shm/src/bell.rs::tests::the_sweep_removes_orphaned_bells_and_keeps_held_ones,
crates/transport-shm/src/posix.rs::tests::the_pair_file_sweep_removes_lone_lock_files_and_keeps_the_rest,
crates/transport-shm/src/posix.rs::tests::a_lock_taken_on_a_removed_lock_file_is_taken_anew.
Gemessen auf dem Linux-Testhost: zwei Teilnehmer per kill -9 beendet; der Start
eines dritten nach 60 s entfernte ihre 2 Ringe und 2 Türglocken
(removed: 4). Vorher blieben Türglocken jedes nicht sauber beendeten
Prozesses liegen (138 nach einem Tag Proben).
Auf macOS und den übrigen Unix-Systemen ohne Shared-Memory im
Dateisystem liegt der Lock auf der Flink-Datei des Paares, und
sweep_orphaned_flinks findet verwaiste Segmente über das
Flink-Verzeichnis (dcps: einmal je Verzeichnis, also je Host und Domain).
Eine Türglocke legt dort eine Markierung <name>.bell an, auf der ihr
Teilnehmer die Sperre hält; der Sweep entfernt Glocke und Markierung,
wenn niemand sie hält. Tests
the_flink_sweep_removes_orphaned_segments_and_keeps_held_ones und
bell.rs::tests::the_flink_sweep_removes_orphaned_bells_and_keeps_held_ones,
auf macOS 26 grün; an_orphaned_segment_is_made_anew_not_attached läuft auf
jedem Unix. Windows: §6.3.
Status: done
§6 Plattform-Support
§6.1 Linux und macOS
Spec: §6 — Linux ist die primäre Plattform, macOS wird unterstützt.
Auf macOS gilt das PSHMNAMLEN-Limit von 31 Zeichen für den
Segment-Namen; der Name wird dort entsprechend gekürzt.
Repo: crates/transport-shm/src/posix.rs (#[cfg(target_os = "macos")]-Zweig
in segment_os_id, #[cfg(any(target_os = "linux", target_os = "macos"))]
für shm_unlink_by_os_id, #[cfg(unix)] für acquire_flock_excl),
crates/transport-shm/src/mapping.rs (ein Mapping hält keinen
Deskriptor: shm_open, ftruncate, mmap, dann close. Mit
shared_memory blieb je Mapping einer offen, zwei Ringe und eine
Türglocke je Peer; ein Prozess einer Testkette aus mehreren Prozessen lief am macOS-Standardlimit
von 256 Deskriptoren aus, 62 davon Shared-Memory-Objekte. Je Peer bleiben
die Lebendsperren der beiden Ringe).
Tests: crates/transport-shm/src/mapping.rs::tests::a_mapping_holds_no_file_descriptor
(100 Mappings, Zahl der offenen Deskriptoren in einem eigenen Prozess
unverändert; macOS 26),
crates/transport-shm/src/mapping.rs::tests::create_open_share_and_the_owner_unlinks,
crates/transport-shm/src/posix.rs::tests::segment_os_id_is_deterministic_and_within_platform_limit
(prüft das 31-Zeichen-Limit unter cfg(target_os = "macos")),
crates/transport-shm/tests/l1_cross_process.rs::l1_shm_datagram_crosses_process_boundary,
crates/transport-shm/src/posix.rs::tests::open_owner_then_consumer_roundtrip_single_frame.
Status: done
§6.2 Windows
Spec: §6 — auf Windows liefert CreateFileMapping die Mappings, der
Create-Race wird über LockFileEx serialisiert. Der Transport MUSS dort
mit derselben Test-Suite laufen wie auf Unix.
Repo: crates/transport-shm/src/posix.rs
(#[cfg(windows)]-Variante von acquire_flock_excl mit
LockFileEx/LOCKFILE_EXCLUSIVE_LOCK,
#[cfg(not(any(target_os = "linux", target_os = "macos")))]-No-op von
shm_unlink_by_os_id); CI-Job windows-smoke in .gitlab-ci.yml ruft
cargo test -p zerodds-transport-shm auf dem Windows-Runner.
Tests: crates/transport-shm/src/posix.rs::tests::open_concurrent_two_threads_both_bound
(der Test, der den LockFileEx-Pfad auf Windows fährt),
crates/transport-shm/src/posix.rs::tests::open_recovers_from_stale_flink_of_crashed_owner.
Status: partial — im Nightly 6555 (windows-smoke, 81d8500d495)
liefen 39 von 40 Lib-Tests von transport-shm auf Windows grün; der
fehlgeschlagene ist §6.3.
§6.3 Nachweis der Windows-Cleanup-Semantik
Spec: §6 — dass ein abgestürzter Owner auf Windows kein Mapping mit den Frames seiner Sitzung hinterlässt, MUSS ein Test auf dem Windows-Runner belegen, nicht nur die Dokumentation behaupten.
Repo: crates/transport-shm/src/posix.rs (#[cfg(windows)]-Variante
von hold_segment mit LockFileEx auf dem Flink, wie die
macOS-Sperre; discard_orphaned_segment entfernt die Backing-Datei).
Die frühere Annahme, Windows gebe das Mapping mit dem letzten Handle
frei, trifft für shared_memory nicht zu: Es legt jedes Mapping auf eine
Datei %TEMP%\shared_memory-rs\<name>, die den Prozess überlebt
(Datei windows.rs der Crate, Funktion new_map). Gemessen im Nightly 6555: Der Test fand 29
Byte der beendeten Sitzung im Ring.
Tests: crates/transport-shm/src/posix.rs::tests::an_orphaned_segment_is_made_anew_not_attached
(ein Kindprozess öffnet den Owner, publiziert einen Frame und endet ohne
Drop; derselbe Name öffnet danach mit leerem Ring). Läuft auf jeder
Plattform, auf Windows im Job windows-smoke.
Status: partial — die Sperre ist gebaut und für
x86_64-pc-windows-gnu mit Clippy geprüft; gelaufen ist sie nicht, das
misst der nächste windows-smoke-Lauf.
§6.4 no_std
Spec: §6 — der SHM-Transport ist std-only. shm_open, mmap,
flock und die Flink-Dateien sind OS-Aufrufe; ein no_std-Ziel ohne
Betriebssystem hat kein Äquivalent. Der Crate ist deshalb hinter
#[cfg(feature = "std")] gestellt.
Repo: —
Tests: —
Status: open — der Transport läuft nur mit std
(crates/transport-shm/src/lib.rs: #[cfg(feature = "std")] pub mod posix):
shm_open, mmap und flock sind POSIX-Aufrufe. Ein Ziel ohne POSIX bräuchte einen Slot-Speicher
über den Speicher, den seine Plattform zwischen Prozessen oder Kernen teilt;
den gibt es nicht.
§7 Zero-Copy-Loan (same-host)
§7.1 Loan-Pfad Writer → Reader
Spec: §7 — für Samples auf demselben Host MUSS ein Pfad existieren, der ohne Staging-Kopie auskommt: der Writer erhält einen Zeiger in einen Shared-Memory-Slot, füllt ihn, und der Commit finalisiert den Slot an Ort und Stelle. Ein Reader auf demselben Host mappt dasselbe Segment und liest den Slot über seinen eigenen Zeiger — die Bytes überqueren nie das Netz und werden nie umkopiert.
Repo: crates/flatdata/src/posix.rs (PosixSlotAllocator,
reserve_slot, slot_data_ptr, commit_in_place, slot_read_ptr,
next_unread_slot, mark_read);
crates/zerodds-c-api/src/shm_loan_ffi.rs (try_loan, try_commit,
try_take, zerodds_dw_enable_shm_loan, zerodds_dr_enable_shm,
zerodds_dr_take_shm, zerodds_dr_release_shm).
Tests: crates/zerodds-c-api/tests/shm_loan_e2e.rs::shm_loan_writer_to_reader_zero_copy
(misst den Loan-Pfad selbst, nicht seinen Aufbau: der Test schreibt die
Nutzlast direkt in den geliehenen Zeiger, committet in place und prüft
beim Reader mit assert_ne!, dass dessen Lesezeiger eine andere Adresse
trägt als der Writer-Zeiger — eigenes Mapping desselben Segments — und
dass die Bytes gleich sind; danach Release, NoData beim nächsten Take
und ein zweites Sample durch denselben Ring),
crates/zerodds-c-api/tests/shm_loan_e2e.rs::shm_loan_runtime_path_writer_to_reader_zero_copy
(derselbe Pfad über die Runtime-FFI, die das ROS-2-RMW nutzt),
crates/zerodds-c-api/tests/shm_loan_e2e.rs::loan_without_shm_falls_back_to_heap
(ohne aktivierten Loan bleibt der Heap-Pfad),
crates/flatdata/src/posix.rs::tests::write_read_through_shm
(Slot-Backend ohne FFI). Das Feature flatdata-loan steht in default
von crates/zerodds-c-api/Cargo.toml; die Tests laufen im Default-Lauf
und nicht hinter einem Schalter.
Status: done
§7.2 Abgrenzung: crates/transport-shm kopiert
Spec: §7 — der Ring aus §2 bis §5 ist kein Zero-Copy-Pfad:
push_frame kopiert die Nutzlast in die Ring-Region, pop_frame kopiert
sie in einen Vec heraus. Der Ring transportiert RTPS-Datagramme
zwischen Prozessen; der Loan-Pfad aus §7.1 transportiert Sample-Bytes
ohne Kopie. Beide Mechanismen sind getrennt und werden nicht
gegeneinander ausgetauscht. Die Sektion hält die Abgrenzung fest und
stellt keine eigene Implementierungs-Anforderung.
Repo: —
Tests: —
Status: n/a (informative) — Abgrenzung zwischen Ring und Loan-Pfad; der Loan-Pfad steht als Item §7.1.
Audit-Status
18 done / 3 partial / 1 open / 0 blocked / 2 n/a (informative).
Test-Lauf: cargo test -p zerodds-transport-shm — 27 Tests (lib) + 1
Test (tests/l1_cross_process.rs) grün, 0 failed;
cargo test -p zerodds-c-api --test shm_loan_e2e — 5 Tests grün, 0
failed. Gemessen auf macOS 15 (Rust-Host aarch64-apple-darwin); der
Windows-Lauf aus §6.2 und der Linux-Futex-Pfad aus §4.4 sind darin nicht
enthalten.
Offene Punkte: zerodds-shm-transport-1.0.open.md.