Packages

Erlang CFFI — call C shared libraries from Erlang via libffi

Current section

Files

Jump to
cffi src cffi.erl
Raw

src/cffi.erl

%% cffi.erl — Erlang CFFI public API
%%
%% Quick start:
%%
%% %% Primitive call
%% {ok, Lib} = cffi:load("libm.so.6"),
%% {ok, 2.0} = cffi:call(Lib, "sqrt", double, [{double, 4.0}]),
%%
%% %% Define a struct, allocate, read/write fields
%% cffi:defcstruct(point, [{x, double}, {y, double}]),
%% Ptr = cffi:alloc_struct(point),
%% cffi:struct_write(Ptr, point, x, 1.5),
%% cffi:struct_write(Ptr, point, y, 2.5),
%% #{x := 1.5, y := 2.5} = cffi:struct_to_map(Ptr, point),
%% cffi:free(Ptr).
%%
%% Type specs:
%% Primitive : void | bool | int8 | uint8 | int16 | uint16
%% | int32 | uint32 | int64 | uint64 | float | double
%% | pointer | string
%% Composite : {array, ElemType, N} | {ptr, PointeeType}
%% | RegisteredName
-module(cffi).
-export([
%% Library
load/1,
%% Type definitions (delegated to cffi_type)
defcstruct/2,
defcunion/2,
defcenum/2,
defctype/2,
%% Calling C functions
call/3, %% call(Lib, Func, RetType)
call/4, %% call(Lib, Func, RetType, [{ArgType, ArgVal}])
call_va/5, %% call_va(Lib, Func, RetType, NFixed, [{ArgType, ArgVal}])
%% Memory allocation
alloc/1, %% alloc(Bytes)
alloc_type/1, %% alloc_type(TypeAtom) → sizeof(Type) bytes
alloc_type/2, %% alloc_type(TypeAtom, N) → N×sizeof(Type) bytes
alloc_struct/1, %% alloc_struct(StructName)
free/1,
%% Raw memory read / write
read/2, %% read(Ptr, TypeSpec)
write/3, %% write(Ptr, TypeSpec, Value)
read_bytes/2,
write_bytes/2,
%% Struct / union field access
field_ptr/3, %% field_ptr(Ptr, StructName, FieldName) -> Ptr
struct_read/3, %% struct_read(Ptr, StructName, FieldName) -> Value
struct_write/4, %% struct_write(Ptr, StructName, FieldName, Value) -> ok
struct_to_map/2, %% struct_to_map(Ptr, StructName) -> map()
map_to_struct/3, %% map_to_struct(Ptr, StructName, map()) -> ok
%% Array element access
array_ptr/3, %% array_ptr(Ptr, ElemType, Index) -> Ptr
array_read/3, %% array_read(Ptr, ElemType, Index) -> Value
array_write/4, %% array_write(Ptr, ElemType, Index, Value) -> ok
%% Pointer utilities
ptr_add/2,
null/0,
is_null/1,
type_size/1,
align_of/1,
%% Scoped allocation
with_alloc/2, %% with_alloc(Bytes, fun(Ptr) -> R) -> R
with_alloc/3 %% with_alloc(Type, Count, fun(Ptr) -> R) -> R
]).
%% -------------------------------------------------------------------------
%% Type definitions
%% -------------------------------------------------------------------------
-spec defcstruct(atom(), [{atom(), term()}]) -> ok.
defcstruct(Name, Fields) -> cffi_type:defcstruct(Name, Fields).
-spec defcunion(atom(), [{atom(), term()}]) -> ok.
defcunion(Name, Fields) -> cffi_type:defcunion(Name, Fields).
-spec defcenum(atom(), list()) -> ok.
defcenum(Name, Values) -> cffi_type:defcenum(Name, Values).
-spec defctype(atom(), term()) -> ok.
defctype(Name, TypeSpec) -> cffi_type:defctype(Name, TypeSpec).
%% -------------------------------------------------------------------------
%% Library loading
%% -------------------------------------------------------------------------
-spec load(string() | binary()) -> {ok, reference()} | {error, term()}.
load(Path) ->
cffi_nif:lib_open(to_list(Path)).
%% -------------------------------------------------------------------------
%% Calling C functions
%% -------------------------------------------------------------------------
-spec call(reference(), string() | binary(), term()) ->
{ok, term()} | {error, term()}.
call(Lib, Func, RetType) ->
call(Lib, Func, RetType, []).
%% call/4 resolves registered types (enums, typedefs) before dispatching
%% to the NIF, and converts the return value back for enum/typedef ret types.
-spec call(reference(), string() | binary(), term(), list()) ->
{ok, term()} | {error, term()}.
call(Lib, Func, RetType, Args) ->
NifRetType = resolve_type_for_nif(RetType),
NifArgs = [resolve_arg_for_nif(A) || A <- Args],
case cffi_nif:call(Lib, to_list(Func), NifRetType, NifArgs) of
{ok, Val} -> {ok, unmarshal_ret(RetType, Val)};
Err -> Err
end.
%% call_va/5 — variadic C function call.
%%
%% NFixed: count of fixed (non-variadic) arguments.
%% Must satisfy 1 =< NFixed =< length(Args).
%% Variadic float args are automatically promoted to double per C rules.
%%
%% Example:
%% {ok, N} = cffi:call_va(Lib, "snprintf", int32, 3,
%% [{pointer, Buf}, {uint64, BufSize},
%% {string, "x=%d y=%.2f"},
%% {int32, 42}, {double, 3.14}])
-spec call_va(reference(), string() | binary(), term(), pos_integer(), list()) ->
{ok, term()} | {error, term()}.
call_va(Lib, Func, RetType, NFixed, Args) ->
NifRetType = resolve_type_for_nif(RetType),
NifArgs = [resolve_arg_for_nif(A) || A <- Args],
case cffi_nif:call_va(Lib, to_list(Func), NifRetType, NFixed, NifArgs) of
{ok, Val} -> {ok, unmarshal_ret(RetType, Val)};
Err -> Err
end.
%% -------------------------------------------------------------------------
%% Memory allocation
%% -------------------------------------------------------------------------
-spec alloc(pos_integer()) -> reference().
alloc(Bytes) when is_integer(Bytes), Bytes > 0 ->
cffi_nif:mem_alloc(Bytes).
-spec alloc_type(term()) -> reference().
alloc_type(Type) -> alloc_type(Type, 1).
-spec alloc_type(term(), pos_integer()) -> reference().
alloc_type(Type, Count) when is_integer(Count), Count > 0 ->
cffi_nif:mem_alloc(cffi_type:sizeof(Type) * Count).
-spec alloc_struct(atom()) -> reference().
alloc_struct(StructName) ->
cffi_nif:mem_alloc(cffi_type:sizeof(StructName)).
-spec free(reference()) -> ok | {error, term()}.
free(Ptr) ->
cffi_nif:mem_free(Ptr).
%% -------------------------------------------------------------------------
%% Raw memory read / write
%%
%% read/2 handles registered types:
%% enum → integer read, then mapped to atom
%% typedef → resolved to underlying type
%% struct/union → returns a borrowed ptr (sub-object access)
%% array → returns a borrowed ptr to first element
%% primitive → delegated to NIF directly
%% -------------------------------------------------------------------------
-spec read(reference(), term()) -> term().
read(Ptr, Type) ->
case cffi_type:lookup(resolve_typedef(Type)) of
not_found ->
cffi_nif:mem_read(Ptr, primitive_type(Type));
{enum, _, ToAtom} ->
Int = cffi_nif:mem_read(Ptr, int32),
maps:get(Int, ToAtom, Int);
{struct, _, _, _} ->
Ptr; %% return pointer to the sub-struct
{union, _, _, _} ->
Ptr;
{typedef, Inner} ->
read(Ptr, Inner)
end.
-spec write(reference(), term(), term()) -> ok | {error, term()}.
write(Ptr, Type, Value) ->
case cffi_type:lookup(resolve_typedef(Type)) of
not_found ->
cffi_nif:mem_write(Ptr, primitive_type(Type), Value);
{enum, ToInt, _} ->
Int = case Value of
A when is_atom(A) ->
case maps:find(A, ToInt) of
{ok, V} -> V;
error -> error({bad_enum_value, Type, A})
end;
N when is_integer(N) -> N
end,
cffi_nif:mem_write(Ptr, int32, Int);
{typedef, Inner} ->
write(Ptr, Inner, Value)
end.
-spec read_bytes(reference(), pos_integer()) -> binary().
read_bytes(Ptr, Size) -> cffi_nif:mem_read_bytes(Ptr, Size).
-spec write_bytes(reference(), binary()) -> ok.
write_bytes(Ptr, Bytes) -> cffi_nif:mem_write_bytes(Ptr, Bytes).
%% -------------------------------------------------------------------------
%% Struct / union field access
%% -------------------------------------------------------------------------
%% Return a borrowed pointer to a named field within a struct/union.
-spec field_ptr(reference(), atom(), atom()) -> reference().
field_ptr(Ptr, TypeName, FieldName) ->
case cffi_type:field_info(TypeName, FieldName) of
{_Type, Offset} -> cffi_nif:ptr_add(Ptr, Offset);
not_found -> error({unknown_field, TypeName, FieldName})
end.
-spec struct_read(reference(), atom(), atom()) -> term().
struct_read(Ptr, TypeName, FieldName) ->
case cffi_type:field_info(TypeName, FieldName) of
{FType, Offset} ->
FPtr = cffi_nif:ptr_add(Ptr, Offset),
read(FPtr, FType);
not_found ->
error({unknown_field, TypeName, FieldName})
end.
-spec struct_write(reference(), atom(), atom(), term()) -> ok.
struct_write(Ptr, TypeName, FieldName, Value) ->
case cffi_type:field_info(TypeName, FieldName) of
{FType, Offset} ->
FPtr = cffi_nif:ptr_add(Ptr, Offset),
write(FPtr, FType, Value);
not_found ->
error({unknown_field, TypeName, FieldName})
end.
%% Read all fields of a struct into a map #{field_name => value}.
-spec struct_to_map(reference(), atom()) -> map().
struct_to_map(Ptr, TypeName) ->
Fields = struct_fields(TypeName),
maps:from_list([
begin
FPtr = cffi_nif:ptr_add(Ptr, Offset),
{FName, read(FPtr, FType)}
end
|| {FName, FType, Offset} <- Fields
]).
%% Write map values into struct fields (unmentioned fields are unchanged).
-spec map_to_struct(reference(), atom(), map()) -> ok.
map_to_struct(Ptr, TypeName, Map) ->
Fields = struct_fields(TypeName),
lists:foreach(fun({FName, FType, Offset}) ->
case maps:find(FName, Map) of
{ok, Val} ->
FPtr = cffi_nif:ptr_add(Ptr, Offset),
write(FPtr, FType, Val);
error -> ok
end
end, Fields).
%% -------------------------------------------------------------------------
%% Array element access
%% -------------------------------------------------------------------------
-spec array_ptr(reference(), term(), non_neg_integer()) -> reference().
array_ptr(Ptr, ElemType, Index) ->
cffi_nif:ptr_add(Ptr, cffi_type:sizeof(ElemType) * Index).
-spec array_read(reference(), term(), non_neg_integer()) -> term().
array_read(Ptr, ElemType, Index) ->
read(array_ptr(Ptr, ElemType, Index), ElemType).
-spec array_write(reference(), term(), non_neg_integer(), term()) -> ok.
array_write(Ptr, ElemType, Index, Value) ->
write(array_ptr(Ptr, ElemType, Index), ElemType, Value).
%% -------------------------------------------------------------------------
%% Pointer utilities
%% -------------------------------------------------------------------------
-spec ptr_add(reference(), integer()) -> reference().
ptr_add(Ptr, Offset) -> cffi_nif:ptr_add(Ptr, Offset).
-spec null() -> reference().
null() -> cffi_nif:ptr_null().
-spec is_null(reference()) -> boolean().
is_null(Ptr) -> cffi_nif:ptr_is_null(Ptr).
-spec type_size(term()) -> non_neg_integer().
type_size(Type) -> cffi_type:sizeof(Type).
-spec align_of(term()) -> pos_integer().
align_of(Type) -> cffi_type:alignof(Type).
%% -------------------------------------------------------------------------
%% Scoped allocation
%% -------------------------------------------------------------------------
-spec with_alloc(pos_integer(), fun((reference()) -> R)) -> R.
with_alloc(Bytes, Fun) ->
Ptr = alloc(Bytes),
try Fun(Ptr)
after free(Ptr)
end.
-spec with_alloc(term(), pos_integer(), fun((reference()) -> R)) -> R.
with_alloc(Type, Count, Fun) ->
with_alloc(cffi_type:sizeof(Type) * Count, Fun).
%% -------------------------------------------------------------------------
%% Internal helpers
%% -------------------------------------------------------------------------
%% Resolve typedef chains to a base type name.
resolve_typedef(T) when is_atom(T) ->
case cffi_type:lookup(T) of
{typedef, Inner} -> resolve_typedef(Inner);
_ -> T
end;
resolve_typedef(T) -> T.
%% Map a type spec to the NIF-level primitive type atom.
%% Composites and registered types collapse to their NIF equivalent.
primitive_type({array, _, _}) -> pointer;
primitive_type({ptr, _}) -> pointer;
primitive_type(T) when is_atom(T) ->
case cffi_type:lookup(T) of
{struct, _, _, _} -> pointer;
{union, _, _, _} -> pointer;
{enum, _, _} -> int32;
{typedef, Inner} -> primitive_type(Inner);
not_found -> T %% assume primitive, NIF will validate
end.
%% For call/4: map user-facing type+value to NIF {type, value} pair.
resolve_arg_for_nif({Type, Value}) ->
NifType = resolve_type_for_nif(Type),
NifVal = resolve_value_for_nif(Type, Value),
{NifType, NifVal}.
resolve_type_for_nif(Type) ->
case resolve_typedef(Type) of
T when is_atom(T) ->
case cffi_type:lookup(T) of
{enum, _, _} -> int32;
{struct, _, _, _} -> pointer;
{union, _, _, _} -> pointer;
{typedef, Inner} -> resolve_type_for_nif(Inner);
_ -> T
end;
{array, _, _} -> pointer;
{ptr, _} -> pointer
end.
resolve_value_for_nif(Type, Value) ->
case cffi_type:lookup(resolve_typedef(Type)) of
{enum, ToInt, _} when is_atom(Value) ->
case maps:find(Value, ToInt) of
{ok, V} -> V;
error -> error({bad_enum_value, Type, Value})
end;
_ ->
Value
end.
%% For call/4 return: convert NIF value back to user-facing type.
unmarshal_ret(RetType, Val) ->
case cffi_type:lookup(resolve_typedef(RetType)) of
{enum, _, ToAtom} when is_integer(Val) ->
maps:get(Val, ToAtom, Val);
{typedef, Inner} ->
unmarshal_ret(Inner, Val);
_ ->
Val
end.
%% Extract field list from a registered struct/union.
struct_fields(TypeName) ->
case cffi_type:lookup(TypeName) of
{struct, _, _, Fields} -> Fields;
{union, _, _, Fields} -> Fields;
not_found -> error({unknown_type, TypeName})
end.
to_list(S) when is_list(S) -> S;
to_list(B) when is_binary(B) -> binary_to_list(B).