%% cffi_port.erl — Port-safe CFFI: C code runs in a separate OS process. %% %% API mirrors cffi.erl. Use this when safety > speed: %% a crash in the C library kills the port process, not the BEAM VM. %% %% Quick start: %% %% {ok, Lib} = cffi_port:load("libm.so.6"), %% {ok, 2.0} = cffi_port:call(Lib, "sqrt", double, [{double, 4.0}]), %% %% Ptr = cffi_port:alloc(Lib, 16), %% ok = cffi_port:write(Ptr, int32, 42), %% 42 = cffi_port:read(Ptr, int32), %% ok = cffi_port:free(Ptr). %% %% Key differences from cffi.erl: %% - alloc/2 takes the Lib handle (to know which port process to use) %% - Pointers are {port_ptr, Pid, Addr} — opaque, not NIF resources %% - Library handles are {port_lib, Pid, Handle} tuples %% - One OS process per cffi_port:load/1 call (isolated per library) %% - If the port crashes: call returns {error, port_crashed} %% %% Type resolution (enum/typedef) is handled the same as cffi.erl via cffi_type. -module(cffi_port). -behaviour(gen_server). %% Public API -export([ load/1, close/1, lib_pid/1, lib_port/1, call/3, call/4, call_va/5, alloc/2, alloc_type/2, alloc_type/3, alloc_struct/2, free/1, read/2, write/3, read_bytes/2, write_bytes/2, ptr_add/2, null/1, is_null/1, field_ptr/3, struct_read/3, struct_write/4, struct_to_map/2, map_to_struct/3, array_ptr/3, array_read/3, array_write/4, with_alloc/3, with_alloc/4 ]). %% gen_server -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% ------------------------------------------------------------------------- %% Wire protocol constants (must match cffi_port.c) %% ------------------------------------------------------------------------- -define(OP_LOAD_LIB, 1). -define(OP_CALL, 2). -define(OP_ALLOC, 3). -define(OP_FREE, 4). -define(OP_READ, 5). -define(OP_WRITE, 6). -define(OP_READ_BYTES, 7). -define(OP_WRITE_BYTES, 8). -define(OP_CALL_VA, 9). -define(PT_VOID, 0). -define(PT_INT8, 1). -define(PT_UINT8, 2). -define(PT_INT16, 3). -define(PT_UINT16, 4). -define(PT_INT32, 5). -define(PT_UINT32, 6). -define(PT_INT64, 7). -define(PT_UINT64, 8). -define(PT_FLOAT, 9). -define(PT_DOUBLE, 10). -define(PT_POINTER, 11). -define(PT_STRING, 12). -define(PT_BOOL, 13). -define(STATUS_OK, 0). -define(STATUS_ERR, 1). %% ------------------------------------------------------------------------- %% State %% ------------------------------------------------------------------------- -record(state, { port :: port(), seq :: non_neg_integer(), %% {From, SeqNum, RespKind} where RespKind = typed | raw_bytes waiting :: undefined | {gen_server:from(), non_neg_integer(), typed | raw_bytes} }). -type port_lib() :: {port_lib, pid(), non_neg_integer()}. -type port_ptr() :: {port_ptr, pid(), non_neg_integer()}. %% ------------------------------------------------------------------------- %% Public API %% ------------------------------------------------------------------------- %% load/1 — starts a dedicated port process for this library. -spec load(string() | binary()) -> {ok, port_lib()} | {error, term()}. load(Path) -> case gen_server:start(?MODULE, [], []) of {ok, Pid} -> case gen_server:call(Pid, {load_lib, to_list(Path)}, 30000) of {ok, Handle} -> {ok, {port_lib, Pid, Handle}}; {error, _} = Err -> gen_server:stop(Pid), Err end; {error, _} = Err -> Err end. %% close/1 — stop the gen_server (and the port subprocess). -spec close(port_lib()) -> ok. close({port_lib, Pid, _}) -> gen_server:stop(Pid). %% lib_pid/1, lib_port/1 — introspection helpers (mainly for tests). -spec lib_pid(port_lib()) -> pid(). lib_pid({port_lib, Pid, _}) -> Pid. -spec lib_port(port_lib()) -> port(). lib_port({port_lib, Pid, _}) -> gen_server:call(Pid, get_port, 5000). -spec call(port_lib(), string() | binary(), term()) -> {ok, term()} | {error, term()}. call(Lib, Func, RetType) -> call(Lib, Func, RetType, []). -spec call(port_lib(), string() | binary(), term(), list()) -> {ok, term()} | {error, term()}. call({port_lib, Pid, Handle}, Func, RetType, Args) -> NifRetType = resolve_type(RetType), NifArgs = [resolve_arg(A) || A <- Args], Req = {call, Handle, to_list(Func), NifRetType, NifArgs}, try gen_server:call(Pid, Req, 30000) of {ok, Val} -> {ok, unmarshal_ret(Pid, RetType, Val)}; Err -> Err catch exit:{noproc, _} -> {error, port_crashed}; exit:{normal, _} -> {error, port_crashed}; exit:{{port_crashed, _}, _} -> {error, port_crashed} end. %% call_va/5 — variadic C function call (port safe mode). %% %% NFixed: count of fixed (non-variadic) arguments. %% Variadic float args are promoted to double automatically. -spec call_va(port_lib(), string() | binary(), term(), pos_integer(), list()) -> {ok, term()} | {error, term()}. call_va({port_lib, Pid, Handle}, Func, RetType, NFixed, Args) when is_integer(NFixed), NFixed >= 1 -> NifRetType = resolve_type(RetType), NifArgs = [resolve_arg(A) || A <- Args], Req = {call_va, Handle, to_list(Func), NifRetType, NFixed, NifArgs}, try gen_server:call(Pid, Req, 30000) of {ok, Val} -> {ok, unmarshal_ret(Pid, RetType, Val)}; Err -> Err catch exit:{noproc, _} -> {error, port_crashed}; exit:{normal, _} -> {error, port_crashed}; exit:{{port_crashed, _}, _} -> {error, port_crashed} end. -spec alloc(port_lib(), pos_integer()) -> port_ptr(). alloc({port_lib, Pid, _}, Bytes) when is_integer(Bytes), Bytes > 0 -> {ok, Addr} = gen_server:call(Pid, {alloc, Bytes}, 10000), {port_ptr, Pid, Addr}. -spec alloc_type(port_lib(), term()) -> port_ptr(). alloc_type(Lib, Type) -> alloc_type(Lib, Type, 1). -spec alloc_type(port_lib(), term(), pos_integer()) -> port_ptr(). alloc_type(Lib, Type, Count) -> alloc(Lib, cffi_type:sizeof(Type) * Count). -spec alloc_struct(port_lib(), atom()) -> port_ptr(). alloc_struct(Lib, StructName) -> alloc(Lib, cffi_type:sizeof(StructName)). -spec free(port_ptr()) -> ok. free({port_ptr, Pid, Addr}) -> gen_server:call(Pid, {free, Addr}, 10000). -spec read(port_ptr(), term()) -> term(). read({port_ptr, Pid, Addr} = Ptr, Type) -> case cffi_type:lookup(resolve_typedef(Type)) of not_found -> {ok, Val} = gen_server:call(Pid, {read, Addr, type_code(Type)}, 10000), Val; {enum, _, ToAtom} -> {ok, Int} = gen_server:call(Pid, {read, Addr, ?PT_INT32}, 10000), maps:get(Int, ToAtom, Int); {struct, _, _, _} -> Ptr; {union, _, _, _} -> Ptr; {typedef, Inner} -> read(Ptr, Inner) end. -spec write(port_ptr(), term(), term()) -> ok. write({port_ptr, Pid, Addr} = Ptr, Type, Value) -> case cffi_type:lookup(resolve_typedef(Type)) of not_found -> gen_server:call(Pid, {write, Addr, type_code(Type), Value}, 10000); {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, gen_server:call(Pid, {write, Addr, ?PT_INT32, Int}, 10000); {typedef, Inner} -> write(Ptr, Inner, Value) end. -spec read_bytes(port_ptr(), pos_integer()) -> binary(). read_bytes({port_ptr, Pid, Addr}, Size) -> {ok, Data} = gen_server:call(Pid, {read_bytes, Addr, Size}, 10000), Data. -spec write_bytes(port_ptr(), binary()) -> ok. write_bytes({port_ptr, Pid, Addr}, Bytes) -> gen_server:call(Pid, {write_bytes, Addr, Bytes}, 10000). -spec ptr_add(port_ptr(), integer()) -> port_ptr(). ptr_add({port_ptr, Pid, Addr}, Offset) -> {port_ptr, Pid, Addr + Offset}. -spec null(port_lib()) -> port_ptr(). null({port_lib, Pid, _}) -> {port_ptr, Pid, 0}. -spec is_null(port_ptr()) -> boolean(). is_null({port_ptr, _, 0}) -> true; is_null({port_ptr, _, _}) -> false. %% ------------------------------------------------------------------------- %% Struct / union field access (same logic as cffi.erl, uses local ops) %% ------------------------------------------------------------------------- -spec field_ptr(port_ptr(), atom(), atom()) -> port_ptr(). field_ptr(Ptr, TypeName, FieldName) -> case cffi_type:field_info(TypeName, FieldName) of {_Type, Offset} -> ptr_add(Ptr, Offset); not_found -> error({unknown_field, TypeName, FieldName}) end. -spec struct_read(port_ptr(), atom(), atom()) -> term(). struct_read(Ptr, TypeName, FieldName) -> case cffi_type:field_info(TypeName, FieldName) of {FType, Offset} -> read(ptr_add(Ptr, Offset), FType); not_found -> error({unknown_field, TypeName, FieldName}) end. -spec struct_write(port_ptr(), atom(), atom(), term()) -> ok. struct_write(Ptr, TypeName, FieldName, Value) -> case cffi_type:field_info(TypeName, FieldName) of {FType, Offset} -> write(ptr_add(Ptr, Offset), FType, Value); not_found -> error({unknown_field, TypeName, FieldName}) end. -spec struct_to_map(port_ptr(), atom()) -> map(). struct_to_map(Ptr, TypeName) -> Fields = struct_fields(TypeName), maps:from_list([{FName, read(ptr_add(Ptr, Off), FType)} || {FName, FType, Off} <- Fields]). -spec map_to_struct(port_ptr(), atom(), map()) -> ok. map_to_struct(Ptr, TypeName, Map) -> Fields = struct_fields(TypeName), lists:foreach(fun({FName, FType, Off}) -> case maps:find(FName, Map) of {ok, V} -> write(ptr_add(Ptr, Off), FType, V); error -> ok end end, Fields). %% ------------------------------------------------------------------------- %% Array access %% ------------------------------------------------------------------------- -spec array_ptr(port_ptr(), term(), non_neg_integer()) -> port_ptr(). array_ptr(Ptr, ElemType, Index) -> ptr_add(Ptr, cffi_type:sizeof(ElemType) * Index). -spec array_read(port_ptr(), term(), non_neg_integer()) -> term(). array_read(Ptr, ElemType, Index) -> read(array_ptr(Ptr, ElemType, Index), ElemType). -spec array_write(port_ptr(), term(), non_neg_integer(), term()) -> ok. array_write(Ptr, ElemType, Index, Value) -> write(array_ptr(Ptr, ElemType, Index), ElemType, Value). %% ------------------------------------------------------------------------- %% Scoped allocation %% ------------------------------------------------------------------------- -spec with_alloc(port_lib(), pos_integer(), fun((port_ptr()) -> R)) -> R. with_alloc(Lib, Bytes, Fun) -> Ptr = alloc(Lib, Bytes), try Fun(Ptr) after free(Ptr) end. -spec with_alloc(port_lib(), term(), pos_integer(), fun((port_ptr()) -> R)) -> R. with_alloc(Lib, Type, Count, Fun) -> with_alloc(Lib, cffi_type:sizeof(Type) * Count, Fun). %% ------------------------------------------------------------------------- %% gen_server callbacks %% ------------------------------------------------------------------------- init([]) -> PrivDir = code:priv_dir(cffi), Exe = filename:join(PrivDir, "cffi_port"), Port = open_port({spawn_executable, Exe}, [{packet, 4}, binary, exit_status, use_stdio]), {ok, #state{port = Port, seq = 0, waiting = undefined}}. handle_call(get_port, _From, S) -> {reply, S#state.port, S}; handle_call(Req, From, #state{waiting = undefined} = S) -> {Payload, Kind, S1} = encode_request(Req, S), port_command(S1#state.port, Payload), SeqNum = S#state.seq, {noreply, S1#state{waiting = {From, SeqNum, Kind}}}; handle_call(Req, From, #state{waiting = _} = S) -> %% Serialise: queue by re-enqueueing after a tiny delay. %% For production use, a proper queue would be better. erlang:send_after(1, self(), {retry, Req, From}), {noreply, S}. handle_cast(_Msg, S) -> {noreply, S}. handle_info({Port, {data, Data}}, #state{port = Port, waiting = {From, _Seq, Kind}} = S) -> Result = case Kind of raw_bytes -> decode_raw_response(Data); typed -> decode_response(Data) end, gen_server:reply(From, Result), {noreply, S#state{waiting = undefined}}; handle_info({retry, Req, From}, S) -> %% Retry after previous request completed handle_call(Req, From, S); handle_info({Port, {exit_status, Status}}, #state{port = Port, waiting = W} = S) -> case W of {From, _, _} -> gen_server:reply(From, {error, {port_crashed, Status}}); undefined -> ok end, {stop, {port_crashed, Status}, S#state{waiting = undefined}}; handle_info({'EXIT', Port, Reason}, #state{port = Port, waiting = W} = S) -> case W of {From, _, _} -> gen_server:reply(From, {error, {port_exited, Reason}}); undefined -> ok end, {stop, {port_exited, Reason}, S#state{waiting = undefined}}; handle_info(_Info, S) -> {noreply, S}. terminate(_Reason, #state{port = Port}) -> catch port_close(Port), ok. code_change(_OldVsn, S, _Extra) -> {ok, S}. %% ------------------------------------------------------------------------- %% Request encoding %% ------------------------------------------------------------------------- encode_request({load_lib, Path}, S) -> PBin = list_to_binary(Path), Len = byte_size(PBin), Payload = <<(S#state.seq):32, ?OP_LOAD_LIB:8, Len:16, PBin/binary>>, {Payload, typed, S#state{seq = S#state.seq + 1}}; encode_request({call, Handle, FuncName, RetType, Args}, S) -> FBin = list_to_binary(FuncName), FLen = byte_size(FBin), Nargs = length(Args), ArgsBin = iolist_to_binary([encode_typed(T, V) || {T, V} <- Args]), RetCode = type_code(RetType), Payload = <<(S#state.seq):32, ?OP_CALL:8, Handle:64, FLen:16, FBin/binary, RetCode:8, Nargs:16, ArgsBin/binary>>, {Payload, typed, S#state{seq = S#state.seq + 1}}; encode_request({call_va, Handle, FuncName, RetType, NFixed, Args}, S) -> FBin = list_to_binary(FuncName), FLen = byte_size(FBin), Nargs = length(Args), ArgsBin = iolist_to_binary([encode_typed(T, V) || {T, V} <- Args]), RetCode = type_code(RetType), Payload = <<(S#state.seq):32, ?OP_CALL_VA:8, Handle:64, FLen:16, FBin/binary, RetCode:8, NFixed:16, Nargs:16, ArgsBin/binary>>, {Payload, typed, S#state{seq = S#state.seq + 1}}; encode_request({alloc, Bytes}, S) -> Payload = <<(S#state.seq):32, ?OP_ALLOC:8, Bytes:64>>, {Payload, typed, S#state{seq = S#state.seq + 1}}; encode_request({free, Addr}, S) -> Payload = <<(S#state.seq):32, ?OP_FREE:8, Addr:64>>, {Payload, typed, S#state{seq = S#state.seq + 1}}; encode_request({read, Addr, TypeCode}, S) -> Payload = <<(S#state.seq):32, ?OP_READ:8, Addr:64, TypeCode:8>>, {Payload, typed, S#state{seq = S#state.seq + 1}}; encode_request({write, Addr, TypeCode, Value}, S) -> ValBin = encode_value(TypeCode, Value), Payload = <<(S#state.seq):32, ?OP_WRITE:8, Addr:64, TypeCode:8, ValBin/binary>>, {Payload, typed, S#state{seq = S#state.seq + 1}}; encode_request({read_bytes, Addr, Size}, S) -> Payload = <<(S#state.seq):32, ?OP_READ_BYTES:8, Addr:64, Size:32>>, {Payload, raw_bytes, S#state{seq = S#state.seq + 1}}; encode_request({write_bytes, Addr, Bytes}, S) -> Len = byte_size(Bytes), Payload = <<(S#state.seq):32, ?OP_WRITE_BYTES:8, Addr:64, Len:32, Bytes/binary>>, {Payload, typed, S#state{seq = S#state.seq + 1}}. %% Encode {TypeAtom, Value} as <> encode_typed(Type, Value) -> Code = type_code(Type), <>. encode_value(?PT_VOID, _) -> <<>>; encode_value(?PT_BOOL, true) -> <<1:8>>; encode_value(?PT_BOOL, false) -> <<0:8>>; encode_value(?PT_BOOL, N) -> <<(N band 1):8>>; encode_value(?PT_INT8, V) -> <>; encode_value(?PT_UINT8, V) -> <>; encode_value(?PT_INT16, V) -> <>; encode_value(?PT_UINT16, V) -> <>; encode_value(?PT_INT32, V) -> <>; encode_value(?PT_UINT32, V) -> <>; encode_value(?PT_INT64, V) -> <>; encode_value(?PT_UINT64, V) -> <>; encode_value(?PT_FLOAT, V) -> <>; encode_value(?PT_DOUBLE, V) -> <>; encode_value(?PT_POINTER, null) -> <<0:64>>; encode_value(?PT_POINTER, {port_ptr, _, Addr}) -> <>; encode_value(?PT_STRING, null) -> <<16#FFFFFFFF:32>>; encode_value(?PT_STRING, B) when is_binary(B) -> <<(byte_size(B)):32, B/binary>>; encode_value(?PT_STRING, L) when is_list(L) -> B = list_to_binary(L), <<(byte_size(B)):32, B/binary>>. %% ------------------------------------------------------------------------- %% Response decoding %% ------------------------------------------------------------------------- decode_response(<<_Seq:32, ?STATUS_ERR:8, Msg/binary>>) -> {error, Msg}; decode_response(<<_Seq:32, ?STATUS_OK:8>>) -> ok; decode_response(<<_Seq:32, ?STATUS_OK:8, Handle:64>>) -> {ok, Handle}; decode_response(<<_Seq:32, ?STATUS_OK:8, TypeCode:8, Rest/binary>>) -> {ok, decode_typed(TypeCode, Rest)}; decode_response(<<_Seq:32, ?STATUS_OK:8, Data/binary>>) -> {ok, Data}. %% raw_bytes: the C op_read_bytes sends <> %% The raw data must NOT be interpreted as a typed value. decode_raw_response(<<_Seq:32, ?STATUS_ERR:8, Msg/binary>>) -> {error, Msg}; decode_raw_response(<<_Seq:32, ?STATUS_OK:8, Data/binary>>) -> {ok, Data}. decode_typed(?PT_VOID, _) -> ok; decode_typed(?PT_BOOL, <>) -> V =/= 0; decode_typed(?PT_INT8, <>) -> V; decode_typed(?PT_UINT8, <>) -> V; decode_typed(?PT_INT16, <>) -> V; decode_typed(?PT_UINT16, <>) -> V; decode_typed(?PT_INT32, <>) -> V; decode_typed(?PT_UINT32, <>) -> V; decode_typed(?PT_INT64, <>) -> V; decode_typed(?PT_UINT64, <>) -> V; decode_typed(?PT_FLOAT, <>) -> V; decode_typed(?PT_DOUBLE, <>) -> V; decode_typed(?PT_POINTER, <<0:64, _/binary>>) -> null; decode_typed(?PT_POINTER, <>) -> {raw_ptr, Addr}; decode_typed(?PT_STRING, <<16#FFFFFFFF:32/unsigned-big, _/binary>>) -> null; decode_typed(?PT_STRING, <>) -> Data; decode_typed(_, _) -> undefined. %% ------------------------------------------------------------------------- %% Type helpers (enum/typedef resolution, same logic as cffi.erl) %% ------------------------------------------------------------------------- type_code(void) -> ?PT_VOID; type_code(bool) -> ?PT_BOOL; type_code(int8) -> ?PT_INT8; type_code(uint8) -> ?PT_UINT8; type_code(int16) -> ?PT_INT16; type_code(uint16) -> ?PT_UINT16; type_code(int32) -> ?PT_INT32; type_code(uint32) -> ?PT_UINT32; type_code(int64) -> ?PT_INT64; type_code(uint64) -> ?PT_UINT64; type_code(float) -> ?PT_FLOAT; type_code(double) -> ?PT_DOUBLE; type_code(pointer) -> ?PT_POINTER; type_code(string) -> ?PT_STRING; type_code(T) -> case cffi_type:lookup(T) of {enum, _, _} -> ?PT_INT32; {typedef, Inner} -> type_code(Inner); _ -> ?PT_POINTER %% structs/unions passed as pointer end. resolve_typedef(T) when is_atom(T) -> case cffi_type:lookup(T) of {typedef, Inner} -> resolve_typedef(Inner); _ -> T end; resolve_typedef(T) -> T. resolve_type(Type) -> case resolve_typedef(Type) of T when is_atom(T) -> case cffi_type:lookup(T) of {enum, _, _} -> int32; {typedef, Inner} -> resolve_type(Inner); _ -> T end; T -> T end. resolve_arg({Type, Value}) -> NifType = resolve_type(Type), NifVal = 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, {NifType, NifVal}. unmarshal_ret(Pid, 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(Pid, Inner, Val); _ -> %% Wrap raw pointer addresses returned by C as port_ptr case Val of {raw_ptr, Addr} -> {port_ptr, Pid, Addr}; Other -> Other end end. 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).