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}