Skip to main content

Native Compilation with Redline

Overview

Experimental

Redline is experimental, see Why for what that means for stability. It also supports less of the WebAssembly specification than the other execution modes, so check Feature support before adopting it.

Redline compiles your Wasm module to native machine code using Cranelift, instead of to JVM bytecode. Compilation happens at build time for every supported platform, and the right one is selected at runtime.

It is a substitute for the Build Time Compiler only, not for the interpreter or the Runtime Compiler, and it is enabled on the same Maven plugin. The bytecode is still generated, and is used at runtime when the architecture or operating system is not supported.

Feature support

FeatureSupported
Core specification
Bulk memory
Tail call
Threads and atomics
Reference type instructions
Multi memory
Exception handling
Garbage collection
Typed function references
SIMD
externref values to and from host functions

If your module uses anything unsupported the build fails. There is no per function fallback, so a single unsupported instruction stops the whole module from compiling.

Platform support

Native code is generated for six platforms:

x86_64aarch64
Linux
macOS
Windows

On any other platform your module still runs, using the compiled bytecode instead. See Falling back.

Usage

Enable it on the compiler plugin:

<plugin>
<groupId>run.endive</groupId>
<artifactId>endive-compiler-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<name>org.acme.wasm.MyModule</name>
<wasmFile>src/main/resources/my.wasm</wasmFile>
<redlineExperimental>true</redlineExperimental>
</configuration>
</execution>
</executions>
</plugin>

and add a runner. This is the only dependency you need, everything else comes transitively:

<dependency>
<groupId>run.endive</groupId>
<artifactId>redline-runner-experimental</artifactId>
<version>${endive.version}</version>
</dependency>

redline-runner-experimental uses the Panama FFM API and requires Java 25 or later. On older versions use redline-runner-jffi-experimental, which needs only Java 11:

<dependency>
<groupId>run.endive</groupId>
<artifactId>redline-runner-jffi-experimental</artifactId>
<version>${endive.version}</version>
</dependency>

If both are present, the Panama runner is used wherever the JDK supports it.

Your module is then used exactly as it would be without redline:

try (var instance = MyModule.builder().build()) {
var f = instance.export("my_function");
}

Falling back

When native code cannot be used, builder() falls back to the bytecode produced by the Build Time Compiler, which is always generated alongside it. This happens on platforms outside the table above, or when no runner is on the classpath.

Your module keeps working either way, so the fallback is silent. To check which one you got:

MyModule.nativeProvider().isPresent()

It is true when native code is in use and false when the bytecode is. MyModule.safeBuilder() always uses the bytecode, which is useful for comparing the two.

nativeProvider() and imports() exist only while redline is enabled. The rest of the generated module is there either way.

What to expect

Jar size. Native code is larger than the Wasm it comes from, and by default one copy is generated per platform. If you know where you deploy, list only those targets:

<redlineTargetsExperimental>
<target>x86_64-unknown-linux-gnu</target>
</redlineTargetsExperimental>

The available triples are x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-apple-darwin, aarch64-apple-darwin, x86_64-pc-windows-msvc and aarch64-pc-windows-msvc.

Build time. Compiling for every platform takes noticeably longer than the build time compiler alone. Narrowing the target list helps here too.

Imported memories, tables and globals. Create anything you pass in through ImportValues from MyModule.imports(), not by constructing it yourself. Redline reads these through a raw address, so it can only use ones it made, and building one yourself fails with a message saying so. Modules that declare their own memory, including anything built for WASI, are unaffected.

var imports = MyModule.imports();

var memory = imports.memory(new MemoryLimits(1, 2));
var table = imports.table(new Table(ValType.FuncRef, new TableLimits(1)), REF_NULL_VALUE);
var counter = imports.global(Value.i32(0), MutabilityType.Var);

var importValues = ImportValues.builder()
.addMemory(new ImportMemory("env", "memory", memory))
.addTable(new ImportTable("env", "table", table))
.addGlobal(new ImportGlobal("env", "counter", counter))
.build();

try (var instance = MyModule.builder().withImportValues(importValues).build()) {
instance.export("my_function").apply();

// what the module wrote, on whichever backend it ran on
System.out.println(counter.getValue());
}

The factory hands back native instances on a platform in the table above and ordinary ones everywhere else, so this code is the same either way and needs no branch of its own. That matters because it is decided at runtime: one jar can run natively on one machine and on bytecode on another.

imports() exists only while redline is enabled. Without it you build these the ordinary way, so this is the one place where turning redline off means editing code.