// gtk_backend.c — Wren <-> GTK3 bridge (module: "gtk") — hardened version // // Build (shared): // cc -O2 -fPIC -shared -o libwren_gtk.so gtk_backend.c \ // $(pkg-config --cflags --libs gtk+-3.0) \ // -I/path/to/wren/include -L/path/to/wren/lib -lwren // // Notes: // - Foreign signatures use trailing underscores for event hookups: // Window.onDestroy_(_), Button.onClicked_(_), Entry.onActivate_(_), Entry.onChanged_(_) // - This variant *does not* release Wren handles in GTK destroy-notify to avoid // use-after-free if the host frees the VM before GTK finalizes. See CLEAN_RELEASE below. #include "wren.h" #include #include #include #include // --- Switch: if you control VM lifetime, set to 1 and call Gtk.quit() before freeing the VM, // then it's safe to release handles in ctx_free. Default 0 = never touch Wren in destroy-notify. #ifndef CLEAN_RELEASE #define CLEAN_RELEASE 0 #endif // Optional: mark VM alive/dead if you want to be precise. // Keep static so callbacks can see it without host integration. static bool g_vm_alive = true; // ---------- Foreign object wrapper ---------- typedef struct { GtkWidget* widget; // strong ref to GTK widget } GtkObj; static void* gtkAllocate(WrenVM* vm) { GtkObj* o = (GtkObj*)wrenSetSlotNewForeign(vm, 0, 0, sizeof(GtkObj)); o->widget = NULL; return o; } static void gtkFinalize(void* data) { GtkObj* o = (GtkObj*)data; if (o && o->widget) { g_object_unref(o->widget); o->widget = NULL; } } // Helpers static GtkObj* get_self(WrenVM* vm) { return (GtkObj*)wrenGetSlotForeign(vm, 0); } static GtkWidget* get_widget(WrenVM* vm) { GtkObj* o = get_self(vm); return o ? o->widget : NULL; } static GtkWidget* get_widget_from_slot(WrenVM* vm, int slot) { GtkObj* other = (GtkObj*)wrenGetSlotForeign(vm, slot); return other ? other->widget : NULL; } // ---------- Callback plumbing ---------- typedef struct { WrenVM* vm; WrenHandle* fn; // user's closure WrenHandle* call0; // handle for "call()" } CallbackCtx; static void ctx_free(gpointer data, GClosure* closure) { CallbackCtx* ctx = (CallbackCtx*)data; if (!ctx) return; #if CLEAN_RELEASE // Only safe if VM is still alive at this time. if (g_vm_alive && ctx->vm) { if (ctx->fn) wrenReleaseHandle(ctx->vm, ctx->fn); if (ctx->call0) wrenReleaseHandle(ctx->vm, ctx->call0); } #endif free(ctx); } static void dispatch_noargs(GtkWidget* widget, gpointer user_data) { CallbackCtx* ctx = (CallbackCtx*)user_data; if (!ctx || !ctx->vm || !ctx->fn || !ctx->call0) return; if (!g_vm_alive) return; // extra safety wrenEnsureSlots(ctx->vm, 1); wrenSetSlotHandle(ctx->vm, 0, ctx->fn); // receiver is the Fn wrenCall(ctx->vm, ctx->call0); } static CallbackCtx* make_ctx(WrenVM* vm, int slot_fn) { CallbackCtx* ctx = (CallbackCtx*)calloc(1, sizeof(CallbackCtx)); ctx->vm = vm; ctx->fn = wrenGetSlotHandle(vm, slot_fn); ctx->call0 = wrenMakeCallHandle(vm, "call()"); return ctx; } // ---------- Gtk (static) ---------- static void gtkInit(WrenVM* vm) { int argc = 0; char** argv = NULL; // gtk_init will g_error() on failure (e.g., no DISPLAY) — that will abort process. // If you need non-fatal behavior, switch to gtk_init_check. if (!gtk_init_check(&argc, &argv)) { // Fail silently to Wren; further GTK calls become no-ops via NULL checks. // You can also wrenAbortFiber here if you prefer a hard error. } g_vm_alive = true; } static void gtkRun(WrenVM* vm) { gtk_main(); } static void gtkQuit(WrenVM* vm){ gtk_main_quit(); g_vm_alive = false; } // ---------- Window ---------- static void windowInit(WrenVM* vm) { // init_(title, width, height) const char* title = wrenGetSlotString(vm, 1); int width = (int)wrenGetSlotDouble(vm, 2); int height = (int)wrenGetSlotDouble(vm, 3); GtkObj* self = get_self(vm); self->widget = gtk_window_new(GTK_WINDOW_TOPLEVEL); if (!self->widget) return; g_object_ref_sink(self->widget); gtk_window_set_title(GTK_WINDOW(self->widget), title ? title : ""); gtk_window_set_default_size(GTK_WINDOW(self->widget), width, height); } static void windowSetTitle(WrenVM* vm) { GtkWidget* w = get_widget(vm); if (!w) return; const char* title = wrenGetSlotString(vm, 1); gtk_window_set_title(GTK_WINDOW(w), title ? title : ""); } static void windowSetDefaultSize(WrenVM* vm) { GtkWidget* w = get_widget(vm); if (!w) return; int wi = (int)wrenGetSlotDouble(vm, 1); int hi = (int)wrenGetSlotDouble(vm, 2); gtk_window_set_default_size(GTK_WINDOW(w), wi, hi); } static void windowAdd(WrenVM* vm) { GtkWidget* w = get_widget(vm); if (!w) return; GtkWidget* child = get_widget_from_slot(vm, 1); if (!child) return; gtk_container_add(GTK_CONTAINER(w), child); } static void windowShowAll(WrenVM* vm) { GtkWidget* w = get_widget(vm); if (!w) return; gtk_widget_show_all(w); } static void windowOnDestroy(WrenVM* vm) { GtkWidget* w = get_widget(vm); if (!w) return; CallbackCtx* ctx = make_ctx(vm, 1); g_signal_connect_data( w, "destroy", G_CALLBACK(dispatch_noargs), ctx, ctx_free, 0); } // ---------- Box ---------- static void boxV(WrenVM* vm) { int spacing = (int)wrenGetSlotDouble(vm, 1); GtkObj* self = get_self(vm); self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, spacing); if (self->widget) g_object_ref_sink(self->widget); } static void boxH(WrenVM* vm) { int spacing = (int)wrenGetSlotDouble(vm, 1); GtkObj* self = get_self(vm); self->widget = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, spacing); if (self->widget) g_object_ref_sink(self->widget); } static void boxPackStart(WrenVM* vm) { GtkWidget* w = get_widget(vm); if (!w) return; GtkWidget* child = get_widget_from_slot(vm, 1); if (!child) return; bool expand = wrenGetSlotBool(vm, 2); bool fill = wrenGetSlotBool(vm, 3); int padding = (int)wrenGetSlotDouble(vm, 4); gtk_box_pack_start(GTK_BOX(w), child, expand, fill, padding); } // ---------- Button ---------- static void buttonInit(WrenVM* vm) { const char* label = wrenGetSlotString(vm, 1); GtkObj* self = get_self(vm); self->widget = gtk_button_new_with_label(label ? label : ""); if (self->widget) g_object_ref_sink(self->widget); } static void buttonSetLabel(WrenVM* vm) { GtkWidget* w = get_widget(vm); if (!w) return; const char* label = wrenGetSlotString(vm, 1); gtk_button_set_label(GTK_BUTTON(w), label ? label : ""); } static void buttonOnClicked(WrenVM* vm) { GtkWidget* w = get_widget(vm); if (!w) return; CallbackCtx* ctx = make_ctx(vm, 1); g_signal_connect_data( w, "clicked", G_CALLBACK(dispatch_noargs), ctx, ctx_free, 0); } // ---------- Entry ---------- static void entryInit(WrenVM* vm) { GtkObj* self = get_self(vm); self->widget = gtk_entry_new(); if (self->widget) g_object_ref_sink(self->widget); } static void entrySetText(WrenVM* vm) { GtkWidget* w = get_widget(vm); if (!w) return; const char* text = wrenGetSlotString(vm, 1); gtk_entry_set_text(GTK_ENTRY(w), text ? text : ""); } static void entryGetText(WrenVM* vm) { GtkWidget* w = get_widget(vm); const char* s = (w ? gtk_entry_get_text(GTK_ENTRY(w)) : ""); wrenSetSlotString(vm, 0, s ? s : ""); } static void entryOnActivate(WrenVM* vm) { GtkWidget* w = get_widget(vm); if (!w) return; CallbackCtx* ctx = make_ctx(vm, 1); g_signal_connect_data( w, "activate", G_CALLBACK(dispatch_noargs), ctx, ctx_free, 0); } static void entryOnChanged(WrenVM* vm) { GtkWidget* w = get_widget(vm); if (!w) return; CallbackCtx* ctx = make_ctx(vm, 1); g_signal_connect_data( w, "changed", G_CALLBACK(dispatch_noargs), ctx, ctx_free, 0); } // ---------- Label ---------- static void labelInit(WrenVM* vm) { const char* text = wrenGetSlotString(vm, 1); GtkObj* self = get_self(vm); self->widget = gtk_label_new(text ? text : ""); if (self->widget) g_object_ref_sink(self->widget); } static void labelSetText(WrenVM* vm) { GtkWidget* w = get_widget(vm); if (!w) return; const char* text = wrenGetSlotString(vm, 1); gtk_label_set_text(GTK_LABEL(w), text ? text : ""); } // ---------- Binder (module "gtk") ---------- WrenForeignMethodFn bindGtkForeignMethod(WrenVM* vm, const char* module, const char* className, bool isStatic, const char* signature) { if (strcmp(module, "gtk") != 0) return NULL; if (strcmp(className, "Gtk") == 0 && isStatic) { if (strcmp(signature, "init_()") == 0) return gtkInit; if (strcmp(signature, "run_()") == 0) return gtkRun; if (strcmp(signature, "quit_()") == 0) return gtkQuit; } if (strcmp(className, "Window") == 0 && !isStatic) { if (strcmp(signature, "init_(_,_,_)") == 0) return windowInit; if (strcmp(signature, "setTitle(_)") == 0) return windowSetTitle; if (strcmp(signature, "setDefaultSize(_,_)")== 0) return windowSetDefaultSize; if (strcmp(signature, "add(_)") == 0) return windowAdd; if (strcmp(signature, "showAll()") == 0) return windowShowAll; if (strcmp(signature, "onDestroy_(_)") == 0) return windowOnDestroy; } if (strcmp(className, "Box") == 0 && !isStatic) { if (strcmp(signature, "vbox_(_)") == 0) return boxV; if (strcmp(signature, "hbox_(_)") == 0) return boxH; if (strcmp(signature, "packStart(_,_,_,_)") == 0) return boxPackStart; } if (strcmp(className, "Button") == 0 && !isStatic) { if (strcmp(signature, "init_(_)") == 0) return buttonInit; if (strcmp(signature, "setLabel(_)") == 0) return buttonSetLabel; if (strcmp(signature, "onClicked_(_)") == 0) return buttonOnClicked; } if (strcmp(className, "Entry") == 0 && !isStatic) { if (strcmp(signature, "init_()") == 0) return entryInit; if (strcmp(signature, "setText(_)") == 0) return entrySetText; if (strcmp(signature, "text") == 0) return entryGetText; if (strcmp(signature, "onActivate_(_)") == 0) return entryOnActivate; if (strcmp(signature, "onChanged_(_)") == 0) return entryOnChanged; } if (strcmp(className, "Label") == 0 && !isStatic) { if (strcmp(signature, "init_(_)") == 0) return labelInit; if (strcmp(signature, "setText(_)") == 0) return labelSetText; } return NULL; } WrenForeignClassMethods bindGtkForeignClass(WrenVM* vm, const char* module, const char* className) { WrenForeignClassMethods methods = (WrenForeignClassMethods){0, 0}; if (strcmp(module, "gtk") == 0) { if (!strcmp(className, "Window") || !strcmp(className, "Box") || !strcmp(className, "Button") || !strcmp(className, "Entry") || !strcmp(className, "Label")) { methods.allocate = gtkAllocate; methods.finalize = gtkFinalize; } // Gtk (static) carries no foreign storage. } return methods; }