Packages

Erlang CFFI — call C shared libraries from Erlang via libffi

Current section

Files

Jump to
cffi c_src cffi_nif.c
Raw

c_src/cffi_nif.c

/*
* cffi_nif.c — Erlang CFFI NIF core
*
* Resource types:
* cffi_lib dlopen handle (GC → dlclose)
* cffi_ptr void* wrapper (owned → GC free; borrowed → no-op)
* May hold a "parent" reference to an cffi_cb keeping it alive.
* cffi_cb libffi closure + pthread sync state (GC → ffi_closure_free)
*
* Callback protocol (C→Erlang):
* 1. cffi_nif:callback_new(RetType, ArgTypes, ServerPid)
* → {ok, {CbHandle :: resource, FuncPtr :: resource}}
* 2. When C calls FuncPtr, the trampoline:
* a. marshals C args → Erlang terms
* b. sends {cffi_callback, CbId :: uint64, ArgList} to ServerPid
* c. blocks on pthread condvar
* 3. Server process calls Fun(Args...) and replies via:
* cffi_nif:callback_return(CbId, RetVal)
* 4. Trampoline wakes, fills C return buffer, returns to C.
*
* Limitation: callbacks are not re-entrant per closure instance.
*/
#include <erl_nif.h>
#include <ffi.h>
#include <dlfcn.h>
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
/* =========================================================
* Resource type declarations
* ========================================================= */
static ErlNifResourceType *g_lib_rtype = NULL;
static ErlNifResourceType *g_ptr_rtype = NULL;
static ErlNifResourceType *g_cb_rtype = NULL;
/* Library handle */
typedef struct {
void *handle;
} cffi_lib_t;
/* Generic C pointer, optionally owning a "parent" cffi_cb resource */
typedef struct {
void *ptr;
int owned; /* 1 → free on GC; 0 → borrowed */
size_t size;
void *parent; /* if non-NULL: an cffi_cb_t* kept alive via enif_keep_resource */
} cffi_ptr_t;
/* Callback closure + synchronization state */
typedef struct {
ffi_closure *closure;
void *code_ptr; /* the callable function pointer given to C */
ffi_cif cif;
ffi_type **arg_types; /* heap-allocated array, length = nargs */
unsigned nargs;
/* Type info needed by the trampoline */
int *arg_tids; /* cffi_tid per arg */
int ret_tid; /* cffi_tid for return */
/* Erlang callback server process */
ErlNifPid server_pid;
/* Synchronization: C trampoline blocks until Erlang replies */
pthread_mutex_t lock;
pthread_cond_t cond;
int waiting; /* 1 = trampoline is blocked */
uint8_t ret_buf[16];/* return value written by callback_return */
} cffi_cb_t;
/* =========================================================
* Destructors
* ========================================================= */
static void lib_dtor(ErlNifEnv *env, void *obj) {
(void)env;
cffi_lib_t *lib = (cffi_lib_t *)obj;
if (lib->handle) { dlclose(lib->handle); lib->handle = NULL; }
}
static void ptr_dtor(ErlNifEnv *env, void *obj) {
(void)env;
cffi_ptr_t *p = (cffi_ptr_t *)obj;
if (p->owned && p->ptr) { free(p->ptr); p->ptr = NULL; p->owned = 0; }
if (p->parent) { enif_release_resource(p->parent); p->parent = NULL; }
}
static void cb_dtor(ErlNifEnv *env, void *obj) {
(void)env;
cffi_cb_t *cb = (cffi_cb_t *)obj;
if (cb->closure) { ffi_closure_free(cb->closure); cb->closure = NULL; }
free(cb->arg_types);
free(cb->arg_tids);
pthread_mutex_destroy(&cb->lock);
pthread_cond_destroy(&cb->cond);
}
/* =========================================================
* Atom cache
* ========================================================= */
static ERL_NIF_TERM am_ok, am_error, am_true, am_false, am_null;
static ERL_NIF_TERM am_void, am_bool;
static ERL_NIF_TERM am_int8, am_uint8, am_int16, am_uint16;
static ERL_NIF_TERM am_int32, am_uint32, am_int64, am_uint64;
static ERL_NIF_TERM am_float, am_double, am_pointer, am_string;
static ERL_NIF_TERM am_not_found, am_bad_type, am_ffi_error;
static ERL_NIF_TERM am_not_owner, am_alloc_failed, am_library_closed;
static ERL_NIF_TERM am_bad_nfixed;
static ERL_NIF_TERM am_cffi_callback; /* message tag for callbacks */
#define MK_ATOM(n_) am_##n_ = enif_make_atom(env, #n_)
static void init_atoms(ErlNifEnv *env) {
MK_ATOM(ok); MK_ATOM(error); MK_ATOM(true); MK_ATOM(false);
MK_ATOM(null); MK_ATOM(void); MK_ATOM(bool);
MK_ATOM(int8); MK_ATOM(uint8);
MK_ATOM(int16); MK_ATOM(uint16);
MK_ATOM(int32); MK_ATOM(uint32);
MK_ATOM(int64); MK_ATOM(uint64);
MK_ATOM(float); MK_ATOM(double);
MK_ATOM(pointer); MK_ATOM(string);
am_not_found = enif_make_atom(env, "not_found");
am_bad_type = enif_make_atom(env, "bad_type");
am_ffi_error = enif_make_atom(env, "ffi_error");
am_bad_nfixed = enif_make_atom(env, "bad_nfixed");
am_not_owner = enif_make_atom(env, "not_owner");
am_alloc_failed = enif_make_atom(env, "alloc_failed");
am_library_closed = enif_make_atom(env, "library_closed");
am_cffi_callback = enif_make_atom(env, "cffi_callback");
}
/* =========================================================
* Type system
* ========================================================= */
typedef enum {
T_VOID = 0,
T_BOOL,
T_INT8, T_UINT8,
T_INT16, T_UINT16,
T_INT32, T_UINT32,
T_INT64, T_UINT64,
T_FLOAT, T_DOUBLE,
T_POINTER,
T_STRING,
T_UNKNOWN = -1
} cffi_tid;
static cffi_tid atom_to_tid(ERL_NIF_TERM a) {
if (enif_is_identical(a, am_void)) return T_VOID;
if (enif_is_identical(a, am_bool)) return T_BOOL;
if (enif_is_identical(a, am_int8)) return T_INT8;
if (enif_is_identical(a, am_uint8)) return T_UINT8;
if (enif_is_identical(a, am_int16)) return T_INT16;
if (enif_is_identical(a, am_uint16)) return T_UINT16;
if (enif_is_identical(a, am_int32)) return T_INT32;
if (enif_is_identical(a, am_uint32)) return T_UINT32;
if (enif_is_identical(a, am_int64)) return T_INT64;
if (enif_is_identical(a, am_uint64)) return T_UINT64;
if (enif_is_identical(a, am_float)) return T_FLOAT;
if (enif_is_identical(a, am_double)) return T_DOUBLE;
if (enif_is_identical(a, am_pointer)) return T_POINTER;
if (enif_is_identical(a, am_string)) return T_STRING;
return T_UNKNOWN;
}
static ffi_type *tid_to_ffi(cffi_tid t) {
switch (t) {
case T_VOID: return &ffi_type_void;
case T_BOOL: return &ffi_type_uint8;
case T_INT8: return &ffi_type_sint8;
case T_UINT8: return &ffi_type_uint8;
case T_INT16: return &ffi_type_sint16;
case T_UINT16: return &ffi_type_uint16;
case T_INT32: return &ffi_type_sint32;
case T_UINT32: return &ffi_type_uint32;
case T_INT64: return &ffi_type_sint64;
case T_UINT64: return &ffi_type_uint64;
case T_FLOAT: return &ffi_type_float;
case T_DOUBLE: return &ffi_type_double;
case T_POINTER: return &ffi_type_pointer;
case T_STRING: return &ffi_type_pointer;
default: return NULL;
}
}
static size_t tid_size(cffi_tid t) {
switch (t) {
case T_VOID: return 0;
case T_BOOL:
case T_INT8: case T_UINT8: return 1;
case T_INT16: case T_UINT16: return 2;
case T_INT32: case T_UINT32: return 4;
case T_INT64: case T_UINT64: return 8;
case T_FLOAT: return sizeof(float);
case T_DOUBLE: return sizeof(double);
case T_POINTER:
case T_STRING: return sizeof(void *);
default: return 0;
}
}
/* =========================================================
* Marshaling
* ========================================================= */
/*
* marshal_arg: Erlang term → raw C bytes in dst (8-byte buffer).
* For T_STRING: allocates null-terminated copy → *str_out (caller frees).
* Returns 0 on success, -1 on type mismatch.
*/
static int marshal_arg(ErlNifEnv *env, cffi_tid tid,
ERL_NIF_TERM val, void *dst, char **str_out) {
ErlNifSInt64 i64;
ErlNifUInt64 u64;
double dbl;
cffi_ptr_t *p;
ErlNifBinary bin;
*str_out = NULL;
switch (tid) {
case T_BOOL:
if (!enif_get_uint64(env, val, &u64)) {
if (enif_is_identical(val, am_true)) u64 = 1;
else if (enif_is_identical(val, am_false)) u64 = 0;
else return -1;
}
*(uint8_t *)dst = (uint8_t)(u64 ? 1 : 0); return 0;
case T_INT8:
if (!enif_get_int64(env, val, &i64)) return -1;
*(int8_t *)dst = (int8_t)i64; return 0;
case T_UINT8:
if (!enif_get_uint64(env, val, &u64)) return -1;
*(uint8_t *)dst = (uint8_t)u64; return 0;
case T_INT16:
if (!enif_get_int64(env, val, &i64)) return -1;
*(int16_t *)dst = (int16_t)i64; return 0;
case T_UINT16:
if (!enif_get_uint64(env, val, &u64)) return -1;
*(uint16_t*)dst = (uint16_t)u64; return 0;
case T_INT32:
if (!enif_get_int64(env, val, &i64)) return -1;
*(int32_t *)dst = (int32_t)i64; return 0;
case T_UINT32:
if (!enif_get_uint64(env, val, &u64)) return -1;
*(uint32_t*)dst = (uint32_t)u64; return 0;
case T_INT64:
if (!enif_get_int64(env, val, &i64)) return -1;
*(int64_t *)dst = i64; return 0;
case T_UINT64:
if (!enif_get_uint64(env, val, &u64)) return -1;
*(uint64_t*)dst = u64; return 0;
case T_FLOAT:
if (!enif_get_double(env, val, &dbl)) return -1;
*(float *)dst = (float)dbl; return 0;
case T_DOUBLE:
if (!enif_get_double(env, val, &dbl)) return -1;
*(double *)dst = dbl; return 0;
case T_POINTER:
if (enif_is_identical(val, am_null)) { *(void **)dst = NULL; return 0; }
if (!enif_get_resource(env, val, g_ptr_rtype, (void **)&p)) return -1;
*(void **)dst = p->ptr; return 0;
case T_STRING:
if (enif_is_identical(val, am_null)) { *(void **)dst = NULL; return 0; }
if (!enif_inspect_iolist_as_binary(env, val, &bin)) return -1;
*str_out = (char *)malloc(bin.size + 1);
if (!*str_out) return -1;
memcpy(*str_out, bin.data, bin.size);
(*str_out)[bin.size] = '\0';
*(char **)dst = *str_out; return 0;
default:
return -1;
}
}
/* marshal_ret: C value at src → Erlang term. src is a pointer to the value. */
static ERL_NIF_TERM marshal_ret(ErlNifEnv *env, cffi_tid tid, void *src) {
cffi_ptr_t *res;
ERL_NIF_TERM bin_term;
unsigned char *bin_data;
char *str;
switch (tid) {
case T_VOID:
return am_ok;
case T_BOOL: return *(uint8_t *)src ? am_true : am_false;
case T_INT8: return enif_make_int64(env, *(int8_t *)src);
case T_UINT8: return enif_make_uint64(env, *(uint8_t *)src);
case T_INT16: return enif_make_int64(env, *(int16_t *)src);
case T_UINT16: return enif_make_uint64(env, *(uint16_t *)src);
case T_INT32: return enif_make_int64(env, *(int32_t *)src);
case T_UINT32: return enif_make_uint64(env, *(uint32_t *)src);
case T_INT64: return enif_make_int64(env, *(int64_t *)src);
case T_UINT64: return enif_make_uint64(env, *(uint64_t *)src);
case T_FLOAT: return enif_make_double(env, (double)*(float *)src);
case T_DOUBLE: return enif_make_double(env, *(double *)src);
case T_POINTER: {
void *raw = *(void **)src;
if (!raw) return am_null;
res = enif_alloc_resource(g_ptr_rtype, sizeof(cffi_ptr_t));
if (!res) return am_null;
res->ptr = raw; res->owned = 0; res->size = 0; res->parent = NULL;
ERL_NIF_TERM t = enif_make_resource(env, res);
enif_release_resource(res);
return t;
}
case T_STRING: {
str = *(char **)src;
if (!str) return am_null;
size_t slen = strlen(str);
bin_data = enif_make_new_binary(env, slen, &bin_term);
memcpy(bin_data, str, slen);
return bin_term;
}
default:
return am_error;
}
}
/* =========================================================
* NIF: lib_open/1
* ========================================================= */
static ERL_NIF_TERM nif_lib_open(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
char path[4096];
void *handle;
cffi_lib_t *lib;
if (argc != 1) return enif_make_badarg(env);
if (!enif_get_string(env, argv[0], path, sizeof(path), ERL_NIF_UTF8))
return enif_make_badarg(env);
dlerror();
handle = dlopen(path, RTLD_LAZY | RTLD_LOCAL);
if (!handle)
return enif_make_tuple2(env, am_error,
enif_make_string(env, dlerror() ?: "unknown", ERL_NIF_UTF8));
lib = enif_alloc_resource(g_lib_rtype, sizeof(cffi_lib_t));
if (!lib) { dlclose(handle); return enif_make_tuple2(env, am_error, am_alloc_failed); }
lib->handle = handle;
ERL_NIF_TERM res = enif_make_resource(env, lib);
enif_release_resource(lib);
return enif_make_tuple2(env, am_ok, res);
}
/* =========================================================
* NIF: call/4 (dirty CPU scheduler)
* call(Lib, FuncName, RetType, [{ArgType, ArgVal}])
* -> {ok, RetVal} | {error, Reason}
* ========================================================= */
static ERL_NIF_TERM nif_call(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
cffi_lib_t *lib;
char func_name[512];
void *fn_ptr;
cffi_tid ret_tid;
unsigned nargs = 0;
ERL_NIF_TERM list, head, tail;
ERL_NIF_TERM result = am_error;
ffi_type **arg_types = NULL;
void **arg_vals = NULL;
uint8_t *val_store = NULL;
char **str_bufs = NULL;
if (argc != 4) return enif_make_badarg(env);
if (!enif_get_resource(env, argv[0], g_lib_rtype, (void **)&lib))
return enif_make_badarg(env);
if (!lib->handle)
return enif_make_tuple2(env, am_error, am_library_closed);
if (!enif_get_string(env, argv[1], func_name, sizeof(func_name), ERL_NIF_UTF8))
return enif_make_badarg(env);
dlerror();
fn_ptr = dlsym(lib->handle, func_name);
if (!fn_ptr) {
const char *err = dlerror();
return enif_make_tuple2(env, am_error,
enif_make_tuple2(env, am_not_found,
enif_make_string(env, err ? err : func_name, ERL_NIF_UTF8)));
}
ret_tid = atom_to_tid(argv[2]);
if (ret_tid == T_UNKNOWN)
return enif_make_tuple2(env, am_error,
enif_make_tuple2(env, am_bad_type, argv[2]));
/* Count args */
list = argv[3];
{ ERL_NIF_TERM t = list;
while (enif_get_list_cell(env, t, &head, &tail)) { nargs++; t = tail; }
if (!enif_is_empty_list(env, t)) return enif_make_badarg(env); }
if (nargs > 0) {
arg_types = malloc(nargs * sizeof(ffi_type *));
arg_vals = malloc(nargs * sizeof(void *));
val_store = calloc(nargs, 8);
str_bufs = calloc(nargs, sizeof(char *));
if (!arg_types || !arg_vals || !val_store || !str_bufs) {
result = enif_make_tuple2(env, am_error, am_alloc_failed);
goto cleanup;
}
}
/* Marshal args */
{ unsigned i = 0;
ERL_NIF_TERM t = list;
while (enif_get_list_cell(env, t, &head, &tail)) {
int ar; const ERL_NIF_TERM *pair;
if (!enif_get_tuple(env, head, &ar, &pair) || ar != 2) {
result = enif_make_badarg(env); goto cleanup;
}
cffi_tid tid = atom_to_tid(pair[0]);
if (tid == T_UNKNOWN || tid == T_VOID) {
result = enif_make_tuple2(env, am_error,
enif_make_tuple2(env, am_bad_type, pair[0]));
goto cleanup;
}
arg_types[i] = tid_to_ffi(tid);
arg_vals[i] = &val_store[i * 8];
if (marshal_arg(env, tid, pair[1], arg_vals[i], &str_bufs[i]) != 0) {
result = enif_make_tuple2(env, am_error,
enif_make_tuple2(env, am_bad_type, head));
goto cleanup;
}
i++; t = tail;
}
}
/* Prepare and call */
{ ffi_cif cif;
ffi_status st = ffi_prep_cif(&cif, FFI_DEFAULT_ABI, nargs,
tid_to_ffi(ret_tid),
nargs ? arg_types : NULL);
if (st != FFI_OK) {
result = enif_make_tuple2(env, am_error,
enif_make_tuple2(env, am_ffi_error, enif_make_int(env, (int)st)));
goto cleanup;
}
uint8_t ret_buf[16] = {0};
ffi_call(&cif, FFI_FN(fn_ptr), ret_buf, nargs ? arg_vals : NULL);
result = enif_make_tuple2(env, am_ok, marshal_ret(env, ret_tid, ret_buf));
}
cleanup:
if (str_bufs) {
for (unsigned i = 0; i < nargs; i++) if (str_bufs[i]) free(str_bufs[i]);
free(str_bufs);
}
free(arg_types); free(arg_vals); free(val_store);
return result;
}
/* =========================================================
* NIF: call_va/5 (dirty CPU scheduler)
* call_va(Lib, FuncName, RetType, NFixed, [{ArgType, ArgVal}])
* -> {ok, RetVal} | {error, Reason}
*
* NFixed: number of fixed (non-variadic) arguments.
* Must satisfy 1 <= NFixed <= length(Args).
* ========================================================= */
static ERL_NIF_TERM nif_call_va(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
cffi_lib_t *lib;
char func_name[512];
void *fn_ptr;
cffi_tid ret_tid;
unsigned nfixed = 0, nargs = 0;
ERL_NIF_TERM list, head, tail;
ERL_NIF_TERM result = am_error;
ffi_type **arg_types = NULL;
void **arg_vals = NULL;
uint8_t *val_store = NULL;
char **str_bufs = NULL;
if (argc != 5) return enif_make_badarg(env);
if (!enif_get_resource(env, argv[0], g_lib_rtype, (void **)&lib))
return enif_make_badarg(env);
if (!lib->handle)
return enif_make_tuple2(env, am_error, am_library_closed);
if (!enif_get_string(env, argv[1], func_name, sizeof(func_name), ERL_NIF_UTF8))
return enif_make_badarg(env);
dlerror();
fn_ptr = dlsym(lib->handle, func_name);
if (!fn_ptr) {
const char *err = dlerror();
return enif_make_tuple2(env, am_error,
enif_make_tuple2(env, am_not_found,
enif_make_string(env, err ? err : func_name, ERL_NIF_UTF8)));
}
ret_tid = atom_to_tid(argv[2]);
if (ret_tid == T_UNKNOWN)
return enif_make_tuple2(env, am_error,
enif_make_tuple2(env, am_bad_type, argv[2]));
if (!enif_get_uint(env, argv[3], &nfixed))
return enif_make_badarg(env);
/* Count args */
list = argv[4];
{ ERL_NIF_TERM t = list;
while (enif_get_list_cell(env, t, &head, &tail)) { nargs++; t = tail; }
if (!enif_is_empty_list(env, t)) return enif_make_badarg(env); }
if (nfixed < 1 || nfixed > nargs)
return enif_make_tuple2(env, am_error,
enif_make_tuple2(env, am_bad_nfixed, enif_make_uint(env, nfixed)));
if (nargs > 0) {
arg_types = malloc(nargs * sizeof(ffi_type *));
arg_vals = malloc(nargs * sizeof(void *));
val_store = calloc(nargs, 8);
str_bufs = calloc(nargs, sizeof(char *));
if (!arg_types || !arg_vals || !val_store || !str_bufs) {
result = enif_make_tuple2(env, am_error, am_alloc_failed);
goto va_cleanup;
}
}
/* Marshal args */
{ unsigned i = 0;
ERL_NIF_TERM t = list;
while (enif_get_list_cell(env, t, &head, &tail)) {
int ar; const ERL_NIF_TERM *pair;
if (!enif_get_tuple(env, head, &ar, &pair) || ar != 2) {
result = enif_make_badarg(env); goto va_cleanup;
}
cffi_tid tid = atom_to_tid(pair[0]);
if (tid == T_UNKNOWN || tid == T_VOID) {
result = enif_make_tuple2(env, am_error,
enif_make_tuple2(env, am_bad_type, pair[0]));
goto va_cleanup;
}
/* Variadic args: promote float→double per C default argument promotions */
if (i >= nfixed && tid == T_FLOAT) tid = T_DOUBLE;
arg_types[i] = tid_to_ffi(tid);
arg_vals[i] = &val_store[i * 8];
if (marshal_arg(env, tid, pair[1], arg_vals[i], &str_bufs[i]) != 0) {
result = enif_make_tuple2(env, am_error,
enif_make_tuple2(env, am_bad_type, head));
goto va_cleanup;
}
i++; t = tail;
}
}
/* Prepare and call using ffi_prep_cif_var */
{ ffi_cif cif;
ffi_status st = ffi_prep_cif_var(&cif, FFI_DEFAULT_ABI, nfixed, nargs,
tid_to_ffi(ret_tid),
arg_types);
if (st != FFI_OK) {
result = enif_make_tuple2(env, am_error,
enif_make_tuple2(env, am_ffi_error, enif_make_int(env, (int)st)));
goto va_cleanup;
}
uint8_t ret_buf[16] = {0};
ffi_call(&cif, FFI_FN(fn_ptr), ret_buf, arg_vals);
result = enif_make_tuple2(env, am_ok, marshal_ret(env, ret_tid, ret_buf));
}
va_cleanup:
if (str_bufs) {
for (unsigned i = 0; i < nargs; i++) if (str_bufs[i]) free(str_bufs[i]);
free(str_bufs);
}
free(arg_types); free(arg_vals); free(val_store);
return result;
}
/* =========================================================
* NIF: mem_alloc/1
* ========================================================= */
static ERL_NIF_TERM nif_mem_alloc(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
ErlNifUInt64 size;
cffi_ptr_t *p;
if (argc != 1) return enif_make_badarg(env);
if (!enif_get_uint64(env, argv[0], &size) || size == 0)
return enif_make_badarg(env);
p = enif_alloc_resource(g_ptr_rtype, sizeof(cffi_ptr_t));
if (!p) return enif_make_tuple2(env, am_error, am_alloc_failed);
p->ptr = calloc(1, (size_t)size);
if (!p->ptr) {
enif_release_resource(p);
return enif_make_tuple2(env, am_error, am_alloc_failed);
}
p->owned = 1; p->size = (size_t)size; p->parent = NULL;
ERL_NIF_TERM res = enif_make_resource(env, p);
enif_release_resource(p);
return res;
}
/* =========================================================
* NIF: mem_free/1
* ========================================================= */
static ERL_NIF_TERM nif_mem_free(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
cffi_ptr_t *p;
if (argc != 1) return enif_make_badarg(env);
if (!enif_get_resource(env, argv[0], g_ptr_rtype, (void **)&p))
return enif_make_badarg(env);
if (!p->owned) return enif_make_tuple2(env, am_error, am_not_owner);
if (p->ptr) { free(p->ptr); p->ptr = NULL; p->owned = 0; }
return am_ok;
}
/* =========================================================
* NIF: mem_read/2
* string → p->ptr IS the char buffer (pass &p->ptr for marshal_ret)
* pointer → p->ptr points to a void* field (dereference it)
* numeric → p->ptr points to the value
* ========================================================= */
static ERL_NIF_TERM nif_mem_read(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
cffi_ptr_t *p;
cffi_tid tid;
if (argc != 2) return enif_make_badarg(env);
if (!enif_get_resource(env, argv[0], g_ptr_rtype, (void **)&p))
return enif_make_badarg(env);
if (!p->ptr) return enif_make_tuple2(env, am_error, am_null);
tid = atom_to_tid(argv[1]);
if (tid == T_UNKNOWN || tid == T_VOID)
return enif_make_tuple2(env, am_error,
enif_make_tuple2(env, am_bad_type, argv[1]));
void *src = (tid == T_STRING) ? (void *)&p->ptr : p->ptr;
return marshal_ret(env, tid, src);
}
/* =========================================================
* NIF: mem_write/3
* ========================================================= */
static ERL_NIF_TERM nif_mem_write(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
cffi_ptr_t *p;
cffi_tid tid;
if (argc != 3) return enif_make_badarg(env);
if (!enif_get_resource(env, argv[0], g_ptr_rtype, (void **)&p))
return enif_make_badarg(env);
if (!p->ptr) return enif_make_tuple2(env, am_error, am_null);
tid = atom_to_tid(argv[1]);
if (tid == T_UNKNOWN || tid == T_VOID)
return enif_make_tuple2(env, am_error,
enif_make_tuple2(env, am_bad_type, argv[1]));
ERL_NIF_TERM val = argv[2];
if (tid == T_STRING) {
ErlNifBinary bin;
if (!enif_inspect_iolist_as_binary(env, val, &bin))
return enif_make_badarg(env);
if (p->size > 0 && bin.size + 1 > p->size)
return enif_make_tuple2(env, am_error,
enif_make_atom(env, "buffer_overflow"));
memcpy(p->ptr, bin.data, bin.size);
((char *)p->ptr)[bin.size] = '\0';
return am_ok;
}
if (tid == T_POINTER) {
void *raw;
if (enif_is_identical(val, am_null)) {
raw = NULL;
} else {
cffi_ptr_t *src_p;
if (!enif_get_resource(env, val, g_ptr_rtype, (void **)&src_p))
return enif_make_badarg(env);
raw = src_p->ptr;
}
memcpy(p->ptr, &raw, sizeof(void *));
return am_ok;
}
uint8_t buf[8] = {0};
char *str_buf = NULL;
if (marshal_arg(env, tid, val, buf, &str_buf) != 0)
return enif_make_tuple2(env, am_error,
enif_make_tuple2(env, am_bad_type, val));
if (str_buf) free(str_buf);
memcpy(p->ptr, buf, tid_size(tid));
return am_ok;
}
/* =========================================================
* NIF: mem_read_bytes/2, mem_write_bytes/2
* ========================================================= */
static ERL_NIF_TERM nif_mem_read_bytes(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
cffi_ptr_t *p;
ErlNifUInt64 size;
ERL_NIF_TERM bin_term;
unsigned char *data;
if (argc != 2) return enif_make_badarg(env);
if (!enif_get_resource(env, argv[0], g_ptr_rtype, (void **)&p))
return enif_make_badarg(env);
if (!enif_get_uint64(env, argv[1], &size) || size == 0)
return enif_make_badarg(env);
if (!p->ptr) return enif_make_tuple2(env, am_error, am_null);
data = enif_make_new_binary(env, (size_t)size, &bin_term);
memcpy(data, p->ptr, (size_t)size);
return bin_term;
}
static ERL_NIF_TERM nif_mem_write_bytes(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
cffi_ptr_t *p;
ErlNifBinary bin;
if (argc != 2) return enif_make_badarg(env);
if (!enif_get_resource(env, argv[0], g_ptr_rtype, (void **)&p))
return enif_make_badarg(env);
if (!enif_inspect_binary(env, argv[1], &bin))
return enif_make_badarg(env);
if (!p->ptr) return enif_make_tuple2(env, am_error, am_null);
memcpy(p->ptr, bin.data, bin.size);
return am_ok;
}
/* =========================================================
* NIF: ptr_add/2, ptr_null/0, ptr_is_null/1, type_size/1
* ========================================================= */
static ERL_NIF_TERM nif_ptr_add(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
cffi_ptr_t *p, *newp;
ErlNifSInt64 offset;
if (argc != 2) return enif_make_badarg(env);
if (!enif_get_resource(env, argv[0], g_ptr_rtype, (void **)&p))
return enif_make_badarg(env);
if (!enif_get_int64(env, argv[1], &offset))
return enif_make_badarg(env);
if (!p->ptr) return enif_make_tuple2(env, am_error, am_null);
newp = enif_alloc_resource(g_ptr_rtype, sizeof(cffi_ptr_t));
if (!newp) return enif_make_tuple2(env, am_error, am_alloc_failed);
newp->ptr = (uint8_t *)p->ptr + offset;
newp->owned = 0;
newp->size = 0;
newp->parent = NULL;
ERL_NIF_TERM res = enif_make_resource(env, newp);
enif_release_resource(newp);
return res;
}
static ERL_NIF_TERM nif_ptr_null(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
(void)argc; (void)argv;
cffi_ptr_t *p = enif_alloc_resource(g_ptr_rtype, sizeof(cffi_ptr_t));
if (!p) return enif_make_tuple2(env, am_error, am_alloc_failed);
p->ptr = NULL; p->owned = 0; p->size = 0; p->parent = NULL;
ERL_NIF_TERM res = enif_make_resource(env, p);
enif_release_resource(p);
return res;
}
static ERL_NIF_TERM nif_ptr_is_null(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
cffi_ptr_t *p;
if (argc != 1) return enif_make_badarg(env);
if (!enif_get_resource(env, argv[0], g_ptr_rtype, (void **)&p))
return enif_make_badarg(env);
return p->ptr ? am_false : am_true;
}
static ERL_NIF_TERM nif_type_size(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
cffi_tid tid;
if (argc != 1) return enif_make_badarg(env);
tid = atom_to_tid(argv[0]);
if (tid == T_UNKNOWN)
return enif_make_tuple2(env, am_error,
enif_make_tuple2(env, am_bad_type, argv[0]));
return enif_make_uint64(env, (uint64_t)tid_size(tid));
}
/* =========================================================
* Callback: trampoline (called by C code via the function pointer)
*
* Runs in whatever thread made the C call (typically a dirty NIF thread).
* Sends {cffi_callback, CbId, [Arg...]} to the server process,
* then blocks until callback_return is called.
* ========================================================= */
static void callback_trampoline(ffi_cif *cif, void *ret,
void **args, void *user_data) {
cffi_cb_t *cb = (cffi_cb_t *)user_data;
(void)cif;
/* Build Erlang arg list in a fresh message env */
ErlNifEnv *msg_env = enif_alloc_env();
ERL_NIF_TERM arg_list = enif_make_list(msg_env, 0);
for (int i = (int)cb->nargs - 1; i >= 0; i--) {
ERL_NIF_TERM t = marshal_ret(msg_env, (cffi_tid)cb->arg_tids[i], args[i]);
arg_list = enif_make_list_cell(msg_env, t, arg_list);
}
/* CbId is the raw pointer as uint64 — used by callback_return to find us */
uint64_t cb_id = (uint64_t)(uintptr_t)cb;
ERL_NIF_TERM msg = enif_make_tuple3(msg_env,
am_cffi_callback,
enif_make_uint64(msg_env, cb_id),
arg_list);
/* Lock, mark waiting, send message */
pthread_mutex_lock(&cb->lock);
cb->waiting = 1;
memset(cb->ret_buf, 0, sizeof(cb->ret_buf));
/* enif_send with NULL env is valid from non-scheduler threads */
enif_send(NULL, &cb->server_pid, msg_env, msg);
enif_free_env(msg_env);
/* Block until callback_return signals us */
while (cb->waiting)
pthread_cond_wait(&cb->cond, &cb->lock);
/* Copy return value into C's return buffer */
size_t rsz = tid_size((cffi_tid)cb->ret_tid);
if (rsz > 0) memcpy(ret, cb->ret_buf, rsz);
pthread_mutex_unlock(&cb->lock);
}
/* =========================================================
* NIF: callback_new/3 (regular scheduler — setup only)
* callback_new(RetType, [ArgType], ServerPid)
* -> {ok, {CbHandle, FuncPtr}} | {error, Reason}
* ========================================================= */
static ERL_NIF_TERM nif_callback_new(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
cffi_tid ret_tid;
ErlNifPid pid;
cffi_cb_t *cb = NULL;
cffi_ptr_t *ptr_res = NULL;
unsigned nargs = 0;
ERL_NIF_TERM list, head, tail;
if (argc != 3) return enif_make_badarg(env);
ret_tid = atom_to_tid(argv[0]);
if (ret_tid == T_UNKNOWN) return enif_make_badarg(env);
if (!enif_get_local_pid(env, argv[2], &pid)) return enif_make_badarg(env);
list = argv[1];
{ ERL_NIF_TERM t = list;
while (enif_get_list_cell(env, t, &head, &tail)) { nargs++; t = tail; }
if (!enif_is_empty_list(env, t)) return enif_make_badarg(env); }
cb = enif_alloc_resource(g_cb_rtype, sizeof(cffi_cb_t));
if (!cb) return enif_make_tuple2(env, am_error, am_alloc_failed);
memset(cb, 0, sizeof(cffi_cb_t));
cb->ret_tid = (int)ret_tid;
cb->nargs = nargs;
cb->server_pid = pid;
pthread_mutex_init(&cb->lock, NULL);
pthread_cond_init(&cb->cond, NULL);
cb->arg_types = nargs ? malloc(nargs * sizeof(ffi_type *)) : NULL;
cb->arg_tids = nargs ? malloc(nargs * sizeof(int)) : NULL;
/* Parse arg types */
{ unsigned i = 0; ERL_NIF_TERM t = list;
while (enif_get_list_cell(env, t, &head, &tail)) {
cffi_tid tid = atom_to_tid(head);
if (tid == T_UNKNOWN || tid == T_VOID) goto fail;
cb->arg_tids[i] = (int)tid;
cb->arg_types[i] = tid_to_ffi(tid);
i++; t = tail;
}
}
/* Prepare CIF */
{ ffi_status st = ffi_prep_cif(&cb->cif, FFI_DEFAULT_ABI, nargs,
tid_to_ffi(ret_tid),
nargs ? cb->arg_types : NULL);
if (st != FFI_OK) goto fail; }
/* Allocate libffi closure (executable memory) */
cb->closure = ffi_closure_alloc(sizeof(ffi_closure), &cb->code_ptr);
if (!cb->closure) goto fail;
{ ffi_status st = ffi_prep_closure_loc(cb->closure, &cb->cif,
callback_trampoline, cb,
cb->code_ptr);
if (st != FFI_OK) goto fail; }
/*
* Build FuncPtr resource.
* The ptr_res->parent = cb keeps the cffi_cb_t alive as long as
* the FuncPtr resource is alive (ptr_dtor calls enif_release_resource).
*/
ptr_res = enif_alloc_resource(g_ptr_rtype, sizeof(cffi_ptr_t));
if (!ptr_res) goto fail;
ptr_res->ptr = cb->code_ptr;
ptr_res->owned = 0;
ptr_res->size = 0;
ptr_res->parent = cb;
enif_keep_resource(cb); /* bump ref count for the parent link */
{ ERL_NIF_TERM cb_term = enif_make_resource(env, cb);
ERL_NIF_TERM ptr_term = enif_make_resource(env, ptr_res);
enif_release_resource(cb);
enif_release_resource(ptr_res);
return enif_make_tuple2(env, am_ok,
enif_make_tuple2(env, cb_term, ptr_term));
}
fail:
if (ptr_res) enif_release_resource(ptr_res);
if (cb) {
if (cb->closure) ffi_closure_free(cb->closure);
free(cb->arg_types); free(cb->arg_tids);
pthread_mutex_destroy(&cb->lock);
pthread_cond_destroy(&cb->cond);
enif_release_resource(cb);
}
return enif_make_tuple2(env, am_error, am_alloc_failed);
}
/* =========================================================
* NIF: callback_return/2
* callback_return(CbId :: uint64, RetVal) -> ok
* Called by the Erlang callback server to unblock the C trampoline.
* ========================================================= */
static ERL_NIF_TERM nif_callback_return(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
ErlNifUInt64 cb_id;
cffi_cb_t *cb;
char *str_buf = NULL;
if (argc != 2) return enif_make_badarg(env);
if (!enif_get_uint64(env, argv[0], &cb_id)) return enif_make_badarg(env);
cb = (cffi_cb_t *)(uintptr_t)cb_id;
pthread_mutex_lock(&cb->lock);
memset(cb->ret_buf, 0, sizeof(cb->ret_buf));
/* Ignore marshal errors — just leave ret_buf zeroed (safe default) */
marshal_arg(env, (cffi_tid)cb->ret_tid, argv[1], cb->ret_buf, &str_buf);
if (str_buf) free(str_buf);
cb->waiting = 0;
pthread_cond_signal(&cb->cond);
pthread_mutex_unlock(&cb->lock);
return am_ok;
}
/* =========================================================
* NIF table
* ========================================================= */
static ErlNifFunc nif_funcs[] = {
{"lib_open", 1, nif_lib_open, 0},
{"call", 4, nif_call, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"call_va", 5, nif_call_va, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"mem_alloc", 1, nif_mem_alloc, 0},
{"mem_free", 1, nif_mem_free, 0},
{"mem_read", 2, nif_mem_read, 0},
{"mem_write", 3, nif_mem_write, 0},
{"mem_read_bytes", 2, nif_mem_read_bytes, 0},
{"mem_write_bytes", 2, nif_mem_write_bytes, 0},
{"ptr_add", 2, nif_ptr_add, 0},
{"ptr_null", 0, nif_ptr_null, 0},
{"ptr_is_null", 1, nif_ptr_is_null, 0},
{"type_size", 1, nif_type_size, 0},
{"callback_new", 3, nif_callback_new, 0},
{"callback_return", 2, nif_callback_return, 0},
};
/* =========================================================
* on_load / on_upgrade
* ========================================================= */
static int on_load(ErlNifEnv *env, void **priv, ERL_NIF_TERM info) {
(void)priv; (void)info;
init_atoms(env);
g_lib_rtype = enif_open_resource_type(env, NULL, "cffi_lib", lib_dtor,
ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL);
if (!g_lib_rtype) return -1;
g_ptr_rtype = enif_open_resource_type(env, NULL, "cffi_ptr", ptr_dtor,
ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL);
if (!g_ptr_rtype) return -1;
g_cb_rtype = enif_open_resource_type(env, NULL, "cffi_cb", cb_dtor,
ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL);
if (!g_cb_rtype) return -1;
return 0;
}
static int on_upgrade(ErlNifEnv *env, void **priv, void **old_priv,
ERL_NIF_TERM info) {
(void)old_priv;
return on_load(env, priv, info);
}
ERL_NIF_INIT(cffi_nif, nif_funcs, on_load, NULL, on_upgrade, NULL)