Files
cell/source/qjs_ffi.c
2025-06-23 04:57:05 -05:00

414 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* qjs_ffi.c QuickJS ↔ libffi bridge
*
* Works on macOS / Linux / Windows. See examples/ffi_test.js.
*/
#include "qjs_ffi.h"
#include <ffi.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <dlfcn.h>
#endif
#ifndef countof
#define countof(x) (sizeof(x) / sizeof((x)[0]))
#endif
/* -------------------------------------------------------------------------- */
/* internal structs */
/* -------------------------------------------------------------------------- */
typedef struct {
ffi_cif cif;
ffi_type **arg_types;
ffi_type *ret_type;
void *fn_ptr;
int nargs;
} ffi_func;
typedef struct {
void *ptr;
int is_library; /* 1 => real dlopen/LoadLibrary handle */
} ffi_pointer;
/* -------------------------------------------------------------------------- */
/* QuickJS class plumbing */
/* -------------------------------------------------------------------------- */
static JSClassID js_ffi_func_id;
static JSClassID js_ffi_pointer_id;
static void ffi_func_free(ffi_func *f)
{
if (!f) return;
if (f->arg_types) {
for (int i = 0; i < f->nargs; i++) {
ffi_type *t = f->arg_types[i];
if (t && t != &ffi_type_void && t != &ffi_type_pointer &&
t != &ffi_type_sint8 && t != &ffi_type_uint8 &&
t != &ffi_type_sint16 && t != &ffi_type_uint16 &&
t != &ffi_type_sint32 && t != &ffi_type_uint32 &&
t != &ffi_type_sint64 && t != &ffi_type_uint64 &&
t != &ffi_type_float && t != &ffi_type_double)
free(t);
}
free(f->arg_types);
}
ffi_type *rtp = f->ret_type;
if (rtp && rtp != &ffi_type_void && rtp != &ffi_type_pointer &&
rtp != &ffi_type_sint8 && rtp != &ffi_type_uint8 &&
rtp != &ffi_type_sint16 && rtp != &ffi_type_uint16 &&
rtp != &ffi_type_sint32 && rtp != &ffi_type_uint32 &&
rtp != &ffi_type_sint64 && rtp != &ffi_type_uint64 &&
rtp != &ffi_type_float && rtp != &ffi_type_double)
free(rtp);
free(f);
}
static void ffi_func_finalizer(JSRuntime *rt, JSValue val)
{
ffi_func *f = JS_GetOpaque(val, js_ffi_func_id);
if (f) ffi_func_free(f);
}
static void ffi_pointer_free(ffi_pointer *p)
{
if (p->is_library && p->ptr) {
#ifdef _WIN32
FreeLibrary((HMODULE)p->ptr);
#else
dlclose(p->ptr);
#endif
}
free(p);
}
static void ffi_pointer_finalizer(JSRuntime *rt, JSValue val)
{
ffi_pointer *f = JS_GetOpaque(val, js_ffi_pointer_id);
if (!f) return;
ffi_pointer_free(f);
}
static JSClassDef js_ffi_func_class = { "FFIFunction", .finalizer = ffi_func_finalizer };
static JSClassDef js_ffi_pointer_class = { "FFIPointer", .finalizer = ffi_pointer_finalizer };
/* -------------------------------------------------------------------------- */
/* helpers */
/* -------------------------------------------------------------------------- */
static JSValue
js_new_ffi_pointer(JSContext *ctx, void *ptr, int is_library)
{
JSValue obj = JS_NewObjectClass(ctx, js_ffi_pointer_id);
if (JS_IsException(obj)) return obj;
ffi_pointer *p = malloc(sizeof *p);
if (!p) { JS_FreeValue(ctx, obj); return JS_ThrowOutOfMemory(ctx); }
p->ptr = ptr;
p->is_library = is_library;
JS_SetOpaque(obj, p);
return obj;
}
static void *
js_get_ffi_pointer(JSContext *ctx, JSValueConst val)
{
ffi_pointer *p = JS_GetOpaque2(ctx, val, js_ffi_pointer_id);
return p ? p->ptr : NULL;
}
static ffi_type *
js_to_ffi_type(JSContext *ctx, const char *name)
{
if (!strcmp(name, "void")) return &ffi_type_void;
if (!strcmp(name, "pointer")) return &ffi_type_pointer;
if (!strcmp(name, "float")) return &ffi_type_float;
if (!strcmp(name, "double")) return &ffi_type_double;
if (!strcmp(name, "int8")) return &ffi_type_sint8;
if (!strcmp(name, "uint8")) return &ffi_type_uint8;
if (!strcmp(name, "int16")) return &ffi_type_sint16;
if (!strcmp(name, "uint16")) return &ffi_type_uint16;
if (!strcmp(name, "int32")) return &ffi_type_sint32;
if (!strcmp(name, "uint32")) return &ffi_type_uint32;
if (!strcmp(name, "int64")) return &ffi_type_sint64;
if (!strcmp(name, "uint64")) return &ffi_type_uint64;
/* synonyms */
if (!strcmp(name, "int")) return &ffi_type_sint32;
if (!strcmp(name, "uint")) return &ffi_type_uint32;
if (!strcmp(name, "char*") || !strcmp(name, "string"))
return &ffi_type_pointer;
JS_ThrowTypeError(ctx, "unknown FFI type \"%s\"", name);
return NULL;
}
/* -------------------------------------------------------------------------- */
/* ffi.dlopen */
/* -------------------------------------------------------------------------- */
static JSValue
js_ffi_dlopen(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
if (argc < 1)
return JS_ThrowTypeError(ctx, "dlopen: expected path or null");
const char *path = NULL;
if (!JS_IsNull(argv[0])) {
path = JS_ToCString(ctx, argv[0]);
if (!path) return JS_EXCEPTION;
}
void *h;
#ifdef _WIN32
h = path ? (void *)LoadLibraryA(path)
: (void *)GetModuleHandle(NULL);
#else
h = path ? dlopen(path, RTLD_LAZY | RTLD_LOCAL)
: dlopen(NULL, RTLD_LAZY | RTLD_LOCAL);
#endif
if (path) JS_FreeCString(ctx, path);
if (!h) return JS_ThrowInternalError(ctx, "dlopen failed");
/* ------------- FIX: only real libraries get is_library = 1 ------------- */
int is_library = (path != NULL); /* self handle must not be closed */
return js_new_ffi_pointer(ctx, h, is_library);
}
/* -------------------------------------------------------------------------- */
/* ffi.prepare (unchanged from previous answer) */
/* -------------------------------------------------------------------------- */
static JSValue
js_ffi_prepare(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
if (argc < 4)
return JS_ThrowTypeError(ctx,
"prepare: expected (lib, symbol, ret_type, arg_type_array)");
void *lib = js_get_ffi_pointer(ctx, argv[0]);
if (!lib) return JS_ThrowTypeError(ctx, "invalid library handle");
const char *sym = JS_ToCString(ctx, argv[1]);
if (!sym) return JS_EXCEPTION;
void *fn;
#ifdef _WIN32
fn = (void *)GetProcAddress((HMODULE)lib, sym);
#else
fn = dlsym(lib, sym);
#endif
if (!fn) {
JS_FreeCString(ctx, sym);
return JS_ThrowTypeError(ctx, "symbol \"%s\" not found", sym);
}
const char *rt_name = JS_ToCString(ctx, argv[2]);
if (!rt_name) { JS_FreeCString(ctx, sym); return JS_EXCEPTION; }
ffi_type *ret_type = js_to_ffi_type(ctx, rt_name);
JS_FreeCString(ctx, rt_name);
if (!ret_type) { JS_FreeCString(ctx, sym); return JS_EXCEPTION; }
if (!JS_IsArray(ctx, argv[3])) {
JS_FreeCString(ctx, sym);
return JS_ThrowTypeError(ctx, "arg_types must be an array");
}
uint32_t nargs = JS_ArrayLength(ctx, argv[3]);
ffi_func *f = calloc(1, sizeof *f);
if (!f) { JS_FreeCString(ctx, sym); return JS_ThrowOutOfMemory(ctx); }
f->fn_ptr = fn;
f->ret_type = ret_type;
f->nargs = (int)nargs;
if (nargs) {
f->arg_types = calloc(nargs, sizeof(ffi_type *));
if (!f->arg_types) {
JS_FreeCString(ctx, sym);
ffi_func_free(f);
return JS_ThrowOutOfMemory(ctx);
}
for (uint32_t i = 0; i < nargs; i++) {
JSValue v = JS_GetPropertyUint32(ctx, argv[3], i);
const char *tn = JS_ToCString(ctx, v);
JS_FreeValue(ctx, v);
if (!tn) { JS_FreeCString(ctx, sym); ffi_func_free(f); return JS_EXCEPTION; }
f->arg_types[i] = js_to_ffi_type(ctx, tn);
JS_FreeCString(ctx, tn);
if (!f->arg_types[i]) {
JS_FreeCString(ctx, sym);
ffi_func_free(f);
return JS_EXCEPTION;
}
}
}
JS_FreeCString(ctx, sym);
if (ffi_prep_cif(&f->cif, FFI_DEFAULT_ABI,
f->nargs, f->ret_type, f->arg_types) != FFI_OK) {
ffi_func_free(f);
return JS_ThrowInternalError(ctx, "ffi_prep_cif failed");
}
JSValue obj = JS_NewObjectClass(ctx, js_ffi_func_id);
JS_SetOpaque(obj, f);
return obj;
}
/* -------------------------------------------------------------------------- */
/* box_result + js_ffi_call (unchanged) */
/* -------------------------------------------------------------------------- */
static JSValue
box_result(JSContext *ctx, ffi_type *t, void *data)
{
if (t == &ffi_type_void) return JS_NULL;
if (t == &ffi_type_pointer) return data ? js_new_ffi_pointer(ctx, *(void **)data, 0)
: JS_NULL;
if (t == &ffi_type_double) return JS_NewFloat64(ctx, *(double *)data);
if (t == &ffi_type_float) return JS_NewFloat64(ctx, (double)*(float *)data);
if (t == &ffi_type_sint8) return JS_NewInt32(ctx, *(int8_t *)data);
if (t == &ffi_type_uint8) return JS_NewUint32(ctx, *(uint8_t *)data);
if (t == &ffi_type_sint16) return JS_NewInt32(ctx, *(int16_t *)data);
if (t == &ffi_type_uint16) return JS_NewUint32(ctx, *(uint16_t *)data);
if (t == &ffi_type_sint32) return JS_NewInt32(ctx, *(int32_t *)data);
if (t == &ffi_type_uint32) return JS_NewUint32(ctx, *(uint32_t *)data);
if (t == &ffi_type_sint64) return JS_NewFloat64(ctx, (double)*(int64_t *)data);
if (t == &ffi_type_uint64) return JS_NewFloat64(ctx, (double)*(uint64_t *)data);
return JS_ThrowTypeError(ctx, "unsupported return type");
}
static JSValue
js_ffi_call(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
ffi_func *f = JS_GetOpaque2(ctx, this_val, js_ffi_func_id);
if (!f) return JS_EXCEPTION;
if (argc != f->nargs)
return JS_ThrowTypeError(ctx, "expected %d arguments, got %d",
f->nargs, argc);
/* one fixed stack block per argument ----------------------------------- */
typedef union {
double d;
float f;
int8_t i8; uint8_t u8;
int16_t i16; uint16_t u16;
int32_t i32; uint32_t u32;
int64_t i64; uint64_t u64;
void *p;
} arg_data_t;
arg_data_t data[f->nargs];
void *values[f->nargs];
const char *tmp_cstr[f->nargs]; /* only for JS strings */
memset(tmp_cstr, 0, sizeof tmp_cstr);
/* ---------- marshal JS → C (no malloc for scalars) ------------------- */
for (int i = 0; i < f->nargs; i++) {
ffi_type *t = f->arg_types[i];
/* numbers ------------------------------------------------------------ */
if (t != &ffi_type_pointer) {
double d;
if (JS_ToFloat64(ctx, &d, argv[i]))
goto arg_error;
if (t == &ffi_type_double) data[i].d = d;
else if (t == &ffi_type_float) data[i].f = (float)d;
else if (t == &ffi_type_sint8) data[i].i8 = (int8_t)d;
else if (t == &ffi_type_uint8) data[i].u8 = (uint8_t)d;
else if (t == &ffi_type_sint16) data[i].i16= (int16_t)d;
else if (t == &ffi_type_uint16) data[i].u16= (uint16_t)d;
else if (t == &ffi_type_sint32) data[i].i32= (int32_t)d;
else if (t == &ffi_type_uint32) data[i].u32= (uint32_t)d;
else if (t == &ffi_type_sint64) data[i].i64= (int64_t)d;
else if (t == &ffi_type_uint64) data[i].u64= (uint64_t)d;
else goto arg_error; /* should be unreachable */
values[i] = &data[i];
continue;
}
/* pointers ----------------------------------------------------------- */
if (JS_IsString(argv[i])) {
const char *s = JS_ToCString(ctx, argv[i]);
if (!s) goto arg_error;
tmp_cstr[i] = s; /* remember to free */
data[i].p = (void *)s;
} else {
data[i].p = js_get_ffi_pointer(ctx, argv[i]);
}
values[i] = &data[i];
}
/* ---------------------------- call ---------------------------------- */
uint8_t ret_space[16] = {0};
void *ret_buf = f->ret_type == &ffi_type_void ? NULL : (void *)ret_space;
ffi_call(&f->cif, FFI_FN(f->fn_ptr), ret_buf, values);
JSValue js_ret = box_result(ctx, f->ret_type, ret_buf);
/* --------------------------- cleanup ---------------------------------- */
for (int i = 0; i < f->nargs; i++)
if (tmp_cstr[i]) JS_FreeCString(ctx, tmp_cstr[i]);
return js_ret;
arg_error:
for (int i = 0; i < f->nargs; i++)
if (tmp_cstr[i]) JS_FreeCString(ctx, tmp_cstr[i]);
return JS_EXCEPTION;
}
/* -------------------------------------------------------------------------- */
/* module init */
/* -------------------------------------------------------------------------- */
static const JSCFunctionListEntry js_ffi_funcs[] = {
JS_CFUNC_DEF("dlopen", 1, js_ffi_dlopen),
JS_CFUNC_DEF("prepare", 4, js_ffi_prepare),
};
static const JSCFunctionListEntry js_ffi_func_funcs[] = {
JS_CFUNC_DEF("call", 1, js_ffi_call),
};
JSValue
js_ffi_use(JSContext *ctx)
{
JS_NewClassID(&js_ffi_func_id);
JS_NewClass(JS_GetRuntime(ctx), js_ffi_func_id, &js_ffi_func_class);
JSValue fn_proto = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, fn_proto, js_ffi_func_funcs, countof(js_ffi_func_funcs));
JS_SetClassProto(ctx, js_ffi_func_id, fn_proto);
JS_NewClassID(&js_ffi_pointer_id);
JS_NewClass(JS_GetRuntime(ctx), js_ffi_pointer_id, &js_ffi_pointer_class);
JSValue mod = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, mod, js_ffi_funcs, countof(js_ffi_funcs));
return mod;
}