Skip to main content

zwasm_sdk/
module.rs

1use crate::config;
2use crate::error;
3use crate::imports;
4use crate::utils;
5use crate::wasi;
6use std::sync::{Arc, Weak};
7
8/// A WebAssembly module instance backed by the zwasm runtime.
9///
10/// This type provides safe, idiomatic Rust bindings to the zwasm engine, supporting
11/// fast JIT execution, full Wasm 3.0 spec coverage, and advanced features like SIMD,
12/// threads, GC, and exception handling. Module instances are not thread-safe and must
13/// not be shared between threads.
14///
15/// See the [zwasm project](https://github.com/zwasm/zwasm) for details on the underlying engine.
16use zwasm_sys as sys;
17
18struct ModuleInner {
19    ptr: *mut sys::zwasm_module_t,
20}
21
22impl Drop for ModuleInner {
23    fn drop(&mut self) {
24        unsafe {
25            sys::zwasm_module_delete(self.ptr);
26        }
27    }
28}
29
30// SAFETY: the native runtime documents cancellation as the only thread-safe module operation.
31// Module itself remains !Send/!Sync; this is only shared so a weak cancel handle can be used
32// from another thread while the owning Module stays alive.
33unsafe impl Send for ModuleInner {}
34unsafe impl Sync for ModuleInner {}
35
36/// A thread-safe handle that can interrupt a running Wasm invocation.
37///
38/// Create this from [`Module::cancel_handle`]. Calling [`Self::cancel`] after the owning
39/// [`Module`] has been dropped becomes a no-op.
40#[derive(Clone)]
41pub struct CancelHandle {
42    inner: Weak<ModuleInner>,
43}
44
45impl CancelHandle {
46    /// Requests cancellation of the currently running Wasm invocation.
47    pub fn cancel(&self) {
48        if let Some(inner) = self.inner.upgrade() {
49            unsafe { sys::zwasm_module_cancel(inner.ptr) };
50        }
51    }
52}
53
54pub struct Module {
55    inner: Arc<ModuleInner>,
56    _not_send_sync: std::marker::PhantomData<std::rc::Rc<()>>,
57}
58
59impl Module {
60    fn from_raw(ptr: *mut sys::zwasm_module_t) -> Result<Self, error::ZwasmError> {
61        if ptr.is_null() {
62            Err(error::last_error()
63                .unwrap_or_else(|| error::ZwasmError("Unknown error".to_string())))
64        } else {
65            Ok(Module {
66                inner: Arc::new(ModuleInner { ptr }),
67                _not_send_sync: std::marker::PhantomData,
68            })
69        }
70    }
71
72    fn raw_ptr(&self) -> *mut sys::zwasm_module_t {
73        self.inner.ptr
74    }
75
76    /* ================================================================
77     * Module lifecycle
78     * ================================================================ */
79
80    /// Creates a new WebAssembly module instance from raw Wasm bytes using the zwasm runtime.
81    ///
82    /// This is the most basic way to instantiate a Wasm module. For WASI or custom configuration, see [`Self::new_wasi`] and [`Self::new_configured`].
83    pub fn new(wasm_bytes: &[u8]) -> Result<Self, error::ZwasmError> {
84        let ptr = unsafe { sys::zwasm_module_new(wasm_bytes.as_ptr(), wasm_bytes.len()) };
85
86        Self::from_raw(ptr)
87    }
88
89    /// Creates a new WASI-enabled module instance from raw Wasm bytes using the zwasm runtime.
90    ///
91    /// WASI syscalls are enabled for this module. For custom WASI configuration, use [`Self::new_wasi_configured`].
92    pub fn new_wasi(wasm_bytes: &[u8]) -> Result<Self, error::ZwasmError> {
93        let ptr = unsafe { sys::zwasm_module_new_wasi(wasm_bytes.as_ptr(), wasm_bytes.len()) };
94
95        Self::from_raw(ptr)
96    }
97
98    /// Creates a module with an explicit runtime configuration (memory, fuel, limits, etc).
99    ///
100    /// Allows fine-grained control over the execution environment. See [`Config`](crate::Config) for options.
101    pub fn new_configured(
102        wasm_bytes: &[u8],
103        config: &config::Config,
104    ) -> Result<Self, error::ZwasmError> {
105        let ptr = unsafe {
106            sys::zwasm_module_new_configured(wasm_bytes.as_ptr(), wasm_bytes.len(), config.ptr)
107        };
108
109        Self::from_raw(ptr)
110    }
111
112    /// Creates a WASI-enabled module with an explicit WASI configuration.
113    ///
114    /// Use this to provide argv, env, preopens, and other WASI settings. See [`WasiConfig`](crate::WasiConfig).
115    pub fn new_wasi_configured(
116        wasm_bytes: &[u8],
117        wasi_config: &wasi::WasiConfig,
118    ) -> Result<Self, error::ZwasmError> {
119        let ptr = unsafe {
120            sys::zwasm_module_new_wasi_configured(
121                wasm_bytes.as_ptr(),
122                wasm_bytes.len(),
123                wasi_config.ptr,
124            )
125        };
126
127        Self::from_raw(ptr)
128    }
129
130    /// Creates a WASI-enabled module with both WASI and runtime configuration.
131    ///
132    /// Combines all options from [`Config`](crate::Config) and [`WasiConfig`](crate::WasiConfig).
133    pub fn new_wasi_configured2(
134        wasm_bytes: &[u8],
135        wasi_config: &wasi::WasiConfig,
136        config: &config::Config,
137    ) -> Result<Self, error::ZwasmError> {
138        let ptr = unsafe {
139            sys::zwasm_module_new_wasi_configured2(
140                wasm_bytes.as_ptr(),
141                wasm_bytes.len(),
142                wasi_config.ptr,
143                config.ptr,
144            )
145        };
146
147        Self::from_raw(ptr)
148    }
149
150    /// Creates a module with host functions registered via [`Imports`](crate::Imports).
151    ///
152    /// Use this to expose custom native functions to Wasm code.
153    pub fn new_with_imports(
154        wasm_bytes: &[u8],
155        imports: &imports::Imports,
156    ) -> Result<Self, error::ZwasmError> {
157        let ptr = unsafe {
158            sys::zwasm_module_new_with_imports(wasm_bytes.as_ptr(), wasm_bytes.len(), imports.ptr)
159        };
160
161        Self::from_raw(ptr)
162    }
163
164    /// Validates raw Wasm bytes for correctness according to the Wasm spec, without instantiating a module.
165    ///
166    /// Returns `Ok(())` if the bytes are valid Wasm, or an error otherwise.
167    pub fn validate(wasm_bytes: &[u8]) -> Result<(), error::ZwasmError> {
168        let ok = unsafe { sys::zwasm_module_validate(wasm_bytes.as_ptr(), wasm_bytes.len()) };
169
170        if !ok {
171            Err(error::last_error()
172                .unwrap_or_else(|| error::ZwasmError("Unknown error".to_string())))
173        } else {
174            Ok(())
175        }
176    }
177
178    /* ================================================================
179     * Function invocation
180     * ================================================================ */
181
182    /// Invokes an exported function by name in the instantiated Wasm module.
183    ///
184    /// Arguments and return values are always 64-bit (Wasm i32/i64/f32/f64 are bitcast as needed).
185    /// This is the main entrypoint for calling Wasm code from Rust.
186    ///
187    /// # Safety
188    /// This method is safe to call from Rust, but it ultimately delegates to the native
189    /// runtime. Callers must ensure no concurrent native access is performed on the same
190    /// module instance through other aliases (for example via raw pointers in foreign code).
191    pub fn invoke(&self, name: &str, args: &[u64]) -> Result<Vec<u64>, error::ZwasmError> {
192        let c_name = std::ffi::CString::new(name)
193            .map_err(|_| error::ZwasmError("function name contains NUL byte".into()))?;
194        let nresults = self.export_result_count_by_name(name)? as usize;
195        let mut results = vec![0u64; nresults];
196
197        let ok = unsafe {
198            sys::zwasm_module_invoke(
199                self.raw_ptr(),
200                c_name.as_ptr(),
201                if args.is_empty() {
202                    std::ptr::null_mut()
203                } else {
204                    args.as_ptr() as *mut u64
205                },
206                args.len() as u32,
207                if results.is_empty() {
208                    std::ptr::null_mut()
209                } else {
210                    results.as_mut_ptr()
211                },
212                results.len() as u32,
213            )
214        };
215
216        if !ok {
217            Err(error::last_error()
218                .unwrap_or_else(|| error::ZwasmError("Unknown error".to_string())))
219        } else {
220            Ok(results)
221        }
222    }
223
224    /// Invokes the module start function, if present.
225    ///
226    /// This is typically used for modules with a `_start` or similar entrypoint.
227    ///
228    /// # Safety
229    /// This method is safe to call from Rust, but it executes in the native runtime.
230    /// The same aliasing/concurrency requirements as [`Self::invoke`] apply.
231    pub fn invoke_start(&self) -> Result<(), error::ZwasmError> {
232        let ok = unsafe { sys::zwasm_module_invoke_start(self.raw_ptr()) };
233
234        if !ok {
235            Err(error::last_error()
236                .unwrap_or_else(|| error::ZwasmError("Unknown error".to_string())))
237        } else {
238            Ok(())
239        }
240    }
241
242    /* ================================================================
243     * Export introspection
244     * ================================================================ */
245
246    /// Returns the number of exported functions in the module.
247    pub fn export_count(&self) -> u32 {
248        unsafe { sys::zwasm_module_export_count(self.raw_ptr()) }
249    }
250
251    /// Returns the export name at `index`, if present.
252    ///
253    /// Returns `None` if the index is out of bounds.
254    pub fn export_name(&self, index: u32) -> Option<String> {
255        let ptr = unsafe { sys::zwasm_module_export_name(self.raw_ptr(), index) };
256
257        if ptr.is_null() {
258            None
259        } else {
260            Some(
261                unsafe { std::ffi::CStr::from_ptr(ptr) }
262                    .to_string_lossy()
263                    .into_owned(),
264            )
265        }
266    }
267
268    /// Returns the number of parameters for the export at `index`.
269    ///
270    /// Parameters are always 64-bit values.
271    pub fn export_param_count(&self, index: u32) -> u32 {
272        unsafe { sys::zwasm_module_export_param_count(self.raw_ptr(), index) }
273    }
274
275    /// Returns the number of results for the export at `index`.
276    ///
277    /// Results are always 64-bit values.
278    pub fn export_result_count(&self, index: u32) -> u32 {
279        unsafe { sys::zwasm_module_export_result_count(self.raw_ptr(), index) }
280    }
281
282    /// Returns a thread-safe cancellation handle for this module.
283    pub fn cancel_handle(&self) -> CancelHandle {
284        CancelHandle {
285            inner: Arc::downgrade(&self.inner),
286        }
287    }
288
289    /// Requests cancellation of currently running Wasm execution in this module.
290    ///
291    /// # Safety
292    /// Cancellation behavior is implemented by the native runtime. Callers should only use
293    /// this as an asynchronous signal and must not assume immediate termination semantics.
294    pub fn cancel(&self) {
295        self.cancel_handle().cancel();
296    }
297
298    /* ================================================================
299     * Memory access
300     * ================================================================ */
301
302    /// Returns a direct view of the module's linear memory (if present).
303    ///
304    /// This is a zero-copy view into the Wasm memory. Use with care.
305    ///
306    /// # Safety
307    /// The returned slice points to memory owned by the runtime. The caller must ensure
308    /// the memory is not mutated or invalidated (for example by invocation or memory growth)
309    /// while this slice is alive.
310    pub unsafe fn memory_data(&self) -> Option<&[u8]> {
311        let ptr = unsafe { sys::zwasm_module_memory_data(self.raw_ptr()) };
312
313        if ptr.is_null() {
314            None
315        } else {
316            Some(unsafe { std::slice::from_raw_parts(ptr, self.memory_size()) })
317        }
318    }
319
320    /// Returns a copy of the current linear memory snapshot.
321    ///
322    /// This is the safest way to access Wasm memory from Rust.
323    ///
324    /// # Safety
325    /// This method avoids exposing a borrowed native memory view and is therefore preferred
326    /// over [`Self::memory_data`] when possible.
327    pub fn memory_data_copy(&self) -> Option<Vec<u8>> {
328        unsafe { self.memory_data().map(|s| s.to_vec()) }
329    }
330
331    /// Returns the current linear memory size in bytes.
332    pub fn memory_size(&self) -> usize {
333        unsafe { sys::zwasm_module_memory_size(self.raw_ptr()) }
334    }
335
336    /// Reads bytes from the module's linear memory into `buf`.
337    ///
338    /// # Safety
339    /// This method is safe to call from Rust, but reads from native-managed memory. The
340    /// same aliasing/concurrency requirements as [`Self::invoke`] apply.
341    pub fn memory_read(&self, offset: u32, buf: &mut [u8]) -> Result<(), error::ZwasmError> {
342        let len = utils::to_u32_len(buf.len())?;
343        let ok =
344            unsafe { sys::zwasm_module_memory_read(self.raw_ptr(), offset, len, buf.as_mut_ptr()) };
345
346        if !ok {
347            Err(error::last_error()
348                .unwrap_or_else(|| error::ZwasmError("Unknown error".to_string())))
349        } else {
350            Ok(())
351        }
352    }
353
354    /// Writes bytes from `data` into the module's linear memory.
355    ///
356    /// # Safety
357    /// This method is safe to call from Rust, but writes to native-managed memory. The
358    /// same aliasing/concurrency requirements as [`Self::invoke`] apply.
359    pub fn memory_write(&self, offset: u32, data: &[u8]) -> Result<(), error::ZwasmError> {
360        let len = utils::to_u32_len(data.len())?;
361        let ok =
362            unsafe { sys::zwasm_module_memory_write(self.raw_ptr(), offset, data.as_ptr(), len) };
363
364        if !ok {
365            Err(error::last_error()
366                .unwrap_or_else(|| error::ZwasmError("Unknown error".to_string())))
367        } else {
368            Ok(())
369        }
370    }
371
372    fn export_result_count_by_name(&self, name: &str) -> Result<u32, error::ZwasmError> {
373        let index = self.export_index_by_name(name)?;
374        Ok(self.export_result_count(index))
375    }
376
377    fn export_index_by_name(&self, name: &str) -> Result<u32, error::ZwasmError> {
378        let count = self.export_count();
379        for index in 0..count {
380            if let Some(export_name) = self.export_name(index) {
381                if export_name == name {
382                    return Ok(index);
383                }
384            }
385        }
386        Err(error::ZwasmError(format!("export not found: {name}")))
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393    use crate::test_fixtures;
394
395    #[test]
396    fn test_module_lifecycle() {
397        let module = Module::new(test_fixtures::MINIMAL_WASM);
398        assert!(module.is_ok(), "module_new minimal");
399
400        let module = Module::new(&[0x00, 0x00, 0x00, 0x00]);
401        assert!(module.is_err(), "module_new invalid returns Err");
402
403        let err = module.err();
404        assert!(
405            !err.unwrap().0.is_empty(),
406            "last_error non-empty after failure"
407        );
408    }
409
410    #[test]
411    fn test_validate() {
412        let result = Module::validate(test_fixtures::RETURN42_WASM);
413        assert!(result.is_ok(), "validate valid module");
414
415        let result = Module::validate(&[0x00, 0x00, 0x00, 0x00]);
416        assert!(result.is_err(), "validate invalid module returns Err");
417
418        let err = result.err();
419        assert!(
420            !err.unwrap().0.is_empty(),
421            "last_error non-empty after failure"
422        );
423    }
424
425    #[test]
426    fn test_invoke_no_args() {
427        let module = Module::new(test_fixtures::RETURN42_WASM).expect("Failed to create module");
428        let results = module.invoke("f", &[]).expect("Failed to invoke function");
429        println!("f() = {}", results[0]);
430        assert_eq!(results[0], 42);
431
432        // Invoke again to test reusability
433        let results = module
434            .invoke("f", &[])
435            .expect("Failed to invoke function second time");
436        println!("f() again = {}", results[0]);
437        assert_eq!(results[0], 42);
438    }
439
440    #[test]
441    fn test_invoke_with_args() {
442        let module = Module::new(test_fixtures::ADD_WASM).expect("Failed to create module");
443        let args = [10, 32];
444        let results = module
445            .invoke("add", &args)
446            .expect("Failed to invoke function");
447        println!("add(10, 32) = {}", results[0]);
448        assert_eq!(results[0], 42);
449
450        // Edge: zero + zero
451        let args = [0, 0];
452        let results = module
453            .invoke("add", &args)
454            .expect("Failed to invoke function");
455        println!("add(0, 0) = {}", results[0]);
456        assert_eq!(results[0], 0);
457
458        // Edge: large i32 values (wrapping)
459        let args = [0xFFFFFFFF, 1];
460        let results = module
461            .invoke("add", &args)
462            .expect("Failed to invoke function");
463        println!("add(MAX, 1) = {}", results[0]);
464        assert_eq!(results[0] & 0xFFFFFFFF, 0);
465    }
466
467    #[test]
468    fn test_invoke_nonexistent() {
469        let module = Module::new(test_fixtures::RETURN42_WASM).expect("Failed to create module");
470        let result = module.invoke("nonexistent", &[]);
471        assert!(result.is_err(), "invoke nonexistent returns Err");
472
473        let err = result.err();
474        assert!(
475            !err.unwrap().0.is_empty(),
476            "last_error non-empty after failure"
477        );
478    }
479
480    #[test]
481    fn test_export_introspection() {
482        let module = Module::new(test_fixtures::RETURN42_WASM).expect("Failed to create module");
483        assert_eq!(module.export_count(), 1, "1 export");
484
485        let name = module.export_name(0).expect("export name not null");
486        assert_eq!(name, "f", "export name == 'f'");
487        assert_eq!(module.export_param_count(0), 0, "0 params");
488        assert_eq!(module.export_result_count(0), 1, "1 result");
489        assert!(module.export_name(99).is_none(), "export_name(99) == None");
490    }
491
492    #[test]
493    fn test_memory_access() {
494        let module = Module::new(test_fixtures::MEMORY_WASM).expect("Failed to create module");
495        let data = unsafe { module.memory_data() }.expect("memory_data not null");
496        let size = module.memory_size();
497        assert!(size >= 65536, "memory >= 1 page");
498        let write_buf = [0xDE, 0xAD, 0xBE, 0xEF];
499        module.memory_write(0, &write_buf).expect("memory_write");
500        let mut read_buf = [0u8; 4];
501        module.memory_read(0, &mut read_buf).expect("memory_read");
502        assert_eq!(write_buf, read_buf, "read == write");
503        assert!(data[0] == 0xDE && data[1] == 0xAD, "data ptr matches write");
504        let oob_write_result = module.memory_write(size as u32, &write_buf);
505        assert!(oob_write_result.is_err(), "OOB write returns Err");
506        let oob_read_result = module.memory_read(size as u32, &mut read_buf);
507        assert!(oob_read_result.is_err(), "OOB read returns Err");
508        module.invoke("f", &[]).expect("invoke store fn");
509        let data = unsafe { module.memory_data() }.expect("memory_data not null after invoke");
510        let val = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
511        assert_eq!(val, 42, "store fn wrote 42 at offset 0");
512    }
513
514    #[test]
515    fn test_no_memory_module() {
516        let module = Module::new(test_fixtures::RETURN42_WASM).expect("Failed to create module");
517        assert!(
518            unsafe { module.memory_data() }.is_none(),
519            "memory_data == None for no-memory"
520        );
521        assert_eq!(module.memory_size(), 0, "memory_size == 0 for no-memory");
522    }
523
524    #[test]
525    fn test_cancel_handle_after_module_drop_is_noop() {
526        let handle = {
527            let module =
528                Module::new(test_fixtures::RETURN42_WASM).expect("Failed to create module");
529            module.cancel_handle()
530        };
531
532        handle.cancel();
533    }
534}