Announcing Chicory 1.0.0-M1: First Milestone Release
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:
- we updated the public API, including a new way to write host functions
- some modules are now marked as experimental
- we are now more confident in the Ahead-of-Time translator:
this translator is a drop-in replacement for the interpreter, and is 100% compatible
with the specs that we support in interpreter mode (
aot
is currently marked as experimental). - we improved our WASI implementation
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 long
s.
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 HostFunction
s
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 HostFunction
s 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 theString
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 theString
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)
.withMatchineFactory(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!