your app in 17 different languages: 2. designed as the first off-the-shelf plugin system 3. universal ABI to do guest/host interaction from 9 wasm-targeting languages: Implemented identically over many popular runtimes: Lots of developer love and contribution on GitHub, ~4,700 stars, 30+ contribs: @evacchi.dev
a runtime that is written in the host language (well, isn’t that true for all run-times?) managed languages - a Wasm runtime for the Go runtime, written in go - a Wasm runtime for the JVM, written in Java chicory @evacchi.dev
- wazero implements an interpreter for WebAssembly, supporting all* the architectures and operating systems where Go is supported - wazero implements an ahead-of-time, load-time, multi-pass, optimizing compiler for WebAssembly, supporting amd64, arm64 and all* major operating systems (*) OSs: macOS/Linux/Windows and in some cases FreeBSD archs: arm64, amd64; RISC-V for some workloads
implements an interpreter for WebAssembly, supporting all the architectures and operating systems where Java is supported - Chicory implements an ahead-of-time, load-time, Java bytecode translator for WebAssembly 11 @evacchi.dev
when they need to interoperate with native code • “foreign function interface” • e.g. Python, Java with JNI, libFFI, or Go with cgo • if you interoperate with C/native, you have to follow its rules 19 @evacchi.dev
you can no longer compile and run everywhere ◦ more difficult to cross-compile, a C compiler must be installed • tooling issues ◦ native code is opaque to language-specific tooling (debuggers, profilers, coverage, fuzzing etc.) • runtime issues ◦ C code takes over native threads, goroutines/virtual threads can no longer cooperate with them, garbage collection issues 21 @evacchi.dev
public static void main(String[] args) throws Exception { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("nashorn"); // evaluate JavaScript code that defines a function with one parameter engine.eval("function hello(name) { print('Hello, ' + name) }"); // create an Invocable object by casting the script engine object Invocable inv = (Invocable) engine; // invoke the function named "hello" with "Scripting!" as the argument inv.invokeFunction("hello", "Scripting!"); } }
is written in the host language (well, isn’t that true for all run-times?) managed languages - a Wasm runtime for the Go runtime, written in go - a Wasm runtime for the JVM, written in Java chicory @evacchi.dev
portability - safe interaction with platform - perf might not be state-of-the art - however: depending on the workload FFI cost might be higher! chicory @evacchi.dev
Wasm bytecode to an Intermediate Representation • A lowered representation of Wasm that is easier to translate ◦ to native code in compiler mode ◦ It is also interpreted straight-away in interpreter mode ◦ fewer opcodes ◦ unstructured control flow // Translate the current Wasm instruction to wazeroir's operations, // and emit the results into c.results. func (c *Compiler) handleInstruction() error { op := c.body[c.pc] ... switch op { ... case wasm.OpcodeI32Sub: c.emit(NewOperationSub(UnsignedTypeI32)) ... case wasm.OpcodeI64Sub: c.emit(NewOperationSub(UnsignedTypeI64)) ... case wasm.OpcodeF32Sub: c.emit(NewOperationSub(UnsignedTypeF32)) ... case wasm.OpcodeF64Sub: c.emit(NewOperationSub(UnsignedTypeF64)) ... } default: 44 @evacchi.dev
draws from several compiler architectures - e.g. V8, LLVM and Go’s own compiler DecodeModule CompileModule Front-End Back-End ssa opt instruction selection regalloc encoding
5; int b = 6; int c; c = a * (b / 2); if (0) { /* DEBUG */ printf("%d\n", c); } return c; } 51 @evacchi.dev int main(void) { int a = 5; int b = 6; int c; c = a * (b / 2); if (0) { /* DEBUG */ printf("%d\n", c); } return c; } dead code elimination
5; int b = 6; int c; c = a * (b / 2); if (0) { /* DEBUG */ printf("%d\n", c); } return c; } 52 @evacchi.dev int main(void) { int a = 5; int b = 6; int c; c = 5 * (6 / 2); if (0) { /* DEBUG */ printf("%d\n", c); } return c; } constant propagation / constant folding
5; int b = 6; int c; c = 5 * (6 / 2); if (0) { /* DEBUG */ printf("%d\n", c); } return c; } 53 @evacchi.dev int main(void) { int a = 5; int b = 6; int c; c = 5 * (6 / 2); if (0) { /* DEBUG */ printf("%d\n", c); } return 15; }
com/dylibso/chicory/$gen/CompiledMachine$AotMethods NESTMEMBER com/dylibso/chicory/$gen/CompiledMachine$MachineCall private final Lcom/dylibso/chicory/runtime/Instance; instance public call(I[J)[J // implements the interface // exported function public static func_0( ILcom/dylibso/chicory/runtime/Memory;Lcom/dylibso/chicory/runtime/Instance;)I @evacchi.dev
com/dylibso/chicory/$gen/CompiledMachine$AotMethods NESTMEMBER com/dylibso/chicory/$gen/CompiledMachine$MachineCall private final Lcom/dylibso/chicory/runtime/Instance; instance public call(I[J)[J // implements the interface // exported function public static func_0( ILcom/dylibso/chicory/runtime/Memory;Lcom/dylibso/chicory/runtime/Instance;)I @evacchi.dev
stack.enterScope(ins.scope(), blockType(ins)); // use the same starting stack sizes for both sides of the branch if (body.instructions().get(ins.labelFalse() - 1).opcode() == OpCode.ELSE) { stack.pushTypes(); } result.add(new AotInstruction(AotOpCode.IFEQ, ins.labelFalse())); break; case ELSE: stack.popTypes(); result.add(new AotInstruction(AotOpCode.GOTO, ins.labelTrue()));
toolchain translates JVM bytecode to Dalvim/ART bytecode at build-time - great for libraries and use cases where you do not need to load NEW modules at run-time (*) in our early experiments
other modules ... var importValues = s.toImportValues(); var inst = Instance.builder(module) .withImportValues(importValues) .withMachineFactory(AotAndroidMachineFactory::create) .build(); MACHINES!
A Return to WebAssembly for the Java Geek (Java Advent 2023) Wasm 4 the Java Geek 3: Electric Boogaloo (Java Advent 2024) mcp.run extism.org wazero.io chicory.dev .dev @mastodon.social