A proof-of-concept showing how to sandbox a Java function and expose it as a standard java.lang.invoke.MethodHandle.
The pipeline: Java source → GraalVM WebImage → Wasm → Endive (JVM) → MethodHandle
┌─────────────────────────────────────────────────────┐
│ Host JVM │
│ │
│ MethodHandle sandboxedAdd = calculator.asMethodHandle(); │
│ int result = (int) sandboxedAdd.invoke(2, 3); // → 5 │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Wasm Sandbox (Endive) │ │
│ │ • No filesystem access │ │
│ │ • No network access │ │
│ │ • stdout captured, not forwarded │ │
│ │ • Host controls all imports │ │
│ │ │ │
│ │ Calculator.add(2, 3) → 5 │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
Guest module (guest/) — a plain Java class compiled to WebAssembly via GraalVM WebImage:
public class Calculator {
public static int add(int a, int b) {
return a + b;
}
}Host module (host/) — loads the .wasm module using Endive, AOT-compiles it to JVM bytecode, and wraps the sandboxed function as a MethodHandle:
var calculator = new SandboxedCalculator();
MethodHandle sandboxedAdd = calculator.asMethodHandle();
int result = (int) sandboxedAdd.invoke(2, 3); // 5The guest code runs inside a Wasm sandbox with zero capabilities — no filesystem, no network, no direct I/O. The host explicitly provides every import the guest can call.
Requires GraalVM 25.1 EA with WebImage support:
./setup-graalvm.sh
export JAVA_HOME="$(pwd)/.graalvm/graalvm-25.1.0-dev+10.1"
export PATH="$JAVA_HOME/bin:$PATH"
# Compile Java → Wasm
mvn package -pl guest -Pnative
# Build host (AOT-compiles the Wasm, generates typed interfaces)
mvn package -pl host -DskipTests
# Run
mvn exec:java -pl host -Dexec.mainClass=com.example.sandbox.host.MainOutput:
MethodHandle type: (int,int)int
sandboxedAdd.invoke(2, 3) = 5
sandboxedAdd.invoke(100, 200) = 300
GraalVM WebImage is designed for browser/Node.js targets, not for embedding individual functions. This creates friction for the sandboxing use case:
- No individual function exports — WebImage only exposes
main(String[]).@JS.Exportexists but is not implemented yet. Arguments and results must be marshaled through string args and stdout capture. - Heavy host function surface — even a trivial guest requires ~27 host function stubs (JS interop, type conversion, I/O) because WebImage assumes a JavaScript host environment.
- Single-use instances — WebImage calls
System.exit()aftermain()returns, so each invocation requires a fresh Wasm instance. - Large binaries — the calculator
.wasmis ~1.2 MB because it includes the full SubstrateVM runtime.
Kotlin/Wasm compiles to WasmGC and produces clean, embeddable modules with individual function exports. Several projects already provide ergonomic Wasm sandboxing for Kotlin:
- Glueball — a Kotlin Multiplatform library for embedding WebAssembly modules with typed bindings
- Wasmline — Kotlin Multiplatform framework for loading and calling WASI-compliant Wasm plugins, powered by Wasmtime
These can export individual @WasmExport functions with primitive signatures, avoiding the main() bottleneck and the host function ceremony entirely.
├── guest/ # The sandboxed Java function
│ ├── pom.xml # GraalVM WebImage compilation
│ └── src/.../Calculator.java # public static int add(int, int)
├── host/ # The JVM host
│ ├── pom.xml # Endive compiler plugin + moduleInterface
│ └── src/.../
│ ├── SandboxedCalculator.java # MethodHandle wrapper (~50 lines)
│ ├── WebImageHostFunctions.java # Host function stubs (~80 lines)
│ └── Main.java # Demo
├── setup-graalvm.sh
└── pom.xml