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 0375ce91246daa0518295ef49a15e0c1191c3b02
Author: [email protected] <[email protected]>
AuthorDate: Fri Mar 13 18:40:31 2026 -0600
feat: move EFL thread to calling goroutine for direct API calls
The Init() function now locks the calling goroutine's OS thread directly
instead of spawning a background goroutine. This makes the calling goroutine
the EFL thread, allowing direct EFL calls without wrapping them in efl.Sync()
callbacks.
Wait() enters ecore_main_loop_begin() on that same goroutine, providing a
simple linear flow: Init() → setup → Wait(). Added Run(setup func()) as a
convenience combining all three.
Object tracking (trackObject/cleanupTrackedObjects) prevents 'efl_del on
object with no parent' errors during shutdown by explicitly unreffing all
Go-owned Eo pointers before elm_shutdown.
This simplifies the API significantly: code now reads naturally as a series
of sequential EFL calls rather than nested callbacks. Tests and harness
updated to run test code in a separate goroutine while Wait() drives the
main loop on the calling goroutine.
---
efl/efl.go | 131 +++++++++++++++++++++++--------------------
efl/efl_test.go | 7 ++-
efl/object.go | 50 ++++++++++++++++-
examples/hello_world/main.go | 50 ++++++++---------
tests/harness/harness.go | 21 ++++---
5 files changed, 158 insertions(+), 101 deletions(-)
diff --git a/efl/efl.go b/efl/efl.go
index ae845c2..0cf954a 100644
--- a/efl/efl.go
+++ b/efl/efl.go
@@ -58,70 +58,42 @@ var (
stopped chan struct{}
)
-// Init starts the EFL runtime. It spawns a dedicated OS thread, pins it with
-// runtime.LockOSThread, initialises elm + ecore_evas, creates an ecore_pipe for
-// cross-thread wakeup, and then blocks that thread inside ecore_main_loop_begin.
-// Init returns once the main loop is running and ready to accept work.
-// Calling Init more than once is a no-op after the first successful call.
+// Init initialises EFL on the calling goroutine's OS thread. After Init
+// returns, EFL calls (creating widgets, setting properties, etc.) can be made
+// directly from the same goroutine — no Sync wrapper is needed. Call Wait when
+// setup is complete to enter the main loop.
+//
+// Other goroutines may use Post and Sync to dispatch work to the EFL thread;
+// queued tasks are processed once Wait enters the main loop.
+//
+// Init, Wait, and Run must all be called from the same goroutine (typically
+// the main goroutine). Calling Init more than once is a no-op.
func Init() {
initOnce.Do(func() {
queue = make(chan task, 256)
stopped = make(chan struct{})
- ready := make(chan struct{})
+ // Pin this goroutine permanently to its OS thread. EFL requires that
+ // all calls originate from the thread that called elm_init.
+ runtime.LockOSThread()
- go func() {
- // Pin this goroutine permanently to its OS thread. EFL requires that
- // all calls originate from the thread that called elm_init.
- runtime.LockOSThread()
- defer runtime.UnlockOSThread()
+ // elm_init requires argc/argv; pass a synthetic single-element argv.
+ progName := C.CString("ego")
+ defer C.free(unsafe.Pointer(progName))
+ argv := []*C.char{progName, nil}
+ argc := C.int(1)
+ // elm_init initialises the full Elementary stack, which includes
+ // ecore_evas internally. Do not call ecore_evas_init separately;
+ // doing so would leave the refcount unbalanced and cause elm_shutdown
+ // to spin the main loop indefinitely waiting for evas objects.
+ C.elm_init(argc, &argv[0])
- // elm_init requires argc/argv; pass a synthetic single-element argv.
- progName := C.CString("ego")
- defer C.free(unsafe.Pointer(progName))
- argv := []*C.char{progName, nil}
- argc := C.int(1)
- // elm_init initialises the full Elementary stack, which includes
- // ecore_evas internally. Do not call ecore_evas_init separately;
- // doing so would leave the refcount unbalanced and cause elm_shutdown
- // to spin the main loop indefinitely waiting for evas objects.
- C.elm_init(argc, &argv[0])
-
- // Create the pipe. The callback drains the queue on every wakeup.
- // The data pointer is unused; we access the package-level state directly.
- pipe = C.ecore_pipe_add(
- (C.Ecore_Pipe_Cb)(unsafe.Pointer(C.pipeCallback)),
- nil,
- )
-
- // Signal callers waiting in Init that the loop is ready.
- close(ready)
-
- // Block the OS thread inside the EFL main loop until Shutdown is called.
- C.ecore_main_loop_begin()
-
- // ecore_main_loop_begin has returned. closed is true (set by the
- // shutdown task). Acquire pipeMu to wait for any concurrent writePipe
- // call that is still inside C.ecore_pipe_write to finish. Once we
- // hold the lock, writePipe will observe closed==true and skip the
- // write, so no further writes can arrive. It is then safe to free
- // the pipe.
- pipeMu.Lock()
- C.ecore_pipe_del(pipe)
- pipeMu.Unlock()
-
- // Unblock Wait() callers. Dispatch is permanently closed.
- // elm_shutdown runs after so its internal main-loop iteration does
- // not hold up Wait callers.
- close(stopped)
-
- // elm_shutdown tears down the full Elementary stack including ecore_evas.
- // This may iterate the main loop internally; it runs on the locked OS
- // thread until complete, independently of any Go callers.
- C.elm_shutdown()
- }()
-
- <-ready
+ // Create the pipe. The callback drains the queue on every wakeup.
+ // The data pointer is unused; we access the package-level state directly.
+ pipe = C.ecore_pipe_add(
+ (C.Ecore_Pipe_Cb)(unsafe.Pointer(C.pipeCallback)),
+ nil,
+ )
})
}
@@ -143,15 +115,50 @@ func Shutdown() {
})
}
-// Wait blocks until the EFL main loop has exited and the dispatch pipe has been
-// freed, meaning no further Post or Sync calls will be delivered. elm_shutdown
-// may still be running on the EFL OS thread after Wait returns. Calling Wait
-// before Init returns immediately.
+// Wait enters the EFL main loop and blocks until Shutdown is called. It must
+// be called from the same goroutine that called Init.
+//
+// After ecore_main_loop_begin returns, Wait tears down the dispatch pipe and
+// releases the OS thread lock. Calling Wait before Init returns immediately.
func Wait() {
if stopped == nil {
return
}
- <-stopped
+ // Enter the main loop on the EFL thread. This blocks until Shutdown
+ // calls ecore_main_loop_quit.
+ C.ecore_main_loop_begin()
+
+ // ecore_main_loop_begin has returned. closed is true (set by the
+ // shutdown task). Acquire pipeMu to wait for any concurrent writePipe
+ // call that is still inside C.ecore_pipe_write to finish. Once we
+ // hold the lock, writePipe will observe closed==true and skip the
+ // write, so no further writes can arrive. It is then safe to free
+ // the pipe.
+ pipeMu.Lock()
+ C.ecore_pipe_del(pipe)
+ pipeMu.Unlock()
+
+ close(stopped)
+
+ // Release all Go-owned Eo references so EFL can tear down the object
+ // hierarchy without "efl_del on object with no parent" warnings.
+ cleanupTrackedObjects()
+
+ // elm_shutdown tears down the full Elementary stack including ecore_evas.
+ // This may iterate the main loop internally; it runs on the locked OS
+ // thread until complete.
+ C.elm_shutdown()
+
+ runtime.UnlockOSThread()
+}
+
+// Run initialises EFL, calls setup on the EFL thread, enters the main loop, and
+// blocks until the main loop exits (i.e. until Shutdown is called). It combines
+// Init + setup + Wait into a single convenience call.
+func Run(setup func()) {
+ Init()
+ setup()
+ Wait()
}
// Post enqueues fn for asynchronous execution on the EFL thread and returns
diff --git a/efl/efl_test.go b/efl/efl_test.go
index 7ec25fa..55c8d13 100644
--- a/efl/efl_test.go
+++ b/efl/efl_test.go
@@ -13,8 +13,11 @@ import (
// because Init/Shutdown are once-per-process operations.
func TestMain(m *testing.M) {
efl.Init()
- code := m.Run()
- efl.Shutdown()
+ var code int
+ go func() {
+ code = m.Run()
+ efl.Shutdown()
+ }()
efl.Wait()
os.Exit(code)
}
diff --git a/efl/object.go b/efl/object.go
index 134698b..6924e6f 100644
--- a/efl/object.go
+++ b/efl/object.go
@@ -67,6 +67,7 @@ import "C"
import (
"runtime"
+ "sync"
"unsafe"
)
@@ -81,15 +82,54 @@ type Object struct {
ptr unsafe.Pointer // Eo*; nil means no underlying object
}
+// trackedObjs records all Go-owned Eo pointers (those created via efl_add_ref
+// and wrapped by WrapObject/SetEo). Before elm_shutdown, cleanupTrackedObjects
+// unrefs them so EFL can tear down the object hierarchy cleanly.
+var (
+ trackedMu sync.Mutex
+ trackedObjs = make(map[unsafe.Pointer]int) // ptr → refcount of Go wrappers
+)
+
+func trackObject(ptr unsafe.Pointer) {
+ trackedMu.Lock()
+ trackedObjs[ptr]++
+ trackedMu.Unlock()
+}
+
+func untrackObject(ptr unsafe.Pointer) {
+ trackedMu.Lock()
+ if trackedObjs[ptr] <= 1 {
+ delete(trackedObjs, ptr)
+ } else {
+ trackedObjs[ptr]--
+ }
+ trackedMu.Unlock()
+}
+
+// cleanupTrackedObjects unrefs all Go-owned Eo pointers that were not already
+// freed by GC finalizers. Must be called on the EFL thread before elm_shutdown.
+func cleanupTrackedObjects() {
+ trackedMu.Lock()
+ for ptr, count := range trackedObjs {
+ ClearCallbacks(ptr)
+ for range count {
+ C._ego_efl_unref((*C.Eo)(ptr))
+ }
+ }
+ clear(trackedObjs)
+ trackedMu.Unlock()
+}
+
// installFinalizer registers a GC finalizer on o that posts an Unref to the
// EFL thread when o is collected. It must only be called with a non-nil o.ptr.
func installFinalizer(o *Object) {
runtime.SetFinalizer(o, func(obj *Object) {
// Post the Unref so it runs on the EFL thread. The closed guard inside
- // Post means this is a no-op after Shutdown — acceptable because the EFL
- // runtime itself cleans up remaining objects during elm_shutdown.
+ // Post means this is a no-op after Shutdown — acceptable because
+ // cleanupTrackedObjects handles any remaining objects before elm_shutdown.
p := obj.ptr
Post(func() {
+ untrackObject(p)
ClearCallbacks(p)
C._ego_efl_unref((*C.Eo)(p))
})
@@ -105,6 +145,7 @@ func WrapObject(ptr unsafe.Pointer) *Object {
return nil
}
o := &Object{ptr: ptr}
+ trackObject(ptr)
installFinalizer(o)
return o
}
@@ -136,8 +177,12 @@ func (o *Object) Eo() unsafe.Pointer {
// SetEo). Used by generated bindings to initialise wrapper structs.
func (o *Object) SetEo(ptr unsafe.Pointer) {
runtime.SetFinalizer(o, nil)
+ if o.ptr != nil {
+ untrackObject(o.ptr)
+ }
o.ptr = ptr
if ptr != nil {
+ trackObject(ptr)
installFinalizer(o)
}
}
@@ -173,6 +218,7 @@ func (o *Object) Unref() {
runtime.SetFinalizer(o, nil)
p := o.ptr
o.ptr = nil
+ untrackObject(p)
ClearCallbacks(p)
C._ego_efl_unref((*C.Eo)(p))
}
diff --git a/examples/hello_world/main.go b/examples/hello_world/main.go
index 4b6a569..d51652e 100644
--- a/examples/hello_world/main.go
+++ b/examples/hello_world/main.go
@@ -14,38 +14,36 @@ import (
func main() {
efl.Init()
- efl.Sync(func() {
- // Window — win_name is a constructor property and must be passed via
- // WithWinName so it is applied during efl_add, not after.
- win := ui.NewWin(nil, ui.WithWinName("Hello World"), canvas.WithHintSizeMin(efl.Size2D{W: 400, H: 200}))
- win.SetText("Hello World")
+ // Window — win_name is a constructor property and must be passed via
+ // WithWinName so it is applied during efl_add, not after.
+ win := ui.NewWin(nil, ui.WithWinName("Hello World"), canvas.WithHintSizeMin(efl.Size2D{W: 400, H: 200}))
+ win.SetText("Hello World")
- // Vertical box as the window content.
- box := ui.NewBox(win)
- win.SetContent(box)
+ // Vertical box as the window content.
+ box := ui.NewBox(win)
+ win.SetContent(box)
- // Label (a non-editable textbox).
- label := ui.NewTextbox(box)
- label.SetText("Hello world!")
- box.PackEnd(label)
+ // Label (a non-editable textbox).
+ label := ui.NewTextbox(box)
+ label.SetText("Hello world!")
+ box.PackEnd(label)
- // Editable text entry, initialized with "world".
- entry := ui.NewTextbox(box)
- entry.SetText("world")
- box.PackEnd(entry)
+ // Editable text entry, initialized with "world".
+ entry := ui.NewTextbox(box)
+ entry.SetText("world")
+ box.PackEnd(entry)
- // Button that updates the label.
- btn := ui.NewButton(box)
- btn.SetText("Greet")
- box.PackEnd(btn)
+ // Button that updates the label.
+ btn := ui.NewButton(box)
+ btn.SetText("Greet")
+ box.PackEnd(btn)
- btn.OnClicked(func() {
- label.SetText("Hello " + entry.Text() + "!")
- })
+ btn.OnClicked(func() {
+ label.SetText("Hello " + entry.Text() + "!")
+ })
- win.OnDeleteRequest(func() {
- efl.Shutdown()
- })
+ win.OnDeleteRequest(func() {
+ efl.Shutdown()
})
efl.Wait()
diff --git a/tests/harness/harness.go b/tests/harness/harness.go
index 550ded3..c1e9b22 100644
--- a/tests/harness/harness.go
+++ b/tests/harness/harness.go
@@ -20,26 +20,29 @@ import (
"git.enlightenment.org/cedric/ego/efl"
)
-// Run initializes headless EFL, calls the optional setup function, runs the
-// test suite, then shuts EFL down and waits for full teardown before returning
-// the exit code. It is intended for use in TestMain:
+// Run initializes headless EFL, calls the optional setup function on the EFL
+// thread, runs the test suite in a separate goroutine, then enters the main loop
+// and waits for tests to complete. When all tests finish, Shutdown is called
+// and Run returns the exit code. It is intended for use in TestMain:
//
// func TestMain(m *testing.M) { os.Exit(harness.Run(m, nil)) }
//
-// If setup is non-nil it is invoked on the EFL thread via efl.Sync after Init
-// returns but before m.Run is called.
+// If setup is non-nil it is called directly after Init (the calling goroutine
+// is the EFL thread).
func Run(m *testing.M, setup func()) int {
efl.Init()
if setup != nil {
- efl.Sync(setup)
+ setup()
}
- code := m.Run()
+ var code int
+ go func() {
+ code = m.Run()
+ efl.Shutdown()
+ }()
- efl.Shutdown()
efl.Wait()
-
return code
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.