# Endive > Endive is a JVM native WebAssembly runtime with zero native dependencies, hosted by the Bytecode Alliance. --- ## Quick start :::info[Requirements] Endive requires **Java 11** or later. SIMD support requires Java 21+. ::: ### Install the dependency [![Maven Central](https://img.shields.io/maven-central/v/run.endive/runtime)](https://central.sonatype.com/artifact/run.endive/runtime) To use the runtime, you need to add the `run.endive:runtime` dependency to your dependency management system. #### Maven ```xml run.endive runtime latest-release ``` #### Gradle ```groovy implementation 'run.endive:runtime:latest-release' ``` ### Loading and Instantiating Wasm Modules First your Wasm module must be loaded from disk and then instantiated. Let's [download a test module](https://raw.githubusercontent.com/bytecodealliance/endive/main/wasm-corpus/src/main/resources/compiled/iterfact.wat.wasm) . This module contains some code to compute factorial: Download from the link or with curl: ```bash curl https://raw.githubusercontent.com/bytecodealliance/endive/main/wasm-corpus/src/main/resources/compiled/iterfact.wat.wasm > factorial.wasm ``` Load this module and instantiate it: ```java import run.endive.runtime.ExportFunction; import run.endive.wasm.types.Value; import run.endive.wasm.Parser; import run.endive.runtime.Instance; import java.io.File; // point this to your path on disk var module = Parser.parse(new File("./factorial.wasm")); Instance instance = Instance.builder(module).build(); ``` :::note[Threading] `Instance` and `Store` are not thread-safe. Create separate instances per thread, or synchronize access externally. Memory operations (used by the Wasm threads proposal) are thread-safe. ::: You can think of the `module` as of inert code, and the `instance` is the run-time representation of that code: a virtual machine ready to execute. ### Invoking a Wasm Function Wasm modules, like all code modules, can export functions to the outside world. This module exports a function called `"iterFact"`. We can get a handle to this function using `Instance#export(String)`: ```java ExportFunction iterFact = instance.export("iterFact"); ``` `iterFact` can be invoked with the `apply()` method. We must map any Java types to raw `long`s and do the reverse when we want to go back to Java. ```java var result = iterFact.apply(5)[0]; System.out.println("Result: " + result); // should print 120 (5!) ``` :::note Functions in Wasm can return multiple values, hence the array. This function only returns one value, so we take the first value. ::: --- ## AI-Friendly Documentation # AI-Friendly Documentation Endive documentation is designed to be easily consumed by LLMs, AI agents, and developer tools. All machine-readable files are auto-generated at build time from the same source as the human-facing site, using the [llms.txt specification](https://llmstxt.org/). ## Available Endpoints | Endpoint | Description | |----------|-------------| | [`/llms.txt`](https://endive.run/llms.txt) | Lightweight index of all docs and blog posts with one-line descriptions | | [`/llms-full.txt`](https://endive.run/llms-full.txt) | Full documentation content concatenated into a single file | | [`/sitemap.xml`](https://endive.run/sitemap.xml) | Standard sitemap for crawler discovery | | [`/robots.txt`](https://endive.run/robots.txt) | Permits all crawlers including AI agents | Every documentation page also has a **raw Markdown** version available by appending `.md` to its path. For example: - HTML: [`/docs/core/host-functions`](https://endive.run/docs/core/host-functions) - Markdown: [`/docs/core/host-functions.md`](https://endive.run/docs/core/host-functions.md) ## Usage Examples ### Feed Endive docs to an LLM in one shot Fetch `llms-full.txt` and include it in your prompt context: ```bash curl -s https://endive.run/llms-full.txt | wc -c # ~86 KB — fits easily in any modern LLM context window ``` ### Let an AI agent discover relevant pages An agent can fetch `llms.txt` first to find the right page, then fetch only that page's Markdown: ```bash # 1. Fetch the index curl -s https://endive.run/llms.txt # 2. Fetch a specific page as clean Markdown curl -s https://endive.run/docs/core/host-functions.md ``` ### Configure AI coding tools Many AI-powered development tools support `llms.txt` natively. Point them to `https://endive.run/llms.txt` to give them access to the full Endive documentation. ## What Gets Included - All **documentation pages** covering installation, core concepts, compilation, WASI, security, and more - All **published blog posts** - Content is cleaned: front matter and test harness comments are stripped, leaving only the documentation text These files are regenerated on every site build, so they always reflect the latest documentation. --- ## Installation # Installation Endive requires **Java 11** or later. Add Endive to your project using Maven or Gradle. [![Maven Central](https://img.shields.io/maven-central/v/run.endive/runtime)](https://central.sonatype.com/artifact/run.endive/runtime) ## Maven Add the runtime dependency to your `pom.xml`: ```xml run.endive runtime ${endive.version} ``` ### Bill of Materials (BOM) To keep the versions of different Endive artifacts aligned, use the provided BOM: ```xml run.endive bom ${endive.version} pom import ``` Then you can use any Endive dependency without declaring the version: ```xml run.endive runtime ``` ## Gradle ```groovy implementation 'run.endive:runtime:${endiveVersion}' ``` Or with the BOM: ```groovy implementation platform('run.endive:bom:${endiveVersion}') implementation 'run.endive:runtime' ``` --- ## Host Functions # Host and guests :::warning[Security Consideration] Host functions cross the sandbox boundary. Validate all arguments received from Wasm code — especially memory offsets and lengths — before accessing host resources. Never trust guest-provided pointers without bounds checking. See [Security Best Practices](/docs/security/best-practices). ::: In Wasm, the instance of a module is generally regarded as the **guest**, and the surrounding runtime environment is called the **host**. For example, an **application** using Endive as a **library** would be the **host** to a Wasm module **guest** you have instantiated. Wasm modules may **export** functions, so that they can be externally invoked. But Wasm modules may also **import** functions. These imports have to be resolved at the time when a module is instantiated; in other words, when a module is instantiated, the runtime has to provide references for all the imports that a module declares. The import/export mechanism is the way through which Wasm interacts with the outside world: without imports, a Wasm module is "pure compute", that is, it cannot perform any kind of I/O, nor can it interact with other modules. ## Host Functions One way to fulfill imports is providing a **host function** written in Java. No matter what source language that your Wasm module was written in, it will be able to call this Java function when needed. It is called a **host** function because it is executed in the environment of the **host** (in this case, a JVM). As opposed to any other Wasm function, a **host function** is _unrestricted_ and it may interact with the surrounding environment in any arbitrary way. This lets you effectively escape the sandbox. As a consequence, host functions are a security boundary and need to be implemented carefully if the Wasm code is not trusted. You can think of host functions as similar to OS system calls or the standard library in your favorite programming language. The key difference is that, instead of relying on a default implementation, you use Java to define their behavior and determine what they do. Let's see it with another example. Download the following Wasm binary: ```bash curl https://raw.githubusercontent.com/bytecodealliance/endive/main/wasm-corpus/src/main/resources/compiled/host-function.wat.wasm > logger.wasm ``` This module expects us to fulfil an import with the name `console.log`. As the name implies, this function allows a caller to log a message to standard output. We could write it as the host function: ```java import run.endive.runtime.Instance; import run.endive.runtime.HostFunction; import run.endive.wasm.types.ValType; import run.endive.wasm.types.FunctionType; var func = new HostFunction( "console", "log", FunctionType.of( List.of(ValType.I32, ValType.I32), List.of() ), (Instance instance, long... args) -> { var len = (int) args[0]; var offset = (int) args[1]; var message = instance.memory().readString(offset, len); System.out.println(message); return null; }); ``` The module calls `console.log` with the length of the string and an index (offset) in its memory. This is essentially a pointer into the Wasm's linear memory. The `Instance` class provides a reference to a `Memory` object instance, that allows to pull a value out of memory. You can use the `readString()` convenience method to read a buffer into a `String`; we can then print that to stdout on behalf of our Wasm program. Note that the `HostFunction` needs 3 things: 1. The namespace and function name of the import (in our case it's `console` and `log` respectively) 2. The Wasm type signature (this function takes two `i32`s as arguments and returns nothing) 3. A lambda to call when the Wasm module invokes the import We now need to pass this host function when we instantiate the module. We can do so by using a `Store`: ```java import run.endive.wasm.Parser; import run.endive.runtime.Store; // instantiate the store var store = new Store(); // registers `console.log` in the store store.addFunction(func); var instance = store.instantiate("logger", Parser.parse(new File("./logger.wasm"))); var logIt = instance.export("logIt"); logIt.apply(); // should print "Hello, World!" 10 times ``` :::tip For an easier way to write host function and interact with a Wasm module, see [Annotations](../annotations/index.md). ::: --- ## Memory # Using Memory to share data Wasm only understands basic integer and float primitives. Passing more complex types across the boundaries involves passing low level pointers. To read, write, or allocate memory in a module, Endive provides the `Memory` class. Let's look at an example where we have a module `count_vowels.wasm`, written in Rust, that takes a string input and counts the number of vowels in the string: ```bash curl https://raw.githubusercontent.com/bytecodealliance/endive/main/wasm-corpus/src/main/resources/compiled/count_vowels.rs.wasm > count_vowels.wasm ``` Build and instantiate this module: ```java import run.endive.runtime.ExportFunction; import run.endive.runtime.Instance; import run.endive.wasm.Parser; Instance instance = Instance.builder(Parser.parse(new File("./count_vowels.wasm"))).build(); ExportFunction countVowels = instance.export("count_vowels"); ``` To pass it a string, we first need to write the string into the module's memory. To make this easier and safe, the module gives us some extra exports to allow us allocate and deallocate memory: ```java ExportFunction alloc = instance.export("alloc"); ExportFunction dealloc = instance.export("dealloc"); ``` Let's allocate Wasm memory for a string and write it into the instance memory: ```java import run.endive.runtime.Memory; Memory memory = instance.memory(); String message = "Hello, World!"; byte[] bytes = message.getBytes(); int len = bytes.length; // allocate {len} bytes of memory, this returns a pointer to that memory int ptr = (int) alloc.apply(len)[0]; // We can now write the message to the module's memory: memory.write(ptr, bytes); ``` Now we can call `countVowels` with this pointer to the string. It will do its job and return the count. We will call `dealloc` to free that memory in the module: ```java var result = countVowels.apply(ptr, len)[0]; dealloc.apply(ptr, len); assert(3L == result); // 3 vowels in Hello, World! ``` --- ## Linking # Linking In the [Host Functions section](host-functions.md) we met the `Store` for the first-time. A [Store][spec] is an intermediate-level abstraction that collects Wasm function, global, memory, and table instances as named entities. It simplifies creating instances, especially when there are a lot of interdependencies. In the simplest case, it allows to register single host functions, globals, memories and tables. For instance, we already saw how to register a `console.log()` host function to the `Store`: ```java import run.endive.wasm.Parser; import run.endive.runtime.Instance; import run.endive.runtime.HostFunction; import run.endive.runtime.Store; import run.endive.wasm.types.ValType; import run.endive.wasm.types.FunctionType; var func = new HostFunction( "console", "log", FunctionType.of( List.of(ValType.I32, ValType.I32), List.of() ), (Instance instance, long... args) -> { // decompiled is: console_log(13, 0); var len = (int) args[0]; var offset = (int) args[1]; var message = instance.memory().readString(offset, len); println(message); return null; }); // instantiate the store var store = new Store(); // registers `console.log` in the store store.addFunction(func); ``` However, the store also automatically exposes the exports of a module to the other instances that are registered. In fact, in the [Host Functions section](host-functions.md), when we created our instance from the `logger.wasm` module, we also passed a string `"logger"`. This is the name of the instance: ```java // create a named `instance` with name `logger` var instance = store.instantiate("logger", Parser.parse(new File("./logger.wasm"))); ``` Because this instance is now named, now any exports in the `logger` module will be automatically qualified. For instance, the exported function `logIt` will be visible by other modules as `logger.logIt`. ## Notes - The invocation `store.instantiate("logger", ...)` is in fact equivalent to the lower-level sequence: ```java var imports = store.toImportValues(); var m = Parser.parse(new File("./logger.wasm")); var instance = Instance.builder(m).withImportValues(imports).build(); store.register("logger", instance); ``` However, in most cases we recommend to use the shorthand form. - Also notice that registering two instances with the same name results in overwriting the functions, globals, memories, tables with matching names. In this case, the new `logger2.logIt` function overwrote the old `logger2.logIt` function. - The current `Store` is a mutable object, not meant to be shared (it is not thread-safe). - A `Store` _does not_ resolve interdependencies between modules in itself: if your set of modules have interdependencies, you will have to instantiate and register them in the right order. [spec]: https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#store%E2%91%A0 --- ## Execution modes ## Overview | Mode | Performance | Dynamic Module Loading | Requirements | Output Format | Ideal Use Case | |---|---|---|---|---|---| | **Interpreter** | 🐢 Slow | ✅ Supported | None | None - fully interpreted | Default mode; highly portable; suitable for development and environments requiring dynamic loading. | | **Runtime Compilation** | 🐇 Fast | ✅ Supported | Requires reflection and ASM dependency | In-memory Java Bytecode | Enhanced performance; suitable when dynamic loading is needed and the usage of reflection is fine. | | **Build time Compilation** | 🐇 Fast | ❌ Not Supported | Build-time tools (e.g., Maven or Gradle plugins) | Plain Java Bytecode | Optimal performance; no dynamic loading; ideal for production with static modules. | ## Summary - **Interpreter**: Executes WebAssembly (Wasm) modules directly without prior compilation. It's the default mode in Endive, offering maximum portability and simplicity. However, it has slower execution speed compared to compiled modes. - **Runtime Compilation**: Compiles Wasm modules to Java bytecode at runtime for fast execution. This mode requires one additional dependency on [ASM](https://asm.ow2.io/), it uses reflection, and it loads bytecode dynamically. It fully supports loading new Wasm modules on-the-fly, but it might not be supported on some platforms (such as Android, or GraalVM's native-image). - **Build time Compilation**: Compiles Wasm modules to Java bytecode during the build process using tools like Maven or Gradle plugins. This mode offers the best performance and eliminates the need for dynamic loading and additional dependencies, making it ideal for production environments with static modules. --- ## Runtime Compilation ## Overview :::warning[Security Consideration] The compiler translates Wasm to JVM bytecode without post-compilation verification. When compiling untrusted modules, prefer the interpreter for higher assurance, or run compiled code in an isolated classloader. See [Security Model](/docs/security/overview). ::: :::info[Resource Limits] Compiling very large Wasm modules can consume significant memory and CPU. Consider setting JVM heap limits and compilation timeouts when processing untrusted input. ::: The runtime compiler backend is a drop-in replacement for the interpreter, and it passes 100% of the same spec tests that the interpreter already supports. This runtime compiler translates the WASM instructions to Java bytecode on-the-fly in-memory. The resulting code is usually expected to evaluate (much) faster and consume less memory than if it was interpreted. At the current time, the compiler will eagerly compile all WASM instructions to Java bytecode. You end up paying a small performance penalty at Instance initialization, but the execution speedup is usually worth it. Use [Build Time Compilation](./build-time-compiler.md) if you want to avoid the penalty. ## Using ### Required Maven Changes Add the following dependency: ```xml run.endive compiler ``` ### Code Changes You enable the runtime compiler by configuring the instance to use `MachineFactoryCompiler::compile` as the machine factory instead of the default `InterpreterMachine`. ```java import run.endive.compiler.MachineFactoryCompiler; import run.endive.wasm.Parser; import run.endive.wasm.WasmModule; var module = Parser.parse(new File("your.wasm")); var instance = Instance.builder(module). withMachineFactory(MachineFactoryCompiler::compile). build(); ``` ### Interpreter Fall Back The WASM to bytecode compiler translates each WASM function into JVM method. Occasionally you will find WASM module where functions are bigger than the maximum method size allowed by the JVM. In these rare cases, we fall back to executing large functions in the interpreter. Since interpreted functions have worse performance, we want to make sure you are aware this is happening so the runtime compiler will log messages to standard error like: ```text Warning: using interpreted mode for WASM function index: 232 ``` By default, the compiler uses `InterpreterFallback.WARN` behavior, which logs warning messages when falling back to the interpreter. If you are happy with these methods being interpreted, you can configure the compiler with `InterpreterFallback.SILENT` to silence those messages: ```java import run.endive.compiler.MachineFactoryCompiler; import run.endive.compiler.InterpreterFallback; import run.endive.runtime.Instance; import run.endive.wasm.Parser; import run.endive.wasm.WasmModule; var module = Parser.parse(new File("your.wasm")); var instance = Instance.builder(module). withMachineFactory( MachineFactoryCompiler.builder(module) .withInterpreterFallback(InterpreterFallback.SILENT) .compile() ). build(); ``` If you want to ensure the functions are never interpreted, you can modify the above to use `InterpreterFallback.FAIL` instead. This will throw an exception if any function is too large to compile. An even better way to silence the use of interpreted functions (this will speed up your compile times) is to explicitly list the function indexes that should be interpreted: ```java import run.endive.compiler.MachineFactoryCompiler; import run.endive.compiler.InterpreterFallback; import run.endive.runtime.Instance; import run.endive.wasm.Parser; import run.endive.wasm.WasmModule; import java.io.File; import java.util.Set; var module = Parser.parse(new File("your.wasm")); var instance = Instance.builder(module). withMachineFactory( MachineFactoryCompiler.builder(module) .withInterpretedFunctions(Set.of(232, 251)) .compile() ). build(); ``` Typically, you can obtain the list of the functions by running the compiler once with `InterpreterFallback.WARN` ### Caveats Please note that compiling and executing Wasm modules at runtime requires: - an external dependency on [ASM](https://asm.ow2.io/) - the usage of runtime reflection This is usually fine when running on a standard JVM, but it involves some additional configuration when using tools like `native-image`. --- ## Build Time Compilation ## Overview :::warning[Security Consideration] The compiler translates Wasm to JVM bytecode without post-compilation verification. Only compile Wasm modules you trust. See [Security Model](/docs/security/overview). ::: The build time compiler backend is a drop-in replacement for the interpreter, and it passes 100% of the same spec tests that the interpreter already supports. This compiler translates the WASM instructions to Java bytecode and stores them as `.class` files that you package in your application. The resulting code is usually expected to evaluate (much) faster and consume less memory than if it was interpreted. The build time compiler has several advantages over the [Runtime Compiler](runtime-compiler.md) such as: - improved instance initialization time: the translation occurs at build time - no reflection needed: easier to use with `native-image` - fewer runtime dependencies: ASM is only needed at build time - distribute Wasm modules as self-contained jars: making it a convenient way to distribute software that was not originally meant to run on the Java platform You can use the compiler at build-time via Maven plug-in, Gradle plug-in, or plain CLI. ### Interpreter Fall Back The WASM to bytecode compiler translates each WASM function into JVM method. Occasionally you will find WASM module where functions are bigger than the maximum method size allowed by the JVM. In these rare cases, we fall back to executing these large functions in the interpreter. Since interpreted functions have worse performance, we want to make sure you are aware this is happening so the build time compiler will FAIL if it finds any functions that are too large. The build tool will produce a message that contains text like: ```text WASM function size exceeds the Java method size limits and cannot be compiled to Java bytecode. It can only be run in the interpreter. Either reduce the size of the function or enable the interpreter fallback mode: WASM function index: 3938 ``` If this happens you can configure your build tool, to just issue warning messages, or to be silent. Another way to silence the message is to configure the build tool with an explicit list of functions that should be interpreted. Typically, you obtain the list of the functions by running the compiler once with `interpreterFallback` set to `WARN` ## Using Maven Example configuration of the Maven plug-in: ```xml run.endive endive-compiler-maven-plugin compiler-gen compile src/main/resources/add.wasm org.acme.wasm.Add ``` In the codebase you can use the generated module by configuring appropriately the `MachineFactory`: ```java import run.endive.runtime.Instance; // load the bundled module var module = Add.load(); // instantiate the module with the pre-compiled code var instance = Instance.builder(module). withMachineFactory(Add::create). build(); ``` ### Generating Module Exports and Imports The build-time compiler can also generate typed Java wrappers for a module's exports and imports, eliminating the need for the [`@WasmModuleInterface` annotation](../annotations/index.md#wasmmoduleinterface) and the annotation processor setup. Add the `moduleInterface` parameter to the plugin configuration: ```xml src/main/resources/demo.wasm org.acme.wasm.DemoModule org.acme.wasm.Demo ``` This generates `Demo_ModuleExports` and `Demo_ModuleImports` classes alongside the compiled module. You can then use them directly in your code without any annotation: ```java var instance = Instance.builder(DemoModule.load()). withMachineFactory(DemoModule::create). build(); var exports = new Demo_ModuleExports(instance); ``` ### The `compile` Goal You can obtain the full description of the Maven Plugin with a command like: `mvn help:describe -DgroupId=run.endive -DartifactId=endive-compiler-maven-plugin -Dversion=999-SNAPSHOT -Ddetail` ``` endive:compile Description: This plugin generates an invokable library from the compiled Wasm Implementation: run.endive.build.time.maven.EndiveCompilerGenMojo Language: java Bound to phase: generate-sources Available parameters: interpretedFunctions The indexes of functions that should be interpreted, separated by commas interpreterFallback (Default: FAIL) Required: true the action to take if the compiler needs to use the interpreter because a function is too big moduleInterface Fully qualified name of the user's class for which to generate _ModuleExports and _ModuleImports wrapper classes. When set, eliminates the need for @WasmModuleInterface annotation and the annotation processor. name Required: true the base name to be used for the generated classes targetClassFolder (Default: ${project.build.directory}/generated-resources/endive-compiler) Required: true the target folder to generate classes targetSourceFolder (Default: ${project.build.directory}/generated-sources/endive-compiler) Required: true the target source folder to generate the Machine implementation targetWasmFolder (Default: ${project.build.directory}/generated-resources/endive-compiler) Required: true the target wasm folder to generate the stripped meta wasm module wasmFile Required: true the wasm module to be used ``` #### IDE shortcomings In some IDEs the sources generated under the standard folder `target/generated-sources` are not automatically recognized. To overcome this limitation you can use an additional Maven Plugin for a smoother IDE experience: ```xml org.codehaus.mojo build-helper-maven-plugin addSource generate-sources add-source ${project.build.directory}/generated-sources/endive-compiler ``` ## Using Gradle [community] Gradle users can leverage the [wasm2class-gradle-plugin](https://github.com/illarionov/wasm2class-gradle-plugin), a third-party plugin that serves as an alternative to the Maven plugin, running the AoT compiler at build time and enabling the use of pre-compiled Wasm code in Java, Kotlin, and Android projects. To set it up, make sure MavenCentral is listed as a repository in the `pluginManagement` block of your `settings.gradle.kts`: ```kotlin pluginManagement { repositories { mavenCentral() gradlePluginPortal() } } ``` Configuration example in the `build.gradle.kts` file for the module: ```kotlin plugins { id("at.released.wasm2class.plugin") version "" } wasm2class { modules { // Target package for the generated classes targetPackage = "org.acme.wasm" // Use "Add" as the base name for generated classes create("Add") { // Translate `add.wasm` into bytecode wasm = file("src/main/resources/add.wasm") } } } ``` This generates the class `org.acme.wasm.Add`, which you can use to instantiate the module just like shown earlier in the Maven example. --- ## Runtime Compiler Cache # Overview of the Runtime Compiler Cache The runtime compiler cache lets the Endive runtime compiler store the results of compiling WASM modules to Java bytecode. Subsequent executions can skip compilation and start faster. Use the experimental directory-based cache, or implement the simple `run.endive.compiler.Cache` interface: ```java public interface Cache { byte[] get(String key) throws IOException; void putIfAbsent(String key, byte[] data) throws IOException; } ``` The compiler uses the module digest (default SHA-256) as the cache key. For example, `"sha-256:KRgyTkCm43c34ksqtA8gmdDw4YCfquC2G0qfIFCpb+w="` could be a key. The cached value is a JAR containing the module's compiled bytecode. ## The Directory Cache :::warning[Security Consideration] The directory cache stores compiled bytecode on disk without integrity verification. Ensure the cache directory has restrictive permissions (`chmod 700`) and is not writable by untrusted users. Do not share caches across trust boundaries. ::: The directory cache stores entries as files under a configured directory. For example, if the cache directory is `/cache`, and you store the following cache key: `sha-256:KRgyTkCm43c34ksqtA8gmdDw4YCfquC2G0qfIFCpb+w=` then that will create the following file: `/cache/sha-256/kr/gytkcm43c34ksqta8gmddw4ycfquc2g0qfifcpb-w.jar` It transforms the key to: * translate characters to be file-system-friendly * use a two-character subdirectory prefix to avoid directory scaling issues The implementation uses file system atomic moves (write to a temp file, then move to the final location). This makes the cache thread-safe and safe to share across processes and avoids partial-write failures. Temp files are written under `/cache/.tmp`. Partially written temp files may be left after a crash; there is no automatic cleanup or eviction. The cache size is not limited — delete files manually to free disk space. ### Using the Directory Cache We assume you already use the runtime compiler. If not, see the [Runtime Compiler](../execution/runtime-compiler) guide first. ### Add the Maven Dependency Add the following dependency: ```xml run.endive dircache-experimental ``` ### Code Create the cache: ```java import java.nio.file.Path; import run.endive.experimental.dircache.DirectoryCache; var cache = new DirectoryCache(Path.of("cache")); ``` Configure the compiler to use the cache via `MachineFactoryCompiler.builder(...)`: ```java var module = Parser.parse(new File("your.wasm")); var instance = Instance.builder(module). withMachineFactory( MachineFactoryCompiler.builder(module).withCache(cache).compile() ). build(); ``` --- ## Wasi Preview 1 # WASI Preview 1 :::warning[Security Consideration] WASI file access does not enforce path sandboxing by default. Always use a virtual filesystem (e.g., [ZeroFs](https://github.com/roastedroot/zerofs) or [Jimfs](https://github.com/google/jimfs)) to restrict guest access to pre-opened directories. Passing the host filesystem directly exposes all files the JVM process can access. See [Security Best Practices](/docs/security/best-practices). ::: The **W**eb**A**ssembly **S**ystem **I**nterface is a suite of host functions that a Wasm module can import to provide system-level capabilities, such as: * stdin / stdout / stderr * environment variables * command line arguments * system clock * random number generation * basic reading and writing of files (through use of a virtual file system) All such capabilities are virtualized; i.e., the _guest_ will not have direct access to the corresponding _host_ resources, but they will be mediated by the WASI layer, which can be configured to limit their surface. Add the dependency to your build: ```xml run.endive wasi latest-release ``` ## How to use As a host who is running Wasm modules, WASI is just a collection of imports that you need to provide to a wasi-compiled module when instantiating it. You'll also need to configure some options for how these functions behave and what the module can and cannot do. Remember that you have full control over those functions, you can use just part of provided implementation or swap in specific implementations to better control what is being executed. ### WasiPreview1 Instantiation In order to instantiate a WASI module you need an instance of `WasiPreview1`. For instance, download the following example from the link or with curl: ```bash curl https://raw.githubusercontent.com/bytecodealliance/endive/main/wasm-corpus/src/main/resources/compiled/hello-wasi.wat.wasm > hello-wasi.wasm ``` ```java import run.endive.log.SystemLogger; import run.endive.wasi.WasiOptions; import run.endive.wasi.WasiPreview1; import run.endive.wasm.Parser; import run.endive.runtime.Store; import java.io.File; var logger = new SystemLogger(); // let's just use the default options for now var options = WasiOptions.builder().build(); // create our instance of wasip1 var wasi = WasiPreview1.builder().withOptions(options).build(); // create the module and connect the host functions var store = new Store().addFunction(wasi.toHostFunctions()); // instantiate and execute the main entry point store.instantiate("hello-wasi", Parser.parse(new File("hello-wasi.wasm"))); ``` :::note Notice that we don't explicitly execute the module. The module will run when you instantiate it. This is part of the WASI spec. A WASI module will implicitly call [`_start`](https://webassembly.github.io/spec/core/syntax/modules.html#start-function). To learn more [read this blog post](https://dylibso.com/blog/wasi-command-reactor/). ::: ### stdin, stdout, and stderr To start with, you want to orchestrate stdin, stdout, and stderr of the module. Often, this is the way you communicate with basic WASI-enabled modules by way of the [command pattern](https://dylibso.com/blog/wasi-command-reactor/). In order to make it easy to manipulate these streams, we expose stdin as an [InputStream](https://docs.oracle.com/javase/8/docs/api/java/io/InputStream.html) and stdout/stderr as an [OutputStream](https://docs.oracle.com/javase/8/docs/api/java/io/OutputStream.html). Download from the link or with curl: ```bash curl https://raw.githubusercontent.com/bytecodealliance/endive/main/wasm-corpus/src/main/resources/compiled/greet-wasi.rs.wasm > greet-wasi.wasm ``` ```java // Let's create a fake stdin stream with the bytes "Endive" var fakeStdin = new ByteArrayInputStream("Endive".getBytes()); // We will create two output streams to capture stdout and stderr var fakeStdout = new ByteArrayOutputStream(); var fakeStderr = new ByteArrayOutputStream(); // now pass those to our wasi options builder var wasiOpts = WasiOptions.builder().withStdout(fakeStdout).withStderr(fakeStderr).withStdin(fakeStdin).build(); var wasi = WasiPreview1.builder().withOptions(wasiOpts).build(); // greet-wasi is a rust program that greets the string passed in stdin var store = new Store().addFunction(wasi.toHostFunctions()); // instantiating will execute the module if it's a WASI command-pattern module store.instantiate("hello-wasi", Parser.parse(new File("greet-wasi.wasm"))); // check that we output the greeting assert(fakeStdout.toString().equals("Hello, Endive!")); // there should be no bytes in stderr! assert(fakeStderr.toString().equals("")); ``` Notice that it is always possible to connect standard output, standard input and standard error to the system's real streams. For instance, you would write: ```java var wasi = WasiOptions.builder().withStdout(System.out).withStderr(System.err).withStdin(System.in).build(); ``` a convenient shorthand for doing the same is: ```java var wasi = WasiOptions.builder().inheritSystemStreams().build() ``` ## arguments Especially when using CLIs, it's useful to provide command line arguments to the Wasm Module. You can do that by using: ```java var wasi = WasiOptions.builder().withArguments(List.of("executable-name", "--more", "--options")).build(); ``` ## environment variables To expose environment variables to your WASI module you can list them in the options: ```java var wasi = WasiOptions.builder(). withEnvironment("ENV_ONE_KEY", "my-one-key-value"). withEnvironment("ENV_TWO_KEY", "my-two-key-value"). build(); ``` ## disk We provide limited support for operations on the disk, we only test on a Virtual FileSystem and we encourage you to use the same. We use [Google's Jimfs](https://github.com/google/jimfs). Example code to use the disk looks like: ```java import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix().toBuilder().setAttributeViews("unix").build())) { Path source = Path.of("my-source"); Path target = fs.getPath("my-source"); run.endive.wasi.Files.copyDirectory(source, target); var wasi = WasiOptions.builder().withDirectory(target.toString(), target).build(); // ... } ``` ## Supported Features If your module calls a WASI function that we don't support, or uses a feature that we don't support, we will throw a `WasmRuntimeException`. For the most up-to-date info, and to see what specific functions we support, see the [WasiPreview1.java](https://github.com/bytecodealliance/endive/blob/main/wasi/src/main/java/run/endive/wasi/WasiPreview1.java) and the following table: | WASI Function | Supported | Notes | |-------------------------|------------|---------------------------------------------------------------------------| | args_get | ✅ | | | args_sizes_get | ✅ | | | clock_res_get | 🟡 | See `clock_time_get`. | | clock_time_get | 🟡 | Clock IDs `process_cputime_id` and `thread_cputime_id` are not supported. | | environ_get | ✅ | | | environ_sizes_get | ✅ | | | fd_advise | ✅ | | | fd_allocate | ✅ | | | fd_close | ✅ | | | fd_datasync | ✅ | | | fd_fdstat_get | ✅ | | | fd_fdstat_set_flags | ✅ | | | fd_fdstat_set_rights | ❌ | | | fd_filestat_get | ✅ | | | fd_filestat_set_size | ✅ | | | fd_filestat_set_times | ✅ | | | fd_pread | ✅ | | | fd_prestat_dir_name | ✅ | | | fd_prestat_get | ✅ | | | fd_pwrite | 🟡 | Not supported for files opened in append mode. | | fd_read | ✅ | | | fd_readdir | ✅ | | | fd_renumber | ✅ | | | fd_seek | ✅ | | | fd_sync | ✅ | | | fd_tell | ✅ | | | fd_write | ✅ | | | path_create_directory | ✅ | | | path_filestat_get | ✅ | | | path_filestat_set_times | ✅ | | | path_link | ✅ | | | path_open | ✅ | | | path_readlink | ✅ | | | path_remove_directory | ✅ | | | path_rename | ✅ | | | path_symlink | 🟡 | Dangling symlinks are not supported. | | path_unlink_file | ✅ | | | poll_oneoff | ✅ | | | proc_exit | ✅ | | | proc_raise | 💀 | This function is no longer part of WASI. | | random_get | ✅ | | | sched_yield | ✅ | | | sock_accept | ❌ | | | sock_recv | ❌ | | | sock_send | ❌ | | | sock_shutdown | ✅ | | --- ## Annotations ## Host Modules Instead of writing host functions by hand, you can write a class containing annotated methods and let the Endive annotation processor generate the host functions for you. This is especially useful when you have many host functions. ```java @HostModule("demo") public final class Demo { public Demo() {} @WasmExport public long add(int a, int b) { return a + b; } @WasmExport // the Wasm name is random_get public void randomGet(Memory memory, int ptr, int len) { byte[] data = new byte[len]; random.nextBytes(data); memory.write(ptr, data); } public HostFunction[] toHostFunctions() { return Demo_ModuleFactory.toHostFunctions(this); } } ``` The `@HostModule` annotation marks the class as a host module and specifies the module name for all the host functions. The `@WasmExport` annotation marks a method as host function and optionally specifies the name of the function. If the name is not specified, then the Java method name is converted from camel case to snake case, as is a common convention in Wasm. The `Demo_ModuleFactory` class in `toHostFunctions()` is generated by the annotation processor. Host functions must be instance methods of the class. Static methods are not supported. This is because host functions will typically interact with instance state in the host class. To use the host module, you need to instantiate the host module and fetch the host functions: ```java import run.endive.runtime.ImportValues; var demo = new Demo(); var imports = ImportValues.builder().addFunction(demo.toHostFunctions()).build(); ``` ### Type conversions The following conversions are supported: | Java Type | Wasm Type | |-------------------|------------| | `int` | `i32` | | `long` | `i64` | | `float` | `f32` | | `double` | `f64` | ## WasmModuleInterface :::tip If you are using the [build-time compiler](../execution/build-time-compiler.md), you can use the `moduleInterface` parameter in the Maven plugin instead of the annotation processor. This is simpler and avoids the setup described below. See [Generating Module Exports and Imports](../execution/build-time-compiler.md#generating-module-exports-and-imports). ::: If you already have a Wasm module and want to generate scaffolded Java code to interact with it, you can use the `@WasmModuleInterface` annotation. ```java @WasmModuleInterface("demo.wasm") public final class Demo {} ``` The annotation accepts a single argument, which can be either: - The location of the Wasm module on the current classpath (transitive references are not supported). - An absolute URI pointing to the Wasm module, in the form of `file://....` This annotation generates several things, depending on the provided module: - `ModuleExports`: Represents the Wasm module's exported functions, mapped to typed Java parameters and return values. - `ModuleImports`: Represents the module's imported host functions and includes a convenient `toImportValues()` method to obtain ImportValues after implementing the interfaces. You can find examples of how to use the generated code in the [`annotations/it` folder of the repository](https://github.com/bytecodealliance/endive/tree/main/annotations/it). ## Enabling the Annotation Processor In order to use host modules, you need to import the relevant annotations, e.g. in Maven: ```xml run.endive annotations latest-release ``` and configure the Java compiler to include the Endive `annotations-processor` as an annotation processor. Exactly how this is done depends on the build system you are using, for instance, with Maven: ```xml org.apache.maven.plugins maven-compiler-plugin run.endive annotations-processor latest-release ``` --- ## Security Model # Security Model Endive executes WebAssembly modules inside the JVM. Understanding the security boundaries is essential when running untrusted code. ## The Wasm Sandbox WebAssembly provides a sandboxed execution environment by design: - **Memory isolation**: Each Wasm module operates on its own linear memory. It cannot access the JVM heap, other modules' memory, or host memory directly. - **No ambient capabilities**: Wasm modules have no access to the filesystem, network, environment variables, or system calls unless the host explicitly provides them via imports. - **Control flow integrity**: Indirect calls are checked against a type table. Modules cannot jump to arbitrary code. - **Deterministic execution**: The core Wasm spec produces deterministic results (with exceptions for floating-point NaN bit patterns and threading). ## Trust Boundaries ``` +------------------------------------------------+ | JVM Host Process | | | | +----------------+ +----------------+ | | | Host Function A| | Host Function B| | | +-------+--------+ +-------+--------+ | | | | | | - - - - | - trust boundary - | - - - - - - | | | | | | +-------v---------------------v--------+ | | | Endive Runtime | | | | | | | | +----------+ +----------+ | | | | | Wasm | | Wasm | | | | | | Module A | | Module B | | | | | +----------+ +----------+ | | | +--------------------------------------+ | +------------------------------------------------+ ``` The critical trust boundary is between **Wasm guest code** and **host functions**. Host functions have full JVM privileges. Any argument passed from Wasm to a host function must be validated before use. ## What the Sandbox Does NOT Guarantee - **CPU limits**: Wasm modules can execute infinite loops. The host must enforce timeouts (see [CPU Limits](/docs/advanced/cpu-limits)). - **Memory growth limits**: Modules can request memory growth up to the declared maximum. The host should set appropriate limits. - **Post-compilation verification**: The build-time and runtime compilers translate Wasm to JVM bytecode without a separate verification pass. For maximum assurance with untrusted code, prefer the interpreter. - **Cache integrity**: The [directory-based compiler cache](/docs/execution/compiler-cache/#the-directory-cache) does not verify bytecode integrity on load. Protect cache directories with restrictive permissions. ## WASI and Capability-Based Security When using WASI, the host controls what capabilities the guest receives: - **Filesystem access** is opt-in. Use a virtual filesystem (e.g., [ZeroFs](https://github.com/roastedroot/zerofs)) to restrict access to specific directories. - **Environment variables** and **command-line arguments** are explicitly passed by the host. - **Standard I/O** streams are host-controlled. See [Best Practices](/docs/security/best-practices) for actionable guidance on securing your Endive deployment. --- ## Security Best Practices # Security Best Practices Practical guidance for running Wasm modules securely with Endive. ## Writing Safe Host Functions Host functions are the primary attack surface — they cross the sandbox boundary and execute with full JVM privileges. **Always validate arguments from Wasm:** ```java title="Example" HostFunction readMemory = (Instance instance, long... args) -> { int offset = (int) args[0]; int length = (int) args[1]; // Validate bounds BEFORE accessing memory var memory = instance.memory(); if (offset < 0 || length < 0 || Math.addExact(offset, length) > memory.pages() * 65536) { throw new TrapException("out of bounds memory access"); } byte[] data = memory.readBytes(offset, length); // ... process data safely return new long[0]; }; ``` **Key rules:** - Validate all memory offsets and lengths before reading/writing - Never use Wasm-provided values as array indices without bounds checking; keep overflow in mind or use overflow-safe methods such as `Math#addExact` or `Objects#checkFromIndexSize` - Be cautious with string decoding — enforce maximum lengths - Avoid exposing file paths, SQL queries, or shell commands derived from Wasm input ## WASI Sandboxing :::warning[Security Consideration] WASI file access does not enforce path sandboxing by default. Passing the host filesystem directly exposes all files the JVM process can access. ::: **Use a virtual filesystem:** ```java title="Example" var options = WasiOptions.builder() // Use ZeroFs to restrict access to specific directories .withDirectory("/guest/data", zeroFs.getPath("/host/sandboxed/data")) .build(); ``` **Provide only access to what is needed by the Wasm module, especially if it is untrusted:** - Pass a virtual file system, and copy needed data to it in advance - Pass only the needed environment variables and not all host environment variables - Inherit the host stdin, stdout and stderr only if needed; alternatively provide a custom `InputStream` and `OutputStream` ## Resource Limits Wasm modules can consume unbounded CPU and memory. Always set limits when running untrusted code. **CPU timeout via thread interruption:** ```java title="Example" var executor = Executors.newSingleThreadExecutor(); var future = executor.submit(() -> instance.export("run").apply()); try { var result = future.get(5, TimeUnit.SECONDS); } catch (TimeoutException e) { future.cancel(true); // interrupts the Wasm execution } ``` See [CPU Limits](/docs/advanced/cpu-limits) for more patterns. ## Compiler Security The runtime and build-time compilers translate Wasm to JVM bytecode for performance. When running untrusted modules: - **Prefer the interpreter** for maximum sandbox assurance — it doesn't generate JVM bytecode - **Protect compiler cache directories** with restrictive permissions (`chmod 700`) if using the [directory cache](/docs/execution/compiler-cache/#the-directory-cache) - **Set JVM heap limits** when compiling large untrusted modules to prevent resource exhaustion ## Dependency Supply Chain - All Endive dependencies are checked against the [Bytecode Alliance allowed license list](/docs/security/overview) in CI - Dependency vulnerabilities are scanned nightly via OWASP Dependency-Check - Dependabot is enabled for automated security updates - All dependency updates are manually reviewed before merging --- ## CPU # Limiting CPU usage :::warning[Security Consideration] Wasm modules can contain infinite loops. When running untrusted code, always set execution timeouts via thread interruption or an ExecutorService with a deadline. Without timeouts, a malicious module can consume 100% CPU indefinitely. ::: Often, when running untrusted user code in our infrastructure, we want to have strong guarantees around the termination of the program. To achieve this result there are, currently, two mechanisms in Endive: ## Interrupts Wasm modules executed using Endive honour the carrier thread interruption mechanism, thus you can leverage it to implement absolute timeouts: ```bash curl https://raw.githubusercontent.com/bytecodealliance/endive/main/wasm-corpus/src/main/resources/compiled/infinite-loop.c.wasm > infinite-loop.wasm ``` Build and instantiate this infinite loop module: ```java import run.endive.runtime.ExportFunction; import run.endive.runtime.Instance; import run.endive.wasm.Parser; Instance instance = Instance.builder(Parser.parse(new File("./infinite-loop.wasm"))).build(); ExportFunction function = instance.export("run"); ``` Now you can execute the Wasm module and control the execution using plain interrupts, with the low level Thread API: ```java var thread = new Thread() { @Override public void run() { function.apply(); } }; thread.start(); thread.join(200); thread.interrupt(); ``` Or using an `ExecutorService`: ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; ExecutorService service = Executors.newSingleThreadExecutor(); var future = service.submit(() -> function.apply()); try { future.get(100, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { future.cancel(true); // handle the failure } ``` ## [unsafe] Execution Listener The Endive interpreter exposes an unsafe listener to granularly control the Wasm Modules execution. Using it is extremely risky as the code will be evaluated for each and every Wasm instruction, use it with extreme caution. ```java var instance = Instance.builder(Parser.parse(new File("./infinite-loop.wasm"))).withUnsafeExecutionListener( (instruction, stack) -> System.out.println("current instruction: " + instruction + ", stack size: " + stack.size())).build(); ``` --- ## SIMD support :::info[Availability] SIMD support is available only for Java 21+ and interpreter mode. ::: If you are using a version of Java that supports [JEP 448 - Vector API](https://openjdk.org/jeps/448) you can leverage [Vector instructions](https://webassembly.github.io/spec/core/syntax/instructions.html#vector-instructions). After adding the dependency: ```xml run.endive simd ``` You can instantiate a module with SIMD support by explicitly providing a `MachineFactory`: ```java import run.endive.simd.SimdInterpreterMachine; var module = Parser.parse(new File("your.wasm")); var instance = Instance.builder(module).withMachineFactory(SimdInterpreterMachine::new).build(); ``` :::warning SIMD support **REQUIRES** validation. Disabling validation (`WasmModule.builder().withValidation(false)`) is likely to produce incorrect results. ::: --- ## WABT # Tools ## WebAssembly Binary Toolkit Since we use them in the build of the project, we publish a few [wabt](https://github.com/WebAssembly/wabt) tools compiled at build time with Endive. Adding support for more tools is easy and we welcome contributions in this direction. The relevant module can be added to the build with: ```xml run.endive wabt latest-release ``` ## Wasm Tools As we need it to catch up with the latest development of the Wasm spec we publish a few [wasm-tools](https://github.com/bytecodealliance/wasm-tools) compiled at build time with Endive. The relevant module can be added to the build with: ```xml run.endive wasm-tools latest-release ``` ## wat2wasm In Endive, we don't have a Wasm text format parser just yet. To overcome this limitation you can use `wat2wasm`, for example: ```java import run.endive.wabt.Wat2Wasm; // or import run.endive.tools.wasm.Wat2Wasm; var wasm = Wat2Wasm.parse( "(module (func (export \"add\") (param $x i32) (param $y i32) (result i32)" + " (i32.add (local.get $x) (local.get $y))))"); var moduleInstance = Instance.builder(Parser.parse(wasm)).build(); var addFunction = moduleInstance.export("add"); var result = addFunction.apply(1, 41)[0]; System.out.println(result); ``` --- ## Logging # Logging For maximum compatibility and to avoid external dependencies we use, by default, the JDK Platform Logging ([JEP 264](https://openjdk.org/jeps/264)). The Platform Logging falls back to the [`java.util.logging` API](https://docs.oracle.com/en/java/javase/21/core/java-logging-overview.html) by default. For more advanced configuration scenarios we encourage you to provide an alternative, compatible, adapter: - [SLF4J](https://www.slf4j.org/manual.html#jep264) - [Log4j2](https://logging.apache.org/log4j/2.x/log4j-jpl.html) It's also possible to provide a custom `run.endive.log.Logger` implementation if JDK Platform Logging is not available or doesn't fit. --- ## Advanced Wasm Memory Customization Different Wasm workloads will be using the memory in very different ways. Make sure you don't do changes without proper benchmarking and analysis. It's possible to provide a custom implementation of the entire Memory used by the Wasm module: ```java var instance = Instance.builder(module).withMemoryFactory(limits -> { return new ByteArrayMemory(limits); }).build(); ``` :::note Endive provides two built-in Memory implementations: - `ByteArrayMemory`: An optimized memory implementation, recommended for recent OpenJDK systems - `ByteBufferMemory`: Recommended for different Java runtimes (in particular, on Android VMs) ::: --- ## Why # Why? Endive is a young project and we acknowledge that we are exploring and spearheading in many aspects the usage of Web Assembly on the JVM. Since there is (always) some degree of experimentation going on, and we want to have feedback by the community and early users, we decide to publish also `experimental` modules; everyone is welcome to try things out and report back the experience. Please note that if "something works" for you, its very unlikely that it will be removed completely, in most cases, expect slight public API changes or module renames to happen. The goal of having `experimental` modules is because they are not stabilized, we are not 100% confident in the design and we want to be able to perform breaking changes without respecting SemVer. This includes renaming artifactIDs, classes, methods, and reworking their usage according to user feedback and development progress. --- ## CLI # Install and use the CLI :::warning[Security Consideration] The experimental CLI uses `inheritSystemStreams()` by default, granting the Wasm module access to stdin, stdout and stderr of the host. Do not use it with untrusted modules in its current form. ::: The experimental Endive CLI is available for download on Maven at the link: ``` https://repo1.maven.org/maven2/run/endive/cli-experimental//cli-experimental-.sh ``` you can download the latest version and use it locally by typing: ```bash export VERSION=$(curl -sS https://api.github.com/repos/bytecodealliance/endive/tags --header "Accept: application/json" | jq -r '.[0].name') curl -L -o endive https://repo1.maven.org/maven2/run/endive/cli-experimental/${VERSION}/cli-experimental-${VERSION}.sh chmod a+x endive ./endive ``` --- ## Using Rust with Endive ## Compile Rust to Wasm Compiling a Rust library to Wasm is easy and can be performed using standard `rustc` options: ```bash rustc --target=wasm32-unknown-unknown --crate-type=cdylib ``` when you need to add support for WASI preview 1 (typically when using CLIs) you can use: ```bash rustc --target=wasm32-wasi --crate-type=bin ``` :::tip For production usage, make sure to produce an optimized Wasm module by using the standard compiler options `-C opt-level = 3` (speed) or `-C opt-level = "z"` (size). ::: ## Using in Endive ```java import run.endive.wasm.Parser; import run.endive.runtime.Instance; var instance = Instance.builder(Parser.parse(new File("count_vowels.rs.wasm"))).build(); var alloc = instance.export("alloc"); var dealloc = instance.export("dealloc"); var countVowels = instance.export("count_vowels"); var memory = instance.memory(); var message = "Hello, World!"; byte[] bytes = message.getBytes(); int len = bytes.length; int ptr = (int) alloc.apply(len)[0]; memory.write(ptr, bytes); var result = countVowels.apply(ptr, len)[0]; System.out.println(result); ``` --- ## Migrating from Chicory # Migrating from Chicory to Endive Endive is a fork of [Chicory](https://github.com/dylibso/chicory) by Dylibso, Inc. This guide documents all breaking changes for users migrating from Chicory. ## Maven Coordinates | Chicory | Endive | |---------|--------| | `com.dylibso.chicory:runtime` | `run.endive:runtime` | | `com.dylibso.chicory:compiler` | `run.endive:compiler` | | `com.dylibso.chicory:wasm` | `run.endive:wasm` | | `com.dylibso.chicory:wasi` | `run.endive:wasi` | | `com.dylibso.chicory:annotations` | `run.endive:annotations` | | `com.dylibso.chicory:annotations-processor` | `run.endive:annotations-processor` | | `com.dylibso.chicory:log` | `run.endive:log` | | `com.dylibso.chicory:bom` | `run.endive:bom` | All module artifact names (`runtime`, `compiler`, `wasm`, etc.) are unchanged. ## Package Names All packages have moved from `com.dylibso.chicory` to `run.endive`: ``` com.dylibso.chicory.runtime -> run.endive.runtime com.dylibso.chicory.compiler -> run.endive.compiler com.dylibso.chicory.wasm -> run.endive.wasm com.dylibso.chicory.wasi -> run.endive.wasi ``` A global find-and-replace of `com.dylibso.chicory` to `run.endive` in your imports covers this. ## Exception Classes The base exception and interruption exception have been renamed to better reflect their semantics: | Chicory | Endive | Rationale | |---------|--------|-----------| | `ChicoryException` | `WasmEngineException` | Base for engine errors, distinct from `WasmException` (Wasm-level tagged exceptions from the exception-handling proposal) | | `ChicoryInterruptedException` | `WasmInterruptedException` | Host-initiated interruption of Wasm execution | All spec-aligned exception names are unchanged: `TrapException`, `InvalidException`, `MalformedException`, `UnlinkableException`, `UninstantiableException`. ## Maven Plugin | Chicory | Endive | |---------|--------| | `com.dylibso.chicory:chicory-compiler-maven-plugin` | `run.endive:endive-compiler-maven-plugin` | | Goal prefix: `chicory:compile` | Goal prefix: `endive:compile` | | Default output: `generated-sources/chicory-compiler` | Default output: `generated-sources/endive-compiler` | ## System Properties | Chicory | Endive | |---------|--------| | `chicory.hugeMethodLimit` | `endive.hugeMethodLimit` | | `chicory.memCopyWorkaround` | `endive.memCopyWorkaround` | | `chicory.compiler.printUseOfInterpretedFunctions` | `endive.compiler.printUseOfInterpretedFunctions` | ## CLI Binaries | Chicory | Endive | |---------|--------| | `chicory` | `endive` | | `chicory-compiler` | `endive-compiler` | ## Logger Name The JUL/System logger name has changed from `"chicory"` to `"endive"`. If you configure logging levels for the runtime, update your logging configuration accordingly. --- # Blog Posts > The following are blog posts, not reference documentation. --- ## Endive 1.0: WebAssembly on the JVM, Now a Bytecode Alliance Project We're excited to announce **Endive 1.0**, the first release of the project under the [Bytecode Alliance](https://bytecodealliance.org/). Endive is a pure-Java WebAssembly runtime with zero native dependencies, continuing the work started as Chicory. For background on the move, see the [announcement article](https://bytecodealliance.org/articles/endive-and-the-next-chapter-of-webassembly-on-the-jvm). If you're migrating from Chicory, the [migration guide](/docs/migration/from-chicory) has you covered. ## WasmGC Host Integration The Java garbage collector now manages WasmGC objects. Structs, arrays, and externref values that cross the Wasm/Java boundary are standard Java Objects on the JVM heap. No separate GC store, no manual reference tracking. They get collected when unreachable, just like any other object in your application. The annotation processor supports `externref` as plain `Object`, so host functions work naturally with GC types. **If you use WasmGC types at the host boundary, your code will need a small update.** Here's what changed. ### Calling exports that use GC types Use `applyWithRefs` instead of `apply`. It returns a `CallResult` with separate accessors for numeric and reference results: ```java // Before: not possible, GC refs were opaque ints long[] result = export.apply(args); // After: GC refs flow as Java Objects CallResult result = export.applyWithRefs(new long[]{42, 10}, null); WasmStruct point = (WasmStruct) result.refResult(0); int x = (int) point.field(0); ``` Calling `apply()` on a function that involves GC types will throw. ### Building structs and arrays Old constructors are deprecated. Use the builder: ```java // Before var struct = new WasmStruct(typeIdx, new long[]{1, 2}); // After: add fields in declaration order var struct = WasmStruct.builder() .typeIdx(typeIdx) .addField(42) // field 0: numeric .addFieldRef(someRef) // field 1: reference .build(); int x = (int) struct.field(0); // read numeric field Object r = struct.fieldRef(1); // read ref field ``` `WasmArray` follows the same pattern with `addElement()` / `addElementRef()`. ### Host functions receiving GC types Override `applyWithRefs` on `WasmFunctionHandle`: ```java WasmFunctionHandle myFunc = new WasmFunctionHandle() { @Override public CallResult applyWithRefs(Instance instance, long[] args, Object[] refArgs) { WasmStruct input = (WasmStruct) refArgs[0]; // ... process the struct return CallResult.of(new long[]{result}, null); } }; ``` See [#55](https://github.com/bytecodealliance/endive/pull/55) for the full set of changes. ## Tail Call Optimizations Community-contributed fixes have improved tail call correctness, and the compiler now optimizes tail-call dispatch by eliminating unnecessary stack frame allocation. CPython 3.14, which adopted tail calls in its interpreter loop, is the main beneficiary. ## Migrating from Chicory For most users, migrating is a find-and-replace. Maven coordinates move from `com.dylibso.chicory` to `run.endive`, Java packages move from `com.dylibso.chicory.*` to `run.endive.*`, and two exception classes have been renamed (`ChicoryException` to `WasmEngineException`, `ChicoryInterruptedException` to `WasmInterruptedException`). The [migration guide](/docs/migration/from-chicory) covers the full list, including Maven plugin goals, system properties, and CLI binary names. If you use WasmGC types at the host boundary, see the section above. ## What's Ahead Community members have already started prototyping Component Model support at [endive-cm](https://github.com/roastedroot/endive-cm). Anyone interested is welcome to contribute. Follow [#52](https://github.com/bytecodealliance/endive/issues/52) for updates. Cranelift-based native compilation is also in the works, bringing near-native execution speed while preserving the pure-Java packaging experience. ## Acknowledgements Thank you to every contributor who has shipped code, reported bugs, tested pre-releases, and pushed WebAssembly forward on the JVM. The [adopters list](https://github.com/bytecodealliance/endive/blob/main/ADOPTERS.md), from JRuby to Trino, from Bazel to Apache Camel, is the best evidence that this runtime is solving real problems. ## Getting Started ```xml run.endive bom 1.0 pom import run.endive runtime ``` [Documentation](https://endive.run/docs/) | [GitHub](https://github.com/bytecodealliance/endive) We'd love to hear what you're building. [Join the conversation on Zulip](https://bytecodealliance.zulipchat.com/#narrow/stream/endive) and let us know how it goes. --- ## Finding a JVM JIT Bug the Hard Way ![Finding a JVM JIT Bug](finding-a-jvm-jit-bug.png) In early 2025, a user reported intermittent wrong results using the build-time compiler. Not a crash, just a quietly wrong answer. A year of debugging, dead ends, and creative workarounds later, we traced the problem to a bug in the JVM's C2 JIT compiler, and had to build an entire compiler from scratch in 3 days just to prove it. ## The wrong answer In February 2025, a user opened [an issue](https://github.com/dylibso/chicory/issues/755) against the project. They were running a large Wasm module, built from a C++ codebase, on the JVM, and something was wrong: an operation that reverses a string was silently producing incorrect results, though not consistently. It only happened on Java 17 Temurin, only with the build-time compiler, and only when the Wasm module was large enough. The key word is *silently*. There was no exception and no segfault, just a comparison operation deep inside the compiled code returning the wrong boolean while everything downstream acted on that corrupted result. Correctness bugs are the most dangerous kind: crashes tell you something is wrong; wrong answers let you ship broken software with confidence. Before you worry: this bug requires an extraordinary set of conditions to trigger. If you're writing ordinary Java applications, the odds of encountering it are vanishingly small. But the story of how we found, worked around, and finally fixed it is worth telling. ## Not our bug [Edoardo Vacchi](https://github.com/evacchi) and I paired on the investigation with the reported module. We narrowed the problem to the implementation of a single WebAssembly opcode: `I32_GE_U`, the unsigned greater-than-or-equal comparison. Here's the entire implementation: ```java public static int I32_GE_U(int a, int b) { return Integer.compareUnsigned(a, b) >= 0 ? TRUE : FALSE; } ``` One line. `Integer.compareUnsigned` is a standard JDK method that has existed since Java 8, and it is a platform intrinsic, meaning the JIT compiler replaces it with optimized machine code rather than executing the Java implementation. There is no bug in this code. But when we added a single JVM flag, `-XX:CompileCommand=dontinline,com/dylibso/chicory/runtime/OpcodeImpl.I32_GE_U`, the problem vanished. That flag tells the C2 JIT compiler not to inline the method. The fact that preventing inlining fixed the issue pointed the finger squarely at how C2 *optimizes* the inlined code, not at our logic. This was our introduction to a genuine [Heisenbug](https://en.wikipedia.org/wiki/Heisenbug): a bug whose behavior changes when you try to observe it. Adding a `System.out.println` inside the method made it vanish because the print statement changes C2's inlining decisions. Attaching a debugger had the same effect since it prevents certain JIT optimizations. Even reducing the Wasm module to a smaller test case killed the reproduction, because the JIT needs approximately 45 million function calls to build up enough type profile information to trigger the aggressive optimization that contains the defect. Every tool in a debugger's standard toolkit made the problem disappear. ## Living with the bug We couldn't fix the JVM, and we couldn't wait for a fix we didn't yet have. So we got creative with workarounds. The first approach, suggested by [Hiram Chirino](https://github.com/chirino), was a [megamorphic dispatch trick](https://github.com/dylibso/chicory/pull/844). The idea: if C2 only miscompiles the code when it inlines through a monomorphic call site, we can force C2 to give up on inlining by making the call site look polymorphic. At static initialization time, we cycle through different lambda implementations of the same interface, training the JIT to see the call site as megamorphic (too many receiver types to optimize): ```java static { I32GEUFunc noop1 = (a, b) -> a; I32GEUFunc noop2 = (a, b) -> b; for (int i = 0; i < 1000; i++) { i32geuFunc = noop1; MemCopyWorkaround.i32_ge_u(0, 0); i32geuFunc = noop2; MemCopyWorkaround.i32_ge_u(0, 0); } i32geuFunc = (a, b) -> OpcodeImpl.I32_GE_U(a, b); } ``` It's an ugly hack, but it keeps the user mostly safe. We [extended it](https://github.com/dylibso/chicory/pull/1178) to cover all affected code paths and added a CI test using a large Wasm module to catch regressions. The workaround only activates on Java 17 and earlier. There's an irony here: on Java 18+, the bug was already masked. An [entirely unrelated optimization](https://github.com/openjdk/jdk/pull/6101) by [Quan Anh Mai](https://github.com/merykitty) in November 2021 (JDK-8276162, "Optimise unsigned comparison pattern") taught C2 to recognize the `Integer.compareUnsigned` pattern earlier in the pipeline and lower it directly to an unsigned machine comparison. With that optimization in place, by the time C2 reaches the buggy if-folding stage, the signed comparison pattern it would have miscompiled no longer exists. The bug is still there, it just never fires because its input has already been optimized away. ## The reproducer problem The workarounds bought us time, but they weren't a fix. I had a bug in CI, workarounds in production, and a deep suspicion about what was going wrong inside C2. What I didn't have was a way to prove it to the OpenJDK team. The OpenJDK project reasonably requires Java source code reproducers with bug reports. They need something that compiles with `javac`, runs with standard JDK tools, and demonstrates the problem in isolation. But our build-time compiler generates JVM bytecode directly from Wasm, freely using `goto` and labels in patterns that have no direct Java source representation. You can't just decompile it. I tried. Every major decompiler (CFR, Procyon, Fernflower) either choked on the input size (the generated bytecode file was enormous) or produced non-compilable output, with issues especially around control flow in nested blocks. I also tried [HotSpot's replay compilation](https://cr.openjdk.org/~thartmann/talks/2020-Debugging_HotSpot.pdf): I could reproduce the bug during replay, but the output didn't give enough insight to isolate the root cause. The only consistent reproducer we had was [wat2wasm](https://github.com/WebAssembly/wabt), a tool from the [WebAssembly Binary Toolkit (WABT)](https://github.com/WebAssembly/wabt) that converts WebAssembly text format to binary format, roughly 195,000 lines of C++ compiled to a single large Wasm module. I tried reducing it. The original generated code was over 213,000 lines of Java. I managed to get the generated code down to about 40,000 lines, but I couldn't go further. The bug required all 294 functions in the module to be present, approximately 45 million function calls to build C2's type profiles, and specific bytecode patterns that emerged only from the full module. Removing any piece meant C2 would never reach the optimization threshold. I spent months chasing down the issue. Multiple branches in [my fork](https://github.com/andreaTP/chicory) (`decompiling-attempt1`, `JDK-8376400-reproducer`, and others) document the dead ends. Every approach that should have worked didn't. The Heisenbug lived up to its name. ## Building a compiler to catch a compiler bug By February 2026, I had the bug on my mind for nearly a year. The megamorphic workaround was holding enough, but the real fix was sitting behind a wall I couldn't climb: I needed Java source code, and the toolchain produced Java bytecode. What if I built a different toolchain? Instead of trying to decompile bytecode back into Java sources, what if I compiled WebAssembly directly to Java source code, skipping bytecode entirely? The generated Java would be verbose and mechanical, but it would be compilable with `javac`. It was a bet: if `javac` happened to produce bytecode with the same patterns that triggered the C2 bug, I'd have my reproducer. The idea was clear, but the scope was daunting. A source compiler that could handle something the size of `wat2wasm` is a real compiler: it needs to handle over a hundred opcodes, translate WebAssembly's structured control flow (blocks, loops, `br_if`, `br_table`) into Java's `while`/`switch`/`break` constructs, split methods to stay under the JVM's 64KB method size limit, and handle edge cases in memory operations, type conversions, and stack manipulation. I wouldn't have attempted it without a machine helping me. ### 3 days later In February 2026, I started using [Claude](https://claude.ai) and wanted to test it on something that was both low-risk (a standalone, one shot tool, not a change to the runtime) and hard enough to be a meaningful test. Building a Wasm-to-Java-source compiler fit perfectly. The approach was informally spec-driven from the start. We already had a test suite generator that creates Java test cases from the official [WebAssembly spec test suite](https://github.com/WebAssembly/testsuite). I hooked the source compiler into that generator on day one, so every opcode implementation was validated against the spec as it was written. From there, I used real-world Wasm modules and the WASI testsuite to catch edge cases the spec tests didn't cover. Claude handled the mechanical work: porting opcodes one by one following established patterns, generating the boilerplate for type conversions, and iterating through the hundreds of Wasm instructions that all follow similar structures. I steered the architecture: the control flow translation strategy, the method splitting heuristic to stay under the JVM's 64KB method limit, and debugging the edge cases where WebAssembly's semantics diverge from Java's. The spec test suite provided continuous validation: if a change broke something, we knew immediately which opcode and which test case failed. This tight feedback loop made it possible to move fast without accumulating hidden bugs or making regressions. In a little more than 3 days of very hard work, I went from zero to a compiler that passed over 25,000 spec tests and all WASI tests. Without the LLM, I would never have started: the effort of writing a full source compiler solo would have been hard to justify against the uncertain payoff. With it, the turnaround was fast enough that building the tool was worth more than spending another month trying to reduce the bytecode reproducer. ### The moment of truth Compiling `wat2wasm` through the source compiler produced approximately 200,000 lines of Java source code. When I compiled that Java source with `javac`, ran it on Java 17, and fed it a large enough `.wat` input, the program produced wrong results. Same `out of bounds memory access` error, same non-deterministic behavior, same Heisenbug, now reproduced in pure Java source code. 🎉 I finally had what the OpenJDK team needed: a self-contained, `javac`-compilable reproducer of a C2 JIT compiler bug. [PR #1200](https://github.com/dylibso/chicory/pull/1200) documents the source compiler and the reproducer. ## The 20-line fix With the 200,000-line reproducer in hand, [Roland Westrelin](https://github.com/rwestrel) took a look. He did what I couldn't: he understood the C2 optimization pipeline well enough to distill our massive reproducer into a [20-line test case](https://github.com/openjdk/jdk/pull/30677): ```java private static void test1(int i) { int v; // (1) C2 sees this signed comparison if (i + MIN_VALUE >= 16 + Integer.MIN_VALUE) { v = 0; } else { v = 1; } // (2) Uncommon trap, C2 assumes this is never taken if (v == 0) { throw new RuntimeException("never taken"); } // (3) Second signed comparison, C2 folds (1) and (3) // into a single unsigned comparison if (i + MIN_VALUE < 8 + Integer.MIN_VALUE) { taken1++; } } ``` The bug lives in the interaction of three C2 optimizations: 1. **Signed comparisons with uncommon traps.** C2 compiles the two `if` statements and records that the `throw` branch is never taken (an "uncommon trap", a deoptimization point for rare paths). 2. **Split-if.** C2's `do_split_if()` optimization duplicates control flow through a Phi node, specializing each branch. This modifies the state captured by the uncommon trap. 3. **If-folding.** `IfNode::fold_compares()` combines the two signed comparisons into a single, more efficient unsigned comparison. But it doesn't know that split-if has already modified the uncommon trap's saved state. When deoptimization fires at runtime, execution resumes at the wrong bytecode point with corrupted state. The fix is surgical: a `_safe_for_fold_compare` flag. When `do_split_if()` modifies an uncommon trap, it marks it as unsafe for if-folding. The fold_compares optimization checks this flag before proceeding. 369 additions (mostly tests), 3 deletions. The fix was [integrated](https://github.com/openjdk/jdk/pull/30677) on April 30, 2026. As one of the reviewers noted: *"these kind of bugs with wrong safepoint states are really hard to catch."* ## Should you worry? No. This bug requires a very specific convergence of conditions: - **A particular code pattern**: multiple signed integer comparisons that C2 can fold into unsigned comparisons, with uncommon traps that the split-if optimization modifies. This pattern is rare in handwritten Java. - **Enormous JIT warmup**: approximately 45 million function calls to build the type profile data that triggers C2's most aggressive optimizations. Most Java methods never reach this threshold. - **JDK 11 through 17**: on JDK 18 and later, an unrelated optimization (JDK-8276162) masks the bug entirely. - **Generated code patterns**: the comparison-heavy, uniform-dispatch patterns that WebAssembly runtimes produce are unusual. Handwritten Java code tends toward more diverse control flow that doesn't trigger the specific optimization sequence. We hit these conditions because we are aggressively leveraging C2's inlining and JIT optimizations to run Wasm as fast as possible on the JVM. The build-time compiler is designed to produce code that C2 can optimize deeply: millions of tiny functions, uniform opcode dispatch, comparison-heavy control flow at enormous scale. That's the whole point, and it's what makes the engine fast. It also means we exercise the JVM in ways that handwritten Java rarely does. In the meantime, the workaround protects all users automatically on affected JDK versions. The upstream fix will ship in a future JDK release. ## What we learned **Correctness bugs are scarier than crashes.** A wrong comparison result is silent, and downstream failures can look completely unrelated to the root cause. If the reporter hadn't been testing carefully, this could have gone unnoticed for much longer. **Heisenbugs demand creative approaches.** Every standard debugging technique (print statements, debuggers, test reduction, bisection) actively prevented this bug from manifesting. When observation collapses the phenomenon you're investigating, you need to find indirect evidence. **Sometimes building new tooling beats reducing existing artifacts.** I spent months trying to shrink a 213,000-line reproducer. In the end, building an entirely new compiler in 3 days produced a better result. The lesson isn't "always build new tools", it's "recognize when reduction has hit a wall and consider alternatives." **Open source ecosystems work.** A user reported the bug, we investigated and built workarounds, [David Lloyd](https://github.com/dmlloyd) helped file the upstream issue, and Roland Westrelin diagnosed the C2 root cause and wrote the fix. This chain from user report to compiler fix only works because the code is open and the communities are connected. If you care about Java, WebAssembly, and the future of really portable software, [come build with us](https://github.com/bytecodealliance/endive).