Custom Devices in C++
Erlang can natively execute C++ functions with almost no overhead via NIF (Native Implemented Function).
Creating a C++ Device
Go to HyperBEAM/native
directory and create a new directory for your C++ device.
mkdir -p dev_mul_nif/include && cd dev_mul_nif
Step 1: Create the C++ Header File
Create include/dev_mul.h
:
#pragma once
extern "C" {
int multiply(const int a, const int b);
}
The extern "C"
linkage is crucial for making C++ functions callable from C/Erlang NIFs.
Step 2: Implement the C++ Logic
Create dev_mul.cpp
:
#include "include/dev_mul.h"
int multiply(const int a, const int b) {
return a * b;
}
Step 3: Create the NIF Wrapper
Create dev_mul_nif.cpp
:
#include <erl_nif.h>
#include "include/dev_mul.h"
static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) {
return 0;
}
static void unload(ErlNifEnv* env, void* priv_data) {}
static ERL_NIF_TERM mul_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int a, b;
if (!enif_get_int(env, argv[0], &a) || !enif_get_int(env, argv[1], &b)) {
return enif_make_badarg(env);
}
int result = multiply(a, b);
return enif_make_int(env, result);
}
static ErlNifFunc nif_funcs[] = {
{"multiply", 2, mul_nif}
};
ERL_NIF_INIT(dev_mul_nif, nif_funcs, load, NULL, NULL, unload)
Step 4: Configure Build in rebar.config
Add the C++ compilation settings to HyperBEAM/rebar.config
:
{port_env, [
{"(linux|darwin|solaris)", "CXX", "g++"},
{"(linux|darwin|solaris)", "CXXFLAGS",
"$CXXFLAGS -std=c++17 -I${REBAR_ROOT_DIR}/native/dev_mul_nif/include -I/usr/local/lib/erlang/usr/include/"},
{"(linux|darwin|solaris)", "LDFLAGS",
"$LDFLAGS -lstdc++"}
]}.
Add the port specification:
{port_specs, [
...
{"./priv/dev_mul.so", [
"./native/dev_mul_nif/dev_mul_nif.cpp",
"./native/dev_mul_nif/dev_mul.cpp"
]}
...
]}.
Add cleanup hooks:
{post_hooks, [
...
{ compile, "rm -f native/dev_mul_nif/*.o native/dev_mul_nif/*.d"}
...
]}.
Step 5: Create the Erlang NIF Module
Create HyperBEAM/src/dev_mul_nif.erl
:
-module(dev_mul_nif).
-export([multiply/2]).
-include("include/hb.hrl").
-include_lib("eunit/include/eunit.hrl").
-on_load(init/0).
-define(NOT_LOADED, not_loaded(?LINE)).
not_loaded(Line) ->
erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, Line}]}).
init() ->
PrivDir = code:priv_dir(hb),
Path = filename:join(PrivDir, "dev_mul"),
case erlang:load_nif(Path, 0) of
ok -> ok;
{error, Reason} -> exit({load_failed, Reason})
end.
multiply(_A, _B) ->
not_loaded(?LINE).
Step 6: Create the Erlang Device Module
Create HyperBEAM/src/dev_mul.erl
:
-module(dev_mul).
-export([mul/3]).
-include("include/hb.hrl").
-include_lib("eunit/include/eunit.hrl").
mul(_, M2, Opts) ->
A = hb_ao:get(<<"a">>, M2, Opts),
B = hb_ao:get(<<"b">>, M2, Opts),
Product = dev_mul_nif:multiply(A, B),
{ok, #{ <<"product">> => Product, <<"a">> => A, <<"b">> => B }}.
multiply_test() ->
M1 = #{<<"device">> => <<"mul@1.0">>},
M2 = #{
<<"path">> => <<"mul">>,
<<"a">> => 2,
<<"b">> => 3
},
{ok, Product} = hb_ao:resolve(M1, M2, #{}),
?assertEqual(6, maps:get(<<"product">>, Product)).
Step 7: Register the Device
Add the device to HyperBEAM/hb_opt.erl
:
preloaded_devices => [
...
#{<<"name">> => <<"mul@1.0">>, <<"module">> => dev_mul},
...
],
Step 8: Build and Test
Run the unit tests:
rebar3 eunit --module=dev_mul
Test the device with WAO:
it("should test mul@1.0", async () => {
const res = await hb.send({ path: "/~mul@1.0/mul", a: 4, b: 5 })
assert.equal(res.headers.get("product"), "20")
})