hb_beamr.erl - WASM Execution via WAMR
Overview
Purpose: WebAssembly execution in BEAM using WAMR (WebAssembly Micro Runtime)
Module: hb_beamr
Pattern: Linked-In Driver (LID) wrapper
Runtime: WAMR engine with async workers
BEAMR is a library that executes WASM modules in BEAM using the WebAssembly Micro Runtime. Each WASM instance runs as an independent async worker, enabling long-running WASM executions that can easily interact with Erlang functions and processes.
Dependencies
- Erlang/OTP:
erl_ddll(dynamic driver loading) - Native:
hb_beamrC driver (WAMR wrapper) - HyperBEAM:
hb_beamr_io - Includes:
include/hb.hrl
Public Functions Overview
%% Control API
-spec start(WasmBinary) -> {ok, Port, Imports, Exports} | {error, Reason}.
-spec start(WasmBinary, Mode) -> {ok, Port, Imports, Exports} | {error, Reason}.
-spec call(Port, FuncName, Args) -> {ok, Result}.
-spec call(Port, FuncName, Args, ImportFun) -> {ok, Result, State}.
-spec call(Port, FuncName, Args, ImportFun, State, Opts) -> {ok, Result, NewState}.
-spec stop(Port) -> ok.
-spec wasm_send(WASM, Message) -> ok.
%% Utility API
-spec serialize(Port) -> {ok, Memory}.
-spec deserialize(Port, Memory) -> ok.
-spec stub(Msg1, Msg2, Opts) -> {ok, [0], Msg1}.Architecture
┌─────────────────────────────────────────────┐
│ Erlang Process (Caller) │
│ │
│ call(WASM, "func", [args]) ──┐ │
│ │ │
└────────────────────────────────┼─────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Worker Process (WASM Instance) │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ Port to hb_beamr.so (LID) │ │
│ │ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ WAMR Runtime │ │ │
│ │ │ (WebAssembly Execution) │ │ │
│ │ │ │ │ │
│ │ │ - Memory management │ │ │
│ │ │ - Function execution │ │ │
│ │ │ - Import/Export handling │ │ │
│ │ └──────────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘Public Functions
1. start/1, start/2
-spec start(WasmBinary) -> {ok, WASM, Imports, Exports} | {error, Reason}
when
WasmBinary :: binary(),
WASM :: pid(),
Imports :: [{Module, Function, Args, Signature}],
Exports :: [{Function, Args, Signature}],
Reason :: term().
-spec start(WasmBinary, Mode) -> {ok, WASM, Imports, Exports} | {error, Reason}
when
Mode :: wasm | aot.Description: Load and initialize a WASM module. Returns worker PID and module metadata.
Modes:wasm- Standard WASM bytecode (default)aot- Ahead-of-time compiled WASM
-module(hb_beamr_start_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
start_basic_test() ->
{ok, File} = file:read_file("test/test.wasm"),
{ok, WASM, Imports, Exports} = hb_beamr:start(File),
?assert(is_pid(WASM)),
?assert(is_list(Imports)),
?assert(is_list(Exports)),
hb_beamr:stop(WASM).
start_with_mode_test() ->
{ok, File} = file:read_file("test/test.wasm"),
{ok, WASM, _, _} = hb_beamr:start(File, wasm),
?assert(is_process_alive(WASM)),
hb_beamr:stop(WASM).
start_invalid_wasm_test() ->
InvalidWASM = <<"not a valid wasm file">>,
Result = hb_beamr:start(InvalidWASM),
?assertMatch({error, _}, Result).
start_exports_list_test() ->
{ok, File} = file:read_file("test/test.wasm"),
{ok, WASM, _, Exports} = hb_beamr:start(File),
?assert(is_list(Exports)),
?assert(length(Exports) > 0),
hb_beamr:stop(WASM).2. call/3, call/4, call/5, call/6
-spec call(WASM, FuncName, Args) -> {ok, Result}
when
WASM :: pid(),
FuncName :: string() | binary() | integer(),
Args :: [number()],
Result :: [number()].
-spec call(WASM, FuncName, Args, ImportFun) -> {ok, Result, State}.
-spec call(WASM, FuncName, Args, ImportFun, State, Opts) ->
{ok, Result, NewState} | {error, Error, Reason, Stack, State}.Description: Call a WASM function. Supports both direct calls (by name) and indirect calls (by table index).
Function Names:- String:
"function_name" - Binary:
<<"function_name">> - Integer: Function table index (indirect call)
Arguments: List of integers or floats
Import Function: fun(State, Import, Opts) -> {ok, Response, NewState}
-module(hb_beamr_call_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
call_simple_test() ->
{ok, File} = file:read_file("test/test.wasm"),
{ok, WASM, _, _} = hb_beamr:start(File),
{ok, [Result]} = hb_beamr:call(WASM, "fac", [5.0]),
?assertEqual(120.0, Result),
hb_beamr:stop(WASM).
call_with_imports_test() ->
{ok, File} = file:read_file("test/pow_calculator.wasm"),
{ok, WASM, _, _} = hb_beamr:start(File),
ImportFun = fun(State, #{args := [A, B]}, _Opts) ->
{ok, [A * B], State}
end,
{ok, [Result], _} = hb_beamr:call(WASM, <<"pow">>, [2, 5], ImportFun),
?assertEqual(32, Result),
hb_beamr:stop(WASM).
call_with_state_test() ->
{ok, File} = file:read_file("test/test.wasm"),
{ok, WASM, _, _} = hb_beamr:start(File),
InitState = #{counter => 0},
ImportFun = fun(State = #{counter := C}, _, _) ->
{ok, [0], State#{counter => C + 1}}
end,
{ok, _, State1} = hb_beamr:call(WASM, "fac", [3.0], ImportFun, InitState, #{}),
?assertEqual(0, maps:get(counter, State1)),
hb_beamr:stop(WASM).
call_binary_function_name_test() ->
{ok, File} = file:read_file("test/test.wasm"),
{ok, WASM, _, _} = hb_beamr:start(File),
{ok, [Result]} = hb_beamr:call(WASM, <<"fac">>, [4.0]),
?assertEqual(24.0, Result),
hb_beamr:stop(WASM).
call_multiple_test() ->
{ok, File} = file:read_file("test/test.wasm"),
{ok, WASM, _, _} = hb_beamr:start(File),
{ok, [R1]} = hb_beamr:call(WASM, "fac", [3.0]),
{ok, [R2]} = hb_beamr:call(WASM, "fac", [4.0]),
?assertEqual(6.0, R1),
?assertEqual(24.0, R2),
hb_beamr:stop(WASM).
call_invalid_args_test() ->
{ok, File} = file:read_file("test/test.wasm"),
{ok, WASM, _, _} = hb_beamr:start(File),
% Use call/6 to get proper error handling
Result = hb_beamr:call(WASM, "fac", [<<"invalid">>], fun hb_beamr:stub/3, #{}, #{}),
?assertMatch({error, {invalid_args, _}}, Result),
hb_beamr:stop(WASM).3. stop/1
-spec stop(WASM) -> ok
when WASM :: pid().Description: Stop a WASM executor, closing the port and terminating the worker process.
Test Code:-module(hb_beamr_stop_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
stop_test() ->
{ok, File} = file:read_file("test/test.wasm"),
{ok, WASM, _, _} = hb_beamr:start(File),
?assert(is_process_alive(WASM)),
ok = hb_beamr:stop(WASM),
timer:sleep(10),
?assert(not is_process_alive(WASM)).
stop_idempotent_test() ->
{ok, File} = file:read_file("test/test.wasm"),
{ok, WASM, _, _} = hb_beamr:start(File),
ok = hb_beamr:stop(WASM),
ok = hb_beamr:stop(WASM), % Should not crash
ok.4. wasm_send/2
-spec wasm_send(WASM, Message) -> ok
when
WASM :: pid(),
Message :: {command, binary()}.Description: Send a raw command message to the WASM worker. Used internally.
Test Code:-module(hb_beamr_wasm_send_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
wasm_send_test() ->
{ok, File} = file:read_file("test/test.wasm"),
{ok, WASM, _, _} = hb_beamr:start(File),
ok = hb_beamr:wasm_send(WASM, {command, term_to_binary({call, "fac", [5.0]})}),
receive
{execution_result, [Result]} ->
?assertEqual(120.0, Result)
after 5000 ->
?assert(false)
end,
hb_beamr:stop(WASM).5. serialize/1
-spec serialize(WASM) -> {ok, Memory}
when
WASM :: pid(),
Memory :: binary().Description: Serialize WASM memory state to binary. Useful for checkpointing or migration.
Test Code:-module(hb_beamr_serialize_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
serialize_test() ->
{ok, File} = file:read_file("test/test-print.wasm"),
{ok, WASM, _, _} = hb_beamr:start(File),
{ok, Memory} = hb_beamr:serialize(WASM),
?assert(is_binary(Memory)),
hb_beamr:stop(WASM).
serialize_size_test() ->
{ok, File} = file:read_file("test/test-print.wasm"),
{ok, WASM, _, _} = hb_beamr:start(File),
{ok, Size} = hb_beamr_io:size(WASM),
{ok, Memory} = hb_beamr:serialize(WASM),
?assertEqual(Size, byte_size(Memory)),
hb_beamr:stop(WASM).6. deserialize/2
-spec deserialize(WASM, Memory) -> ok
when
WASM :: pid(),
Memory :: binary().Description: Deserialize WASM memory state from binary. Restores previous state.
Test Code:-module(hb_beamr_deserialize_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
deserialize_roundtrip_test() ->
{ok, File} = file:read_file("test/test-print.wasm"),
{ok, WASM, _, _} = hb_beamr:start(File),
% Write some data
TestData = <<"Hello, World!">>,
ok = hb_beamr_io:write(WASM, 0, TestData),
% Serialize
{ok, Memory} = hb_beamr:serialize(WASM),
% Overwrite with different data
ok = hb_beamr_io:write(WASM, 0, <<"Different!">>),
% Deserialize
ok = hb_beamr:deserialize(WASM, Memory),
% Verify original data restored
{ok, Restored} = hb_beamr_io:read(WASM, 0, byte_size(TestData)),
?assertEqual(TestData, Restored),
hb_beamr:stop(WASM).7. stub/3
-spec stub(Msg1, Msg2, Opts) -> {ok, [0], Msg1}.Description: Default stub import function. Returns [0] for all imports.
-module(hb_beamr_stub_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
stub_test() ->
State = #{test => value},
Import = #{func => <<"test">>, args => [1, 2]},
{ok, [0], NewState} = hb_beamr:stub(State, Import, #{}),
?assertEqual(State, NewState).Import Function Specification
ImportFun = fun(State, ImportInfo, Opts) ->
{ok, Response, NewState}
end
%% ImportInfo structure
#{
instance => WASM, % PID of WASM instance
module => Module, % Module name (binary)
func => Function, % Function name (binary)
args => [Arg1, Arg2], % Arguments (numbers)
func_sig => Signature % Function signature
}
%% Response must be
[ReturnValue1, ReturnValue2, ...] % List of numbersCommon Patterns
%% Load and execute WASM
{ok, File} = file:read_file("module.wasm"),
{ok, WASM, _, _} = hb_beamr:start(File),
{ok, [Result]} = hb_beamr:call(WASM, "compute", [10.0]),
hb_beamr:stop(WASM).
%% Handle imports
ImportFun = fun(State, #{module := Mod, func := Fun, args := Args}, _Opts) ->
io:format("Import called: ~s:~s(~p)~n", [Mod, Fun, Args]),
Result = my_import_handler(Mod, Fun, Args),
{ok, [Result], State}
end,
{ok, Result, _} = hb_beamr:call(WASM, "main", [], ImportFun, #{}, #{}).
%% State management
InitState = #{counter => 0, data => []},
ImportFun = fun(State = #{counter := C, data := D}, #{args := [Val]}, _) ->
NewState = State#{counter => C + 1, data => [Val | D]},
{ok, [C], NewState}
end,
{ok, _, FinalState} = hb_beamr:call(WASM, "process", [42.0], ImportFun, InitState, #{}).
%% Checkpoint and restore
{ok, WASM, _, _} = hb_beamr:start(WasmBinary),
{ok, _} = hb_beamr:call(WASM, "setup", []),
{ok, Checkpoint} = hb_beamr:serialize(WASM),
% Later...
{ok, WASM2, _, _} = hb_beamr:start(WasmBinary),
ok = hb_beamr:deserialize(WASM2, Checkpoint),
{ok, Result} = hb_beamr:call(WASM2, "continue", []).
%% Multi-process execution
{ok, WASM, _, _} = hb_beamr:start(WasmBinary),
Clients = [spawn(fun() ->
{ok, [R]} = hb_beamr:call(WASM, "work", [N]),
Parent ! {done, R}
end) || N <- lists:seq(1, 10)],
% Note: Only one process can call at a time
% Concurrent calls will execute sequentiallyMemory Management
WASM memory can never shrink, only grow. See hb_beamr_io for memory operations:
%% Get memory size
{ok, Size} = hb_beamr_io:size(WASM),
%% Read/write memory
ok = hb_beamr_io:write(WASM, Offset, Data),
{ok, Data} = hb_beamr_io:read(WASM, Offset, Size),
%% Allocate/free via WASM malloc/free
{ok, Ptr} = hb_beamr_io:malloc(WASM, Size),
ok = hb_beamr_io:free(WASM, Ptr).Performance Considerations
Async Workers
- Each WASM instance uses one async thread
- Configure BEAM:
erl +A N(N = number of async threads) - Default is usually 10, may need more for many instances
Benchmarks
% Simple factorial call: ~1000-10000 ops/sec
% Complex operations: Varies by WASM codeError Handling
%% Start errors
{error, driver_load_failed}
{error, invalid_wasm}
{error, unsupported_feature}
%% Call errors
{error, {invalid_args, Args}}
{error, function_not_found}
{error, wasm_trap}
{error, Error, Reason, Stack, State} % Import function error
%% Import errors propagate
ImportFun = fun(State, Import, Opts) ->
throw({my_error, reason}) % Caught and returned as error
end.References
- WAMR - WebAssembly Micro Runtime
- WebAssembly Spec - WASM standard
- Erlang Ports - Port communication
- hb_beamr_io - Memory management
Notes
- One Call at a Time: Worker processes are single-threaded
- Memory Persistence: WASM memory persists between calls
- Memory Growth: WASM pages can grow but never shrink
- Async Threads: Requires
+A Nfor concurrent instances - Import Blocking: Imports block until response provided
- State Threading: State must be manually threaded through imports
- Number Types: Only integers and floats supported in args/returns
- Worker PID: Returned PID is wrapper, not actual port
- Serialization: Full memory snapshot, can be large
- AOT Support: Can load ahead-of-time compiled WASM