defmodule QuickBEAM.WasmAPI do @moduledoc false use GenServer alias QuickBEAM.WASM.ImportRewriter @type module_handle :: {reference(), binary(), [map()], [map()], list()} @type instance_handle :: {reference(), reference(), [map()], [map()], list()} @type state :: %{ next_id: pos_integer(), modules: %{integer() => module_handle()}, instances: %{integer() => instance_handle()} } @spec start_link(keyword()) :: GenServer.on_start() def start_link(opts \\ []) do GenServer.start_link(__MODULE__, :ok, Keyword.put_new(opts, :name, __MODULE__)) end @doc false def ensure_started do case Process.whereis(__MODULE__) do nil -> case GenServer.start(__MODULE__, :ok, name: __MODULE__) do {:ok, _pid} -> :ok {:error, {:already_started, _pid}} -> :ok end _pid -> :ok end end @impl true def init(:ok) do {:ok, %{next_id: 1, modules: %{}, instances: %{}}} end @spec compile([binary()]) :: map() def compile([bytes]) when is_binary(bytes) do ensure_started() GenServer.call(__MODULE__, {:compile, bytes}, :infinity) end @spec validate([binary()]) :: boolean() def validate([bytes]) when is_binary(bytes) do case QuickBEAM.Native.wasm_compile(bytes) do {:ok, mod_ref} -> _ = mod_ref true {:error, _} -> false end end @spec prepare(list()) :: map() def prepare([bytes, import_payload]) when is_binary(bytes) and is_list(import_payload) do case prepare_bytes(bytes, import_payload) do {:ok, rewritten_bytes, memory_initializers, function_imports} -> %{ "ok" => %{ "bytes" => {:bytes, rewritten_bytes}, "memory_initializers" => Enum.map(memory_initializers, &{:bytes, &1}), "function_imports" => Enum.map(function_imports, &normalize_desc/1) } } {:error, msg} -> %{"error" => msg} end end @spec start(list(), pid() | nil) :: map() def start([mod_id]) when is_integer(mod_id), do: start([mod_id, []], nil) def start([mod_id, import_payload], caller) when is_integer(mod_id) and is_list(import_payload) do ensure_started() GenServer.call( __MODULE__, {:start, mod_id, import_payload, runtime_resource(caller)}, :infinity ) end @spec call(list()) :: map() def call([inst_id, func_name, params]) when is_integer(inst_id) and is_binary(func_name) and is_list(params) do ensure_started() GenServer.call(__MODULE__, {:call, inst_id, func_name, params}, :infinity) end @spec module_exports([integer()]) :: [map()] def module_exports([mod_id]) when is_integer(mod_id) do ensure_started() GenServer.call(__MODULE__, {:module_exports, mod_id}, :infinity) end @spec module_imports([integer()]) :: [map()] def module_imports([mod_id]) when is_integer(mod_id) do ensure_started() GenServer.call(__MODULE__, {:module_imports, mod_id}, :infinity) end @spec memory_size([integer()]) :: map() def memory_size([inst_id]) when is_integer(inst_id) do ensure_started() GenServer.call(__MODULE__, {:memory_size, inst_id}, :infinity) end @spec memory_grow(list()) :: map() def memory_grow([inst_id, delta]) when is_integer(inst_id) and is_integer(delta) and delta >= 0 do ensure_started() GenServer.call(__MODULE__, {:memory_grow, inst_id, delta}, :infinity) end @spec read_memory(list()) :: map() def read_memory([inst_id, offset, length]) when is_integer(inst_id) and is_integer(offset) and is_integer(length) and offset >= 0 and length >= 0 do ensure_started() GenServer.call(__MODULE__, {:read_memory, inst_id, offset, length}, :infinity) end @spec write_memory(list()) :: map() def write_memory([inst_id, offset, data]) when is_integer(inst_id) and is_integer(offset) and offset >= 0 and is_binary(data) do ensure_started() GenServer.call(__MODULE__, {:write_memory, inst_id, offset, data}, :infinity) end @spec read_global(list()) :: map() def read_global([inst_id, name]) when is_integer(inst_id) and is_binary(name) do ensure_started() GenServer.call(__MODULE__, {:read_global, inst_id, name}, :infinity) end @spec write_global(list()) :: map() def write_global([inst_id, name, value]) when is_integer(inst_id) and is_binary(name) do ensure_started() GenServer.call(__MODULE__, {:write_global, inst_id, name, value}, :infinity) end @spec module_custom_sections(list()) :: [{:bytes, binary()}] def module_custom_sections([mod_id, section_name]) when is_integer(mod_id) and is_binary(section_name) do ensure_started() GenServer.call(__MODULE__, {:module_custom_sections, mod_id, section_name}, :infinity) end @impl true def handle_call({:compile, bytes}, _from, state) do case QuickBEAM.Native.wasm_compile(bytes) do {:ok, mod_ref} -> {exports, imports, custom_sections} = module_metadata(bytes) {id, next_state} = put_module(state, {mod_ref, bytes, exports, imports, custom_sections}) {:reply, %{"ok" => id}, next_state} {:error, msg} -> {:reply, %{"error" => msg}, state} end end def handle_call({:start, mod_id, import_payload, runtime_resource}, _from, state) do case Map.fetch(state.modules, mod_id) do {:ok, {mod_ref, bytes, exports, imports, custom_sections}} -> with {:ok, compiled_mod_ref, memory_initializers, function_imports} <- prepare_module(mod_ref, bytes, imports, import_payload), {:ok, inst_ref} <- start_instance(compiled_mod_ref, runtime_resource, function_imports), :ok <- initialize_imported_memories(inst_ref, memory_initializers) do instance = {inst_ref, compiled_mod_ref, exports, imports, custom_sections} {id, next_state} = put_instance(state, instance) {:reply, %{"ok" => id}, next_state} else {:error, msg} -> {:reply, %{"error" => msg}, state} end :error -> {:reply, %{"error" => "module not found"}, state} end end def handle_call({:call, inst_id, func_name, params}, _from, state) do case fetch_instance(state, inst_id) do {:ok, inst_ref, exports} -> export = find_export(exports, func_name) reply = case QuickBEAM.Native.wasm_call(inst_ref, func_name, params) do {:ok, result} -> %{"ok" => encode_result(result, Map.get(export || %{}, "results", []))} {:error, msg} -> %{"error" => msg} end {:reply, reply, state} {:error, msg} -> {:reply, %{"error" => msg}, state} end end def handle_call({:module_exports, mod_id}, _from, state) do exports = case Map.fetch(state.modules, mod_id) do {:ok, {_mod_ref, _bytes, exports, _imports, _custom_sections}} -> exports :error -> [] end {:reply, exports, state} end def handle_call({:module_imports, mod_id}, _from, state) do imports = case Map.fetch(state.modules, mod_id) do {:ok, {_mod_ref, _bytes, _exports, imports, _custom_sections}} -> imports :error -> [] end {:reply, imports, state} end def handle_call({:memory_size, inst_id}, _from, state) do reply = with {:ok, inst_ref, _exports} <- fetch_instance(state, inst_id), {:ok, size} <- QuickBEAM.Native.wasm_memory_size(inst_ref) do %{"ok" => size} else {:error, msg} -> %{"error" => msg} end {:reply, reply, state} end def handle_call({:memory_grow, inst_id, delta}, _from, state) do reply = with {:ok, inst_ref, _exports} <- fetch_instance(state, inst_id), {:ok, pages} <- QuickBEAM.Native.wasm_memory_grow(inst_ref, delta) do %{"ok" => pages} else {:error, msg} -> %{"error" => msg} end {:reply, reply, state} end def handle_call({:read_memory, inst_id, offset, length}, _from, state) do reply = with {:ok, inst_ref, _exports} <- fetch_instance(state, inst_id), {:ok, bytes} <- QuickBEAM.Native.wasm_read_memory(inst_ref, offset, length) do %{"ok" => {:bytes, bytes}} else {:error, msg} -> %{"error" => msg} end {:reply, reply, state} end def handle_call({:write_memory, inst_id, offset, data}, _from, state) do reply = with {:ok, inst_ref, _exports} <- fetch_instance(state, inst_id), :ok <- QuickBEAM.Native.wasm_write_memory(inst_ref, offset, data) do %{"ok" => true} else {:error, msg} -> %{"error" => msg} end {:reply, reply, state} end def handle_call({:read_global, inst_id, name}, _from, state) do reply = with {:ok, inst_ref, exports} <- fetch_instance(state, inst_id), export when not is_nil(export) <- find_global_export(exports, name), {:ok, value} <- QuickBEAM.Native.wasm_read_global(inst_ref, name) do %{"ok" => encode_scalar(value, export["type"])} else nil -> %{"error" => "global not found"} {:error, msg} -> %{"error" => msg} end {:reply, reply, state} end def handle_call({:write_global, inst_id, name, value}, _from, state) do reply = with {:ok, inst_ref, exports} <- fetch_instance(state, inst_id), export when not is_nil(export) <- find_global_export(exports, name), :ok <- QuickBEAM.Native.wasm_write_global(inst_ref, name, value) do %{"ok" => encode_scalar(value, export["type"])} else nil -> %{"error" => "global not found"} {:error, msg} -> %{"error" => msg} end {:reply, reply, state} end def handle_call({:module_custom_sections, mod_id, section_name}, _from, state) do sections = case Map.fetch(state.modules, mod_id) do {:ok, {_mod_ref, _bytes, _exports, _imports, custom_sections}} -> custom_sections |> Enum.filter(&(&1.name == section_name)) |> Enum.map(&{:bytes, &1.data}) :error -> [] end {:reply, sections, state} end @impl true def terminate(_reason, state) do Enum.each(state.instances, fn {_id, {inst_ref, _mod_ref, _exports, _imports, _custom_sections}} -> QuickBEAM.Native.wasm_stop(inst_ref) end) :ok end defp put_module(state, module_handle) do id = state.next_id next_state = %{state | next_id: id + 1, modules: Map.put(state.modules, id, module_handle)} {id, next_state} end defp put_instance(state, instance_handle) do id = state.next_id next_state = %{ state | next_id: id + 1, instances: Map.put(state.instances, id, instance_handle) } {id, next_state} end defp prepare_bytes(bytes, import_payload) do {_exports, imports, _custom_sections} = module_metadata(bytes) ImportRewriter.rewrite(bytes, imports, import_payload) end defp prepare_module(mod_ref, _bytes, [], []), do: {:ok, mod_ref, [], []} defp prepare_module(_mod_ref, bytes, imports, import_payload) do case ImportRewriter.rewrite(bytes, imports, import_payload) do {:ok, rewritten_bytes, memory_initializers, function_imports} -> case QuickBEAM.Native.wasm_compile(rewritten_bytes) do {:ok, rewritten_mod_ref} -> {:ok, rewritten_mod_ref, memory_initializers, atomize_function_imports(function_imports)} {:error, msg} -> {:error, msg} end {:error, msg} -> {:error, msg} end end defp initialize_imported_memories(_inst_ref, []), do: :ok defp initialize_imported_memories(inst_ref, [bytes]) do QuickBEAM.Native.wasm_write_memory(inst_ref, 0, bytes) end defp initialize_imported_memories(_inst_ref, _many), do: {:error, "multiple memory imports are not supported yet"} defp start_instance(mod_ref, nil, []), do: QuickBEAM.Native.wasm_start(mod_ref, 65_536, 65_536) defp start_instance(mod_ref, _runtime_resource, []), do: QuickBEAM.Native.wasm_start(mod_ref, 65_536, 65_536) defp start_instance(_mod_ref, nil, [_ | _]), do: {:error, "runtime resource not available for function imports"} defp start_instance(mod_ref, runtime_resource, function_imports) do QuickBEAM.Native.wasm_start_with_imports( mod_ref, runtime_resource, function_imports, 65_536, 65_536 ) end defp runtime_resource(nil), do: nil defp runtime_resource(caller) when is_pid(caller), do: QuickBEAM.Runtime.resource(caller) defp atomize_function_imports(function_imports) do Enum.map(function_imports, fn import -> %{ module_name: import.module_name, symbol: import.symbol, signature: import.signature, callback_name: import.callback_name } end) end defp fetch_instance(state, inst_id) do case Map.fetch(state.instances, inst_id) do {:ok, {inst_ref, _mod_ref, exports, _imports, _custom_sections}} -> {:ok, inst_ref, exports} :error -> {:error, "instance not found"} end end defp module_metadata(bytes) do case QuickBEAM.WASM.disasm(bytes) do {:ok, mod} -> { Enum.map(mod.exports, &normalize_desc/1), Enum.map(mod.imports, &normalize_desc/1), mod.custom_sections } {:error, _} -> {[], [], []} end end defp normalize_desc(desc) do Enum.into(desc, %{}, fn {:kind, :func} -> {"kind", "function"} {key, value} -> {to_string(key), normalize_value(value)} end) end defp normalize_value(nil), do: nil defp normalize_value(value) when is_boolean(value), do: value defp normalize_value(value) when is_atom(value), do: Atom.to_string(value) defp normalize_value(value) when is_list(value), do: Enum.map(value, &normalize_value/1) defp normalize_value(value) when is_map(value), do: normalize_desc(value) defp normalize_value(value), do: value defp find_export(exports, func_name) do Enum.find(exports, &(&1["name"] == func_name and &1["kind"] == "function")) end defp find_global_export(exports, name) do Enum.find(exports, &(&1["name"] == name and &1["kind"] == "global")) end defp encode_result(_result, []), do: nil defp encode_result(result, [type]), do: encode_scalar(result, type) defp encode_result(result, types) when is_list(result) do result |> Enum.zip(types) |> Enum.map(fn {value, type} -> encode_scalar(value, type) end) end defp encode_result(result, _types), do: result defp encode_scalar(value, "i64") when is_integer(value), do: Integer.to_string(value) defp encode_scalar(value, _type), do: value end