README.md

# 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

- **ABI mode (NIF)** — zero-copy calls via libffi in a NIF; fastest path
- **Port safe mode** — C code runs in a separate OS process; a crash cannot
  kill the BEAM VM
- **Rich type system** — structs, unions, enums, typedefs, arrays, nested types
- **C→Erlang callbacks** — pass Erlang closures as C function pointers
  (`qsort`, event hooks, …)
- **Variadic functions** — call `printf`/`snprintf`/… with `call_va/5`
- **Parse transform** — declare C bindings as module attributes, get typed
  wrappers generated at compile time

---

## Quick start

### Prerequisites

```
libffi-dev
```

### Build

```sh
rebar3 compile
```

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

### Basic call

```erlang
{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 |
|-------------|--------|
| `void`      | `void` |
| `bool`      | `_Bool` |
| `int8`      | `int8_t` |
| `uint8`     | `uint8_t` |
| `int16`     | `int16_t` |
| `uint16`    | `uint16_t` |
| `int32`     | `int32_t` |
| `uint32`    | `uint32_t` |
| `int64`     | `int64_t` |
| `uint64`    | `uint64_t` |
| `float`     | `float` |
| `double`    | `double` |
| `pointer`   | `void *` |
| `string`    | `const 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

```erlang
%% 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`)

```erlang
%% 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`)

```erlang
%% 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:**

```erlang
{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:**
- Each closure instance is not re-entrant (do not invoke it from two C threads simultaneously).
- The callback server process is linked to the spawning process; it dies when the spawning process dies.
- Errors in the Erlang fun are caught; the C call continues with return value `0`.

### 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.

```erlang
%% 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 |
| `alloc` | `cffi: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.

```erlang
-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:

```erlang
sqrt(X)       -> case cffi:call('$cffi_lib$'(), "sqrt",  double, [{double,X}]) of ...
pow(X, Y)     -> case cffi:call('$cffi_lib$'(), "pow",   double, [...]) of ...
cbrt_val(X)   -> case cffi:call('$cffi_lib$'(), "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 `...`:

```erlang
{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

```sh
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

- **Callbacks in port mode** — not yet implemented (requires a duplex protocol).
- **Variadic callbacks** — `cffi_callback` does not support variadic C signatures.
- **Windows** — not tested; the Makefile and rebar port_env only cover Linux/macOS.
- **Closures are not re-entrant** — each `cffi_callback` closure instance serialises calls through a single Erlang process.