Skip to content

Commit 09d98d2

Browse files
authored
add inter-module lending tests and example with scripts (#10)
* feat: add inter-module lending example with tests and scripts * fix: add wabt to system dependencies in CI workflow
1 parent 8a5d923 commit 09d98d2

8 files changed

Lines changed: 464 additions & 9 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
- uses: Swatinem/rust-cache@v2
2222

2323
- name: Install system dependencies
24-
run: sudo apt-get install -y clang lld
24+
run: sudo apt-get install -y clang lld wabt
2525

2626
- name: Install kani-verifier
2727
run: cargo install --locked kani-verifier && cargo kani setup
@@ -40,3 +40,6 @@ jobs:
4040

4141
- name: Example (C → Wasm → Rust)
4242
run: ./examples/c-to-wasm-to-rust/run.sh
43+
44+
- name: Example (Inter-Module Lending)
45+
run: ./examples/inter-module-lending/run.sh

Cargo.toml

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
[workspace]
22
resolver = "2"
3-
members = [
4-
"crates/herkos-runtime",
5-
"crates/herkos",
6-
"crates/herkos-tests",
7-
]
8-
exclude = [
9-
"examples/c-to-wasm-to-rust",
10-
]
3+
members = ["crates/herkos-runtime", "crates/herkos", "crates/herkos-tests"]
4+
5+
exclude = ["examples/c-to-wasm-to-rust", "examples/inter-module-lending"]
116

127
[workspace.dependencies]
138
anyhow = "1"
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
//! End-to-end tests for inter-module lending.
2+
//!
3+
//! These tests verify the complete inter-module interaction pattern:
4+
//! 1. A host owns memory (simulating a Module's IsolatedMemory)
5+
//! 2. A LibraryModule borrows that memory for read/write operations
6+
//! 3. Import traits enable communication between modules and host
7+
//! 4. Multiple library modules can operate on the same borrowed memory
8+
//! 5. A single host can service import traits from different module types
9+
10+
use herkos_runtime::{IsolatedMemory, WasmResult};
11+
use herkos_tests::{import_basic, import_memory, import_multi};
12+
13+
/// Host that owns memory and coordinates between multiple library modules.
14+
struct InterModuleHost {
15+
/// Values logged by import_memory's print_i32
16+
logged_values: Vec<i32>,
17+
/// Value returned by import_basic's read_i32
18+
read_value: i32,
19+
/// Tracks add calls for import_multi
20+
add_call_count: usize,
21+
}
22+
23+
impl InterModuleHost {
24+
fn new() -> Self {
25+
InterModuleHost {
26+
logged_values: Vec::new(),
27+
read_value: 42,
28+
add_call_count: 0,
29+
}
30+
}
31+
}
32+
33+
// -- import_memory traits --
34+
35+
impl import_memory::EnvImports for InterModuleHost {
36+
fn print_i32(&mut self, value: i32) -> WasmResult<()> {
37+
self.logged_values.push(value);
38+
Ok(())
39+
}
40+
}
41+
42+
// -- import_basic traits --
43+
44+
impl import_basic::EnvImports for InterModuleHost {
45+
fn print_i32(&mut self, value: i32) -> WasmResult<()> {
46+
self.logged_values.push(value);
47+
Ok(())
48+
}
49+
50+
fn read_i32(&mut self) -> WasmResult<i32> {
51+
Ok(self.read_value)
52+
}
53+
}
54+
55+
impl import_basic::WasiSnapshotPreview1Imports for InterModuleHost {
56+
fn fd_write(&mut self, _: i32, _: i32, _: i32, _: i32) -> WasmResult<i32> {
57+
Ok(0)
58+
}
59+
}
60+
61+
// -- import_multi traits --
62+
63+
impl import_multi::EnvImports for InterModuleHost {
64+
fn add(&mut self, a: i32, b: i32) -> WasmResult<i32> {
65+
self.add_call_count += 1;
66+
Ok(a + b)
67+
}
68+
69+
fn mul(&mut self, a: i32, b: i32) -> WasmResult<i32> {
70+
Ok(a * b)
71+
}
72+
73+
fn log(&mut self, value: i32) -> WasmResult<()> {
74+
self.logged_values.push(value);
75+
Ok(())
76+
}
77+
}
78+
79+
impl import_multi::WasiSnapshotPreview1Imports for InterModuleHost {
80+
fn fd_write(&mut self, _: i32, _: i32, _: i32, _: i32) -> WasmResult<i32> {
81+
Ok(0)
82+
}
83+
}
84+
85+
// ─── Tests ───────────────────────────────────────────────────────────────────
86+
87+
#[test]
88+
fn test_host_writes_library_reads() {
89+
let mut memory = Box::new(IsolatedMemory::<4>::try_new(2).unwrap());
90+
let mut lib = import_memory::new().unwrap();
91+
92+
// Host writes values at several offsets
93+
memory.store_i32(0, 100).unwrap();
94+
memory.store_i32(4, 200).unwrap();
95+
memory.store_i32(8, 300).unwrap();
96+
97+
// Library borrows memory and reads back
98+
assert_eq!(lib.read_at(0, &mut memory).unwrap(), 100);
99+
assert_eq!(lib.read_at(4, &mut memory).unwrap(), 200);
100+
assert_eq!(lib.read_at(8, &mut memory).unwrap(), 300);
101+
}
102+
103+
#[test]
104+
fn test_library_writes_host_reads() {
105+
let mut memory = Box::new(IsolatedMemory::<4>::try_new(2).unwrap());
106+
let mut lib = import_memory::new().unwrap();
107+
108+
// Library writes via borrowed memory
109+
lib.write_at(0, 999, &mut memory).unwrap();
110+
lib.write_at(4, 888, &mut memory).unwrap();
111+
112+
// Host reads directly from its own memory
113+
assert_eq!(memory.load_i32(0).unwrap(), 999);
114+
assert_eq!(memory.load_i32(4).unwrap(), 888);
115+
}
116+
117+
#[test]
118+
fn test_roundtrip_through_library() {
119+
let mut memory = Box::new(IsolatedMemory::<4>::try_new(2).unwrap());
120+
let mut lib = import_memory::new().unwrap();
121+
122+
// Host writes initial data
123+
memory.store_i32(0, 50).unwrap();
124+
125+
// Library reads the value
126+
let val = lib.read_at(0, &mut memory).unwrap();
127+
assert_eq!(val, 50);
128+
129+
// Library writes a transformed value at a different offset
130+
lib.write_at(4, val * 3, &mut memory).unwrap();
131+
132+
// Host reads the transformed value
133+
assert_eq!(memory.load_i32(4).unwrap(), 150);
134+
}
135+
136+
#[test]
137+
fn test_library_with_imports_and_memory() {
138+
let mut host = InterModuleHost::new();
139+
let mut memory = Box::new(IsolatedMemory::<4>::try_new(2).unwrap());
140+
let mut lib = import_memory::new().unwrap();
141+
142+
// Host writes a value into memory
143+
memory.store_i32(0, 77).unwrap();
144+
145+
// Library's `process` reads from memory at offset 0 (gets 77),
146+
// calls import print_i32(77), then stores 77 at offset 0.
147+
lib.process(0, &mut memory, &mut host).unwrap();
148+
149+
// Verify the import was called with the value from memory
150+
assert_eq!(host.logged_values, vec![77]);
151+
152+
// Verify the value was stored back
153+
assert_eq!(memory.load_i32(0).unwrap(), 77);
154+
}
155+
156+
#[test]
157+
fn test_two_libraries_same_memory() {
158+
let mut memory = Box::new(IsolatedMemory::<4>::try_new(2).unwrap());
159+
let mut lib_a = import_memory::new().unwrap();
160+
let mut lib_b = import_memory::new().unwrap();
161+
162+
// Library A writes to the shared memory
163+
lib_a.write_at(0, 111, &mut memory).unwrap();
164+
lib_a.write_at(4, 222, &mut memory).unwrap();
165+
166+
// Library B reads from the same memory — should see A's writes
167+
assert_eq!(lib_b.read_at(0, &mut memory).unwrap(), 111);
168+
assert_eq!(lib_b.read_at(4, &mut memory).unwrap(), 222);
169+
170+
// Library B overwrites
171+
lib_b.write_at(0, 333, &mut memory).unwrap();
172+
173+
// Library A sees B's change
174+
assert_eq!(lib_a.read_at(0, &mut memory).unwrap(), 333);
175+
}
176+
177+
#[test]
178+
fn test_multiple_module_types_shared_host() {
179+
let mut host = InterModuleHost::new();
180+
let mut basic_mod = import_basic::new().unwrap();
181+
let mut multi_mod = import_multi::new().unwrap();
182+
183+
// Use import_basic: test_imports calls print_i32 and read_i32
184+
host.read_value = 10;
185+
let result = basic_mod.test_imports(5, &mut host).unwrap();
186+
// test_imports: increments counter, calls print_i32(5), returns read_i32() + 10 = 20
187+
assert_eq!(result, 20);
188+
assert!(host.logged_values.contains(&5));
189+
190+
// Use import_multi: mixed_calls uses env.add import + local mul
191+
let result = multi_mod.mixed_calls(3, 4, &mut host).unwrap();
192+
// mixed_calls(3, 4): add(3,4)=7, local_mul(7,2)=14
193+
assert_eq!(result, 14);
194+
assert_eq!(host.add_call_count, 1);
195+
196+
// Use import_multi: call_local_only uses no imports at all
197+
let result = multi_mod.call_local_only(2, 5).unwrap();
198+
// local_add(2,5)=7, local_mul(7,3)=21
199+
assert_eq!(result, 21);
200+
}
201+
202+
#[test]
203+
fn test_memory_grow_visible_across_modules() {
204+
let mut memory = Box::new(IsolatedMemory::<4>::try_new(1).unwrap());
205+
let mut lib_a = import_memory::new().unwrap();
206+
let mut lib_b = import_memory::new().unwrap();
207+
208+
// Initial size: 1 page
209+
assert_eq!(lib_a.memory_size(&mut memory).unwrap(), 1);
210+
211+
// Library A grows memory by 1 page
212+
let prev = lib_a.try_grow(1, &mut memory).unwrap();
213+
assert_eq!(prev, 1, "previous size should be 1");
214+
215+
// Library B sees the new size
216+
assert_eq!(lib_b.memory_size(&mut memory).unwrap(), 2);
217+
218+
// Library B can write to the grown region (page 1 = offset 65536)
219+
lib_b.write_at(65536, 42, &mut memory).unwrap();
220+
assert_eq!(lib_a.read_at(65536, &mut memory).unwrap(), 42);
221+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Generated artifacts (regenerated by run.sh)
2+
fibonacci.wasm
3+
src/math_library_wasm.rs
4+
target/
5+
Cargo.lock
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "inter-module-lending"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[dependencies]
8+
herkos-runtime = { path = "../../crates/herkos-runtime" }
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
;; Math library module — imports memory from host, provides utility functions.
2+
;; Demonstrates the LibraryModule pattern: this module has no owned memory.
3+
;;
4+
;; Pipeline: math_library.wat → math_library.wasm → src/math_library_wasm.rs
5+
(module
6+
;; Import memory from the host (makes this a LibraryModule)
7+
(import "env" "memory" (memory 1 16))
8+
9+
;; Import a logging function from the host
10+
(import "env" "log_result" (func $log_result (param i32)))
11+
12+
;; sum_array(offset, count): sums count i32 values starting at offset
13+
(func (export "sum_array") (param $offset i32) (param $count i32) (result i32)
14+
(local $sum i32)
15+
(local $i i32)
16+
(local.set $sum (i32.const 0))
17+
(local.set $i (i32.const 0))
18+
(block $done
19+
(loop $loop
20+
(br_if $done (i32.ge_u (local.get $i) (local.get $count)))
21+
(local.set $sum
22+
(i32.add
23+
(local.get $sum)
24+
(i32.load
25+
(i32.add
26+
(local.get $offset)
27+
(i32.mul (local.get $i) (i32.const 4))))))
28+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
29+
(br $loop)))
30+
(local.get $sum))
31+
32+
;; double_array(offset, count): doubles each i32 value in place
33+
(func (export "double_array") (param $offset i32) (param $count i32)
34+
(local $i i32)
35+
(local $addr i32)
36+
(local.set $i (i32.const 0))
37+
(block $done
38+
(loop $loop
39+
(br_if $done (i32.ge_u (local.get $i) (local.get $count)))
40+
(local.set $addr
41+
(i32.add
42+
(local.get $offset)
43+
(i32.mul (local.get $i) (i32.const 4))))
44+
(i32.store
45+
(local.get $addr)
46+
(i32.mul (i32.load (local.get $addr)) (i32.const 2)))
47+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
48+
(br $loop))))
49+
50+
;; sum_and_log(offset, count): sums values and calls log_result with the sum
51+
(func (export "sum_and_log") (param $offset i32) (param $count i32) (result i32)
52+
(local $sum i32)
53+
(local $i i32)
54+
(local.set $sum (i32.const 0))
55+
(local.set $i (i32.const 0))
56+
(block $done
57+
(loop $loop
58+
(br_if $done (i32.ge_u (local.get $i) (local.get $count)))
59+
(local.set $sum
60+
(i32.add
61+
(local.get $sum)
62+
(i32.load
63+
(i32.add
64+
(local.get $offset)
65+
(i32.mul (local.get $i) (i32.const 4))))))
66+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
67+
(br $loop)))
68+
(call $log_result (local.get $sum))
69+
(local.get $sum))
70+
71+
;; memory_info: returns current memory size in pages
72+
(func (export "memory_info") (result i32)
73+
memory.size)
74+
)

0 commit comments

Comments
 (0)