1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![warn(missing_docs)]
3use std::path::{Path, PathBuf};
14use std::sync::Arc;
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::time::{Instant, UNIX_EPOCH};
17
18use sha2::{Digest, Sha256};
19use wasmtime::{
20 Config, Engine, Linker, Module, OptLevel, Store, StoreLimits, StoreLimitsBuilder, Trap,
21};
22use wasmtime_wasi::I32Exit;
23use wasmtime_wasi::p1::{self, WasiP1Ctx};
24use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
25use wasmtime_wasi::{DirPerms, FilePerms, WasiCtxBuilder};
26
27use mimobox_core::{Sandbox, SandboxConfig, SandboxError, SandboxResult};
28
29struct StoreData {
35 wasi: WasiP1Ctx,
36 limits: StoreLimits,
37}
38
39fn wasm_logging_enabled() -> bool {
41 std::env::var_os("MIMOBOX_WASM_QUIET").is_none()
42}
43
44macro_rules! log_info {
45 ($($arg:tt)*) => {
46 if wasm_logging_enabled() {
47 eprintln!("[mimobox:wasm:info] {}", format!($($arg)*))
48 }
49 };
50}
51
52macro_rules! log_warn {
53 ($($arg:tt)*) => {
54 if wasm_logging_enabled() {
55 eprintln!("[mimobox:wasm:warn] {}", format!($($arg)*))
56 }
57 };
58}
59
60const FUEL_PER_SECOND: u64 = 15_000_000;
62
63const DEFAULT_FUEL_LIMIT: u64 = 10_000_000;
65
66const OUTPUT_MAX_CAPACITY: usize = 1024 * 1024;
70
71const MAX_OUTPUT_SIZE: usize = 4 * 1024 * 1024;
73
74const MAX_WASM_FILE_SIZE: u64 = 100 * 1024 * 1024;
76
77const DEFAULT_MEMORY_LIMIT_MB: u64 = 64;
79
80const EPOCH_TICK_INTERVAL_MS: u64 = 10;
82
83fn fuel_from_timeout(timeout_secs: Option<u64>) -> u64 {
90 match timeout_secs {
91 Some(secs) => secs.saturating_mul(FUEL_PER_SECOND),
92 None => DEFAULT_FUEL_LIMIT,
93 }
94}
95
96fn truncate_output(data: Vec<u8>, label: &str) -> Vec<u8> {
98 if data.len() > MAX_OUTPUT_SIZE {
99 log_warn!(
100 "{} output exceeded limit ({} > {} bytes), truncated",
101 label,
102 data.len(),
103 MAX_OUTPUT_SIZE
104 );
105 data[..MAX_OUTPUT_SIZE].to_vec()
106 } else {
107 data
108 }
109}
110
111pub struct WasmSandbox {
116 engine: Arc<Engine>,
117 config: SandboxConfig,
118 cache_dir: PathBuf,
119 epoch_running: Arc<AtomicBool>,
120 epoch_thread: Option<std::thread::JoinHandle<()>>,
121}
122
123fn content_hash(data: &[u8]) -> String {
128 let hash = Sha256::digest(data);
129 format!("{:x}", hash)
130}
131
132fn file_fingerprint(path: &Path) -> Option<(u64, u64)> {
137 let meta = std::fs::metadata(path).ok()?;
138 let size = meta.len();
139 let mtime = meta
140 .modified()
141 .ok()?
142 .duration_since(UNIX_EPOCH)
143 .ok()?
144 .as_nanos() as u64;
145 Some((size, mtime))
146}
147
148fn compile_module_from_bytes(
149 engine: &Engine,
150 wasm_path: &Path,
151 bytes: &[u8],
152) -> Result<Module, SandboxError> {
153 Module::from_binary(engine, bytes).map_err(|e| {
156 SandboxError::ExecutionFailed(format!(
157 "Failed to load Wasm module ({:?}): {}",
158 wasm_path, e
159 ))
160 })
161}
162
163fn get_cached_module(
172 engine: &Engine,
173 wasm_path: &Path,
174 cache_dir: &Path,
175) -> Result<Module, SandboxError> {
176 let _ = std::fs::create_dir_all(cache_dir);
177
178 let fingerprint = match file_fingerprint(wasm_path) {
179 Some(fp) => fp,
180 None => {
181 let file_data = std::fs::read(wasm_path).map_err(|e| {
183 SandboxError::ExecutionFailed(format!("Failed to read Wasm file: {}", e))
184 })?;
185 return compile_module_from_bytes(engine, wasm_path, &file_data);
186 }
187 };
188
189 let map_file = cache_dir.join(format!("{}_{}.map", fingerprint.0, fingerprint.1));
192
193 if let Ok(hash) = std::fs::read_to_string(&map_file) {
195 let cache_path = cache_dir.join(format!("{}.cwasm", hash.trim()));
196 match std::fs::read(&cache_path) {
197 Ok(cached) => {
198 match unsafe { Module::deserialize(engine, &cached) } {
202 Ok(module) => {
203 log_info!("Loaded module from cache: {:?}", wasm_path);
204 return Ok(module);
205 }
206 Err(e) => {
207 log_warn!("Cache deserialization failed, recompiling: {}", e);
209 let _ = std::fs::remove_file(&cache_path);
210 let _ = std::fs::remove_file(&map_file);
211 }
212 }
213 }
214 Err(_) => {
215 let _ = std::fs::remove_file(&map_file);
217 }
218 }
219 }
220
221 let file_data = std::fs::read(wasm_path)
223 .map_err(|e| SandboxError::ExecutionFailed(format!("Failed to read Wasm file: {}", e)))?;
224 let hash = content_hash(&file_data);
225 let cache_path = cache_dir.join(format!("{}.cwasm", hash));
226
227 if let Ok(cached) = std::fs::read(&cache_path) {
229 match unsafe { Module::deserialize(engine, &cached) } {
233 Ok(module) => {
234 let _ = std::fs::write(&map_file, &hash);
236 log_info!("Loaded module from cache (content match): {:?}", wasm_path);
237 return Ok(module);
238 }
239 Err(e) => {
240 log_warn!("Cache deserialization failed, recompiling: {}", e);
241 let _ = std::fs::remove_file(&cache_path);
242 }
243 }
244 }
245
246 let module = compile_module_from_bytes(engine, wasm_path, &file_data)?;
248
249 if let Ok(serialized) = module.serialize() {
251 let tmp_path = cache_path.with_extension("cwasm.tmp");
252 if std::fs::write(&tmp_path, &serialized).is_ok() {
253 if let Err(e) = std::fs::rename(&tmp_path, &cache_path) {
255 log_warn!("Failed to rename cache file: {}", e);
256 let _ = std::fs::remove_file(&tmp_path);
257 }
258 }
259 let _ = std::fs::write(&map_file, &hash);
261 }
262
263 log_info!("Compiled and cached module: {:?}", wasm_path);
264 Ok(module)
265}
266
267fn create_engine_config() -> Config {
269 let mut config = Config::new();
270 config.cranelift_opt_level(OptLevel::Speed);
271 config.consume_fuel(true);
272 config.epoch_interruption(true);
273 config.max_wasm_stack(512 * 1024); config.parallel_compilation(true);
275 config
276}
277
278fn build_wasi_ctx(
283 config: &SandboxConfig,
284 args: &[String],
285 stdout_pipe: MemoryOutputPipe,
286 stderr_pipe: MemoryOutputPipe,
287) -> WasiP1Ctx {
288 let mut builder = WasiCtxBuilder::new();
289
290 for arg in args {
292 builder.arg(arg);
293 }
294
295 builder.env("HOME", "/tmp");
297 builder.env("PATH", "/usr/bin:/bin");
298 builder.env("TERM", "dumb");
299 builder.env("SANDBOX", "wasm");
300
301 builder.stdout(Box::new(stdout_pipe));
303 builder.stderr(Box::new(stderr_pipe));
304
305 for path in &config.fs_readonly {
307 if let Some(path_str) = path.to_str() {
308 if path.exists() {
309 if let Err(e) =
311 builder.preopened_dir(path, path_str, DirPerms::READ, FilePerms::READ)
312 {
313 log_warn!("Failed to preopen read-only dir {:?}: {}", path, e);
314 }
315 } else {
316 log_warn!("Read-only path does not exist: {:?}", path);
317 }
318 }
319 }
320 for path in &config.fs_readwrite {
321 if let Some(path_str) = path.to_str() {
322 if path.exists() {
323 if let Err(e) =
324 builder.preopened_dir(path, path_str, DirPerms::all(), FilePerms::all())
325 {
326 log_warn!("Failed to preopen read-write dir {:?}: {}", path, e);
327 }
328 } else {
329 log_warn!("Read-write path does not exist: {:?}", path);
330 }
331 }
332 }
333
334 if config.deny_network {
335 log_info!("WASI network denied by SandboxConfig; no sockets are preopened");
336 } else {
337 log_warn!(
338 "SandboxConfig allows network, but WASI backend cannot enable network access; keeping network denied"
339 );
340 }
341
342 builder.build_p1()
346}
347
348impl Sandbox for WasmSandbox {
349 fn new(config: SandboxConfig) -> Result<Self, SandboxError> {
350 let engine_config = create_engine_config();
351 let engine = Arc::new(Engine::new(&engine_config).map_err(|e| {
352 SandboxError::ExecutionFailed(format!("Failed to create Wasmtime Engine: {}", e))
353 })?);
354
355 let epoch_running = Arc::new(AtomicBool::new(true));
356 let epoch_thread_engine = engine.clone();
357 let epoch_thread_running = epoch_running.clone();
358 let epoch_thread = std::thread::Builder::new()
359 .name("mimobox-wasm-epoch-ticker".to_string())
360 .spawn(move || {
361 let tick_interval = std::time::Duration::from_millis(EPOCH_TICK_INTERVAL_MS);
362 while epoch_thread_running.load(Ordering::Relaxed) {
363 std::thread::sleep(tick_interval);
364 epoch_thread_engine.increment_epoch();
365 }
366 })
367 .map_err(|e| {
368 SandboxError::ExecutionFailed(format!("Failed to start Wasm epoch ticker: {}", e))
369 })?;
370
371 let uid = unsafe { libc::geteuid() };
374 let cache_dir = std::env::temp_dir().join(format!("mimobox-cache-{}", uid));
375
376 log_info!(
377 "Created Wasm sandbox backend, memory_limit={:?}MB, timeout={:?}s, cache_dir={:?}",
378 config.memory_limit_mb,
379 config.timeout_secs,
380 cache_dir,
381 );
382
383 Ok(Self {
384 engine,
385 config,
386 cache_dir,
387 epoch_running,
388 epoch_thread: Some(epoch_thread),
389 })
390 }
391
392 fn execute(&mut self, cmd: &[String]) -> Result<SandboxResult, SandboxError> {
393 let start = Instant::now();
394
395 if cmd.is_empty() {
396 return Err(SandboxError::ExecutionFailed("Command is empty".into()));
397 }
398
399 let wasm_path = Path::new(&cmd[0]);
400 if !wasm_path.exists() {
401 return Err(SandboxError::ExecutionFailed(format!(
402 "Wasm file does not exist: {:?}",
403 wasm_path
404 )));
405 }
406
407 if let Ok(meta) = std::fs::metadata(wasm_path)
409 && meta.len() > MAX_WASM_FILE_SIZE
410 {
411 return Err(SandboxError::ExecutionFailed(format!(
412 "Wasm file too large: {} bytes (limit {} bytes)",
413 meta.len(),
414 MAX_WASM_FILE_SIZE
415 )));
416 }
417
418 let module = get_cached_module(&self.engine, wasm_path, &self.cache_dir)?;
420
421 let stdout_pipe = MemoryOutputPipe::new(OUTPUT_MAX_CAPACITY);
426 let stdout_reader = stdout_pipe.clone(); let stderr_pipe = MemoryOutputPipe::new(OUTPUT_MAX_CAPACITY);
428 let stderr_reader = stderr_pipe.clone(); let wasi_ctx = build_wasi_ctx(&self.config, cmd, stdout_pipe, stderr_pipe);
432
433 let mut linker: Linker<StoreData> = Linker::new(&self.engine);
436 p1::add_to_linker_sync(&mut linker, |data| &mut data.wasi).map_err(|e| {
437 SandboxError::ExecutionFailed(format!("Failed to register WASI Preview 1: {}", e))
438 })?;
439
440 let memory_limit_bytes: usize = self
443 .config
444 .memory_limit_mb
445 .map(|mb| mb * 1024 * 1024)
446 .unwrap_or(DEFAULT_MEMORY_LIMIT_MB * 1024 * 1024)
447 .try_into()
448 .unwrap_or(usize::MAX);
449
450 let limits = StoreLimitsBuilder::new()
451 .memory_size(memory_limit_bytes)
452 .memories(1) .tables(4) .instances(1) .trap_on_grow_failure(true) .build();
457
458 let store_data = StoreData {
459 wasi: wasi_ctx,
460 limits,
461 };
462 let mut store = Store::new(&self.engine, store_data);
463 store.limiter(|data| &mut data.limits);
464
465 let fuel_limit = fuel_from_timeout(self.config.timeout_secs);
467 store
468 .set_fuel(fuel_limit)
469 .map_err(|e| SandboxError::ExecutionFailed(format!("Failed to set fuel: {}", e)))?;
470
471 store.epoch_deadline_trap();
475 let epoch_deadline_ticks = self
476 .config
477 .timeout_secs
478 .map(|s| s.saturating_mul(100)) .unwrap_or(3000); store.set_epoch_deadline(epoch_deadline_ticks);
481
482 let instance = linker.instantiate(&mut store, &module).map_err(|e| {
484 SandboxError::ExecutionFailed(format!("Failed to instantiate Wasm module: {}", e))
485 })?;
486
487 let exit_code = match instance.get_typed_func::<(), ()>(&mut store, "_start") {
491 Ok(start_func) => {
492 match start_func.call(&mut store, ()) {
493 Ok(()) => Some(0),
494 Err(e) => {
495 if let Some(exit) = find_exit_code(&e) {
498 Some(exit)
499 } else if is_fuel_exhausted(&store) || is_epoch_interrupt(&e) {
500 log_warn!(
501 "Execution timed out (fuel exhausted or epoch deadline exceeded)"
502 );
503 let elapsed = start.elapsed();
504 let stdout =
505 truncate_output(stdout_reader.contents().to_vec(), "stdout");
506 let stderr =
507 truncate_output(stderr_reader.contents().to_vec(), "stderr");
508 return Ok(SandboxResult {
509 stdout,
510 stderr,
511 exit_code: None,
512 elapsed,
513 timed_out: true,
514 });
515 } else if is_memory_trap(&e) {
516 log_info!("Wasm memory limit exceeded, mapping to exit code 1");
517 Some(1)
518 } else {
519 log_warn!("Wasm execution error: {}", e);
520 None
521 }
522 }
523 }
524 }
525 Err(_) => {
526 match instance.get_typed_func::<(), i32>(&mut store, "main") {
528 Ok(main_func) => match main_func.call(&mut store, ()) {
529 Ok(code) => Some(code),
530 Err(e) => {
531 if let Some(exit) = find_exit_code(&e) {
532 Some(exit)
533 } else if is_memory_trap(&e) {
534 log_info!("Wasm memory limit exceeded, mapping to exit code 1");
535 Some(1)
536 } else {
537 log_warn!("main function execution failed: {}", e);
538 None
539 }
540 }
541 },
542 Err(_) => {
543 return Err(SandboxError::ExecutionFailed(
544 "Wasm module has no _start or main export function".into(),
545 ));
546 }
547 }
548 }
549 };
550
551 let elapsed = start.elapsed();
552
553 let stdout = truncate_output(stdout_reader.contents().to_vec(), "stdout");
555 let stderr = truncate_output(stderr_reader.contents().to_vec(), "stderr");
556
557 log_info!(
558 "Wasm execution completed, exit_code={:?}, elapsed={:.2}ms",
559 exit_code,
560 elapsed.as_secs_f64() * 1000.0
561 );
562
563 Ok(SandboxResult {
564 stdout,
565 stderr,
566 exit_code,
567 elapsed,
568 timed_out: false,
569 })
570 }
571
572 fn destroy(self) -> Result<(), SandboxError> {
573 log_info!("Destroying Wasm sandbox backend");
574 Ok(())
575 }
576}
577
578impl Drop for WasmSandbox {
579 fn drop(&mut self) {
580 self.epoch_running.store(false, Ordering::Relaxed);
581 if let Some(epoch_thread) = self.epoch_thread.take()
582 && let Err(e) = epoch_thread.join()
583 {
584 log_warn!("Wasm epoch ticker thread join failed: {:?}", e);
585 }
586 }
587}
588
589fn is_fuel_exhausted(store: &Store<StoreData>) -> bool {
591 store.get_fuel().is_ok_and(|f| f == 0)
592}
593
594fn is_epoch_interrupt(error: &wasmtime::Error) -> bool {
596 if let Some(trap) = error.downcast_ref::<Trap>() {
597 matches!(trap, Trap::Interrupt)
598 } else {
599 false
600 }
601}
602
603fn is_memory_trap(error: &wasmtime::Error) -> bool {
605 if let Some(trap) = error.downcast_ref::<Trap>()
606 && matches!(trap, Trap::MemoryOutOfBounds)
607 {
608 return true;
609 }
610
611 let message = format!("{error:#}").to_ascii_lowercase();
612 message.contains("memory")
613 && (message.contains("out of bounds")
614 || message.contains("grow")
615 || message.contains("growth"))
616}
617
618fn find_exit_code(error: &wasmtime::Error) -> Option<i32> {
623 if let Some(exit) = error.downcast_ref::<I32Exit>() {
625 return Some(exit.0);
626 }
627
628 let root = error.root_cause();
630 if let Some(exit) = root.downcast_ref::<I32Exit>() {
631 return Some(exit.0);
632 }
633
634 None
635}
636
637pub fn run_wasm_benchmark(
639 wasm_path: &str,
640 iterations: usize,
641) -> Result<(), Box<dyn std::error::Error>> {
642 println!("=== mimobox Wasm Sandbox Benchmark ===\n");
643
644 let mut config = SandboxConfig::default();
645 config.deny_network = true;
646 config.memory_limit_mb = Some(64);
647 config.timeout_secs = Some(30);
648 config.fs_readonly = vec![];
649 config.fs_readwrite = vec![];
650 config.seccomp_profile = mimobox_core::SeccompProfile::Essential;
651 config.allow_fork = false;
652 config.allowed_http_domains = vec![];
653
654 if !Path::new(wasm_path).exists() {
655 return Err(format!("Wasm file does not exist: {}", wasm_path).into());
656 }
657
658 let cmd = vec![wasm_path.to_string()];
659
660 println!("Testing Engine creation overhead...");
662 let engine_start = Instant::now();
663 let mut sb = WasmSandbox::new(config.clone())?;
664 let engine_elapsed = engine_start.elapsed();
665 println!(
666 " Engine creation: {:.2}ms",
667 engine_elapsed.as_secs_f64() * 1000.0
668 );
669
670 println!("\nTesting first module compilation...");
672 let compile_start = Instant::now();
673 let result = sb.execute(&cmd)?;
674 let compile_elapsed = compile_start.elapsed();
675 println!(
676 " First execution (with compilation): {:.2}ms, exit_code={:?}",
677 compile_elapsed.as_secs_f64() * 1000.0,
678 result.exit_code
679 );
680
681 println!(
683 "\nCold start test ({} iterations, each with new + execute)...",
684 iterations
685 );
686 let mut cold_times = Vec::with_capacity(iterations);
687 for i in 0..iterations {
688 let start = Instant::now();
689 let mut sb = WasmSandbox::new(config.clone())?;
690 let result = sb.execute(&cmd)?;
691 let elapsed = start.elapsed();
692 cold_times.push(elapsed.as_micros() as f64 / 1000.0);
693
694 if result.exit_code != Some(0) {
695 eprintln!("Iteration {} failed: exit code {:?}", i, result.exit_code);
696 }
697 }
698
699 println!(
701 "\nHot path test ({} iterations, reusing Engine)...",
702 iterations
703 );
704 let mut hot_times = Vec::with_capacity(iterations);
705 for _ in 0..iterations {
706 let start = Instant::now();
707 let result = sb.execute(&cmd)?;
708 let elapsed = start.elapsed();
709 hot_times.push(elapsed.as_micros() as f64 / 1000.0);
710
711 if result.exit_code != Some(0) {
712 eprintln!("Hot path execution failed: {:?}", result.exit_code);
713 }
714 }
715
716 cold_times.sort_by(f64::total_cmp);
718 hot_times.sort_by(f64::total_cmp);
719
720 fn print_stats(label: &str, times: &[f64]) {
721 let n = times.len();
722 if n == 0 {
723 println!("{} no data", label);
724 return;
725 }
726 let p50 = times[n / 2];
727 let p95_idx = ((n as f64 * 0.95) as usize).min(n - 1);
728 let p99_idx = ((n as f64 * 0.99) as usize).min(n - 1);
729 let avg: f64 = times.iter().sum::<f64>() / n as f64;
730
731 println!("\n{} latency:", label);
732 println!(" Min: {:.2}ms", times[0]);
733 println!(" P50: {:.2}ms", p50);
734 println!(" P95: {:.2}ms", times[p95_idx]);
735 println!(" P99: {:.2}ms", times[p99_idx]);
736 println!(" Avg: {:.2}ms", avg);
737 println!(" Max: {:.2}ms", times[n - 1]);
738 }
739
740 print_stats("Cold start ", &cold_times);
741 print_stats("Hot path ", &hot_times);
742
743 let cold_p50 = cold_times[cold_times.len() / 2];
745 let hot_p50 = hot_times[hot_times.len() / 2];
746 println!("\nTarget check:");
747 println!(
748 " Cold start P50: {:.2}ms {}",
749 cold_p50,
750 if cold_p50 < 5.0 { "[PASS]" } else { "[FAIL]" }
751 );
752 println!(
753 " Hot path P50: {:.2}ms {}",
754 hot_p50,
755 if hot_p50 < 1.0 { "[PASS]" } else { "[FAIL]" }
756 );
757
758 println!("\n=== Test completed ===");
759 Ok(())
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765 use mimobox_core::Sandbox;
766 use wasmtime::{Instance, Store};
767
768 fn test_config() -> SandboxConfig {
769 let mut config = SandboxConfig::default();
770 config.timeout_secs = Some(10);
771 config.memory_limit_mb = Some(64);
772 config.fs_readonly = vec![];
773 config.fs_readwrite = vec![];
774 config.deny_network = true;
775 config.seccomp_profile = mimobox_core::SeccompProfile::Essential;
776 config.allow_fork = false;
777 config.allowed_http_domains = vec![];
778 config
779 }
780
781 #[test]
782 fn test_wasm_sandbox_create() {
783 let sb = WasmSandbox::new(test_config());
784 assert!(sb.is_ok(), "Failed to create Wasm sandbox: {:?}", sb.err());
785 }
786
787 #[test]
788 fn test_wasm_sandbox_empty_command() {
789 let mut sb = WasmSandbox::new(test_config()).expect("Failed to create");
790 let result = sb.execute(&[]);
791 assert!(result.is_err(), "Empty command should return error");
792 }
793
794 #[test]
795 fn test_wasm_sandbox_nonexistent_file() {
796 let mut sb = WasmSandbox::new(test_config()).expect("Failed to create");
797 let result = sb.execute(&["/nonexistent/file.wasm".to_string()]);
798 assert!(result.is_err(), "Nonexistent file should return error");
799 }
800
801 #[test]
802 fn test_wasm_sandbox_destroy() {
803 let sb = WasmSandbox::new(test_config()).expect("Failed to create");
804 let result = sb.destroy();
805 assert!(
806 result.is_ok(),
807 "Failed to destroy sandbox: {:?}",
808 result.err()
809 );
810 }
811
812 #[test]
813 fn test_compile_module_from_bytes_is_not_affected_by_path_swap() {
814 let engine = Engine::default();
815 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
816 let wasm_path = temp_dir.path().join("swap.wasm");
817 let module_a = wat::parse_str(
818 r#"
819 (module
820 (func (export "main") (result i32)
821 i32.const 1))
822 "#,
823 )
824 .expect("Failed to compile module A WAT");
825 let module_b = wat::parse_str(
826 r#"
827 (module
828 (func (export "main") (result i32)
829 i32.const 2))
830 "#,
831 )
832 .expect("Failed to compile module B WAT");
833
834 std::fs::write(&wasm_path, &module_a).expect("Failed to write initial module");
835 let module = compile_module_from_bytes(&engine, &wasm_path, &module_a)
836 .expect("Should compile from read bytes");
837 std::fs::write(&wasm_path, &module_b).expect("Failed to overwrite module");
838
839 let mut store = Store::new(&engine, ());
840 let instance = Instance::new(&mut store, &module, &[])
841 .expect("Failed to instantiate module from read bytes");
842 let main = instance
843 .get_typed_func::<(), i32>(&mut store, "main")
844 .expect("Failed to get main export");
845
846 assert_eq!(main.call(&mut store, ()).expect("Failed to call main"), 1);
847 }
848}