Skip to main content

Announcing Chicory 1.0.0-M1: First Milestone Release

· 8 min read
Edoardo Vacchi
Staff Software Engineer @ Dylibso

It's been a while since our pure-Java WebAssembly runtime released version 0.0.12, so you might say a new release was almost overdue.

In the previous months we concentrated on correctness and spec-compliance of our WebAssembly runtime; now it is time to harden the codebase and make it a solid platform to build upon.

This release is somewhat special:

  • first of all, you will notice we have a brand-new website. We are continuously working on improving the documentation to make it a convenient and detailed resource to get started
  • but also, this release marks the beginning of our path toward a final 1.0 version.

The reason why this release took some time is we wanted to get many things right. In this release:

Let's take a look in detail.

Public API

In the name of simplicity and readability (but also encapsulation), we refactored a lot of code, so we made some important changes to our public API. You will find all the updated instructions on the docs.

There is now a clearer boundary between Parser, Module and Instance:

var module = Parser.parse("path/to/your.wasm");
var instance = Instance.builder(module).build();

We have also introduced a new unified mechanism to define host functions and instantiate collections of related modules together, all at once. We call this utility the Store:

// instantiate the store
var store = new Store();
// registers a host function in the store
store.addFunction(myFunction);
// create a named `instance` with name `logger`
var module = Parser.parse(new File("logger.wasm"));
var instance = store.instantiate("logger", module);

You can read more about the Store in the docs.

Host Functions

We changed the signature for HostFunction. This is probably the biggest surface change you will notice. Not only has the order of the arguments changed, but also their types:

// defines `console.log`
var func = new HostFunction(
"console",
"log",
List.of(ValueType.I32, ValueType.I32),
List.of(),
(Instance instance, long... args) -> {
var len = (int) args[0];
var offset = (int) args[1];
var message = instance.memory().readString(offset, len);
println(message);
return new long[0];
});

Specifically, you will notice that function arguments and return values are all now arrays of primitive longs. This coincided with a huge refactoring/rewrite of the internals of our engine.

Such long values are treated as raw bits, and should be usually converted back and forth to the appropriate types by downcasting or using the public methods of the Value class

For instance, in the example above, we know it is safe to assume that those longs were originally widened integers.

We have however introduced a higher-level abstraction for convenience.

Generated Host Modules

The new HostFunction API gives you raw access to the arguments of a function as they are represented in the Wasm runtime, but it can be inconvenient to use. Thus, we are now also providing an annotation processor to express HostFunctions in a more ergonomic way.

This feature requires you to depend on the experimental annotation processor:

<dependency>
<groupId>com.dylibso.chicory</groupId>
<artifactId>host-module-annotations-experimental</artifactId>
</dependency>

You can now annotate a class as @HostModule("your-wasm-module-name") and export instance members of such a class by annotating them as @WasmExport.

They will generate a HostFunction called wasm_module_name.your_function_name.

For instance, the following generates a HostFunction called wasi_snapshot_preview1.fd_close:

@HostModule("wasi_snapshot_preview1")
public final class WasiPreview1 {
...
@WasmExport
public int fdClose(int fd) {
...
}
}

As you might have noticed from the example above, we are now using this feature ourselves to implement the WASI host functions.

The annotation processor generates a companion <YourClassName>_ModuleFactory, with a static member <YourClassName>_ModuleFactory#toHostFunctions(<YourClassName>).

This static member returns an array of HostFunctions that perform the conversion between raw longs to the type signature you have declared.

You can expose this member in the most convenient way for your end users.

For instance in this case:

@HostModule("wasi_snapshot_preview1")
public final class WasiPreview1 {
...

public HostFunction[] toHostFunctions() {
return WasiPreview1_ModuleFactory.toHostFunctions(this);
}
...
}

Supported Types

Supported input and output types for @WasmExport are primitives int, long, float, double, which correspond to WASM types I32, I64, F32, F64.

We also support java.lang.String arguments, provided that they are only passed as an input value, and they are annotated. We support two annotations:

  • @Buffer: the code generator translates the String value to a pair of integers:
    • the first is the index of (or pointer to) the start of the string UTF-8 bytes in the linear memory
    • the second is the length of the string in bytes
  • @CString: the code generator translates the String into an index (pointer to) the beginning of a null-terminated string of UTF-8 bytes

For instance:

@WasmExport
public int pathFilestatSetTimes(
int fd,
int lookupFlags,
@Buffer String rawPath,
long accessTime,
long modifiedTime,
int fstFlags) {
...
}

is translated to the host function wasi_snapshot_preview1.path_filestat_set_times with a signature equivalent to:

public long[] path_filestat_set_times(
long fd // ValueType.I32
long lookup_flags // ValueType.I32
long raw_path_ptr // ValueType.I32 => string pointer
long raw_path_size // ValueType.I32 => string length
long access_time // ValueType.I64
long modified_time // ValueType.I64
long fst_flags // ValueType.I32
)

You can additionally accept com.dylibso.chicory.runtime.Instance and com.dylibso.chicory.runtime.Memory as input arguments; for example:

@WasmExport
public int fdFdstatGet(Memory memory, int fd, int buf) {
...
}

Experimental Modules

We are now clearly marking some modules as experimental: while we might still need to introduce a few breaking changes here and there before the final 1.0 release throughout the codebase, we explicitly call a module experimental if it is still under development and might be reworked in the near future.

You might notice that some old modules might have changed their name: for instance, aot is now called aot-experimental and the package name has been renamed from com.dylibso.chicory.aot to com.dylibso.chicory.experimental.aot

Nonetheless, we invite you to try them and welcome any feedback.

Ahead of Time Translation

Chicory has been initially developed as a Java interpreter for WebAssembly; but it has soon acquired a bytecode translator from Wasm bytecode to Java bytecode. This is what we call the Ahead-of-Time backend. The Ahead-of-Time backend is a drop-in replacement for the interpreter, and it passes 100% of the same spec tests that the interpreter already supports.

You can instantiate a module by explicitly providing a MachineFactory. The default Machine implementation is the InterpreterMachine. You can opt in to the AoT mode by writing:

var module = Parser.parse("path/to/your.wasm");
var instance = Instance.builder(module)
.withMachineFactory(AotMachine::new)
.build();

You must add the dependency for AoT:

<dependency>
<groupId>com.dylibso.chicory</groupId>
<artifactId>aot-experimental</artifactId>
</dependency>

This will translate every module you instantiate into Java bytecode on-the-fly and in-memory. The resulting code is usually expected to evaluate faster and consume less memory. This was already present in our last release.

With 1.0.0-M1 we are also introducing a new mode of execution: build-time code generation. This mode of execution reduces startup time and will remove the need for distributing the original Wasm binary.

  • this improves startup time because the translation occurs only once, when you are packaging your application
  • it virtually allows you to 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
  • it still maintains the same performance properties as the in-memory compiler (in fact, the compilation backend is virtually the same)

The translation is performed during your build using a Maven plug-in.

Build-Time Ahead-of-Time Translation

Note: This mode of execution requires configuring a Maven plug-in.

An example can be found in the Chicory codebase. The WABT suite of tools allows manipulating Wasm bytecode in a number of ways. For instance, the wat2wasm tool takes WAT source files (written in "WebAssembly Text Format") and translates them into Wasm.

The WABT tool set is written in C++, and the tools can be compiled to Wasm themselves. So, in the wabt submodule in the Chicory codebase, we translate the Wasm version of wat2wasm into Java bytecode so that we can invoke it without calling a native executable.

<build>
<plugins>
<plugin>
<groupId>com.dylibso.chicory</groupId>
<artifactId>aot-maven-plugin-experimental</artifactId>
<executions>
<execution>
<id>wat2wasm</id>
<goals>
<goal>wasm-aot-gen</goal>
</goals>
<configuration>
<!-- Translate the Wasm binary `wat2wasm` into bytecode -->
<wasmFile>src/main/resources/wat2wasm</wasmFile>
<!-- Generate the following class file as a result -->
<name>com.dylibso.chicory.wabt.Wat2WasmModule</name>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

Refer to the wabt module in the Chicory codebase to see how we expose the wat2wasm tool as a library.

WebAssembly System Interface Improvements

Besides using the new Host Module annotation processor, we introduced many more WASI functions. This improves the compatibility of our engine with the output of the majority of compilers targeting wasip1; at this point, the most used functions should be all supported. Refer to the WASI docs for details.

Conclusion

Chicory has come a long way since its first release, and we have also to thank all of our wonderful contributors: without you we would not be where we are today!

There is still some way to go before our final release, but we are already looking forward to what you will be building. Check out Chicory 1.0.0-M1 right now and let us know what you think!