Current section

Files

Jump to
libero src libero_ffi.erl
Raw

src/libero_ffi.erl

%% Libero RPC panic-catching FFI.
%%
%% try_call(F) runs the zero-arg function F and returns {ok, Result}
%% on success, or {error, ReasonBinary} if the function panics or
%% throws. The reason is stringified so the caller can log it
%% alongside a trace_id without pattern-matching on arbitrary
%% Erlang term shapes.
-module(libero_ffi).
-export([try_call/1, encode/1, decode/1, decode_safe/1, identity/1, trap_signals/0, unwrap_msg_from_server/1]).
identity(X) -> X.
encode(Term) ->
erlang:term_to_binary(Term).
decode(Bin) ->
erlang:binary_to_term(Bin, [safe]).
decode_safe(Bin) ->
try erlang:binary_to_term(Bin, [safe]) of
Term -> {ok, Term}
catch
_:Reason ->
Msg = erlang:iolist_to_binary(
io_lib:format("~p", [Reason])
),
{error, {decode_error, Msg}}
end.
%% Install signal handlers so libero exits cleanly when its parent
%% build script is killed (Ctrl-C, SIGTERM from sandbox, etc.).
%% Without this, a stuck or in-progress libero process can survive
%% its parent and spin at 99% CPU.
trap_signals() ->
os:set_signal(sigterm, handle),
os:set_signal(sighup, handle),
spawn(fun signal_loop/0),
nil.
signal_loop() ->
receive
{signal, sigterm} -> erlang:halt(1);
{signal, sighup} -> erlang:halt(1);
_Other -> signal_loop()
end.
%% Strip the MsgFromServer envelope from a dispatch response.
%%
%% Every MsgFromServer variant carries exactly one field - the response
%% payload. In Erlang a Gleam custom type variant `Foo(payload)` compiles
%% to the tuple `{foo, Payload}`, so element(2, ...) extracts the payload.
%% The variant tag is redundant for request/response (FIFO matching on
%% the client pairs each response to its request), so dispatch strips it
%% before encoding to give the client a cleaner wire shape:
%% `Result(payload, RpcError)` instead of
%% `Result(MsgFromServer.SomeVariant(payload), RpcError)`.
%%
%% Push messages take a different code path that keeps the envelope so
%% the client can route by variant.
%%
%% 0-arity MsgFromServer variants compile to bare atoms in Erlang and
%% have no payload - we send `nil` (Gleam's Nil) so the client receives
%% a typed empty acknowledgment.
unwrap_msg_from_server(Tuple) when is_tuple(Tuple), tuple_size(Tuple) >= 2 ->
element(2, Tuple);
unwrap_msg_from_server(Atom) when is_atom(Atom) ->
nil;
unwrap_msg_from_server(Other) ->
Other.
try_call(F) ->
try F() of
Result -> {ok, Result}
catch
Class:Reason:Stacktrace ->
Message = io_lib:format(
"~p: ~p~nstacktrace: ~p",
[Class, Reason, Stacktrace]
),
{error, erlang:iolist_to_binary(Message)}
end.