cffi

Erlang C Foreign Function Interface — call C library functions from Erlang at runtime, without writing C bindings or NIFs by hand.

Inspired by Common Lisp's CFFI and Python's ctypes/cffi.


Features


Quick start

Prerequisites

libffi-dev

Build

rebar3 compile

This compiles both priv/cffi_nif.so (via the pc plugin) and priv/cffi_port (via make).

Basic call

{ok, Lib} = cffi:load("libm.so.6"),
{ok, 2.0} = cffi:call(Lib, "sqrt", double, [{double, 4.0}]),
{ok, 8.0} = cffi:call(Lib, "pow",  double, [{double, 2.0}, {double, 3.0}]).

API reference

Type specs

Erlang atom C type
voidvoid
bool_Bool
int8int8_t
uint8uint8_t
int16int16_t
uint16uint16_t
int32int32_t
uint32uint32_t
int64int64_t
uint64uint64_t
floatfloat
doubledouble
pointervoid *
stringconst char * (null-terminated; encoded as binary on the Erlang side)

Named registered types (structs, unions, enums, typedefs) can be used anywhere a type spec is expected.

cffi — NIF mode

%% Load a shared library.
{ok, Lib} = cffi:load("libm.so.6").

%% Call a C function.
{ok, Val} = cffi:call(Lib, "func_name", RetType, [{ArgType, ArgVal}, ...]).

%% Call a variadic C function (NFixed = number of fixed arguments).
{ok, N} = cffi:call_va(Lib, "snprintf", int32, 3,
              [{pointer, Buf}, {uint64, BufSize}, {string, "%d"}, {int32, 42}]).

%% Allocate C memory (zeroed).
Ptr = cffi:alloc(Bytes).
Ptr = cffi:alloc_type(TypeAtom).            %% sizeof(Type) bytes
Ptr = cffi:alloc_type(TypeAtom, Count).     %% Count × sizeof(Type) bytes
Ptr = cffi:alloc_struct(StructName).

%% Free C memory.
ok  = cffi:free(Ptr).

%% Read / write a typed value at a pointer.
Val = cffi:read(Ptr, TypeSpec).
ok  = cffi:write(Ptr, TypeSpec, Value).

%% Raw byte I/O.
Bin = cffi:read_bytes(Ptr, Size).
ok  = cffi:write_bytes(Ptr, Binary).

%% Pointer arithmetic.
Ptr2 = cffi:ptr_add(Ptr, ByteOffset).
Null = cffi:null().
true = cffi:is_null(Null).

%% Scoped allocation (pointer freed on exit, even on exception).
Result = cffi:with_alloc(Bytes, fun(Ptr) -> ... end).
Result = cffi:with_alloc(TypeAtom, Count, fun(Ptr) -> ... end).

Type system (cffi_type / cffi)

%% Define a C struct (fields laid out per System V AMD64 ABI).
cffi:defcstruct(point, [{x, double}, {y, double}]).

%% Define a C union.
cffi:defcunion(int_or_float, [{i, int32}, {f, float}]).

%% Define an enum (auto-numbered from 0, or explicit values).
cffi:defcenum(color, [red, green, blue]).
cffi:defcenum(errno_t, [{ok, 0}, {eperm, 1}, {enoent, 2}]).

%% typedef alias.
cffi:defctype(size_t, uint64).
cffi:defctype(my_double, double).

%% Introspect.
16 = cffi:type_size(point).   %% 2 × 8
8  = cffi:align_of(point).

%% Struct field access.
FPtr = cffi:field_ptr(Ptr, point, x).
1.5  = cffi:struct_read(Ptr, point, x).
ok   = cffi:struct_write(Ptr, point, y, 2.5).
#{x := 1.5, y := 2.5} = cffi:struct_to_map(Ptr, point).
ok   = cffi:map_to_struct(Ptr, point, #{x => 0.0, y => 1.0}).

%% Array element access.
Ptr2 = cffi:array_ptr(Ptr, int32, 3).        %% pointer to element 3
42   = cffi:array_read(Ptr, int32, 0).
ok   = cffi:array_write(Ptr, int32, 0, 42).

C→Erlang callbacks (cffi_callback)

%% Create a C-callable function pointer backed by an Erlang fun.
{ok, Cb} = cffi_callback:new(RetType, [ArgType, ...], fun(A, B, ...) -> ... end).

%% Extract the C function pointer (pass to cffi:call as {pointer, FnPtr}).
FnPtr = cffi_callback:func_ptr(Cb).

%% Free when no longer needed (do not free while C code may still call it).
ok = cffi_callback:free(Cb).

Example — qsort with an Erlang comparator:

{ok, Libc} = cffi:load("libc.so.6"),
Arr = cffi:alloc_type(int32, 5),
%% ... fill array ...

{ok, Cb} = cffi_callback:new(int32, [pointer, pointer],
    fun(PA, PB) ->
        A = cffi:read(PA, int32), B = cffi:read(PB, int32),
        if A < B -> -1; A > B -> 1; true -> 0 end
    end),

{ok, ok} = cffi:call(Libc, "qsort", void,
               [{pointer, Arr}, {uint64, 5}, {uint64, 4},
                {pointer, cffi_callback:func_ptr(Cb)}]),

cffi_callback:free(Cb),
cffi:free(Arr).

Notes:

Port safe mode (cffi_port)

The port mode API mirrors cffi exactly. A crash in the C library terminates the port subprocess but leaves the BEAM VM alive.

%% Load — starts a dedicated OS subprocess.
{ok, Lib} = cffi_port:load("libm.so.6").

%% Same call / alloc / read / write / free / ptr_add / struct / array API.
{ok, 2.0} = cffi_port:call(Lib, "sqrt", double, [{double, 4.0}]).
{ok, N}   = cffi_port:call_va(Lib, "snprintf", int32, 3, [...]).

Ptr = cffi_port:alloc(Lib, 8).    %% NB: takes Lib, not just bytes
3.14 = cffi_port:read(Ptr, double).
ok   = cffi_port:write(Ptr, double, 3.14).
P2   = cffi_port:ptr_add(Ptr, 4). %% returns {port_ptr, Pid, Addr}

%% Explicit close (or the subprocess exits with the gen_server).
ok = cffi_port:close(Lib).

Differences from NIF mode:

NIF mode Port mode
Crash safety C crash kills VM C crash kills subprocess only
Speed Fast (in-process) Slower (inter-process RPC)
Pointers NIF resource (reference()) {port_ptr, Pid, Addr} tuple
alloccffi:alloc(Bytes)cffi_port:alloc(Lib, Bytes)
Callbacks cffi_callback:new/3 Not supported

Parse transform (cffi_transform)

Declare C bindings at the module level; the transform generates typed wrapper functions at compile time.

-module(my_math).
-compile({parse_transform, cffi_transform}).

-cffi_lib("libm.so.6").
-cffi_fun({sqrt,  double, [double]}).
-cffi_fun({pow,   double, [double, double]}).
%% Different Erlang name / C symbol:
-cffi_fun({{cbrt_val, "cbrt"}, double, [double]}).

This generates:

sqrt(X)       -> case cffi:call(&#39;$cffi_lib$&#39;(), "sqrt",  double, [{double,X}]) of ...
pow(X, Y)     -> case cffi:call(&#39;$cffi_lib$&#39;(), "pow",   double, [...]) of ...
cbrt_val(X)   -> case cffi:call(&#39;$cffi_lib$&#39;(), "cbrt",  double, [{double,X}]) of ...

The library is loaded lazily on first call and cached via persistent_term.


Variadic functions

Use call_va/5 whenever the C function takes ...:

{ok, Lib} = cffi:load("libc.so.6"),
Buf = cffi:alloc(64),

%% snprintf(buf, 64, "%d + %d = %d", 1, 2, 3)
{ok, 7} = cffi:call_va(Lib, "snprintf", int32, 3,
              [{pointer, Buf}, {uint64, 64}, {string, "%d + %d = %d"},
               {int32, 1}, {int32, 2}, {int32, 3}]),

cffi:read_bytes(Buf, 7).   %% <<"1 + 2 = 3">> (minus null)

NFixed (third integer argument) is the number of fixed parameters before .... For snprintf that is 3 (buf, size, fmt).

C default argument promotions are applied automatically to variadic arguments: float arguments are promoted to double as required by the C standard.

Port mode uses the identical cffi_port:call_va/5 signature.


Testing

rebar3 ct
Suite Cases Coverage
cffi_nif_SUITE 15 load, call, void return, all primitive types, ptr arithmetic, bytes I/O, typedef, enum, struct, union, nested struct, arrays, scoped alloc, callbacks, varargs
cffi_port_SUITE 6 load/close, call, alloc/rw, struct via ptr_add, array, crash isolation
cffi_va_SUITE 7 NIF varargs (int/float/string/multi), port varargs (int/float/string)

Architecture

cffi.erl              ← public API, type resolution, NIF mode
cffi_port.erl         ← public API, port safe mode (gen_server)
cffi_type.erl         ← type registry (ETS), C layout engine
cffi_callback.erl     ← C→Erlang callback server
cffi_transform.erl    ← parse transform
cffi_nif.erl          ← NIF stub (loads priv/cffi_nif.so)

c_src/cffi_nif.c      ← NIF: libffi dispatch, resource GC, callbacks
c_src/cffi_port.c     ← Port executable: same ops over stdin/stdout

The ETS type registry (cffi_type_registry) is created lazily on first use. It is a public named table; ownership follows the process that first calls defcstruct/defcenum/defctype. In long-running applications, define your types in a supervisor init or application start callback so the table owner is a persistent process.


Limitations