Skip to main content

pathland_core/
memory.rs

1//! Linear-memory layout and the 80-byte header block.
2//!
3//! The guest engine and host share a single linear memory with fixed regions:
4//! header block → ring buffer → arena → event arena. See `spec/OPCODE.md`.
5
6use crate::opcode::OPCODE_SIZE;
7
8/// Magic value written at header offset 0 (`"PLPL"`).
9pub const MAGIC: u32 = 0x504C_504C;
10/// Wire protocol version.
11pub const PROTOCOL_VERSION: u16 = 1;
12
13/// Default ring capacity in slots (power of two).
14pub const DEFAULT_SLOT_COUNT: usize = 4096;
15/// Default arena capacity in bytes.
16pub const DEFAULT_ARENA_BYTES: usize = 1 << 20; // 1 MiB
17/// Default host → guest (event) arena capacity in bytes.
18pub const DEFAULT_EVENT_ARENA_BYTES: usize = 1 << 20; // 1 MiB
19
20// --- Header field offsets (80-byte header) ---
21/// Byte offset of `magic: u32`.
22pub const OFF_MAGIC: usize = 0x00;
23/// Byte offset of `version: u16`.
24pub const OFF_VERSION: usize = 0x04;
25/// Byte offset of `ringOffset: u32`.
26pub const OFF_RING_OFFSET: usize = 0x06;
27/// Byte offset of `ringBytes: u32`.
28pub const OFF_RING_BYTES: usize = 0x0A;
29/// Byte offset of `slotCount: u32`.
30pub const OFF_SLOT_COUNT: usize = 0x0E;
31/// Byte offset of `arenaOffset: u32`.
32pub const OFF_ARENA_OFFSET: usize = 0x12;
33/// Byte offset of `arenaBytes: u32`.
34pub const OFF_ARENA_BYTES: usize = 0x16;
35/// Byte offset of `arenaCursor: u32` (guest-owned).
36pub const OFF_ARENA_CURSOR: usize = 0x1A;
37/// Byte offset of `readCursor: u32` (host-owned).
38pub const OFF_READ_CURSOR: usize = 0x1E;
39/// Byte offset of `writeCursor: u32` (guest-owned).
40pub const OFF_WRITE_CURSOR: usize = 0x22;
41/// Byte offset of `frameCount: u32` (guest-owned).
42pub const OFF_FRAME_COUNT: usize = 0x26;
43/// Byte offset of `eventRingOffset: u32`.
44pub const OFF_EVENT_RING_OFFSET: usize = 0x2A;
45/// Byte offset of `eventRingBytes: u32`.
46pub const OFF_EVENT_RING_BYTES: usize = 0x2E;
47/// Byte offset of `eventSlotCount: u32`.
48pub const OFF_EVENT_SLOT_COUNT: usize = 0x32;
49/// Byte offset of `eventReadCursor: u32` (guest-owned).
50pub const OFF_EVENT_READ_CURSOR: usize = 0x36;
51/// Byte offset of `eventWriteCursor: u32` (host-owned).
52pub const OFF_EVENT_WRITE_CURSOR: usize = 0x3A;
53/// Byte offset of `eventArenaOffset: u32`.
54pub const OFF_EVENT_ARENA_OFFSET: usize = 0x40;
55/// Byte offset of `eventArenaBytes: u32`.
56pub const OFF_EVENT_ARENA_BYTES: usize = 0x44;
57/// Byte offset of `eventArenaCursor: u32` (host-owned).
58pub const OFF_EVENT_ARENA_CURSOR: usize = 0x48;
59/// Total header size in bytes.
60pub const HEADER_SIZE: usize = 0x50;
61
62/// The fixed layout of the shared linear memory.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub struct MemoryLayout {
65    /// Ring capacity in slots (must be a power of two).
66    pub slot_count: usize,
67    /// Guest arena capacity in bytes.
68    pub arena_bytes: usize,
69    /// Host → guest (event) arena capacity in bytes.
70    pub event_arena_bytes: usize,
71}
72
73impl Default for MemoryLayout {
74    fn default() -> Self {
75        Self {
76            slot_count: DEFAULT_SLOT_COUNT,
77            arena_bytes: DEFAULT_ARENA_BYTES,
78            event_arena_bytes: DEFAULT_EVENT_ARENA_BYTES,
79        }
80    }
81}
82
83impl MemoryLayout {
84    /// Ring region byte length.
85    #[inline]
86    pub fn ring_bytes(&self) -> usize {
87        self.slot_count * OPCODE_SIZE
88    }
89
90    /// Event (host → guest) ring region byte length. Reuses `slot_count` so
91    /// both rings share the same capacity (power of two).
92    #[inline]
93    pub fn event_ring_bytes(&self) -> usize {
94        self.slot_count * OPCODE_SIZE
95    }
96
97    /// Total linear memory size: header + ring + event ring + arena + event
98    /// arena.
99    pub fn total_bytes(&self) -> usize {
100        HEADER_SIZE
101            + self.ring_bytes()
102            + self.event_ring_bytes()
103            + self.arena_bytes
104            + self.event_arena_bytes
105    }
106
107    /// Byte offset of the ring region (directly after the header).
108    #[inline]
109    pub fn ring_offset(&self) -> usize {
110        HEADER_SIZE
111    }
112
113    /// Byte offset of the host → guest event ring region (after the main ring).
114    #[inline]
115    pub fn event_ring_offset(&self) -> usize {
116        self.ring_offset() + self.ring_bytes()
117    }
118
119    /// Byte offset of the guest arena region (directly after the event ring).
120    #[inline]
121    pub fn arena_offset(&self) -> usize {
122        self.event_ring_offset() + self.event_ring_bytes()
123    }
124
125    /// Byte offset of the host → guest (event) arena region (after the guest
126    /// arena). Owned by the host: it bump-allocates event string payloads here.
127    #[inline]
128    pub fn event_arena_offset(&self) -> usize {
129        self.arena_offset() + self.arena_bytes
130    }
131
132    /// Ring mask for wrapping a cursor (slot count is a power of two).
133    #[inline]
134    pub fn ring_mask(&self) -> usize {
135        debug_assert!(self.slot_count.is_power_of_two());
136        self.slot_count - 1
137    }
138}
139
140/// Low-level accessors for the header block.
141///
142/// All accesses use explicit little-endian byte reads so the header can sit at
143/// an arbitrary alignment within the linear memory.
144pub(crate) mod header {
145    use super::*;
146
147    #[inline]
148    pub(crate) fn get_u32(buf: &[u8], off: usize) -> u32 {
149        u32::from_le_bytes(buf[off..off + 4].try_into().unwrap())
150    }
151
152    #[inline]
153    pub(crate) fn set_u32(buf: &mut [u8], off: usize, value: u32) {
154        buf[off..off + 4].copy_from_slice(&value.to_le_bytes());
155    }
156
157    #[inline]
158    pub(crate) fn get_u16(buf: &[u8], off: usize) -> u16 {
159        u16::from_le_bytes(buf[off..off + 2].try_into().unwrap())
160    }
161
162    /// Initialize a zeroed header for the given layout.
163    pub(crate) fn init(buf: &mut [u8], layout: &MemoryLayout) {
164        buf[..HEADER_SIZE].fill(0);
165        set_u32(buf, OFF_MAGIC, MAGIC);
166        buf[OFF_VERSION..OFF_VERSION + 2].copy_from_slice(&PROTOCOL_VERSION.to_le_bytes());
167        set_u32(buf, OFF_RING_OFFSET, layout.ring_offset() as u32);
168        set_u32(buf, OFF_RING_BYTES, layout.ring_bytes() as u32);
169        set_u32(buf, OFF_SLOT_COUNT, layout.slot_count as u32);
170        set_u32(buf, OFF_ARENA_OFFSET, layout.arena_offset() as u32);
171        set_u32(buf, OFF_ARENA_BYTES, layout.arena_bytes as u32);
172        set_u32(buf, OFF_ARENA_CURSOR, 0);
173        set_u32(buf, OFF_READ_CURSOR, 0);
174        set_u32(buf, OFF_WRITE_CURSOR, 0);
175        set_u32(buf, OFF_FRAME_COUNT, 0);
176        set_u32(buf, OFF_EVENT_RING_OFFSET, layout.event_ring_offset() as u32);
177        set_u32(buf, OFF_EVENT_RING_BYTES, layout.event_ring_bytes() as u32);
178        set_u32(buf, OFF_EVENT_SLOT_COUNT, layout.slot_count as u32);
179        set_u32(buf, OFF_EVENT_READ_CURSOR, 0);
180        set_u32(buf, OFF_EVENT_WRITE_CURSOR, 0);
181        set_u32(buf, OFF_EVENT_ARENA_OFFSET, layout.event_arena_offset() as u32);
182        set_u32(buf, OFF_EVENT_ARENA_BYTES, layout.event_arena_bytes as u32);
183        set_u32(buf, OFF_EVENT_ARENA_CURSOR, 0);
184    }
185
186    /// Validate a header against a layout; returns false if the memory is not a
187    /// valid Pathland linear memory.
188    #[allow(dead_code)]
189    pub(crate) fn validate(buf: &[u8], layout: &MemoryLayout) -> bool {
190        if buf.len() < HEADER_SIZE {
191            return false;
192        }
193        let magic = get_u32(buf, OFF_MAGIC);
194        if magic != MAGIC {
195            return false;
196        }
197        let version = get_u16(buf, OFF_VERSION);
198        if version != PROTOCOL_VERSION {
199            return false;
200        }
201        get_u32(buf, OFF_SLOT_COUNT) as usize == layout.slot_count
202            && get_u32(buf, OFF_ARENA_BYTES) as usize == layout.arena_bytes
203            && get_u32(buf, OFF_EVENT_RING_OFFSET) as usize == layout.event_ring_offset()
204            && get_u32(buf, OFF_EVENT_RING_BYTES) as usize == layout.event_ring_bytes()
205            && get_u32(buf, OFF_EVENT_SLOT_COUNT) as usize == layout.slot_count
206            && get_u32(buf, OFF_EVENT_ARENA_OFFSET) as usize == layout.event_arena_offset()
207            && get_u32(buf, OFF_EVENT_ARENA_BYTES) as usize == layout.event_arena_bytes
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use alloc::vec;
214    use super::*;
215
216    #[test]
217    fn header_size_is_80() {
218        assert_eq!(HEADER_SIZE, 80);
219    }
220
221    #[test]
222    fn validate_rejects_wrong_magic() {
223        let layout = MemoryLayout::default();
224        let mut buf = vec![0u8; layout.total_bytes()];
225        header::init(&mut buf, &layout);
226        buf[OFF_MAGIC] ^= 0xFF;
227        assert!(!header::validate(&buf, &layout));
228    }
229
230    #[test]
231    fn validate_rejects_wrong_version() {
232        let layout = MemoryLayout::default();
233        let mut buf = vec![0u8; layout.total_bytes()];
234        header::init(&mut buf, &layout);
235        buf[OFF_VERSION] = (PROTOCOL_VERSION + 1) as u8;
236        assert!(!header::validate(&buf, &layout));
237    }
238
239    #[test]
240    fn validate_rejects_mismatched_layout() {
241        let layout = MemoryLayout::default();
242        let mut buf = vec![0u8; layout.total_bytes()];
243        header::init(&mut buf, &layout);
244
245        // A different arena size must be rejected.
246        let other = MemoryLayout {
247            slot_count: layout.slot_count,
248            arena_bytes: layout.arena_bytes + 4096,
249            event_arena_bytes: layout.event_arena_bytes,
250        };
251        assert!(!header::validate(&buf, &other));
252
253        // A different event arena size must be rejected.
254        let other = MemoryLayout {
255            slot_count: layout.slot_count,
256            arena_bytes: layout.arena_bytes,
257            event_arena_bytes: layout.event_arena_bytes + 4096,
258        };
259        assert!(!header::validate(&buf, &other));
260
261        // A different slot count must be rejected.
262        let other = MemoryLayout {
263            slot_count: layout.slot_count / 2,
264            arena_bytes: layout.arena_bytes,
265            event_arena_bytes: layout.event_arena_bytes,
266        };
267        assert!(!header::validate(&buf, &other));
268    }
269
270    #[test]
271    fn validate_rejects_short_buffer() {
272        let layout = MemoryLayout::default();
273        let tiny = vec![0u8; HEADER_SIZE - 1];
274        assert!(!header::validate(&tiny, &layout));
275    }
276
277    #[test]
278    fn default_layout_is_contiguous() {
279        let layout = MemoryLayout::default();
280        assert_eq!(layout.ring_offset(), HEADER_SIZE);
281        assert_eq!(
282            layout.event_ring_offset(),
283            HEADER_SIZE + layout.ring_bytes()
284        );
285        assert_eq!(
286            layout.arena_offset(),
287            HEADER_SIZE + layout.ring_bytes() + layout.event_ring_bytes()
288        );
289        assert_eq!(
290            layout.event_arena_offset(),
291            HEADER_SIZE + layout.ring_bytes() + layout.event_ring_bytes() + layout.arena_bytes
292        );
293        assert_eq!(
294            layout.total_bytes(),
295            HEADER_SIZE
296                + layout.ring_bytes()
297                + layout.event_ring_bytes()
298                + layout.arena_bytes
299                + layout.event_arena_bytes
300        );
301        assert!(layout.slot_count.is_power_of_two());
302    }
303
304    #[test]
305    fn header_round_trips() {
306        let layout = MemoryLayout::default();
307        let mut buf = vec![0u8; layout.total_bytes()];
308        header::init(&mut buf, &layout);
309        assert!(header::validate(&buf, &layout));
310        assert_eq!(header::get_u32(&buf, OFF_SLOT_COUNT), layout.slot_count as u32);
311        assert_eq!(header::get_u32(&buf, OFF_ARENA_BYTES), layout.arena_bytes as u32);
312        assert_eq!(header::get_u32(&buf, OFF_ARENA_CURSOR), 0);
313        assert_eq!(header::get_u32(&buf, OFF_READ_CURSOR), 0);
314        assert_eq!(header::get_u32(&buf, OFF_WRITE_CURSOR), 0);
315        assert_eq!(header::get_u32(&buf, OFF_FRAME_COUNT), 0);
316        assert_eq!(
317            header::get_u32(&buf, OFF_EVENT_RING_OFFSET),
318            layout.event_ring_offset() as u32
319        );
320        assert_eq!(
321            header::get_u32(&buf, OFF_EVENT_RING_BYTES),
322            layout.event_ring_bytes() as u32
323        );
324        assert_eq!(
325            header::get_u32(&buf, OFF_EVENT_READ_CURSOR),
326            0
327        );
328        assert_eq!(
329            header::get_u32(&buf, OFF_EVENT_WRITE_CURSOR),
330            0
331        );
332        assert_eq!(
333            header::get_u32(&buf, OFF_EVENT_ARENA_OFFSET),
334            layout.event_arena_offset() as u32
335        );
336        assert_eq!(
337            header::get_u32(&buf, OFF_EVENT_ARENA_BYTES),
338            layout.event_arena_bytes as u32
339        );
340        assert_eq!(
341            header::get_u32(&buf, OFF_EVENT_ARENA_CURSOR),
342            0
343        );
344    }
345}