Skip to main content

herkos_runtime/
module.rs

1//! Wasm module containers — `Module` and `LibraryModule`.
2//!
3//! A `Module` owns its own linear memory (like a POSIX process).
4//! A `LibraryModule` borrows the caller's memory (like a shared library).
5//!
6//! Both contain module-specific globals (`G`, transpiler-generated) and an
7//! indirect call table. The transpiler generates `impl` blocks with the
8//! concrete exported/internal functions.
9
10use crate::memory::IsolatedMemory;
11use crate::table::Table;
12
13/// A module that defines its own memory (§4.1).
14///
15/// - `G`: transpiler-generated globals struct (one typed field per Wasm global)
16/// - `MAX_PAGES`: maximum linear memory size (Wasm pages, 64 KiB each)
17/// - `TABLE_SIZE`: maximum indirect call table entries
18///
19/// The transpiler generates an `impl` block on this struct with the
20/// module's exported and internal functions.
21pub struct Module<G, const MAX_PAGES: usize, const TABLE_SIZE: usize> {
22    /// Owned linear memory — isolated by the Rust type system.
23    pub memory: IsolatedMemory<MAX_PAGES>,
24    /// Module-level global variables.
25    pub globals: G,
26    /// Indirect call table for `call_indirect`.
27    pub table: Table<TABLE_SIZE>,
28}
29
30impl<G, const MAX_PAGES: usize, const TABLE_SIZE: usize> Module<G, MAX_PAGES, TABLE_SIZE> {
31    /// Create a new module with the given initial memory size, globals, and table.
32    ///
33    /// The transpiler generates a wrapper that calls this with the correct
34    /// initial values derived from the Wasm binary (data segments, element
35    /// segments, global initializers).
36    ///
37    /// # Errors
38    /// Returns `ConstructionError` if `initial_pages` exceeds `MAX_PAGES`.
39    #[inline(never)]
40    pub fn try_new(
41        initial_pages: usize,
42        globals: G,
43        table: Table<TABLE_SIZE>,
44    ) -> Result<Self, crate::ConstructionError> {
45        Ok(Self {
46            memory: IsolatedMemory::try_new(initial_pages)?,
47            globals,
48            table,
49        })
50    }
51
52    /// Initialize a `Module` in-place within a caller-provided slot.
53    ///
54    /// Unlike `try_new`, this writes directly into `slot` without ever creating
55    /// a large `Result<Self, E>` on the call stack. Use this when `MAX_PAGES`
56    /// is large, to avoid stack overflow in debug builds.
57    ///
58    /// # Errors
59    /// Returns `ConstructionError` if `initial_pages` exceeds `MAX_PAGES`.
60    #[inline(never)]
61    pub fn try_init(
62        slot: &mut core::mem::MaybeUninit<Self>,
63        initial_pages: usize,
64        globals: G,
65        table: Table<TABLE_SIZE>,
66    ) -> Result<(), crate::ConstructionError> {
67        let ptr = slot.as_mut_ptr();
68        // SAFETY: ptr comes from MaybeUninit so it is valid for writes and
69        // correctly aligned. We initialise all three fields before the caller
70        // can call assume_init on the slot. The cast of the memory field pointer
71        // to *mut MaybeUninit<IsolatedMemory<MAX_PAGES>> is valid because
72        // MaybeUninit<T> has the same memory layout as T (guaranteed by the
73        // standard library), and the field is currently uninitialized.
74        unsafe {
75            IsolatedMemory::try_init(
76                &mut *(core::ptr::addr_of_mut!((*ptr).memory)
77                    as *mut core::mem::MaybeUninit<IsolatedMemory<MAX_PAGES>>),
78                initial_pages,
79            )?;
80            core::ptr::addr_of_mut!((*ptr).globals).write(globals);
81            core::ptr::addr_of_mut!((*ptr).table).write(table);
82        }
83        Ok(())
84    }
85}
86
87/// A module that does NOT define its own memory (§4.1).
88///
89/// Operates on borrowed memory from the caller, like a shared library
90/// using the host process's address space. Rust's borrow checker enforces
91/// that the library cannot retain the memory reference beyond a call.
92///
93/// - `G`: transpiler-generated globals struct
94/// - `TABLE_SIZE`: maximum indirect call table entries
95pub struct LibraryModule<G, const TABLE_SIZE: usize> {
96    /// Module-level global variables.
97    pub globals: G,
98    /// Indirect call table for `call_indirect`.
99    pub table: Table<TABLE_SIZE>,
100}
101
102impl<G, const TABLE_SIZE: usize> LibraryModule<G, TABLE_SIZE> {
103    /// Create a new library module with the given globals and table.
104    #[inline]
105    pub fn new(globals: G, table: Table<TABLE_SIZE>) -> Self {
106        Self { globals, table }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::table::FuncRef;
114    use crate::WasmTrap;
115
116    /// Example transpiler-generated globals struct.
117    #[derive(Debug, Default, PartialEq)]
118    struct TestGlobals {
119        g0: i32,
120        g1: i64,
121    }
122
123    #[test]
124    fn module_new_owns_memory() {
125        let module = Module::<TestGlobals, 2, 0>::try_new(
126            2,
127            TestGlobals { g0: 42, g1: -1 },
128            Table::try_new(0).unwrap(),
129        )
130        .unwrap();
131        assert_eq!(module.memory.page_count(), 2);
132        assert_eq!(module.globals.g0, 42);
133        assert_eq!(module.globals.g1, -1);
134        assert_eq!(module.table.size(), 0);
135    }
136
137    #[test]
138    fn module_memory_is_isolated() {
139        let mut m1 = Module::<TestGlobals, 2, 0>::try_new(
140            1,
141            TestGlobals::default(),
142            Table::try_new(0).unwrap(),
143        )
144        .unwrap();
145        let m2 = Module::<TestGlobals, 2, 0>::try_new(
146            1,
147            TestGlobals::default(),
148            Table::try_new(0).unwrap(),
149        )
150        .unwrap();
151
152        // Write to m1's memory — m2 is unaffected.
153        m1.memory.store_i32(0, 0xDEAD_BEEF_u32 as i32).unwrap();
154        assert_eq!(m2.memory.load_i32(0).unwrap(), 0);
155    }
156
157    #[test]
158    fn library_module_borrows_caller_memory() {
159        let mut caller = Module::<TestGlobals, 2, 0>::try_new(
160            1,
161            TestGlobals::default(),
162            Table::try_new(0).unwrap(),
163        )
164        .unwrap();
165        let lib = LibraryModule::<TestGlobals, 0>::new(
166            TestGlobals { g0: 7, g1: 0 },
167            Table::try_new(0).unwrap(),
168        );
169
170        // Caller writes to its own memory.
171        caller.memory.store_i32(0, 99).unwrap();
172
173        // Library can borrow caller's memory and read what was written.
174        // (In real transpiled code, this borrow happens inside generated methods.)
175        let val = caller.memory.load_i32(0).unwrap();
176        assert_eq!(val, 99);
177        assert_eq!(lib.globals.g0, 7);
178    }
179
180    #[test]
181    fn module_with_table() {
182        let mut table = Table::<4>::try_new(2).unwrap();
183        table
184            .set(
185                0,
186                Some(FuncRef {
187                    type_index: 0,
188                    func_index: 3,
189                }),
190            )
191            .unwrap();
192
193        let module = Module::<(), 2, 4>::try_new(1, (), table).unwrap();
194        let entry = module.table.get(0).unwrap();
195        assert_eq!(entry.func_index, 3);
196        assert_eq!(module.table.get(1), Err(WasmTrap::UndefinedElement));
197    }
198
199    #[test]
200    fn library_module_no_globals() {
201        // G = () for modules with no globals
202        let lib = LibraryModule::<(), 0>::new((), Table::try_new(0).unwrap());
203        assert_eq!(lib.globals, ());
204    }
205}