defmodule QuickBEAM.WASM do @moduledoc """ WebAssembly runtime for the BEAM. Compile `.wasm` binaries and run them as supervised WASM instances, or use `disasm/1` to decode them into structured Elixir data. ## Quick start wasm = File.read!("add.wasm") {:ok, pid} = QuickBEAM.WASM.start(module: wasm) {:ok, 42} = QuickBEAM.WASM.call(pid, "add", [40, 2]) QuickBEAM.WASM.stop(pid) ## Supervision children = [ {QuickBEAM.WASM, name: :renderer, module: File.read!("priv/wasm/md.wasm")} ] Supervisor.start_link(children, strategy: :one_for_one) QuickBEAM.WASM.call(:renderer, "render", [...]) ## Disassembly {:ok, mod} = QuickBEAM.WASM.disasm(wasm_bytes) hd(mod.functions).opcodes # [{0, :local_get, 0}, {2, :local_get, 1}, {4, :i32_add}, {5, :end}] ## Options * `:module` — WASM binary (required) * `:name` — GenServer name registration * `:stack_size` — execution stack in bytes (default: 65536) * `:heap_size` — auxiliary heap in bytes (default: 65536) """ @type instance :: GenServer.server() alias QuickBEAM.WASM.{Module, Parser} @doc false def child_spec(opts) do id = Keyword.get(opts, :id, Keyword.get(opts, :name, __MODULE__)) %{id: id, start: {__MODULE__, :start_link, [opts]}} end @doc """ Start a WASM instance. {:ok, pid} = QuickBEAM.WASM.start(module: wasm_bytes) {:ok, 42} = QuickBEAM.WASM.call(pid, "add", [40, 2]) """ alias QuickBEAM.WASM.Server @spec start(keyword()) :: GenServer.on_start() def start(opts) do Server.start_link(opts) end @doc """ Start a WASM instance linked to the calling process. Same as `start/1` — the instance is always linked. Use in supervision trees via `child_spec/1`. """ @spec start_link(keyword()) :: GenServer.on_start() def start_link(opts) do Server.start_link(opts) end @doc """ Call an exported WASM function by name. {:ok, 42} = QuickBEAM.WASM.call(pid, "add", [40, 2]) """ @spec call(instance(), String.t(), [number() | integer()]) :: {:ok, term()} | {:error, String.t()} def call(instance, func_name, params \\ []) do GenServer.call(instance, {:call, func_name, params}, :infinity) end @doc "Stop a WASM instance." @spec stop(instance()) :: :ok def stop(instance) do GenServer.stop(instance) end @doc """ Get the current memory size of a WASM instance in bytes. """ @spec memory_size(instance()) :: {:ok, non_neg_integer()} def memory_size(instance) do GenServer.call(instance, :memory_size, :infinity) end @doc """ Grow the memory of a WASM instance by `delta` pages (64KB each). """ @spec memory_grow(instance(), non_neg_integer()) :: {:ok, non_neg_integer()} | {:error, String.t()} def memory_grow(instance, delta) do GenServer.call(instance, {:memory_grow, delta}, :infinity) end @doc """ Read bytes from a WASM instance's linear memory. """ @spec read_memory(instance(), non_neg_integer(), non_neg_integer()) :: {:ok, binary()} | {:error, String.t()} def read_memory(instance, offset, length) do GenServer.call(instance, {:read_memory, offset, length}, :infinity) end @doc """ Write bytes to a WASM instance's linear memory. """ @spec write_memory(instance(), non_neg_integer(), binary()) :: :ok | {:error, String.t()} def write_memory(instance, offset, data) do GenServer.call(instance, {:write_memory, offset, data}, :infinity) end @doc false @spec compile(binary()) :: {:ok, reference()} | {:error, String.t()} def compile(wasm_bytes) when is_binary(wasm_bytes) do QuickBEAM.Native.wasm_compile(wasm_bytes) end @doc """ Disassemble a `.wasm` binary into a `%QuickBEAM.WASM.Module{}` struct. Decodes all sections including function bodies with opcodes. Does not require a running instance. {:ok, mod} = QuickBEAM.WASM.disasm(File.read!("add.wasm")) hd(mod.functions).opcodes # [{0, :local_get, 0}, {2, :local_get, 1}, {4, :i32_add}, {5, :end}] """ @spec disasm(binary()) :: {:ok, Module.t()} | {:error, String.t()} def disasm(wasm_bytes) when is_binary(wasm_bytes) do Parser.parse(wasm_bytes) end @doc """ Validate a `.wasm` binary (structural validation). QuickBEAM.WASM.validate(File.read!("add.wasm")) # => true QuickBEAM.WASM.validate("not wasm") # => false """ @spec validate(binary()) :: boolean() def validate(wasm_bytes) when is_binary(wasm_bytes) do Parser.validate(wasm_bytes) end @doc """ List exports from a `.wasm` binary or a parsed module. """ @spec exports(binary() | Module.t()) :: [Module.export_desc()] | {:error, String.t()} def exports(%Module{} = mod), do: mod.exports def exports(wasm_bytes) when is_binary(wasm_bytes) do case disasm(wasm_bytes) do {:ok, mod} -> mod.exports {:error, _} = err -> err end end @doc """ List imports from a `.wasm` binary or a parsed module. """ @spec imports(binary() | Module.t()) :: [Module.import_desc()] | {:error, String.t()} def imports(%Module{} = mod), do: mod.imports def imports(wasm_bytes) when is_binary(wasm_bytes) do case disasm(wasm_bytes) do {:ok, mod} -> mod.imports {:error, _} = err -> err end end end