Skip to main content

GraalWasm Integration

This integration allows you to run WebAssembly binaries that use either WASI Preview 1 or Emscripten functions on the JVM with GraalWasm.

Requirements

  • JVM JDK 23+ (it may also work on JDK21+)
  • GraalVM SDK Polyglot API 24.1.X

The current implementation heavily relies on internal GraalWasm APIs, making it compatible only with the GraalVM SDK Polyglot API 24.1.X for JDK23.

WASI Preview 1 Bindings Integration

Check WASI Preview 1 to see the limitations of the WASI P1 implementation.

Installation

Add the required dependencies:

dependencies {
implementation("at.released.weh:bindings-graalvm241-wasip1:0.1")
implementation("org.graalvm.polyglot:polyglot:24.1.1")
implementation("org.graalvm.polyglot:wasm:24.1.1")
}

Usage

Below is an example demonstrating the execution of helloworld.wasm, build using Emscripten with the STANDALONE_WASM flag.

import at.released.weh.bindings.graalvm241.wasip1.GraalvmWasiPreview1Builder
import at.released.weh.host.EmbedderHost
import org.graalvm.polyglot.Context
import org.graalvm.polyglot.PolyglotException
import org.graalvm.polyglot.Source

internal object App

const val HELLO_WORLD_MODULE_NAME: String = "helloworld"

fun main() {
// Create Host and run code
EmbedderHost {
fileSystem {
addPreopenedDirectory(".", "/data")
}
}.use(::executeCode)
}

private fun executeCode(embedderHost: EmbedderHost) {
// Prepare Source
val source = Source.newBuilder("wasm", App::class.java.getResource("helloworld_wasi.wasm"))
.name(HELLO_WORLD_MODULE_NAME)
.build()

// Setup Polyglot Context
val context: Context = Context.newBuilder().build()
context.use {
// Context must be initialized before installing modules
context.initialize("wasm")

// Setup WASI Preview 1 module
GraalvmWasiPreview1Builder {
host = embedderHost
}.build(context)

// Evaluate the WebAssembly module
context.eval(source)

// Run code
val startFunction = context.getBindings("wasm").getMember(HELLO_WORLD_MODULE_NAME).getMember("_start")

try {
startFunction.execute()
} catch (re: PolyglotException) {
if (re.message?.startsWith("Program exited with status code") == false) {
throw re
} else {
// Handle exit code
}
Unit
}
}
}

GraalVM's Built-in WASI Preview 1 Functions

It is worth noting that GraalWasm provides its own implementation of the WASI Preview 1 interfaces.
In many cases, it may be a more suitable choice rather than this library.

To use it, you should add the wasm.Builtins option with the value wasi_snapshot_preview1.

Below is an example of running helloworld.wasm using the built-in implementation:

import org.graalvm.polyglot.Context
import org.graalvm.polyglot.Source

const val HELLO_WORLD_MODULE_NAME: String = "helloworld"

fun main() {
val source = Source.newBuilder("wasm", App::class.java.getResource("helloworld.wasm"))
.name(HELLO_WORLD_MODULE_NAME)
.build()
val context: Context = Context.newBuilder()
.option("wasm.Builtins", "wasi_snapshot_preview1")
.build()
context.use {
context.eval(source)
run(context)
}
}

See also example in documentation: GraalVM: Running WebAssembly Embedded in Java

Emscripten bindings integration

Installation

Add the required dependencies:

dependencies {
implementation("at.released.weh:bindings-graalvm241-emscripten:0.1")
implementation("org.graalvm.polyglot:polyglot:24.1.1")
implementation("org.graalvm.polyglot:wasm:24.1.1")
}

Usage

Below is an example demonstrating the execution of helloworld.wasm, prepared in the "Emscripten Example".

import at.released.weh.bindings.graalvm241.GraalvmHostFunctionInstaller
import at.released.weh.host.EmbedderHost
import org.graalvm.polyglot.Context
import org.graalvm.polyglot.Source

internal object App

const val HELLO_WORLD_MODULE_NAME: String = "helloworld"

fun main() {
// Create Host and run code
EmbedderHost {
fileSystem {
unrestricted = true
}
}.use(::executeCode)
}

private fun executeCode(embedderHost: EmbedderHost) {
// Prepare Source
val source = Source.newBuilder("wasm", App::class.java.getResource("helloworld.wasm"))
.name(HELLO_WORLD_MODULE_NAME)
.build()

// Setup Polyglot Context
val context: Context = Context.newBuilder().build()
context.use {
// Context must be initialized before installing modules
context.initialize("wasm")

// Setup modules
val installer = GraalvmHostFunctionInstaller(context) {
host = embedderHost
}
installer.setupWasiPreview1Module()
val emscriptenInstaller = installer.setupEmscriptenFunctions()

// Evaluate the WebAssembly module
context.eval(source)

// Finish initialization after module instantiation
emscriptenInstaller.finalize(HELLO_WORLD_MODULE_NAME).use { emscriptenEnv ->
// Initialize Emscripten runtime environment
emscriptenEnv.emscriptenRuntime.initMainThread()
run(context)
}
}
}

private fun run(
context: Context,
) {
val mainFunction = context.getBindings("wasm").getMember(HELLO_WORLD_MODULE_NAME).getMember("main")
mainFunction.execute(
/* argc */ 0,
/* argv */ 0,
).asInt()
}

Other samples

You can also check out samples in the repository:

Runtime optimizations

By default, GraalVM executes code in interpreter mode, which can be slow, However, it offers runtime optimizations to improve performance. For more details, check this link: GraalVM: Enable Optimization on OpenJDK and Oracle JDK.

You can use Gradle toolchains to run your application on the GraalVM JVM with optimizations enabled:

java {
toolchain {
languageVersion = JavaLanguageVersion.of(23)
vendor = JvmVendorSpec.GRAAL_VM
}
}

If you need to run optimized code on OpenJDK or other non-GraalVM JDKs, you'll need to activate JVM Compiler Interface (JVMCI) using the -XX:+EnableJVMCI option and add the GraalVM compiler to the --upgrade-module-path classpath.

This can be tricky to set up with Gradle. Additionally, the GraalVM version used in the project requires JDK 22 or later to run GraalVM Compiler. For an example of how to enable the GraalVM compiler, take a look at this gist.

Other optimizations

To speed up initialization, you can reuse a single instance of the GraalVM Engine across multiple instances of Context. Check this link for more information: GraalVM: Managing the Code Cache