This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch main
in repository ego.
View the commit online.
commit 4419895d0ad9eed25b35e3a43125066801d122a2
Author: [email protected] <[email protected]>
AuthorDate: Sun Mar 8 22:42:31 2026 -0600
feat: add EFL event callback registration and dispatch mechanism
EFL objects emit events throughout their lifecycle that applications need
to observe (object deletion, property changes, user interactions, etc).
This implements a bridge layer that allows Go code to register callbacks
on EFL events.
The design uses a global map keyed by atomic IDs to store Go closures,
with a single C-callable entry point (egoEventCallback) that dispatches
to the appropriate closure. RWMutex-protected access ensures thread-safe
concurrent dispatch during event handling and callback registration.
Handles are provided for idempotent cleanup via Disconnect(), and the
implementation includes test helpers to safely create and delete test
objects that trigger synchronous event firing on the EFL thread.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
efl/event.go | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++++
efl/event_test.go | 107 ++++++++++++++++++++++++++++++++++++++++++++
efl/export_test.go | 13 ++++++
efl/object.go | 32 ++++++++++++++
4 files changed, 279 insertions(+)
diff --git a/efl/event.go b/efl/event.go
new file mode 100644
index 0000000..36757fb
--- /dev/null
+++ b/efl/event.go
@@ -0,0 +1,127 @@
+package efl
+
+/*
+#cgo pkg-config: elementary
+
+#include <Elementary.h>
+
+// _ego_event_cb_go_fn is the function-pointer type that matches the signature
+// cgo generates for the exported Go function: non-const Efl_Event pointer.
+typedef void (*_ego_event_cb_go_fn)(void *, Efl_Event *);
+
+// egoEventCallback forward declaration matching cgo's generated (non-const)
+// signature. The actual implementation is in Go via //export.
+extern void egoEventCallback(void *data, Efl_Event *event);
+
+// _ego_event_cb_add registers egoEventCallback on obj for the given event
+// description, passing id (cast to void*) as the user data pointer. The cast
+// to Efl_Event_Cb is safe because the const qualifier on the event argument is
+// the only difference; EFL itself discards constness on the registered pointer.
+static void _ego_event_cb_add(Eo *obj, const Efl_Event_Description *desc, uintptr_t id) {
+ efl_event_callback_add(obj, desc, (Efl_Event_Cb)((_ego_event_cb_go_fn)egoEventCallback), (void *)(id));
+}
+
+// _ego_event_cb_del removes the previously registered egoEventCallback from
+// obj for the given event description and id.
+static void _ego_event_cb_del(Eo *obj, const Efl_Event_Description *desc, uintptr_t id) {
+ efl_event_callback_del(obj, desc, (Efl_Event_Cb)((_ego_event_cb_go_fn)egoEventCallback), (void *)(id));
+}
+
+// _ego_efl_event_info returns the info field of an Efl_Event struct.
+static void *_ego_efl_event_info(const Efl_Event *event) {
+ return event->info;
+}
+*/
+import "C"
+
+import (
+ "sync"
+ "sync/atomic"
+ "unsafe"
+)
+
+// cbMap holds all registered Go event callbacks keyed by their unique ID.
+// RLock is used during dispatch; Lock is used for register and disconnect.
+var (
+ cbMu sync.RWMutex
+ cbMap = make(map[uintptr]func(unsafe.Pointer))
+ cbSeq atomic.Uintptr
+)
+
+// Handle represents a registered EFL event callback. Call Disconnect to remove
+// the callback and release all associated resources. A zero Handle is invalid.
+type Handle struct {
+ obj unsafe.Pointer // Eo* the callback is registered on
+ desc unsafe.Pointer // Efl_Event_Description* for the event
+ id uintptr // key into cbMap
+}
+
+// RegisterCallback registers fn as an EFL event callback on obj for the event
+// described by desc. It returns a Handle that can later be used to remove the
+// callback. Must be called on the EFL thread.
+func RegisterCallback(obj unsafe.Pointer, desc unsafe.Pointer, fn func(eventInfo unsafe.Pointer)) Handle {
+ id := cbSeq.Add(1)
+
+ cbMu.Lock()
+ cbMap[id] = fn
+ cbMu.Unlock()
+
+ C._ego_event_cb_add(
+ (*C.Eo)(obj),
+ (*C.Efl_Event_Description)(desc),
+ C.uintptr_t(id),
+ )
+
+ return Handle{obj: obj, desc: desc, id: id}
+}
+
+// Disconnect removes the callback from the EFL object and deletes the Go-side
+// map entry. Must be called on the EFL thread. Calling Disconnect on a zero or
+// already-disconnected Handle is a no-op.
+func (h *Handle) Disconnect() {
+ if h == nil || h.id == 0 {
+ return
+ }
+
+ C._ego_event_cb_del(
+ (*C.Eo)(h.obj),
+ (*C.Efl_Event_Description)(h.desc),
+ C.uintptr_t(h.id),
+ )
+
+ cbMu.Lock()
+ delete(cbMap, h.id)
+ cbMu.Unlock()
+
+ // Zero the handle so subsequent calls are no-ops.
+ h.id = 0
+ h.obj = nil
+ h.desc = nil
+}
+
+// eflEventDelDesc returns the EFL_EVENT_DEL event description pointer as an
+// unsafe.Pointer. Used by tests that need to register against EFL_EVENT_DEL
+// without importing cgo directly.
+func eflEventDelDesc() unsafe.Pointer {
+ return unsafe.Pointer(C.EFL_EVENT_DEL)
+}
+
+// egoEventCallback is the single C-callable entry point for all registered EFL
+// event callbacks. It looks up the Go closure by the ID encoded in data and
+// calls it with the event's info pointer.
+//
+//export egoEventCallback
+func egoEventCallback(data unsafe.Pointer, event *C.Efl_Event) {
+ id := uintptr(data)
+
+ cbMu.RLock()
+ fn := cbMap[id]
+ cbMu.RUnlock()
+
+ if fn == nil {
+ return
+ }
+
+ info := unsafe.Pointer(C._ego_efl_event_info(event))
+ fn(info)
+}
diff --git a/efl/event_test.go b/efl/event_test.go
new file mode 100644
index 0000000..f4975b9
--- /dev/null
+++ b/efl/event_test.go
@@ -0,0 +1,107 @@
+package efl_test
+
+import (
+ "sync/atomic"
+ "testing"
+ "unsafe"
+
+ "git.enlightenment.org/cedric/ego/efl"
+)
+
+// newTestObject allocates a loop-timer Eo object that is exclusively owned by
+// the main loop (no extra caller reference). Calling efl.DelEoObject on the
+// returned pointer sets the parent to null and drops the refcount to zero,
+// triggering EFL_EVENT_DEL synchronously on the EFL thread. Must be called on
+// the EFL thread.
+func newTestObject(t *testing.T) unsafe.Pointer {
+ t.Helper()
+ raw := efl.NewEoObjectOwned()
+ if raw == nil {
+ t.Fatal("NewEoObjectOwned returned nil")
+ }
+ return raw
+}
+
+// TestRegisterCallback verifies that a callback registered for EFL_EVENT_DEL
+// is invoked exactly once when the object is deleted via efl_del.
+func TestRegisterCallback(t *testing.T) {
+ var called atomic.Int32
+
+ efl.Sync(func() {
+ raw := newTestObject(t)
+
+ h := efl.RegisterCallback(raw, efl.EFLEventDelDesc(), func(_ unsafe.Pointer) {
+ called.Add(1)
+ })
+ _ = h
+
+ // efl_del removes the parent and drops refcount to zero, firing
+ // EFL_EVENT_DEL synchronously before returning.
+ efl.DelEoObject(raw)
+ })
+
+ if n := called.Load(); n != 1 {
+ t.Fatalf("callback called %d time(s), want exactly 1", n)
+ }
+}
+
+// TestDisconnect verifies that a disconnected callback is NOT invoked when the
+// object is subsequently deleted.
+func TestDisconnect(t *testing.T) {
+ var called atomic.Int32
+
+ efl.Sync(func() {
+ raw := newTestObject(t)
+
+ h := efl.RegisterCallback(raw, efl.EFLEventDelDesc(), func(_ unsafe.Pointer) {
+ called.Add(1)
+ })
+
+ // Disconnect before the object is deleted.
+ h.Disconnect()
+
+ // Deleting the object must NOT fire the disconnected callback.
+ efl.DelEoObject(raw)
+ })
+
+ if n := called.Load(); n != 0 {
+ t.Fatalf("disconnected callback called %d time(s), want 0", n)
+ }
+}
+
+// TestDisconnectIdempotent verifies that calling Disconnect more than once on
+// the same Handle does not panic or otherwise misbehave.
+func TestDisconnectIdempotent(t *testing.T) {
+ efl.Sync(func() {
+ raw := newTestObject(t)
+
+ h := efl.RegisterCallback(raw, efl.EFLEventDelDesc(), func(_ unsafe.Pointer) {})
+
+ h.Disconnect()
+ h.Disconnect() // must not panic or double-free
+
+ efl.DelEoObject(raw)
+ })
+}
+
+// TestRegisterMultipleCallbacks verifies that multiple callbacks registered for
+// the same event all fire when the event occurs.
+func TestRegisterMultipleCallbacks(t *testing.T) {
+ var count atomic.Int32
+
+ efl.Sync(func() {
+ raw := newTestObject(t)
+
+ for range 3 {
+ efl.RegisterCallback(raw, efl.EFLEventDelDesc(), func(_ unsafe.Pointer) {
+ count.Add(1)
+ })
+ }
+
+ efl.DelEoObject(raw)
+ })
+
+ if n := count.Load(); n != 3 {
+ t.Fatalf("callbacks fired %d time(s), want 3", n)
+ }
+}
diff --git a/efl/export_test.go b/efl/export_test.go
index 7acfd02..2eb85ad 100644
--- a/efl/export_test.go
+++ b/efl/export_test.go
@@ -6,3 +6,16 @@ import "unsafe"
// main loop. Must be called on the EFL thread. Exposed only for use by package
// tests.
func NewEoObject() unsafe.Pointer { return newEoObject() }
+
+// EFLEventDelDesc exposes the EFL_EVENT_DEL event description pointer for use
+// by external package tests that cannot import cgo directly.
+func EFLEventDelDesc() unsafe.Pointer { return eflEventDelDesc() }
+
+// DelEoObject calls efl_del on the raw Eo pointer, triggering EFL_EVENT_DEL
+// once the object's refcount reaches zero. Must be called on the EFL thread.
+func DelEoObject(ptr unsafe.Pointer) { delEoObject(ptr) }
+
+// NewEoObjectOwned creates a loop-timer Eo object without the extra caller
+// reference. Use DelEoObject to delete it and trigger EFL_EVENT_DEL synchronously.
+// Must be called on the EFL thread.
+func NewEoObjectOwned() unsafe.Pointer { return newEoObjectOwned() }
diff --git a/efl/object.go b/efl/object.go
index 1498788..09a540c 100644
--- a/efl/object.go
+++ b/efl/object.go
@@ -17,6 +17,23 @@ static void _ego_efl_unref(Eo *obj) {
efl_unref(obj);
}
+// _ego_efl_del unrefs obj and reparents it to NULL, which causes EFL to emit
+// EFL_EVENT_DEL once the object's reference count reaches zero.
+// efl_del is a real EO_API function, not a macro.
+static void _ego_efl_del(Eo *obj) {
+ efl_del(obj);
+}
+
+// _ego_new_object_owned creates a loop-timer Eo object without the extra caller
+// reference (efl_add rather than efl_add_ref). The object is owned exclusively
+// by its parent (the main loop). Calling efl_del on it immediately removes the
+// parent relationship and drops the refcount to zero, causing EFL_EVENT_DEL to
+// fire synchronously. Intended for tests that observe the DEL event.
+static Eo *_ego_new_object_owned(void) {
+ return efl_add(EFL_LOOP_TIMER_CLASS, efl_main_loop_get(),
+ efl_loop_timer_interval_set(efl_added, 999.0));
+}
+
// _ego_new_object creates a loop timer Eo object under the main loop.
// EFL_OBJECT_CLASS is abstract and cannot be directly instantiated; a loop
// timer is the lightest concrete Eo type available without a display context.
@@ -115,3 +132,18 @@ func (o *Object) Unref() {
func newEoObject() unsafe.Pointer {
return unsafe.Pointer(C._ego_new_object())
}
+
+// delEoObject calls efl_del on the raw Eo pointer, which unrefs the object and
+// reparents it to NULL, triggering EFL_EVENT_DEL once its refcount hits zero.
+// Must be called on the EFL thread.
+func delEoObject(ptr unsafe.Pointer) {
+ C._ego_efl_del((*C.Eo)(ptr))
+}
+
+// newEoObjectOwned allocates a loop-timer Eo instance without the extra caller
+// reference. The object is owned exclusively by the main loop as its parent.
+// Calling delEoObject on it immediately fires EFL_EVENT_DEL synchronously.
+// Must be called on the EFL thread.
+func newEoObjectOwned() unsafe.Pointer {
+ return unsafe.Pointer(C._ego_new_object_owned())
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.