|
// 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;
|
|
}
|
|
|