LiveState meets WebAssembly (thanks to Extism)
1 November 2023

LiveState meets WebAssembly (thanks to Extism)

LiveState is a solution for building embedded web applications. To come up to speed on what it is and how it works, check out our prior blog posts. I’ve had it in my mind for awhile that it might be really interesting to allow for writing event handlers in multiple programming languages (in addition to Elixir). WebAssembly is a language independent technology for executing code that is garnering wide support as a compilation target. This makes it a great tool for adding additional language support to LiveState. In this post, I’ll do a deep dive into a successful experiment to support WebAssembly event handlers using a project called Extism

In general, event handlers in LiveState are functions that take as arguments an event, a current state, and return a new state. An event handler function, generically, looks like this:

(event, current_state) -> new state

Right now, event handler functions can only be written in Elixir. My first thought for languages I’d like to support was Javascript, due it’s ubiquity. I did some experiments with calling Node and Deno from Elixir, but nothing seemed really practical. And then last week, something truly fascinating showed up in my rss feed: this post about how Extism is starting to make calling into WebAssembly modules a practical idea. The advantage of this approach is that supporting event handler functions as WebAssembly modules would mean that any language that can compile to WebAssembly (the list is long and growing!) would be an option for use with LiveState.

If you haven’t been tracking WebAssembly, it started as a language and architecture independent machine instruction format for a simple stack based virtual machine. It was developed originally to target the web browser, and allow devs to have a choice of programming language for building in-browser apps. As it matured, a lot of people started to see it as a compelling way to build secure, portable server applications as well. If you’re a Geek Geezer like I am, it seems obvious that this is an almost exact repeat of Java, but because WebAssembly is well supported by a standards body rather than owned by a single company (and for other technical reasons) I am very bullish on its bright future.

The WebAssembly space is incredibly exciting right now. It started, rightly so IMO, as a fairly low-level spec. Until recently, integrating with WebAssembly modules meant dealing with pointers and memory allocation if you were trying to pass in or return anything other than numeric values. But recent developments have overcome this limitation. Extism is a project that is practical to use now, and longer term WebAssembly Components will be a more standards based approach.

Rewriting a LiveState todo list

For our example, we’ll convert the backend portion of the todo list in the LiveState testbed project that runs the integration tests for LiveState. For context, the front end consists of a <todo-list> element and a <todo-form> element. With some configuration and details elided, they look like this:

export class TodoListElement extends LitElement {
  /**
   * Copy for the read the docs hint.
   */
  @property()
  todos: Array<string> | undefined;
 
  render() {
    return html`
      <div>
        This is my todo list
        <ul>
          ${this.todos?.map(todo => html`<li>${todo}</li>`)}
        </ul>
      </div>
    `
  }
}
export class TodoFormElement extends LitElement {

  @query("input[name='todo']")
  todoInput: HTMLInputElement | undefined;

  render() {
    return html`
      <div>
        <input name="todo" />
        <button @click=${this.addTodo}>Add Todo</button>
      </div>
    `
  }

  addTodo(_event : Event) {
    this.sending = true;
    this.dispatchEvent(new CustomEvent('addTodo', {detail: {todo: this.todoInput!.value}}));
    this.todoInput!.value = '';
  }
}

The <todo-list> renders the todo items, and the <todo-form> component dispatches an addTodo event when a todo is added. LiveState takes care of sending the events and receiving state updates to and from the server. The server side code for a LiveState application is a Phoenix Channel, and it looks like this:

defmodule LivestateTestbedWeb.TodoChannel do
  use LiveState.Channel, web_module: LivestateTestbedWeb

  def init(_channel, _payload, _socket) do
    {:ok, %{todos: []}}
  end

  def handle_event("addTodo", %{"todo" => todo}, %{todos: todos}) do
    {:noreply, %{todos: todos ++ [todo]}}
  end
end

We start with an empty list as our initial state, and build a new state with an item added every time we get an addTodo event.

To the problem at hand

How can we write the LiveState server code as a WebAssembly module? To do so, we have to discuss one more topic:

Guests and hosts

For our scenario, we want to call Javascript that has been compiled into a WebAssembly module from Elixir. This makes Javascript the “guest” language and Elixir the “host” language. This communcation between guests and hosts is the the primary problem that Extism aims to simplify. Existism does this by providing SDKs (software development kits) for host languages and PDKs (platform development kits) for guest languages. Fortunately for us, they have a working Elixir SDK and Javascript PDK. To be clear, these are pre-1.0 products, so things may change in the future but they have been plenty good enough to get our example to work. Let’s get to it!

LiveState meets Extism.

In order to implement our LiveState server code in a WebAssembly module, we can use the Extism Elixir SDK. Extism works by passing in and receiving strings. A bit limited, but a giant improvement over the previous limitations of integrating with WASM. We can serialize/deserialize our data as JSON to make things work. We’ll just need to have our channel call into our WASM module in both the init and handle_event callbacks. Here’s what that looks like:

defmodule LivestateTestbedWeb.TodoWasmChannel do
  use LiveState.Channel, web_module: LivestateTestbedWeb

  def init(_channel, _payload, socket) do
    manifest = %{wasm: [ %{ path: "priv/wasm/dist/plugin.wasm" } ] }
    {:ok, plugin} = Extism.Plugin.new(manifest, true)
    {:ok, initial_state} = Extism.Plugin.call(plugin, "init", "")
    {:ok, Jason.decode!(initial_state), socket |> assign(:plugin, plugin)}
  end

  def handle_event(event, payload, state, %{assigns: %{plugin: plugin}} = socket) do
    {:ok, new_state} = Extism.Plugin.call(plugin, event, Jason.encode!([payload, state]))
    {:noreply, Jason.decode!(new_state), socket}
  end
end

In init, the first thing we do is load our WebAssembly module using Existism. The manifest just points to our compiled wasm code on the filesystem, and Extism.Plugin.new builds a reference to it. I’m a little sketchy on the meaning of the second boolean arg, but false didn’t work :)

Finally, we call the init function in our wasm module. We don’t need to give it any data, so we just pass it an empty string. Notice there’s no way to specify a contract between us and the WASM code just yet. This is the problem that WebAssembly Components aim to address: more about that in a future installment. But for now, it’s on us to make sure we pass the expected data structures in and out. Once we have the data back from init, we parse the JSON and return it as the initial state for our LiveState channel. We also add the plugin (which is reference to our WASM module) to the socket so we don’t need to reload it later.

Our handle_event function is simpler. We make an assumption (again, no contract just yet) that there exists in our WASM module a function with the same name as our event. We call it and pass it a JSON serialized list of our normal handle_event arguments: the event payload and the current state. We then parse the return value from the WASM function and return it as the new state.

To the Javascripts…

Now let’s see how we can create a WebAssembly module for our elixir code to call. We’ll make use of the Extism Javascript PDK to help us out. The PDK only supports the CommonJS module format right now, and I greatly prefer the more modern ESM module syntax. Fortunately, they have instructions for using esbuild to do a transpile step before it hands off to the extism-js command line tool that builds our WASM file. Rather than go into all the details of setting up your package.json and esbuild config I’ll refer you to the repo. It is basically all copy pastad from the Extism instructions.

Finally, we are ready to look at some Javascript code! The Extism PDK provides two functions we need to send and receive data from WebAssembly: Host.inputString() and Host.outputString(string). Just like we do in the Elixir code, we’ll need to serialize and deserialize the JSON ourselves. Because I wanted to hide these details and keep my event handling and state intialization functions simple, I wrote a wrapper function to take care of the Extism stuff:

export function wrap(f) {
  return function() {
    const inputStr = Host.inputString();
    let output
    if (inputStr != "") {
      const args = JSON.parse(inputStr);
      output = f(...args);
    } else {
      output = f();
    }
    Host.outputString(JSON.stringify(output));  
  }
}

This gets passed a function, which it calls with the deserialized JSON coming in (if there is any), and then serializes the return value from the function and sends it back. All this lets us finally write a couple pretty simple functions that correspond to our original Elixir TodoChannel code:


import { wrap } from "./wrap";

export const init = wrap(function() {
  return { todos: ["Hello", "WASM"]};
});

export const addTodo = wrap(function({ todo }, { todos }) {
  return { todos: [`${todo} from WASM!`, ...todos]};
});

Putting it all together

To see all the pieces put together, you can check out the extism branch of the livestate testbed project. In addition to the normal instructions for running a Phoenix app, you’ll need to do a couple more steps to build the WebAssembly module. Go into the priv/wasm directory and do:

npm install
npm run build

You should now have the Javascript in priv/wasm/src/index.js compiled to priv/wasm/dist/plugin.wasm. Now, you should be able to run the main phoenix app and hit http://localhost:4000. If everything works, you should see something like this:

Where to go from here?

For me, the possibilities this opens up are pretty exciting. We’ve been working on a product built on top of LiveState called Launch Elements. Being able to extend and customize the behaviour of these elements with WebAssembly is a pretty compelling idea. One of the next things I’ll be exploring is using a WebAssemply module to handle a form submission from our <launch-form> element. Expect to see more in a future installment.

And don’t sleep on WebAssembly Components

The example we’ve shown with Extism is great, but WebAssembly Components will give us a way to specify the interface (including type information) between host and guest. It’s hard for me to not go hyperbolic on how much this would positively impact the software development landscape. Imagine a world where you leverage any library from any language in any other language. This is the promise of WebAssembly Components and why I’m so excited. My next goal is to build a working WebAssembly Component example to show (and prove to myself!) the viablity of all this. Stay tuned :)

Related Posts

Want to learn more about the work we do?

Explore our work

Ready to start your software journey with us?

Contact Us