From 0cc53a8964ca1e86d2073c67db850987b16d2280 Mon Sep 17 00:00:00 2001 From: Calvin Rose Date: Sat, 11 Jun 2022 14:47:35 -0500 Subject: [PATCH] Get a GTK example working. Good proof of concept. --- ffitest/gtk.janet | 57 ++++++++++++++++++++++++++++++++++++++++++ src/core/ffi.c | 63 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 ffitest/gtk.janet diff --git a/ffitest/gtk.janet b/ffitest/gtk.janet new file mode 100644 index 00000000..e6de3672 --- /dev/null +++ b/ffitest/gtk.janet @@ -0,0 +1,57 @@ +# FFI is best used with a wrapper like the one below +# An even more sophisticated macro wrapper could add +# better doc strings, better parameter checking, etc. + +(defn defnative-context + "Load a dynamic library and set it as the context for following declarations" + [location] + (setdyn :raw-native (raw-native location))) + +(defmacro defnative + "Declare a native binding" + [name ret-type & body] + (def signature-args (last body)) + (def defn-args (seq [_ :in signature-args] (gensym))) + (def raw-symbol (string/replace-all "-" "_" name)) + (def $sig (symbol name "-signature-")) + (def $pointer (symbol name "-raw-pointer-")) + ~(upscope + (def ,$pointer :private (as-macro ,assert (,native-lookup (,dyn :raw-native) ,raw-symbol))) + (def ,$sig :private (,native-signature :default ,ret-type ,;signature-args)) + (defn ,name [,;defn-args] + (,native-call ,$pointer ,$sig ,;defn-args)))) + +(defnative-context "/usr/lib/libgtk-3.so") + +(defnative gtk-application-new :ptr [:ptr :uint]) +(defnative g-signal-connect-data :ulong [:ptr :ptr :ptr :ptr :ptr :int]) +(defnative g-application-run :int [:ptr :int :ptr]) +(defnative gtk-application-window-new :ptr [:ptr]) +(defnative gtk-button-new-with-label :ptr [:ptr]) +(defnative gtk-container-add :void [:ptr :ptr]) +(defnative gtk-widget-show-all :void [:ptr]) +(defnative gtk-button-set-label :void [:ptr :ptr]) + +# GTK follows a strict convention for callbacks. This lets us use +# a single "standard" callback whose behavior is specified by userdata. +# This lets use callbacks without code generation, so no issues with iOS, SELinux, etc. +# Limitation is that we cannot generate arbitrary closures to pass into apis. +# However, any stubs we need we would simply need to compile ourselves, so +# Janet includes a common stub out of the box. +(def cb (native-trampoline :default)) + +(defn on-active + [app] + (def window (gtk-application-window-new app)) + (def btn (gtk-button-new-with-label "Click Me!")) + (g-signal-connect-data btn "clicked" cb + (fn [btn] (gtk-button-set-label btn "Hello World")) + nil 1) + (gtk-container-add window btn) + (gtk-widget-show-all window)) + +(defn main + [&] + (def app (gtk-application-new "org.janet-lang.example.HelloApp" 0)) + (g-signal-connect-data app "activate" cb on-active nil 1) + (g-application-run app 0 nil)) diff --git a/src/core/ffi.c b/src/core/ffi.c index da3e7ffb..768df678 100644 --- a/src/core/ffi.c +++ b/src/core/ffi.c @@ -328,14 +328,22 @@ JANET_CORE_FN(cfun_ffi_struct, static void *janet_ffi_getpointer(const Janet *argv, int32_t n) { switch (janet_type(argv[n])) { default: - janet_panicf("bad slot #%d, expected pointer convertable type, got %v", argv[n]); + janet_panicf("bad slot #%d, expected ffi pointer convertable type, got %v", argv[n]); case JANET_POINTER: case JANET_STRING: case JANET_KEYWORD: case JANET_SYMBOL: + case JANET_ABSTRACT: return janet_unwrap_pointer(argv[n]); case JANET_BUFFER: return janet_unwrap_buffer(argv[n])->data; + case JANET_FUNCTION: + /* Users may pass in a function. Any function passed is almost certainly + * being used as a callback, so we add it to the root set. */ + janet_gcroot(argv[n]); + return janet_unwrap_pointer(argv[n]); + case JANET_NIL: + return NULL; } } @@ -609,6 +617,25 @@ JANET_CORE_FN(cfun_ffi_signature, return janet_wrap_abstract(abst); } +/* A common callback function signature. To avoid runtime code generation, which is prohibited + * on many platforms, often buggy (see libffi), and generally complicated, instead provide + * a single (or small set of commonly used function signatures). All callbacks should + * eventually call this. */ +void janet_ffi_trampoline(void *ctx, void *userdata) { + if (NULL == userdata) { + /* Userdata not set. */ + janet_eprintf("no userdata found for janet callback"); + return; + } + Janet context = janet_wrap_pointer(ctx); + JanetFunction *fun = userdata; + janet_call(fun, 1, &context); +} + +static void janet_ffi_sysv64_standard_callback(void *ctx, void *userdata) { + janet_ffi_trampoline(ctx, userdata); +} + static Janet janet_ffi_sysv64(JanetFFISignature *signature, void *function_pointer, const Janet *argv) { uint64_t ret[2]; uint64_t regs[6]; @@ -741,14 +768,37 @@ JANET_CORE_FN(cfun_ffi_buffer_write, JANET_CORE_FN(cfun_ffi_buffer_read, "(native-read ffi-type bytes &opt offset)", "Parse a native struct out of a buffer and convert it to normal Janet data structures. " - "This function is the inverse of `native-write`.") { + "This function is the inverse of `native-write`. `bytes` can also be a raw pointer, although " + "this is unsafe.") { janet_arity(argc, 2, 3); JanetFFIType type = decode_ffi_type(argv[0]); - size_t el_size = type_size(type); - JanetByteView bytes = janet_getbytes(argv, 1); size_t offset = (size_t) janet_optnat(argv, argc, 2, 0); - if ((size_t) bytes.len < offset + el_size) janet_panic("read out of range"); - return janet_ffi_read_one(bytes.bytes, type, JANET_FFI_MAX_RECUR); + if (janet_checktype(argv[1], JANET_POINTER)) { + uint8_t *ptr = janet_unwrap_pointer(argv[1]); + return janet_ffi_read_one(ptr + offset, type, JANET_FFI_MAX_RECUR); + } else { + size_t el_size = type_size(type); + JanetByteView bytes = janet_getbytes(argv, 1); + if ((size_t) bytes.len < offset + el_size) janet_panic("read out of range"); + return janet_ffi_read_one(bytes.bytes + offset, type, JANET_FFI_MAX_RECUR); + } +} + +JANET_CORE_FN(cfun_ffi_get_callback_trampoline, + "(native-trampoline cc)", + "Get a native function pointer that can be used as a callback and passed to C libraries. " + "This callback trampoline has the signature `void trampoline(void *ctx, void *userdata)` in " + "the given calling convention. This is the only function signature supported. " + "It is up to the programmer to ensure that the `userdata` argument contains a janet function " + "the will be called with one argument, `ctx` which is an opaque pointer. This pointer can " + "be further inspected with `native-read`.") { + janet_fixarity(argc, 1); + JanetFFICallingConvention cc = decode_ffi_cc(janet_getkeyword(argv, 0)); + switch (cc) { + default: + case JANET_FFI_CC_SYSV_64: + return janet_wrap_pointer(janet_ffi_sysv64_standard_callback); + } } void janet_lib_ffi(JanetTable *env) { @@ -758,6 +808,7 @@ void janet_lib_ffi(JanetTable *env) { JANET_CORE_REG("native-struct", cfun_ffi_struct), JANET_CORE_REG("native-write", cfun_ffi_buffer_write), JANET_CORE_REG("native-read", cfun_ffi_buffer_read), + JANET_CORE_REG("native-trampoline", cfun_ffi_get_callback_trampoline), JANET_REG_END }; janet_core_cfuns_ext(env, NULL, ffi_cfuns);