Embedding Zipp
Zipp is built to be embedded. In Rust, zipp_vm::embed keeps a compiled VM alive across host calls with a small, explicit API. In the browser, the same design surfaces as the Engine class of the WebAssembly module. This page is the practical guide to both.
The embedding model
A host compiles a script once into a ScriptState, runs its top level, and then re-enters the same global context as often as it likes: evaluate an expression, call a function by name or by stable slot index, read and write top-level bindings. The VM never recompiles between entries and never exposes its internal Value handles; everything crosses the boundary as an owned HostValue tree.
[dependencies]
zipp-vm = { git = "https://github.com/f2i-com/zipp.org", tag = "v0.0.18" }use zipp_vm::embed::{compile_script, HostValue};
fn main() -> Result<(), String> {
let source = std::fs::read_to_string("rules.js").map_err(|e| e.to_string())?;
let mut st = compile_script(&source)?;
// Host calls are the only way out of the VM, and they are inert until installed.
st.set_host_call(Box::new(|kind, args| match kind {
"log" => { println!("{}", args[0]); Ok(HostValue::Undefined) }
_ => Err(format!("TypeError: unknown host call {kind}")),
}));
st.run_init()?; // top-level code, once
let total = st.eval_in_context("price(42)")?; // later, same globals
println!("{total:?}");
Ok(())
}call_slotcalls a top-level function by index, drains microtasks afterwards, and is the right choice in hot host loops.call_globalandhas_global_functionresolve by name on every call, compile nothing, and do not drain microtasks.compile_script_with_optionschooses the grammar goal: theCompatgoal accepts top-levelreturnfor CommonJS-shaped scripts;Pureis the ECMAScript Script goal.compile_script_with_preamblecompiles engine plumbing ahead of a guest while keeping the guest's own"use strict"in force.- Arrays and plain objects marshal structurally. Functions, classes, maps, dates, proxies and cycles cross as
Opaque, and a structural write-back declines to overwrite an opaque slot.
Embedding Python
The same module compiles Python: compile_source(source, Frontend::Python { .. }) for one file, compile_python_project(entry, &modules) for a folder of modules, and compile_python_program(entry, &modules, &files, &argv, hosted) when the program needs a virtual filesystem and sys.argv. Python and JavaScript states share the runtime, the frame stack and the collector, and the Python page explains the ABI.
In the browser
The WebAssembly Engine class is the same model with a JSON-shaped surface. Capabilities are default-deny: before initScript, grant exactly the synchronous operations the tenant needs, then install bridges.
import init, { Engine } from './zipp_wasm.js'
await init()
const engine = new Engine()
engine.setSyncHostCapabilities(['db.query', 'ls.getItem'])
engine.setDbBridge(tenantScopedDb)
engine.setLocalStorageBridge(tenantScopedStorage)
engine.setInstructionBudget(500_000_000)
engine.initScript(source)
// Later entries, same globals:
const rendered = engine.callFunction('render', [{ user: 'ada' }])
for (const call of engine.drainPendingHostCalls()) {
engine.resolveHostCallback(call.id, await handle(call))
}
engine.pump()The Worker is the lifetime boundary: a deadline timer in the page calls worker.terminate() and creates a fresh one. host-sdk/ in the repository is a reference adapter tested in real Chromium, Firefox and WebKit Workers, and the WebAssembly page lists every limit the module enforces.
The CLI as a host
zipp js <file.js> run a script (compat goal; --script-goal for pure Script)
zipp mjs <file.mjs> run a file as an ES module
zipp py <file.py | dir> run a Python program or project
zipp run [--lang=L] <file> pick the frontend by flag, extension or shebang
zipp sandbox <flags> <file> hardened run with explicit limits
zipp --version --json build identity: source commit, dirty flag, rustc, jitFrontend detection precedence is explicit selection, then a recognized extension (.js, .cjs, .mjs, .py, .pyw), then a recognized shebang, then a leading directive, then an error. Bare source never silently defaults to one language. There is no REPL.
Use cases
Let users script your application without letting them script your machine: closed capabilities, metered instructions, a heap ceiling.
Rules and workflowsCompile a rule set once, evaluate it millions of times by slot, keep state across calls instead of recompiling.
Browser sandboxesRun a stranger's JavaScript inside your JavaScript, as Softn does, with a Worker as the kill switch.
Two languages, one hostOffer Python and JavaScript to the same users with one engine, one set of limits and one host-call surface.