// 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 <gtk/gtk.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
// --- 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;
}