Skip to main content

subetha_core/
marshal.rs

1//! The `Marshal` trait - the type-system contract for "this value can
2//! cross an address-space boundary byte-identically."
3//!
4//! `Marshal` is strictly stronger than `Send`. A `Send` value can
5//! travel between threads inside one process, where pointers and
6//! references mean the same thing in both threads. A `Marshal` value
7//! can travel between *processes* (or be serialised to disk and read
8//! back), where pointers into the originating process's heap, file
9//! descriptors, and any other resource handle that means different
10//! things in different address spaces are forbidden.
11//!
12//! # The contract
13//!
14//! - [`Marshal::PAYLOAD_BYTES`] is the exact byte width of the
15//!   marshalled form.
16//! - [`Marshal::marshal`] writes exactly `PAYLOAD_BYTES` into a
17//!   caller-supplied buffer.
18//! - [`Marshal::unmarshal`] reads exactly `PAYLOAD_BYTES` and
19//!   reconstructs a value byte-identical to the original.
20//! - Round-tripping: `unmarshal(&buf)` after `marshal(&v, &mut buf)`
21//!   produces a value indistinguishable from `v` for every value `v`.
22//!
23//! # Why `unsafe`
24//!
25//! Correctness depends on every reachable byte of the value being
26//! position-independent across address spaces. The compiler cannot
27//! check this for arbitrary user types - a type with an inner
28//! `Box<u8>` plus a manual `marshal` impl that copies the box's
29//! *raw bytes* compiles cleanly and crashes at runtime. The trait is
30//! therefore `unsafe` to implement; the implementer asserts the
31//! contract holds.
32//!
33//! # Auto-impls
34//!
35//! Safe blanket impls are provided for the primitive integer and
36//! floating-point types, `bool`, `()`, and `[T; N]` where `T:
37//! Marshal`. These cover the common case (move a `u64` job ID, an
38//! `[u8; 48]` argument blob, a `(u32, u32)` pair) without requiring
39//! any unsafe code at the call site.
40//!
41//! # Connection to `pass_registry`
42//!
43//! `subetha_cxc::pass_registry` solves the same problem (closures
44//! that need to execute in another process) at the *runtime* layer,
45//! by registering closure handlers by integer ID. `Marshal` is the
46//! *compile-time* counterpart: a closure whose captured environment
47//! reduces to a `Marshal` payload can be shipped across processes by
48//! marshalling the payload and looking up the handler by ID. Both
49//! layers cooperate to make cross-process execution byte-safe.
50
51use core::fmt;
52
53/// Error returned when [`Marshal::unmarshal`] cannot reconstruct a
54/// value from the source buffer.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum MarshalError {
57    /// The source buffer is shorter than `PAYLOAD_BYTES`.
58    ShortBuffer { expected: usize, got: usize },
59    /// The source bytes do not encode a valid value of this type
60    /// (e.g. a `bool` byte that is neither 0 nor 1).
61    InvalidEncoding,
62}
63
64impl fmt::Display for MarshalError {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            Self::ShortBuffer { expected, got } => {
68                write!(f, "Marshal source buffer too short: expected {expected} bytes, got {got}")
69            }
70            Self::InvalidEncoding => write!(f, "Marshal source bytes do not encode a valid value"),
71        }
72    }
73}
74
75impl std::error::Error for MarshalError {}
76
77/// Type-system contract for "this value can be flattened into a
78/// fixed-size byte payload and reconstructed byte-identically in
79/// another address space."
80///
81/// See the [module docs](self) for the full contract.
82///
83/// # Safety
84///
85/// Implementer asserts that:
86/// - [`marshal`](Self::marshal) writes exactly `PAYLOAD_BYTES` into
87///   the destination buffer.
88/// - [`unmarshal`](Self::unmarshal) reads exactly `PAYLOAD_BYTES`
89///   from the source buffer.
90/// - Round-tripping is byte-identical and value-identical for every
91///   valid value of the type.
92/// - The marshalled bytes contain NO pointers, references, file
93///   descriptors, or other handles that mean different things in
94///   different address spaces.
95pub unsafe trait Marshal: Sized {
96    /// Exact byte width of the marshalled form.
97    const PAYLOAD_BYTES: usize;
98
99    /// Write the marshalled form of `self` into `dst`.
100    ///
101    /// `dst.len()` must be at least `PAYLOAD_BYTES`; implementations
102    /// write to `dst[..PAYLOAD_BYTES]` and leave any remaining bytes
103    /// unmodified. Panics on a short buffer.
104    fn marshal(&self, dst: &mut [u8]);
105
106    /// Read the marshalled form from `src` and reconstruct the value.
107    ///
108    /// `src.len()` must be at least `PAYLOAD_BYTES`; implementations
109    /// read from `src[..PAYLOAD_BYTES]`. Returns
110    /// [`MarshalError::ShortBuffer`] on a short buffer and
111    /// [`MarshalError::InvalidEncoding`] when the bytes do not encode
112    /// a valid value (e.g. an out-of-range discriminant).
113    fn unmarshal(src: &[u8]) -> Result<Self, MarshalError>;
114}
115
116// ---------------------------------------------------------------
117// Primitive impls. Each is sound because the type's bytes are
118// position-independent: an integer's bit pattern means the same
119// thing in every address space.
120// ---------------------------------------------------------------
121
122macro_rules! impl_marshal_for_primitive {
123    ($t:ty, $bytes:expr) => {
124        // SAFETY: $t has no internal pointers or handles; its raw
125        // bytes are position-independent across address spaces.
126        // Little-endian encoding is canonical and stable.
127        unsafe impl Marshal for $t {
128            const PAYLOAD_BYTES: usize = $bytes;
129
130            fn marshal(&self, dst: &mut [u8]) {
131                let bytes = self.to_le_bytes();
132                dst[..$bytes].copy_from_slice(&bytes);
133            }
134
135            fn unmarshal(src: &[u8]) -> Result<Self, MarshalError> {
136                if src.len() < $bytes {
137                    return Err(MarshalError::ShortBuffer {
138                        expected: $bytes,
139                        got: src.len(),
140                    });
141                }
142                let mut buf = [0u8; $bytes];
143                buf.copy_from_slice(&src[..$bytes]);
144                Ok(<$t>::from_le_bytes(buf))
145            }
146        }
147    };
148}
149
150impl_marshal_for_primitive!(u8, 1);
151impl_marshal_for_primitive!(u16, 2);
152impl_marshal_for_primitive!(u32, 4);
153impl_marshal_for_primitive!(u64, 8);
154impl_marshal_for_primitive!(u128, 16);
155impl_marshal_for_primitive!(i8, 1);
156impl_marshal_for_primitive!(i16, 2);
157impl_marshal_for_primitive!(i32, 4);
158impl_marshal_for_primitive!(i64, 8);
159impl_marshal_for_primitive!(i128, 16);
160impl_marshal_for_primitive!(f32, 4);
161impl_marshal_for_primitive!(f64, 8);
162
163// SAFETY: bool's two valid bit patterns are 0 and 1; the encoding
164// rejects any other byte.
165unsafe impl Marshal for bool {
166    const PAYLOAD_BYTES: usize = 1;
167    fn marshal(&self, dst: &mut [u8]) {
168        dst[0] = u8::from(*self);
169    }
170    fn unmarshal(src: &[u8]) -> Result<Self, MarshalError> {
171        if src.is_empty() {
172            return Err(MarshalError::ShortBuffer { expected: 1, got: 0 });
173        }
174        match src[0] {
175            0 => Ok(false),
176            1 => Ok(true),
177            _ => Err(MarshalError::InvalidEncoding),
178        }
179    }
180}
181
182// SAFETY: () has no bytes.
183unsafe impl Marshal for () {
184    const PAYLOAD_BYTES: usize = 0;
185    fn marshal(&self, _dst: &mut [u8]) {}
186    fn unmarshal(_src: &[u8]) -> Result<Self, MarshalError> { Ok(()) }
187}
188
189// SAFETY: an array of Marshal is Marshal: the concatenation of each
190// element's bytes is position-independent if every element is.
191unsafe impl<T: Marshal + Copy + Default, const N: usize> Marshal for [T; N] {
192    const PAYLOAD_BYTES: usize = T::PAYLOAD_BYTES * N;
193    fn marshal(&self, dst: &mut [u8]) {
194        for (i, item) in self.iter().enumerate() {
195            let off = i * T::PAYLOAD_BYTES;
196            item.marshal(&mut dst[off..off + T::PAYLOAD_BYTES]);
197        }
198    }
199    fn unmarshal(src: &[u8]) -> Result<Self, MarshalError> {
200        let need = T::PAYLOAD_BYTES * N;
201        if src.len() < need {
202            return Err(MarshalError::ShortBuffer { expected: need, got: src.len() });
203        }
204        let mut out = [T::default(); N];
205        for (i, slot) in out.iter_mut().enumerate() {
206            let off = i * T::PAYLOAD_BYTES;
207            *slot = T::unmarshal(&src[off..off + T::PAYLOAD_BYTES])?;
208        }
209        Ok(out)
210    }
211}
212
213// SAFETY: tuple of two Marshal values is Marshal by component-wise
214// concatenation; same argument as the array impl.
215unsafe impl<A: Marshal, B: Marshal> Marshal for (A, B) {
216    const PAYLOAD_BYTES: usize = A::PAYLOAD_BYTES + B::PAYLOAD_BYTES;
217    fn marshal(&self, dst: &mut [u8]) {
218        self.0.marshal(&mut dst[..A::PAYLOAD_BYTES]);
219        self.1.marshal(&mut dst[A::PAYLOAD_BYTES..A::PAYLOAD_BYTES + B::PAYLOAD_BYTES]);
220    }
221    fn unmarshal(src: &[u8]) -> Result<Self, MarshalError> {
222        let need = A::PAYLOAD_BYTES + B::PAYLOAD_BYTES;
223        if src.len() < need {
224            return Err(MarshalError::ShortBuffer { expected: need, got: src.len() });
225        }
226        let a = A::unmarshal(&src[..A::PAYLOAD_BYTES])?;
227        let b = B::unmarshal(&src[A::PAYLOAD_BYTES..need])?;
228        Ok((a, b))
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    fn round_trip<T: Marshal + PartialEq + std::fmt::Debug>(v: T) {
237        let mut buf = vec![0u8; T::PAYLOAD_BYTES];
238        v.marshal(&mut buf);
239        let back = T::unmarshal(&buf).unwrap();
240        assert_eq!(v, back);
241    }
242
243    #[test] fn u8_round_trip()   { round_trip(0u8); round_trip(255u8); }
244    #[test] fn u32_round_trip()  { round_trip(0u32); round_trip(u32::MAX); round_trip(0xDEAD_BEEFu32); }
245    #[test] fn u64_round_trip()  { round_trip(0u64); round_trip(u64::MAX); round_trip(0xCAFEBABE_DEADBEEFu64); }
246    #[test] fn i64_round_trip()  { round_trip(i64::MIN); round_trip(0i64); round_trip(i64::MAX); }
247    #[test] fn f64_round_trip()  { round_trip(0.0_f64); round_trip(-1.5_f64); round_trip(f64::INFINITY); }
248    #[test] fn bool_round_trip() { round_trip(true); round_trip(false); }
249    #[test] fn unit_round_trip() { round_trip(()); }
250
251    #[test]
252    fn array_round_trip() {
253        round_trip([1u8, 2, 3, 4]);
254        round_trip([0u64; 8]);
255        round_trip([0xDEAD_BEEF_CAFE_BABE_u64, 0x1234_5678_9ABC_DEF0]);
256    }
257
258    #[test]
259    fn tuple_round_trip() {
260        round_trip((42u32, 7u64));
261        round_trip((true, 99i32));
262    }
263
264    #[test]
265    fn nested_array_in_tuple() {
266        let v: (u32, [u8; 16]) = (0xCAFEBABE, [9; 16]);
267        round_trip(v);
268    }
269
270    #[test]
271    fn bool_rejects_invalid_byte() {
272        match bool::unmarshal(&[42u8]) {
273            Err(MarshalError::InvalidEncoding) => {}
274            other => panic!("expected InvalidEncoding, got {other:?}"),
275        }
276    }
277
278    #[test]
279    fn short_buffer_rejected() {
280        match u64::unmarshal(&[0u8; 3]) {
281            Err(MarshalError::ShortBuffer { expected: 8, got: 3 }) => {}
282            other => panic!("expected ShortBuffer{{expected:8,got:3}}, got {other:?}"),
283        }
284    }
285
286    #[test]
287    fn payload_bytes_constants_match_sizes() {
288        assert_eq!(u8::PAYLOAD_BYTES, 1);
289        assert_eq!(u32::PAYLOAD_BYTES, 4);
290        assert_eq!(u64::PAYLOAD_BYTES, 8);
291        assert_eq!(u128::PAYLOAD_BYTES, 16);
292        assert_eq!(<[u64; 8]>::PAYLOAD_BYTES, 64);
293        assert_eq!(<(u32, u64)>::PAYLOAD_BYTES, 12);
294    }
295}