Why Wasm Needs a System Interface
WebAssembly's core specification deliberately defines no system calls at all: a Wasm module can only do pure computation on its linear memory unless the host embedding it (a browser, or a standalone runtime like Wasmtime or Wasmedge) explicitly imports functions for it to call. In the browser, that host is JavaScript, which supplies whatever DOM or fetch functions a module needs. Outside the browser, WASI (the WebAssembly System Interface) standardizes a common set of those imported functions, covering file I/O, clock access, random number generation, and environment variables, so that any WASI-compliant runtime can run the same compiled .wasm module without the module needing to be rewritten per host, the same way POSIX lets a C program call open() consistently across different Unix-like operating systems.
Cricket analogy: This is like the ICC standardizing playing conditions (DRS rules, ball type) across venues so a player like Jasprit Bumrah can perform identically whether playing in Mumbai or Melbourne, WASI standardizes system calls so a Wasm module behaves identically across different host runtimes.
Capability-Based Security
Unlike POSIX, where any process can call open("/etc/passwd") and the OS checks permissions after the fact, WASI is capability-based: a module receives only the specific file descriptors (preopens) explicitly granted to it by the host at startup, and it has no ambient authority to reach any other path even if it knows the name. A runtime like Wasmtime exposes this via CLI flags such as --dir=./data::/data, which maps a real host directory to a virtual path only that module can see, meaning a compromised or buggy Wasm module can't escape its granted sandbox even with arbitrary code execution inside the module, because there's no syscall path to anything it wasn't explicitly handed a handle to.
Cricket analogy: This is like a substitute fielder being allowed onto the field only for specific overs the umpire has explicitly approved, rather than having free run of the ground for the whole match, WASI grants a module only the specific 'field access' (file handles) it was explicitly given.
WASI Preview 1, Preview 2, and the Component Model
The original WASI, now called Preview 1 (wasi_snapshot_preview1), defined a flat, POSIX-like set of import functions using raw pointers and integers, functional but with a C-shaped API that didn't compose well across languages. WASI Preview 2, built on the Wasm Component Model, redefines the same capabilities (filesystem, sockets, clocks, random) using WIT (Wasm Interface Type) definitions with proper high-level types like strings, records, and variants, plus a formal component packaging format that lets independently compiled components (say, a Rust HTTP handler and a Python request logger) be composed together with typed, checked interfaces instead of raw numeric imports. This is what enables runtimes like Wasmtime and tools like Jco to treat Wasm components as language-agnostic, pluggable building blocks rather than opaque binaries with an undocumented calling convention.
Cricket analogy: Preview 1's raw pointer-based API is like an old-style paper scorebook using cryptic abbreviations only insiders understand, while Preview 2's WIT types are like a modern digital scoring app with structured fields, both record the same match, but one is far easier for new tools and people to interoperate with.
# Compile a Rust program targeting WASI Preview 1
rustup target add wasm32-wasip1
cargo build --release --target wasm32-wasip1
# Run it under Wasmtime, granting access to only one directory
wasmtime run \
--dir=./data::/data \
--env APP_MODE=production \
target/wasm32-wasip1/release/app.wasm
# app.rs (excerpt)
# use std::fs;
# fn main() {
# // Only /data is reachable; any other path fails with a permission error
# let contents = fs::read_to_string("/data/input.txt").unwrap();
# println!("{}", contents.len());
# }Because WASI modules declare no ambient filesystem or network access, the same compiled .wasm binary can be run by a developer locally with --dir=./data and, unmodified, deployed to a multi-tenant edge platform where the operator grants a completely different, more restricted set of preopened paths, no recompilation needed.
WASI Preview 1 and Preview 2 are not binary compatible: a module compiled against wasi_snapshot_preview1 imports won't run on a Preview-2-only host without an adapter layer, and tooling support for Preview 2 is still less mature across languages than Preview 1's, so check your target runtime's support before committing to one.
- Core Wasm defines no system calls at all; WASI standardizes the imports a host provides for file I/O, clocks, randomness, and env vars.
- WASI's model is capability-based: a module only gets file handles explicitly preopened for it, with no ambient filesystem authority.
- This capability model means a compromised module can't escape its sandbox even with full code execution, since there's no path to ungranted resources.
- WASI Preview 1 (wasi_snapshot_preview1) uses a flat, POSIX-like API with raw pointers and integers.
- WASI Preview 2 is built on the Component Model, using WIT to define typed, composable interfaces across languages.
- Preview 1 and Preview 2 are not binary compatible without an adapter layer.
- Runtimes like Wasmtime and Wasmedge implement WASI, letting the same .wasm binary run unmodified across different host environments.
Practice what you learned
1. What does core WebAssembly provide for system access like file I/O, without WASI?
2. How does WASI's security model fundamentally differ from traditional POSIX permission checks?
3. What is the key architectural difference between WASI Preview 1 and WASI Preview 2?
4. In Wasmtime, what does the flag `--dir=./data::/data` do?
Was this page helpful?
You May Also Like
Rust and WebAssembly
How Rust's toolchain and ownership model make it a natural fit for compiling safe, high-performance code to WebAssembly.
The Wasm Binary Format
A tour of the actual bytes inside a .wasm file: the module header, sections, and how instructions are encoded.
Compiling C/C++ to Wasm with Emscripten
Learn how the Emscripten toolchain turns C and C++ source code into WebAssembly modules that run in browsers and Node.js.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics