zenwasm [](https://github.com/imazen/zenwasm/actions/workflows/ci.yml) [](https://crates.io/crates/zenwasm-api) [](https://lib.rs/crates/zenwasm-api) [](https://docs.rs/zenwasm-api) [](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) [](#license)

June 28, 2026 · View on GitHub

zenwasm runs sandboxed WASM modules — compiled C/C++ codecs, image filters, anything untrusted — from a Rust host without linking wasmtime into your application binary. wasmtime lives behind a cdylib plugin you load at runtime; your app depends only on a small host loader (libloading + thiserror). Reads out of WASM linear memory are zero-copy. The shared-types crate is no_std + #![forbid(unsafe_code)]; the host loader and plugin confine unsafe to the FFI boundary.

Quick start

[dependencies]
zenwasm-api = "0.1.0"   # host loader — no wasmtime in your dependency tree

Build zenwasm-abi into a plugin (libzenwasm_abi.so / .dll / .dylib) and ship it next to your binary, then load it and run a module:

use zenwasm_api::{WasmRuntime, WasmCodec};

// Load the plugin once at startup. wasmtime lives inside the cdylib, not your app.
let runtime = WasmRuntime::load("libzenwasm_abi.so")?;

// Load a sandboxed module — e.g. a codec compiled from C with -msimd128.
let wasm = std::fs::read("heic_decoder.wasm")?;
let module = runtime.load_module(&wasm)?;

// Decode through the codec convenience wrapper.
let codec = WasmCodec::new(module);
let info = codec.probe(&heic_bytes)?;                  // ImageInfo { width, height, channels, .. }
let (pixels, w, h, channels) = codec.decode(&heic_bytes)?;

The loader checks the plugin's ABI_VERSION on load() and refuses a mismatched one, so a stale .so fails loudly instead of corrupting memory.

For a custom module ABI, drop to the low-level path — one host→WASM copy on the way in, zero-copy on the way out:

let module = runtime.load_module(&wasm)?;
let in_off = module.write_bytes(&input)?;                    // the only copy: host → WASM memory
let out = module.call_func("filter_process", &[in_off as i64, w as i64, h as i64, 4], 1)?;
let out_off = out[0] as u32;
let pixels: &[u8] = module.memory_slice(out_off, out_len)?;  // zero-copy read from WASM memory

Why

wasmtime is a solid way to sandbox untrusted C/C++ (codecs, user-submitted filters), but it adds several megabytes to whatever links it. If your application already has a plugin architecture, you don't want that weight compiled into the main binary.

zenwasm puts wasmtime behind a cdylib. Your application links only zenwasm-api (just libloading + thiserror); the wasmtime-bearing plugin is loaded at runtime via dlopen. The boundary is a small, versioned C ABI — the host checks ABI_VERSION on load and refuses a mismatched plugin.

The output path is zero-copy: the host gets a direct pointer into WASM linear memory via memory_slice(), so decoded pixels are read in place. The only copy is the input write (host buffer → WASM memory), which is unavoidable.

Write:   1 memcpy   (host → WASM linear memory)
Compute: 0 copies   (the module works in its own memory)
Read:    0 copies   (host reads in place via memory_base + offset)

Lifetime note: a zero-copy slice borrows the module and is valid only until the next alloc, write_bytes, call_func, or drop — any of which may grow and relocate WASM memory. Copy it out (.to_vec()) if you need to keep the data.

Crates

CrateTypeRole
zenwasm-apilibHost loader — depend on this. libloading + thiserror, no wasmtime.
zenwasm-abicdylibThe plugin. Wraps wasmtime behind a C ABI; build and ship the .so / .dll / .dylib.
zenwasm-typeslib (no_std)Shared #[repr(C)] types + WASM export-name conventions. #![forbid(unsafe_code)].

All three are versioned together (currently 0.1.0) and built on wasmtime 43.

WASM module convention

A module the plugin can host must export a small allocator so the host can place input bytes:

  • wasm_alloc(size: u32) -> u32 — allocate in WASM linear memory
  • wasm_dealloc(offset: u32, size: u32) — free an allocation
  • abi_version() -> u32optional; if present, it's checked against the host's ABI_VERSION

On top of that, codec modules export codec_probe, codec_decode, and codec_encode; filter modules export filter_process. The exact signatures and the shared #[repr(C)] structs (ImageInfo, WasmBuffer, DecodeOutput, EncodeOutput, ErrorCode) live in zenwasm-types — mirror them in your C header.

Compile image-processing modules with SIMD128:

clang --target=wasm32-wasi -O3 -msimd128 -o codec.wasm codec.c

Building

cargo build --release

produces target/release/libzenwasm_abi.so (the plugin to ship) and makes zenwasm-api available to downstream crates. MSRV is 1.91 (required by wasmtime 43).

Binary sizes

Approximate, release build, stripped:

ArtifactSize
libzenwasm_abi.so (wasmtime inside)~10 MB
Host application (with zenwasm-api only)~350 KB
Typical .wasm module~1 MB

wasmtime's footprint is paid once, in a plugin loaded on demand, instead of being compiled into every build of your application.

License

Dual-licensed: AGPL-3.0 or commercial.

I've maintained and developed open-source image server software — and the 40+ library ecosystem it depends on — full-time since 2011. Fifteen years of continual maintenance, backwards compatibility, support, and the (very rare) security patch. That kind of stability requires sustainable funding, and dual-licensing is how we make it work without venture capital or rug-pulls. Support sustainable and secure software; swap patch tuesday for patch leap-year.

Our open-source products

Your options:

  • Startup license — $1 if your company has under $1M revenue and fewer than 5 employees. Get a key →
  • Commercial subscription — Governed by the Imazen Site-wide Subscription License v1.1 or later. Apache 2.0-like terms, no source-sharing requirement. Sliding scale by company size. Pricing & 60-day free trial →
  • AGPL v3 — Free and open. Share your source if you distribute.

See LICENSE-COMMERCIAL for details.

Image tech I maintain

Codecs ¹zenjpeg · zenpng · zenwebp · zengif · zenavif · zenjxl · zenbitmaps · heic · zentiff · zenpdf · zensvg · zenjp2 · zenraw · ultrahdr
Codec internalszenjxl-decoder · jxl-encoder · zenrav1e · rav1d-safe · zenavif-parse · zenavif-serialize
Compressionzenflate · zenzop · zenzstd
Processingzenresize · zenquant · zenblend · zenfilters · zensally · zentone
Pixels & colorzenpixels · zenpixels-convert · linear-srgb · garb
Pipeline & frameworkzenpipe · zencodec · zencodecs · zenlayout · zennode · zenwasm · zentract
Metricszensim · fast-ssim2 · butteraugli · zenmetrics · resamplescope-rs
Pickers & MLzenanalyze · zenpredict · zenpicker
ProductsImageflow image engine (.NET · Node · Go) · Imageflow Server · ImageResizer (C#)

¹ pure-Rust, #![forbid(unsafe_code)] codecs, as of 2026

General Rust awesomeness

zenbench · archmage · magetypes · enough · whereat · cargo-copter

Open source · @imazen · @lilith · lib.rs/~lilith