Run WASM functions from Vert-x & Kotlin thanks to Extism

Extism?

Extism is a WASM framework that implements the WASI specification. Extism offers two types of SDKs:

  • Host SDKs for writing "host" applications (i.e. applications that can run WASM module functions) in multiple languages such as Rust, Go, C, C++, Erlang, Elixir, Haskell, .Net, Node.js, OCaml, PHP, Python, Ruby, Zig and... drumroll: Java!

  • And finally, Plugin SDKs (or PDKs) for coding WASM modules that host applications can call. The PDKs exist for Rust, Go (TinyGo), Haskell, AssemblyScript, C, and Zig.

In summary, the Extism project makes writing applications that can execute WASM functions easy. For example, we can create a Kotlin (and therefore Java) application server with Vert-x that can, thanks to Extism, load WASM modules and execute the module's functions (coded in Rust or Go, or more specifically TinyGo because it can compile to a WASI target and other languages).

GraalVM was supposed to do this (execute WASM), but for now, the WASM support seems to be limited (no implementation of the WASI specification apparently), the documentation is succinct, and no helper is offered to work around the limitations of WASM such as the problem of data types (WASM supports a limited set of data types, including 32 and 64-bit integers, 32 and 64-bit floats).

So, the Graal of Java+WASM, today it is Extism 🥰

Install Extism

First, you need to install the Extism CLI:

sudo apt-get update -y
sudo apt-get install -y pkg-config

sudo apt install python3-pip -y
pip3 install poetry
pip3 install git+https://github.com/extism/cli

echo "export EXTISM_HOME=\"\$HOME/.local\"" >> ${HOME}/.bashrc
echo "export PATH=\"\$EXTISM_HOME/bin:\$PATH\"" >> ${HOME}/.bashrc

source ${HOME}/.bashrc

extism install latest

Extism documentation: https://extism.org/docs/install

Create a TinyGolang WASM function

Creating a WASM plugin with the Extism Go PDK is straightforward:

  • Create a GoLang project

  • Create a main.go file

package main

import (
    "github.com/extism/go-pdk"
)

//export helloWorld
func helloWorld() int32 {
    input := pdk.Input()

    output := `{"message": "👋 Hello World 🌍","input": "` + string(input) + `"}`

    mem := pdk.AllocateString(output)
    // zero-copy output to host
    pdk.OutputMemory(mem)

    return 0
}

func main() {}

To build the WASM plugin, use the below command:

tinygo build -o hello-world.wasm -target wasi main.go

Create a Kotlin Vert-x project

I used the https://start.vertx.io to generate the skeleton of the project with the below command:

curl -G https://start.vertx.io/starter.zip \
-d "groupId=garden.bots" \
-d "artifactId=starter" \
-d "vertxVersion=4.3.7" \
-d "vertxDependencies=vertx-web" \
-d "language=kotlin" \
-d "jdkVersion=17" \
-d "buildTool=maven" \
--output starter.zip

unzip starter.zip

pom.xml

Once the project is generated, downloaded and unzipped, change the pom.xml file by adding the below dependency to the dependencies node:

<!-- Extism -->
<dependency>
  <groupId>org.extism.sdk</groupId>
  <artifactId>extism</artifactId>
  <version>0.1.0</version>
</dependency>
<!-- Extism -->

MainVerticle.kt

Change the source code of MainVerticle.kt like this:

package garden.bots.starter

import io.vertx.core.AbstractVerticle
import io.vertx.core.Promise

import org.extism.sdk.Context
import org.extism.sdk.manifest.Manifest
import org.extism.sdk.wasm.WasmSourceResolver
import java.nio.file.Path
import kotlin.system.exitProcess

class MainVerticle : AbstractVerticle() {

  override fun start(startPromise: Promise<Void>) {

    val httpPort = System.getenv("HTTP_PORT")
    val functionName = System.getenv("FUNCTION_NAME")

    //! Load the wasm file
    //! Get an instance of the plugin
    val resolver = WasmSourceResolver()
    val manifest = Manifest(resolver.resolve(Path.of(System.getenv("WASM_FILE"))))
    val extismCtx = Context() 
    val plugin = extismCtx.newPlugin(manifest, true)

    vertx
      .createHttpServer()
      .requestHandler { req ->

        //! Get the query parameter
        req.body().andThen({ asyncRes ->
          var name = asyncRes.result().toString()

          //! Call the wasm function
          val output = plugin.call(functionName, name)

          req.response()
          .putHeader("content-type", "application/json; charset=utf-8")
          .end(output)
        })
      }
      .listen(Integer.parseInt(httpPort)) { http ->
        when (http.succeeded()) {
          true -> {
            startPromise.complete()
            println("HTTP server started on port ${httpPort}")
          }
          false -> {
            startPromise.fail(http.cause())
          }
        }
      }
  }
}

The most important parts are:

//! Load the wasm file
//! Get an instance of the plugin
val resolver = WasmSourceResolver()
val manifest = Manifest(resolver.resolve(Path.of(System.getenv("WASM_FILE"))))
val extismCtx = Context() 
val plugin = extismCtx.newPlugin(manifest, true)

We use the Extism framework to load the WASM file, create a context and then get an instance of an Extism plugin.

Thanks to the instance of the Extism plugin, now; you can call the WASM function of the module:

//! Call the wasm function
val output = plugin.call(functionName, name)

Serve the WASM module

To build and run the Vert-x application server, use this command:

LD_LIBRARY_PATH="/usr/local/lib" \
WASM_FILE="/home/ubuntu/samples/extism-vert-x-kotlin/hello-world/hello-world.wasm" \
HTTP_PORT="8888" \
FUNCTION_NAME="helloWorld" \
./mvnw clean compile exec:java

When the HTTP server is running (and listening), you can query the WASM microservice:

curl -X POST -d 'Jane Doe' http://localhost:8888
{"message": "👋 Hello World 🌍","input": "Jane Doe"}

curl -X POST -d 'John Doe' http://localhost:8888
{"message": "👋 Hello World 🌍","input": "John Doe"}

The possibilities for using WebAssembly with Java using Extism are vast; as you can see, it is extremely easy to create a function launcher. The WASM/Java world of possibilities is expanding.