WASM, web-assembly, is a low-level stack execution engine. This library gives a JNI based binding to the Wasmtime WASM runtime.
For more information, please see github.com/bytecodealliance/wasmtime.
This is very early stages. All interfaces are subject to change. Supported platforms, x86_64 for Linux and macOS. Windows should also be supported, but currently not due to lack of resources.
Use the provided Makefile. You need Java 11 (not 17), Maven and Rustup (the error messages will prompt you to install that if you don't have it).
Ensure tools are installed:
> make init
Run tests:
> make test
Install the maven packages locally:
> make install
Maven co-ordinates for the installed artifacts: net.bluejekyll:wasmtime-java:1.0-SNAPSHOT
.
Initialize the WASM runtime. Much of this is not threadsafe, so be aware...
// Initiale the Wasmtime JNI bindings (this will happen once per ClassLoader)
Wasmtime wasmtime = new Wasmtime();
// Get a new Engine (one per thread)
WasmEngine engine = wasmtime.newWasmEngine();
// Compile The module, this can be reused, i.e. is cacheable for multiple executions
WasmModule module = engine.newModule(${PATH_TO_MODULE});
Once the runtime is initialized, create a new instance. This requires a linker, and exported functions if the module has imports that need to be met.
// create a new store and linker
WasmStore store = engine.newStore();
WasmLinker linker = store.newLinker();
// This will link in exports from Java to the WASM module, if that module has imports, see below
linker.defineFunctions(this.store, new TestExport());
To export functions into WASM, that are bound to the WASM module's imports, a class must implement the WasmExportable
interface. Each method to export and be bound to functions the corresponding function in WASM must be annotated with WasmExport
.
See TestExport
for example:
// Specify the module name that we need to export functions into (imports in the WASM)
@WasmModule(name = "test")
public class TestExport implements WasmExportable {
// Export the function to WASM and give the name that corresponds to the import in WASM
@WasmExport(name = "reverse_bytes_java")
public final byte[] reverseBytesJava(ByteBuffer buffer) {
ByteBuffer toReverse = buffer.duplicate();
byte[] bytes = new byte[toReverse.remaining()];
for (int i = bytes.length - 1; i >= 0; i--) {
bytes[i] = toReverse.get();
}
return bytes;
}
}
After Wasmtime is initiated and the module compiled, then it can be linked and an instance created:
linker.defineFunctions(this.store, new TestExport());
WasmInstance instance = linker.instantiate(module);
WasmImportProxy
To work with the Proxy builder, an interface
must extend the WasmImportable
interface. It should be annotated to specify the WASM module being bound to.
See TestImportProxy
for example:
// This annotation allows for the WASM module name to be specified
@WasmModule(name = "test")
public interface TestImportProxy extends WasmImportable {
// This annotation allows for the function name to be set as it is defined in WASM
@WasmImport(name = "add_i32")
int addInteger(int a, int b);
}
This can now be used with WasmImportProxy
to create a proxy that will call into the specified functions in WASM, see ImportTests
:
WasmInstance instance = linker.instantiate(this.module);
WasmImportProxy importProxy = new WasmImportProxy(instance);
TestImportProxy proxy = importProxy.newWasmProxy(TestImportProxy.class);
int ret = proxy.addInteger(3, 2);
The Java is meant to be as minimal as possible. All Wasmtime object references are stored in Java objects as opaque pointers (longs). The safety in this area has not yet been proven. Thread safety in particular is non-existent. The WasmEngine should be safe to share across threads, though we should most likely introduce a custom clone method for this purpose.
The Java compiler will automatically output the headers for the JNI bindings based on the native methods defined in the various classes. While the headers generated by the Java compiler aren't directly used during the Rust JNI compilation, they are useful for seeing the C signature that the Rust needs to export. The files can be found in target/generated-sources/*.h
.
The tests should all run regardless of platform. Windows hasn't been fully tested due to lack of resources. If tests fail to run, there are a few different environments at play which will make discovering which component failed and why difficult. At the moment, all output from Java is captured in txt
files at in the target/surfire-reports/{CLASS_NAME}-output.txt
. All output from the JNI bindings is currently captured in target/wasm-logs/java_{DATE}.log
, this may be combined into the same place in the future. In the pom.xml
Maven project file, the surfire
test configuration has RUST_LOG
set to debug
by default. This can be set to any other value to increase or decrease logging output.
WASI is supported for things like printing to stdout. This is supplied during linking in the Java bindings. It is not required, in Rust this can be targeted with cargo build --target wasm32-wasi
, that target must be installed with rustup
before hand.