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.

Reply via email to