Skip to content

Enable support for the WebAssembly exception handling proposal#942

Open
mjrusso wants to merge 1 commit into
tessi:mainfrom
mjrusso:wasm-exceptions
Open

Enable support for the WebAssembly exception handling proposal#942
mjrusso wants to merge 1 commit into
tessi:mainfrom
mjrusso:wasm-exceptions

Conversation

@mjrusso
Copy link
Copy Markdown

@mjrusso mjrusso commented Jan 10, 2026

This PR adds an option to enable the WebAssembly exception handling proposal (Phase 4, Wasm 3.0 with exnref) via a new wasm_exceptions option in EngineConfig. This defaults to being disabled, which matches the status quo.

Add support for the WebAssembly exception handling proposal (Phase 4,
Wasm 3.0 with exnref) via a new `wasm_exceptions` option in EngineConfig.

This enables running WASM modules that use setjmp/longjmp emulation, for
example modules compiled using Emscripten with the following flags:

```
-s SUPPORT_LONGJMP=wasm -s WASM_LEGACY_EXCEPTIONS=0
```
@mjrusso
Copy link
Copy Markdown
Author

mjrusso commented Jan 10, 2026

(A bit of context: I'm working on automating WASM builds of Micro QuickJS. The most sane way to get this to work seems to be to use setjmp/longjmp emulation, building with Emscripten using the -s SUPPORT_LONGJMP=wasm -s WASM_LEGACY_EXCEPTIONS=0 flags.)

@mjrusso
Copy link
Copy Markdown
Author

mjrusso commented Jan 15, 2026

(A bit of context: I'm working on automating WASM builds of Micro QuickJS. The most sane way to get this to work seems to be to use setjmp/longjmp emulation, building with Emscripten using the -s SUPPORT_LONGJMP=wasm -s WASM_LEGACY_EXCEPTIONS=0 flags.)

If anyone wants to test out mquickjs, automated builds are available at https://github.com/mjrusso/mquickjs-wasm. Download the latest release and run this single-file Elixir script (passing the path to the the WASM build you downloaded as an argument):

#!/usr/bin/env elixir

Mix.install(
  [
    {:wasmex, github: "mjrusso/wasmex", branch: "wasm-exceptions"},
    {:rustler, ">= 0.0.0", optional: true}
  ]
)

defmodule MQuickJSSandbox do
  defstruct [:pid, :store, :memory]

  def new(wasm_path) do
    wasm_bytes = File.read!(wasm_path)

    {:ok, engine} = Wasmex.Engine.new(%Wasmex.EngineConfig{wasm_exceptions: true})
    {:ok, store} = Wasmex.Store.new_wasi(%Wasmex.Wasi.WasiOptions{}, nil, engine)
    {:ok, module} = Wasmex.Module.compile(store, wasm_bytes)
    {:ok, pid} = Wasmex.start_link(%{store: store, module: module})
    {:ok, store} = Wasmex.store(pid)
    {:ok, memory} = Wasmex.memory(pid)

    %__MODULE__{pid: pid, store: store, memory: memory}
  end

  def init(sandbox, mem_size \\ 1024 * 1024) do
    {:ok, [result]} = Wasmex.call_function(sandbox.pid, "sandbox_init", [mem_size])
    result == 1
  end

  def eval(sandbox, js_code) do
    code_bytes = js_code <> <<0>>
    code_len = byte_size(code_bytes)

    {:ok, [code_ptr]} = Wasmex.call_function(sandbox.pid, "malloc", [code_len])
    {:ok, store} = Wasmex.store(sandbox.pid)

    Wasmex.Memory.write_binary(store, sandbox.memory, code_ptr, code_bytes)
    {:ok, [result_ptr]} = Wasmex.call_function(sandbox.pid, "sandbox_eval", [code_ptr])

    {:ok, store} = Wasmex.store(sandbox.pid)

    {success, result} =
      if result_ptr == 0 do
        {:ok, [error_ptr]} = Wasmex.call_function(sandbox.pid, "sandbox_get_error", [])
        {:ok, store} = Wasmex.store(sandbox.pid)
        {false, read_string(store, sandbox.memory, error_ptr)}
      else
        {true, read_string(store, sandbox.memory, result_ptr)}
      end

    Wasmex.call_function(sandbox.pid, "free", [code_ptr])

    {success, result}
  end

  def free(sandbox) do
    Wasmex.call_function(sandbox.pid, "sandbox_free", [])
  end

  defp read_string(store, memory, ptr) do
    read_string_loop(store, memory, ptr, 0, [])
  end

  defp read_string_loop(store, memory, ptr, offset, acc) do
    byte = Wasmex.Memory.get_byte(store, memory, ptr + offset)

    if byte == 0 do
      acc |> Enum.reverse() |> IO.iodata_to_binary()
    else
      read_string_loop(store, memory, ptr, offset + 1, [byte | acc])
    end
  end
end

case System.argv() do
  [wasm_path] ->
    IO.puts("Loading: #{wasm_path}")

    sandbox = MQuickJSSandbox.new(wasm_path)
    MQuickJSSandbox.init(sandbox)

    IO.puts("\n--- System time via Date.now() ---")

    case MQuickJSSandbox.eval(sandbox, "Date.now()") do
      {true, result} ->
        ts = String.to_integer(result)
        IO.puts("Timestamp: #{ts}")
        dt = DateTime.from_unix!(div(ts, 1000))
        IO.puts("Date/time: #{dt}")

      {false, error} ->
        IO.puts("Error: #{error}")
    end

    IO.puts("\n--- Random numbers via Math.random() ---")

    for _ <- 1..3 do
      {true, result} = MQuickJSSandbox.eval(sandbox, "Math.random()")
      IO.puts("  #{result}")
    end

    MQuickJSSandbox.free(sandbox)
    IO.puts("\nDone!")

  _ ->
    IO.puts("Usage: elixir demo_wasi.exs <path-to-wasi-wasm>")
    System.halt(1)
end

Sample output:

$ elixir demo_wasi.exs ../result/lib/mquickjs_wasi.wasm
Loading: ../result/lib/mquickjs_wasi.wasm

--- System time via Date.now() ---
Timestamp: 1768440747657
Date/time: 2026-01-15 01:32:27Z

--- Random numbers via Math.random() ---
  0.17455423595901598
  0.10950832305733194
  0.78521201730975432

Done!

This should compile the wasmex NIF from source, provided that you have a Rust toolchain installed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant