Packages

Erlang CFFI — call C shared libraries from Erlang via libffi

Current section

Files

Jump to
cffi src cffi_transform.erl
Raw

src/cffi_transform.erl

%% cffi_transform.erl — Compile-time parse transform for cffi bindings.
%%
%% Usage: add to your module:
%%
%% -module(my_lib).
%% -compile({parse_transform, cffi_transform}).
%%
%% -cffi_lib("libm.so.6").
%% -cffi_fun({sqrt, double, [double]}).
%% -cffi_fun({pow, double, [double, double]}).
%% -cffi_fun({floor, double, [double]}).
%%
%% The transform generates:
%%
%% (1) A private '$cffi_lib$'/0 function that loads the library lazily
%% via persistent_term on first call.
%%
%% (2) One public wrapper function per -cffi_fun:
%% sqrt(Arg0) ->
%% case cffi:call('$cffi_lib$'(), "sqrt", double, [{double, Arg0}]) of
%% {ok, R} -> R;
%% {error, E} -> error(E)
%% end.
%%
%% (3) An -export attribute covering all generated functions.
%%
%% Attribute formats:
%%
%% -cffi_lib(LibPath).
%% LibPath : string, e.g. "libm.so.6"
%%
%% -cffi_fun({FuncName, RetType, [ArgType]}).
%% FuncName : atom — used as both Erlang name and C symbol
%% RetType : type atom (void | int32 | double | pointer | ...)
%% [ArgType]: list of type atoms
%%
%% -cffi_fun({{ErlName, "c_name"}, RetType, [ArgType]}).
%% ErlName : atom for the Erlang function
%% "c_name" : C symbol to look up via dlsym
-module(cffi_transform).
-export([parse_transform/2]).
parse_transform(Forms, _Opts) ->
LibPath = find_attr(cffi_lib, Forms),
FunSpecs = find_all_attr(cffi_fun, Forms),
case LibPath =:= undefined andalso FunSpecs =:= [] of
true -> Forms; %% nothing to do
false -> transform(Forms, LibPath, FunSpecs)
end.
transform(Forms, LibPath, RawSpecs) ->
ModName = find_module(Forms),
Specs = [parse_fun_spec(S) || S <- RawSpecs],
%% Validate
case LibPath =:= undefined andalso Specs =/= [] of
true -> error({cffi_transform, missing_cffi_lib, ModName});
false -> ok
end,
%% Strip cffi_* module attributes
Clean = [F || F <- Forms, not is_cffi_attr(F)],
%% Generate new forms
Generated =
gen_exports(Specs) ++
gen_lib_loader(ModName, LibPath) ++
[gen_fun_wrapper(S) || S <- Specs],
insert_before_funs(Clean, Generated).
%% -------------------------------------------------------------------------
%% Attribute extraction
%% -------------------------------------------------------------------------
find_module(Forms) ->
case [M || {attribute, _, module, M} <- Forms] of
[M | _] -> M;
[] -> undefined
end.
find_attr(Name, Forms) ->
case [V || {attribute, _, N, V} <- Forms, N =:= Name] of
[V | _] -> V;
[] -> undefined
end.
find_all_attr(Name, Forms) ->
[V || {attribute, _, N, V} <- Forms, N =:= Name].
is_cffi_attr({attribute, _, cffi_lib, _}) -> true;
is_cffi_attr({attribute, _, cffi_fun, _}) -> true;
is_cffi_attr(_) -> false.
%% -------------------------------------------------------------------------
%% Fun spec parsing
%%
%% Returns {ErlName :: atom, CName :: string, RetType :: atom, ArgTypes :: [atom]}
%% -------------------------------------------------------------------------
parse_fun_spec({Name, Ret, Args}) when is_atom(Name) ->
{Name, atom_to_list(Name), Ret, Args};
parse_fun_spec({{ErlName, CName}, Ret, Args})
when is_atom(ErlName), is_list(CName) ->
{ErlName, CName, Ret, Args}.
%% -------------------------------------------------------------------------
%% Code generation (produces Erlang AST forms)
%% -------------------------------------------------------------------------
%% Line 0 for all generated code (standard practice for transforms)
-define(L, 0).
gen_exports(Specs) ->
Exports = [{ErlName, length(ArgTypes)}
|| {ErlName, _CName, _Ret, ArgTypes} <- Specs],
case Exports of
[] -> [];
_ -> [attr(export, Exports)]
end.
%% Generate:
%% '$cffi_lib$'() ->
%% case persistent_term:get({cffi_lib, ModName}, undefined) of
%% {Lib_} -> Lib_;
%% undefined ->
%% {ok, Lib_} = cffi:load(LibPath),
%% persistent_term:put({cffi_lib, ModName}, {Lib_}),
%% Lib_
%% end.
gen_lib_loader(ModName, LibPath) ->
Key = tup([atm(cffi_lib), atm(ModName)]),
LoadedClause = clause(
[tup([var('Lib_')])],
[var('Lib_')]),
UndefClause = clause(
[atm(undefined)],
[match(tup([atm(ok), var('Lib_')]),
rcall(cffi, load, [str(LibPath)])),
rcall(persistent_term, put, [Key, tup([var('Lib_')])]),
var('Lib_')]),
Body = [{'case', ?L,
rcall(persistent_term, get, [Key, atm(undefined)]),
[LoadedClause, UndefClause]}],
[func('$cffi_lib$', 0, [clause([], Body)])].
%% Generate wrapper function for one -cffi_fun spec:
%% FuncName(Arg0, Arg1, ...) ->
%% case cffi:call('$cffi_lib$'(), "c_name", RetType,
%% [{T0, Arg0}, {T1, Arg1}, ...]) of
%% {ok, R_} -> R_;
%% {error, E_} -> error(E_)
%% end.
gen_fun_wrapper({ErlName, CName, RetType, ArgTypes}) ->
Arity = length(ArgTypes),
ArgVars = [var(arg_var(I)) || I <- lists:seq(0, Arity - 1)],
ArgPairs = mk_list([tup([atm(T), var(arg_var(I))])
|| {T, I} <- lists:zip(ArgTypes, lists:seq(0, Arity - 1))]),
CallExpr = rcall(cffi, call,
[lcall('$cffi_lib$', []),
str(CName),
atm(RetType),
ArgPairs]),
OkClause = clause([tup([atm(ok), var('R_')])], [var('R_')]),
ErrClause = clause([tup([atm(error), var('E_')])],
[rcall(erlang, error, [var('E_')])]),
Body = [{'case', ?L, CallExpr, [OkClause, ErrClause]}],
func(ErlName, Arity, [clause(ArgVars, Body)]).
arg_var(I) -> list_to_atom("Arg" ++ integer_to_list(I)).
%% -------------------------------------------------------------------------
%% AST construction helpers
%% -------------------------------------------------------------------------
atm(A) -> {atom, ?L, A}.
var(N) -> {var, ?L, N}.
str(S) -> {string, ?L, S}.
tup(Es) -> {tuple, ?L, Es}.
match(P, E) -> {match, ?L, P, E}.
mk_list([]) -> {nil, ?L};
mk_list([H | T]) -> {cons, ?L, H, mk_list(T)}.
lcall(F, Args) ->
{call, ?L, atm(F), Args}.
rcall(M, F, Args) ->
{call, ?L, {remote, ?L, atm(M), atm(F)}, Args}.
clause(Pats, Body) ->
{clause, ?L, Pats, [], Body}.
func(Name, Arity, Clauses) ->
{function, ?L, Name, Arity, Clauses}.
attr(Name, Value) ->
{attribute, ?L, Name, Value}.
%% -------------------------------------------------------------------------
%% Insert generated forms before the first function definition
%% -------------------------------------------------------------------------
insert_before_funs(Forms, NewForms) ->
{Before, After} =
lists:splitwith(fun(F) -> element(1, F) =/= function end, Forms),
Before ++ NewForms ++ After.