diff --git a/meson.build b/meson.build index c414d03c..bbd17432 100644 --- a/meson.build +++ b/meson.build @@ -190,6 +190,8 @@ else deps += chipmunk_dep endif +deps += dependency('libffi', static:true) + if host_machine.system() != 'emscripten' # Try to find system-installed enet first enet_dep = dependency('enet', static: true, required: false) @@ -271,7 +273,7 @@ src += [ 'anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c', 'render.c','simplex.c','spline.c', 'transform.c','cell.c', 'wildmatch.c', 'sprite.c', 'rtree.c', 'qjs_nota.c', 'qjs_soloud.c', 'qjs_sdl.c', 'qjs_sdl_input.c', 'qjs_sdl_video.c', 'qjs_sdl_surface.c', 'qjs_math.c', 'qjs_geometry.c', 'qjs_transform.c', 'qjs_sprite.c', 'qjs_io.c', 'qjs_fd.c', 'qjs_os.c', 'qjs_actor.c', - 'qjs_qr.c', 'qjs_wota.c', 'monocypher.c', 'qjs_blob.c', 'qjs_crypto.c', 'qjs_time.c', 'qjs_http.c', 'qjs_rtree.c', 'qjs_spline.c', 'qjs_js.c', 'qjs_debug.c', 'picohttpparser.c', 'qjs_miniz.c', 'qjs_num.c', 'timer.c', 'qjs_socket.c', 'qjs_kim.c', 'qjs_utf8.c', 'qjs_fit.c', 'qjs_text.c' + 'qjs_qr.c', 'qjs_wota.c', 'monocypher.c', 'qjs_blob.c', 'qjs_crypto.c', 'qjs_time.c', 'qjs_http.c', 'qjs_rtree.c', 'qjs_spline.c', 'qjs_js.c', 'qjs_debug.c', 'picohttpparser.c', 'qjs_miniz.c', 'qjs_num.c', 'timer.c', 'qjs_socket.c', 'qjs_kim.c', 'qjs_utf8.c', 'qjs_fit.c', 'qjs_text.c', 'point.c', 'qjs_ffi.c' ] # quirc src src += [ diff --git a/source/jsffi.c b/source/jsffi.c index 4245f9c2..4cdaef65 100644 --- a/source/jsffi.c +++ b/source/jsffi.c @@ -1546,6 +1546,7 @@ JSC_CCALL(os_value_id, #include "qjs_wota.h" #include "qjs_socket.h" #include "qjs_nota.h" +#include "qjs_ffi.h" //JSValue js_imgui_use(JSContext *js); #define MISTLINE(NAME) (ModuleEntry){#NAME, js_##NAME##_use} @@ -1581,6 +1582,7 @@ void ffi_load(JSContext *js) arrput(rt->module_registry, MISTLINE(text)); arrput(rt->module_registry, MISTLINE(wota)); arrput(rt->module_registry, MISTLINE(nota)); + arrput(rt->module_registry, MISTLINE(ffi)); // power user arrput(rt->module_registry, MISTLINE(js)); diff --git a/source/qjs_ffi.c b/source/qjs_ffi.c new file mode 100644 index 00000000..d83d05cd --- /dev/null +++ b/source/qjs_ffi.c @@ -0,0 +1,413 @@ +/* + * qjs_ffi.c – QuickJS ↔ libffi bridge + * + * Works on macOS / Linux / Windows. See examples/ffi_test.js. + */ + +#include "qjs_ffi.h" +#include +#include +#include + +#ifdef _WIN32 + #include +#else + #include +#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; +} diff --git a/source/qjs_ffi.h b/source/qjs_ffi.h new file mode 100644 index 00000000..5a5981b7 --- /dev/null +++ b/source/qjs_ffi.h @@ -0,0 +1,8 @@ +#ifndef QJS_FFI_H +#define QJS_FFI_H + +#include "cell.h" + +JSValue js_ffi_use(JSContext *ctx); + +#endif