diff --git a/Cargo.lock b/Cargo.lock index d8071c9246..506ffac368 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1502,6 +1502,25 @@ dependencies = [ "uuid", ] +[[package]] +name = "datadog-shm" +version = "0.1.0" +dependencies = [ + "anyhow", + "datadog-ipc", + "libc", +] + +[[package]] +name = "datadog-shm-ffi" +version = "0.1.0" +dependencies = [ + "anyhow", + "build_common", + "datadog-shm", + "libdd-common-ffi", +] + [[package]] name = "datadog-sidecar" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 245187c1a4..5f03127076 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,8 @@ members = [ "libdd-dogstatsd-client", "libdd-log", "libdd-log-ffi", "libdd-libunwind-sys", + "datadog-shm", + "datadog-shm-ffi", ] # https://doc.rust-lang.org/cargo/reference/resolver.html diff --git a/datadog-shm-ffi/Cargo.toml b/datadog-shm-ffi/Cargo.toml new file mode 100644 index 0000000000..811712b57d --- /dev/null +++ b/datadog-shm-ffi/Cargo.toml @@ -0,0 +1,25 @@ +# Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "datadog-shm-ffi" +version = "0.1.0" +edition.workspace = true +license.workspace = true +publish = false + +[lib] +crate-type = ["staticlib", "cdylib", "lib"] +bench = false + +[dependencies] +datadog-shm = { path = "../datadog-shm" } +libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false } +anyhow = "1.0" + +[features] +default = ["cbindgen"] +cbindgen = ["build_common/cbindgen", "libdd-common-ffi/cbindgen"] + +[build-dependencies] +build_common = { path = "../build-common" } diff --git a/datadog-shm-ffi/build.rs b/datadog-shm-ffi/build.rs new file mode 100644 index 0000000000..085920aa3b --- /dev/null +++ b/datadog-shm-ffi/build.rs @@ -0,0 +1,10 @@ +// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 +extern crate build_common; + +use build_common::generate_and_configure_header; + +fn main() { + let header_name = "datadog-shm.h"; + generate_and_configure_header(header_name); +} diff --git a/datadog-shm-ffi/src/lib.rs b/datadog-shm-ffi/src/lib.rs new file mode 100644 index 0000000000..e4716a74db --- /dev/null +++ b/datadog-shm-ffi/src/lib.rs @@ -0,0 +1,354 @@ +// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! C FFI for cross-platform named shared memory primitives. +//! +//! Provides simple create / open / read / update / drop operations on +//! named SHM segments. SDKs link against this instead of dealing with +//! platform-specific APIs directly. +//! +//! ## Typical usage from C +//! +//! ### Writer +//! ```c +//! DdogShmWriter *writer = NULL; +//! DdogMaybeError err = ddog_shm_create( +//! "dd-session-42", // name +//! data_ptr, data_len, // payload +//! 0, // capacity (0 = default 64 KiB) +//! &writer +//! ); +//! // … segment is readable by other processes … +//! ddog_shm_writer_drop(writer); +//! ``` +//! +//! ### Reader +//! ```c +//! DdogShmReader *reader = NULL; +//! DdogMaybeError err = ddog_shm_open("dd-session-42", &reader); +//! if (reader != NULL) { +//! const uint8_t *ptr = NULL; +//! size_t len = 0; +//! ddog_shm_read_data(reader, &ptr, &len); +//! // … use ptr[0..len] … +//! ddog_shm_reader_drop(reader); +//! } +//! ``` + +#![cfg_attr(not(test), deny(clippy::panic))] +#![cfg_attr(not(test), deny(clippy::unwrap_used))] +#![cfg_attr(not(test), deny(clippy::expect_used))] +#![cfg_attr(not(test), deny(clippy::todo))] +#![cfg_attr(not(test), deny(clippy::unimplemented))] + +use libdd_common_ffi as ffi; +use std::ffi::CStr; +use std::os::raw::c_char; + +macro_rules! try_c { + ($failable:expr) => { + match $failable { + Ok(o) => o, + Err(e) => return ffi::MaybeError::Some(ffi::Error::from(format!("{e:?}"))), + } + }; +} + +/// Opaque writer handle. Keeps the SHM segment alive until dropped. +pub struct DdogShmWriter { + inner: datadog_shm::ShmWriter, +} + +/// Opaque reader handle. +pub struct DdogShmReader { + inner: datadog_shm::ShmReader, +} + +// --------------------------------------------------------------------------- +// Writer +// --------------------------------------------------------------------------- + +/// Create a named SHM segment and write `data` into it. +/// +/// If `capacity` is 0 the default (64 KiB) is used. +/// +/// # Safety +/// - `name` must be a valid null-terminated C string. +/// - `data` must point to at least `data_len` readable bytes +/// (may be NULL if `data_len` is 0). +/// - `out` must be a valid pointer. +#[no_mangle] +pub unsafe extern "C" fn ddog_shm_create( + name: *const c_char, + data: *const u8, + data_len: usize, + capacity: usize, + out: *mut *mut DdogShmWriter, +) -> ffi::MaybeError { + if name.is_null() || out.is_null() { + return ffi::MaybeError::Some(ffi::Error::from( + "ddog_shm_create: null name or out pointer".to_string(), + )); + } + + let name_str = try_c!(unsafe { CStr::from_ptr(name) } + .to_str() + .map_err(|e| anyhow::anyhow!("{e}"))); + + let bytes: &[u8] = if data.is_null() || data_len == 0 { + &[] + } else { + unsafe { std::slice::from_raw_parts(data, data_len) } + }; + + let writer = if capacity == 0 { + try_c!(datadog_shm::ShmWriter::create(name_str, bytes)) + } else { + try_c!(datadog_shm::ShmWriter::create_with_capacity( + name_str, bytes, capacity, + )) + }; + + unsafe { + *out = Box::into_raw(Box::new(DdogShmWriter { inner: writer })); + } + ffi::MaybeError::None +} + +/// Overwrite the segment contents. +/// +/// # Safety +/// - `writer` must have been returned by `ddog_shm_create`. +/// - `data` / `data_len` must be valid. +#[no_mangle] +pub unsafe extern "C" fn ddog_shm_update( + writer: *mut DdogShmWriter, + data: *const u8, + data_len: usize, +) -> ffi::MaybeError { + if writer.is_null() { + return ffi::MaybeError::Some(ffi::Error::from( + "ddog_shm_update: null writer".to_string(), + )); + } + + let bytes: &[u8] = if data.is_null() || data_len == 0 { + &[] + } else { + unsafe { std::slice::from_raw_parts(data, data_len) } + }; + + let w = unsafe { &mut *writer }; + try_c!(w.inner.update(bytes)); + ffi::MaybeError::None +} + +/// Drop a writer, unmapping and unlinking the segment. +/// +/// # Safety +/// - `writer` must have been returned by `ddog_shm_create`, or be null. +#[no_mangle] +pub unsafe extern "C" fn ddog_shm_writer_drop(writer: *mut DdogShmWriter) { + if !writer.is_null() { + unsafe { drop(Box::from_raw(writer)) }; + } +} + +// --------------------------------------------------------------------------- +// Reader +// --------------------------------------------------------------------------- + +/// Open an existing named SHM segment for reading. +/// +/// Sets `*out` to a reader handle, or to NULL if the segment does not +/// exist (this is **not** an error — the returned `MaybeError` will be +/// `None`). +/// +/// # Safety +/// - `name` must be a valid null-terminated C string. +/// - `out` must be a valid pointer. +#[no_mangle] +pub unsafe extern "C" fn ddog_shm_open( + name: *const c_char, + out: *mut *mut DdogShmReader, +) -> ffi::MaybeError { + if name.is_null() || out.is_null() { + return ffi::MaybeError::Some(ffi::Error::from( + "ddog_shm_open: null name or out pointer".to_string(), + )); + } + + let name_str = try_c!(unsafe { CStr::from_ptr(name) } + .to_str() + .map_err(|e| anyhow::anyhow!("{e}"))); + + match datadog_shm::ShmReader::open(name_str) { + Ok(Some(reader)) => { + unsafe { *out = Box::into_raw(Box::new(DdogShmReader { inner: reader })) }; + } + Ok(None) => { + unsafe { *out = std::ptr::null_mut() }; + } + Err(e) => { + unsafe { *out = std::ptr::null_mut() }; + return ffi::MaybeError::Some(ffi::Error::from(format!("{e:?}"))); + } + } + + ffi::MaybeError::None +} + +/// Get a pointer to the non-zero data prefix in the segment. +/// +/// Sets `*out_ptr` and `*out_len`. The pointer is valid until the reader +/// is dropped. +/// +/// # Safety +/// - `reader`, `out_ptr`, `out_len` must all be valid. +#[no_mangle] +pub unsafe extern "C" fn ddog_shm_read_data( + reader: *const DdogShmReader, + out_ptr: *mut *const u8, + out_len: *mut usize, +) -> ffi::MaybeError { + if reader.is_null() || out_ptr.is_null() || out_len.is_null() { + return ffi::MaybeError::Some(ffi::Error::from( + "ddog_shm_read_data: null argument".to_string(), + )); + } + + let r = unsafe { &*reader }; + let data = r.inner.data_bytes(); + unsafe { + *out_ptr = data.as_ptr(); + *out_len = data.len(); + } + ffi::MaybeError::None +} + +/// Get a pointer to the entire mapped region (including trailing zeros). +/// +/// # Safety +/// - `reader`, `out_ptr`, `out_len` must all be valid. +#[no_mangle] +pub unsafe extern "C" fn ddog_shm_read_raw( + reader: *const DdogShmReader, + out_ptr: *mut *const u8, + out_len: *mut usize, +) -> ffi::MaybeError { + if reader.is_null() || out_ptr.is_null() || out_len.is_null() { + return ffi::MaybeError::Some(ffi::Error::from( + "ddog_shm_read_raw: null argument".to_string(), + )); + } + + let r = unsafe { &*reader }; + let bytes = r.inner.as_bytes(); + unsafe { + *out_ptr = bytes.as_ptr(); + *out_len = bytes.len(); + } + ffi::MaybeError::None +} + +/// Drop a reader. +/// +/// # Safety +/// - `reader` must have been returned by `ddog_shm_open`, or be null. +#[no_mangle] +pub unsafe extern "C" fn ddog_shm_reader_drop(reader: *mut DdogShmReader) { + if !reader.is_null() { + unsafe { drop(Box::from_raw(reader)) }; + } +} + +// --------------------------------------------------------------------------- +// PID-keyed convenience +// --------------------------------------------------------------------------- + +/// Create a PID-keyed segment (`/dd--`). +/// +/// # Safety +/// - `prefix` must be a valid null-terminated C string. +/// - `data` / `data_len` must be valid. +/// - `out` must be a valid pointer. +#[no_mangle] +pub unsafe extern "C" fn ddog_shm_create_pid_keyed( + prefix: *const c_char, + pid: u32, + data: *const u8, + data_len: usize, + out: *mut *mut DdogShmWriter, +) -> ffi::MaybeError { + if prefix.is_null() || out.is_null() { + return ffi::MaybeError::Some(ffi::Error::from( + "ddog_shm_create_pid_keyed: null argument".to_string(), + )); + } + + let prefix_str = try_c!(unsafe { CStr::from_ptr(prefix) } + .to_str() + .map_err(|e| anyhow::anyhow!("{e}"))); + + let bytes: &[u8] = if data.is_null() || data_len == 0 { + &[] + } else { + unsafe { std::slice::from_raw_parts(data, data_len) } + }; + + let writer = try_c!(datadog_shm::create_pid_keyed(prefix_str, pid, bytes)); + + unsafe { + *out = Box::into_raw(Box::new(DdogShmWriter { inner: writer })); + } + ffi::MaybeError::None +} + +/// Open a PID-keyed segment. Sets `*out` to NULL if it doesn't exist. +/// +/// # Safety +/// - `prefix` must be a valid null-terminated C string. +/// - `out` must be a valid pointer. +#[no_mangle] +pub unsafe extern "C" fn ddog_shm_open_pid_keyed( + prefix: *const c_char, + pid: u32, + out: *mut *mut DdogShmReader, +) -> ffi::MaybeError { + if prefix.is_null() || out.is_null() { + return ffi::MaybeError::Some(ffi::Error::from( + "ddog_shm_open_pid_keyed: null argument".to_string(), + )); + } + + let prefix_str = try_c!(unsafe { CStr::from_ptr(prefix) } + .to_str() + .map_err(|e| anyhow::anyhow!("{e}"))); + + match datadog_shm::open_pid_keyed(prefix_str, pid) { + Ok(Some(reader)) => { + unsafe { *out = Box::into_raw(Box::new(DdogShmReader { inner: reader })) }; + } + Ok(None) => { + unsafe { *out = std::ptr::null_mut() }; + } + Err(e) => { + unsafe { *out = std::ptr::null_mut() }; + return ffi::MaybeError::Some(ffi::Error::from(format!("{e:?}"))); + } + } + + ffi::MaybeError::None +} + +/// Return the current process ID. +#[no_mangle] +pub extern "C" fn ddog_shm_current_pid() -> u32 { + datadog_shm::current_pid() +} + +/// Return the parent process ID. +#[no_mangle] +pub extern "C" fn ddog_shm_parent_pid() -> u32 { + datadog_shm::parent_pid() +} diff --git a/datadog-shm/Cargo.toml b/datadog-shm/Cargo.toml new file mode 100644 index 0000000000..50b42b4ca3 --- /dev/null +++ b/datadog-shm/Cargo.toml @@ -0,0 +1,16 @@ +# Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "datadog-shm" +version = "0.1.0" +edition.workspace = true +license.workspace = true +publish = false + +[dependencies] +datadog-ipc = { path = "../datadog-ipc" } +anyhow = "1.0" + +[target.'cfg(unix)'.dependencies] +libc = "0.2" diff --git a/datadog-shm/src/lib.rs b/datadog-shm/src/lib.rs new file mode 100644 index 0000000000..12d6800bcf --- /dev/null +++ b/datadog-shm/src/lib.rs @@ -0,0 +1,369 @@ +// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Cross-platform named shared memory primitives for Datadog SDKs. +//! +//! This crate provides a thin, ergonomic layer on top of +//! [`datadog_ipc::platform::NamedShmHandle`] for creating, writing, and +//! reading named shared memory segments. It hides platform-specific +//! details: +//! +//! - **Unix (Linux)**: `memfd_create` or `shm_open` with `/tmp/libdatadog` +//! fallback (e.g. AWS Lambda) +//! - **Unix (macOS)**: `shm_open` with reserve/commit pattern +//! - **Windows**: `CreateFileMapping` in the `Local\` namespace +//! +//! ## Intended use cases +//! +//! Any scenario where a Datadog SDK process needs to share a small blob +//! of data with related processes (children, sidecars, etc.) via a +//! well-known name: +//! +//! - Propagating session / instance identifiers across `fork`/`exec` +//! - Sharing configuration snapshots between tracer and sidecar +//! - Publishing lightweight status that other processes can poll +//! +//! ## Naming convention +//! +//! Callers supply a plain name (e.g. `"dd-session-12345"`). The name +//! **must** start with `/` (POSIX requirement); this crate will prepend +//! one if missing. On Windows the name is automatically translated to +//! `Local\…` by the underlying `NamedShmHandle`. +//! +//! ## Example +//! +//! ```rust,no_run +//! use datadog_shm::{ShmWriter, ShmReader}; +//! +//! // Writer — create a segment and keep it alive +//! let writer = ShmWriter::create("dd-session-42", b"hello world")?; +//! +//! // Reader — open and read (returns None if segment doesn't exist) +//! if let Some(reader) = ShmReader::open("dd-session-42")? { +//! assert_eq!(reader.as_bytes(), b"hello world"); +//! } +//! # Ok::<(), anyhow::Error>(()) +//! ``` + +#![cfg_attr(not(test), deny(clippy::panic))] +#![cfg_attr(not(test), deny(clippy::unwrap_used))] +#![cfg_attr(not(test), deny(clippy::expect_used))] +#![cfg_attr(not(test), deny(clippy::todo))] +#![cfg_attr(not(test), deny(clippy::unimplemented))] + +use datadog_ipc::platform::{FileBackedHandle, MappedMem, NamedShmHandle}; +use std::ffi::CString; + +/// Default maximum segment size (64 KiB). Callers can override via +/// [`ShmWriter::create_with_capacity`]. +pub const DEFAULT_MAX_SIZE: usize = 65_536; + +// -- helpers ---------------------------------------------------------------- + +fn normalize_name(name: &str) -> anyhow::Result { + let normalized = if name.starts_with('/') { + name.to_owned() + } else { + format!("/{name}") + }; + Ok(CString::new(normalized)?) +} + +/// Platform-specific check for "segment does not exist" OS errors. +#[cfg(unix)] +fn is_not_found_error(e: &std::io::Error) -> bool { + match e.raw_os_error() { + Some(raw) => raw == libc::ENOENT || raw == libc::ENOSYS || raw == libc::ENOTSUP, + None => e.kind() == std::io::ErrorKind::NotFound, + } +} + +#[cfg(windows)] +fn is_not_found_error(e: &std::io::Error) -> bool { + // ERROR_FILE_NOT_FOUND = 2 + e.raw_os_error() == Some(2) || e.kind() == std::io::ErrorKind::NotFound +} + +// -- writer ----------------------------------------------------------------- + +/// A named shared memory segment open for writing. +/// +/// The segment remains accessible to other processes for as long as the +/// `ShmWriter` is alive. Dropping it unmaps (and on POSIX unlinks) +/// the segment. +pub struct ShmWriter { + mapped: MappedMem, + len: usize, +} + +impl ShmWriter { + /// Create a new named SHM segment containing `data`. + /// + /// The segment is sized to [`DEFAULT_MAX_SIZE`] or `data.len()`, + /// whichever is larger. Use [`create_with_capacity`](Self::create_with_capacity) + /// for explicit control. + pub fn create(name: &str, data: &[u8]) -> anyhow::Result { + let cap = DEFAULT_MAX_SIZE.max(data.len()); + Self::create_with_capacity(name, data, cap) + } + + /// Create a new named SHM segment with an explicit capacity. + /// + /// `capacity` must be `>= data.len()`. + pub fn create_with_capacity( + name: &str, + data: &[u8], + capacity: usize, + ) -> anyhow::Result { + if data.len() > capacity { + anyhow::bail!( + "data length ({}) exceeds capacity ({})", + data.len(), + capacity + ); + } + + let cname = normalize_name(name)?; + let handle = NamedShmHandle::create(cname, capacity)?; + let mut mapped = handle.map()?; + + let buf = mapped.as_slice_mut(); + buf[..data.len()].copy_from_slice(data); + // Zero the remainder so readers can detect end-of-data + for byte in &mut buf[data.len()..] { + *byte = 0; + } + + Ok(Self { + mapped, + len: data.len(), + }) + } + + /// Overwrite the segment contents with new data. + /// + /// Fails if `data` is larger than the segment capacity. + pub fn update(&mut self, data: &[u8]) -> anyhow::Result<()> { + let cap = self.mapped.get_size(); + if data.len() > cap { + anyhow::bail!( + "data length ({}) exceeds segment capacity ({})", + data.len(), + cap + ); + } + + let buf = self.mapped.as_slice_mut(); + buf[..data.len()].copy_from_slice(data); + for byte in &mut buf[data.len()..] { + *byte = 0; + } + self.len = data.len(); + Ok(()) + } + + /// The number of live data bytes currently written. + pub fn len(&self) -> usize { + self.len + } + + /// Whether the segment is empty. + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// The total capacity of the segment. + pub fn capacity(&self) -> usize { + self.mapped.get_size() + } +} + +// -- reader ----------------------------------------------------------------- + +/// A read-only view of a named shared memory segment. +/// +/// Dropping the reader unmaps the segment from this process, but the +/// segment itself remains alive as long as the writer holds it open. +pub struct ShmReader { + mapped: MappedMem, +} + +impl ShmReader { + /// Open an existing named SHM segment for reading. + /// + /// Returns `Ok(None)` if no segment with that name exists. + pub fn open(name: &str) -> anyhow::Result> { + let cname = normalize_name(name)?; + let handle = match NamedShmHandle::open(&cname) { + Ok(h) => h, + Err(e) if is_not_found_error(&e) => return Ok(None), + Err(e) => return Err(e.into()), + }; + let mapped = handle.map()?; + Ok(Some(Self { mapped })) + } + + /// The raw bytes of the entire mapped region. + /// + /// The region may contain trailing zero bytes beyond the actual + /// payload. Use [`data_bytes`](Self::data_bytes) if you want + /// only the non-zero prefix. + pub fn as_bytes(&self) -> &[u8] { + self.mapped.as_slice() + } + + /// The non-zero prefix of the mapped region. + /// + /// Assumes the writer zero-filled the remainder after the payload. + pub fn data_bytes(&self) -> &[u8] { + let slice = self.mapped.as_slice(); + let end = slice.iter().position(|&b| b == 0).unwrap_or(slice.len()); + &slice[..end] + } + + /// Total mapped size. + pub fn mapped_size(&self) -> usize { + self.mapped.get_size() + } +} + +// -- convenience: PID-keyed helpers ----------------------------------------- + +/// Create a SHM segment keyed by a PID with a well-known prefix. +/// +/// The segment name will be `/dd--`. +pub fn create_pid_keyed(prefix: &str, pid: u32, data: &[u8]) -> anyhow::Result { + let name = format!("/dd-{prefix}-{pid}"); + ShmWriter::create(&name, data) +} + +/// Open a SHM segment keyed by a PID. +pub fn open_pid_keyed(prefix: &str, pid: u32) -> anyhow::Result> { + let name = format!("/dd-{prefix}-{pid}"); + ShmReader::open(&name) +} + +/// Return the current process ID. +pub fn current_pid() -> u32 { + std::process::id() +} + +/// Return the parent process ID. +#[cfg(unix)] +pub fn parent_pid() -> u32 { + unsafe { libc::getppid() as u32 } +} + +/// Return the parent process ID. +#[cfg(windows)] +pub fn parent_pid() -> u32 { + use winapi::um::processthreadsapi::GetCurrentProcessId; + use winapi::um::tlhelp32::{ + CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32, TH32CS_SNAPPROCESS, + }; + unsafe { + let our_pid = GetCurrentProcessId(); + let snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if snap == winapi::um::handleapi::INVALID_HANDLE_VALUE { + return 0; + } + let mut entry: PROCESSENTRY32 = std::mem::zeroed(); + entry.dwSize = std::mem::size_of::() as u32; + if Process32First(snap, &mut entry) != 0 { + loop { + if entry.th32ProcessID == our_pid { + winapi::um::handleapi::CloseHandle(snap); + return entry.th32ParentProcessID; + } + if Process32Next(snap, &mut entry) == 0 { + break; + } + } + } + winapi::um::handleapi::CloseHandle(snap); + 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg_attr(miri, ignore)] + fn writer_reader_roundtrip() { + let data = b"hello shared memory"; + let _writer = + ShmWriter::create("dd-test-roundtrip-1", data).expect("create"); + + let reader = + ShmReader::open("dd-test-roundtrip-1").expect("open").expect("should exist"); + assert_eq!(reader.data_bytes(), data); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn writer_update() { + let _writer = ShmWriter::create("dd-test-update-1", b"first") + .expect("create"); + + // Re-bind as mutable + let mut writer = _writer; + writer.update(b"second-value").expect("update"); + + let reader = + ShmReader::open("dd-test-update-1").expect("open").expect("should exist"); + assert_eq!(reader.data_bytes(), b"second-value"); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn open_nonexistent_returns_none() { + let result = ShmReader::open("dd-test-nonexistent-9999999") + .expect("should not error"); + assert!(result.is_none()); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn pid_keyed_roundtrip() { + let fake_pid = 9_800_001u32; + let data = b"{\"session_id\":\"abc-123\"}"; + + let _writer = create_pid_keyed("session", fake_pid, data) + .expect("create pid-keyed"); + + let reader = open_pid_keyed("session", fake_pid) + .expect("open pid-keyed") + .expect("should exist"); + assert_eq!(reader.data_bytes(), data.as_slice()); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn custom_capacity() { + let data = b"small"; + let writer = ShmWriter::create_with_capacity("dd-test-cap-1", data, 128) + .expect("create"); + assert!(writer.capacity() >= 128); + assert_eq!(writer.len(), 5); + } + + #[test] + fn data_exceeding_capacity_fails() { + let big = vec![0xFFu8; 200]; + let result = ShmWriter::create_with_capacity("dd-test-toobig", &big, 100); + assert!(result.is_err()); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn empty_data_roundtrip() { + let _writer = + ShmWriter::create("dd-test-empty-1", b"").expect("create"); + + let reader = + ShmReader::open("dd-test-empty-1").expect("open").expect("should exist"); + assert!(reader.data_bytes().is_empty()); + } +}