mechutil 0.8.9

Utility structures and functions for mechatronics applications.
Documentation
//
// Self-describing header for the cyclic GM shared-memory segment, plus a stable
// layout fingerprint.
//
// The segment is laid out as:
//
//     [ ShmHeader (SHM_HEADER_SIZE bytes) ][ GlobalMemory variables ... ][ signals ][ __ready__ ]
//
// The header sits at offset 0 so any reader can `mmap` the segment and validate
// compatibility BEFORE trusting the variable region. The server writes it on
// segment creation; the control program (autocore-std) reads and validates it
// before casting the variable region to its compiled `GlobalMemory` struct.
//
// Performance: the header is read exactly once, at attach. The cyclic
// read/write loop only touches the `GlobalMemory` region, so steady-state
// performance is unaffected.

/// Magic number identifying an autocore cyclic GM segment ("ACGM" + version
/// nibble). Lets a blind reader distinguish our segment from arbitrary bytes.
pub const SHM_MAGIC: u64 = 0xAC_C0_47_4D_5F_53_48_01; // ..GM_SH\x01

/// Version of the header layout / fingerprint scheme itself. Bump when this
/// file's encoding changes (header fields, hash algorithm, reserved layout).
pub const SHM_FORMAT_VERSION: u32 = 1;

/// Bytes reserved at offset 0 for [`ShmHeader`]. Cache-line sized. Variables
/// begin at this offset. Changing this is a format change (bump
/// [`SHM_FORMAT_VERSION`]) and shifts every variable offset.
pub const SHM_HEADER_SIZE: usize = 64;

/// Header written at offset 0 of the cyclic GM segment. `#[repr(C)]` with a
/// fixed 64-byte layout: magic(8) format(4) abi(4) hash(8) total(8) reserved(32).
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShmHeader {
    /// Must equal [`SHM_MAGIC`].
    pub magic: u64,
    /// Must equal [`SHM_FORMAT_VERSION`].
    pub format_version: u32,
    /// mechutil `SHM_ABI_VERSION` the segment creator was built with.
    pub shm_abi_version: u32,
    /// Fingerprint of the variable layout (see [`layout_hash`]).
    pub layout_hash: u64,
    /// Total segment size in bytes, including this header.
    pub total_size: u64,
    /// Reserved for future fields; zeroed.
    pub _reserved: [u8; 32],
}

// Compile-time guarantee that the header is exactly SHM_HEADER_SIZE bytes.
const _: () = assert!(core::mem::size_of::<ShmHeader>() == SHM_HEADER_SIZE);

impl ShmHeader {
    /// Construct a header for a segment with the given fingerprint and size.
    pub fn new(layout_hash: u64, total_size: u64, shm_abi_version: u32) -> Self {
        ShmHeader {
            magic: SHM_MAGIC,
            format_version: SHM_FORMAT_VERSION,
            shm_abi_version,
            layout_hash,
            total_size,
            _reserved: [0u8; 32],
        }
    }
}

/// One variable's contribution to the layout fingerprint. Callers supply these
/// in a deterministic order (the order the segment is laid out).
pub struct LayoutEntry<'a> {
    pub name: &'a str,
    pub offset: u64,
    pub size: u64,
    pub data_type: &'a str,
}

// FNV-1a (64-bit). Deliberately NOT std's DefaultHasher: SipHash is seeded and
// not guaranteed stable across builds, which would make the hash useless as a
// cross-process / cross-build contract.
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;

#[inline]
fn mix(h: &mut u64, bytes: &[u8]) {
    for &b in bytes {
        *h ^= b as u64;
        *h = h.wrapping_mul(FNV_PRIME);
    }
}

/// Deterministic, stable-across-builds fingerprint of a GM layout. Folds in the
/// format version, ABI version, and total size so any of (scheme, ABI, size,
/// or any variable's name/offset/size/type) changing flips the hash.
///
/// The server computes this from its runtime layout; codegen bakes the same
/// value into the control program. They MUST feed identical entries in
/// identical order — in this codebase both go through autocore-server's
/// `layout_fingerprint::compute_layout`, the single source of the walk.
pub fn layout_hash(entries: &[LayoutEntry<'_>], total_size: u64, shm_abi_version: u32) -> u64 {
    let mut h = FNV_OFFSET;
    mix(&mut h, &SHM_FORMAT_VERSION.to_le_bytes());
    mix(&mut h, &shm_abi_version.to_le_bytes());
    mix(&mut h, &total_size.to_le_bytes());
    for e in entries {
        mix(&mut h, e.name.as_bytes());
        mix(&mut h, &[0]);
        mix(&mut h, &e.offset.to_le_bytes());
        mix(&mut h, &e.size.to_le_bytes());
        mix(&mut h, e.data_type.as_bytes());
        mix(&mut h, &[0]);
    }
    h
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn header_is_64_bytes() {
        assert_eq!(core::mem::size_of::<ShmHeader>(), 64);
    }

    #[test]
    fn hash_is_order_and_value_sensitive() {
        let a = [LayoutEntry { name: "x", offset: 64, size: 4, data_type: "u32" },
                 LayoutEntry { name: "y", offset: 68, size: 8, data_type: "f64" }];
        let b = [LayoutEntry { name: "y", offset: 68, size: 8, data_type: "f64" },
                 LayoutEntry { name: "x", offset: 64, size: 4, data_type: "u32" }];
        let h1 = layout_hash(&a, 128, 1);
        assert_eq!(h1, layout_hash(&a, 128, 1), "stable");
        assert_ne!(h1, layout_hash(&b, 128, 1), "order-sensitive");
        assert_ne!(h1, layout_hash(&a, 128, 2), "abi-sensitive");
        assert_ne!(h1, layout_hash(&a, 256, 1), "size-sensitive");
        // a type change on one field must flip it
        let c = [LayoutEntry { name: "x", offset: 64, size: 4, data_type: "i32" },
                 LayoutEntry { name: "y", offset: 68, size: 8, data_type: "f64" }];
        assert_ne!(h1, layout_hash(&c, 128, 1), "type-sensitive");
    }
}