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.