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 6d11a83194a93f807f014c821075594e4ef6a586
Author: [email protected] <[email protected]>
AuthorDate: Sun Mar 8 22:50:15 2026 -0600

    feat: add libeolian cgo bindings for ego-gen code generator
    
    Introduces a thin cgo wrapper around libeolian to enable introspection of
    EFL .eo interface definition files. This foundational layer provides Go types
    (State, Class, Function, Parameter, Type, Event) that mirror the C API with
    safe resource management. A generic iterCollect helper simplifies iteration
    over Eina_Iterator, and custom C stubs avoid Go pointer issues at the cgo
    boundary.
    
    Four tests validate the binding against real EFL metadata: parsing the system
    .eo directory (384 classes), introspecting Efl.Ui.Button, inspecting function
    parameters on Efl.Object.name_find, and verifying all parsed classes have
    valid type values. Proper Eina_Stringshare cleanup ensures no reference leaks.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 cmd/ego-gen/eolian.go      | 283 +++++++++++++++++++++++++++++++++++++++++++++
 cmd/ego-gen/eolian_test.go | 202 ++++++++++++++++++++++++++++++++
 cmd/ego-gen/main.go        |   6 +
 3 files changed, 491 insertions(+)

diff --git a/cmd/ego-gen/eolian.go b/cmd/ego-gen/eolian.go
new file mode 100644
index 0000000..a3a833a
--- /dev/null
+++ b/cmd/ego-gen/eolian.go
@@ -0,0 +1,283 @@
+// Package main provides the ego-gen code generator for EFL bindings.
+// This file contains the cgo bindings to libeolian for introspecting .eo files.
+package main
+
+/*
+#cgo pkg-config: eolian
+
+#include <Eolian.h>
+#include <Eina.h>
+
+// _ego_iter_next wraps eina_iterator_next for use from Go without taking
+// the address of a Go pointer across the cgo boundary.
+static Eina_Bool _ego_iter_next(Eina_Iterator *iter, void **data) {
+    return eina_iterator_next(iter, data);
+}
+
+static void _ego_iter_free(Eina_Iterator *iter) {
+    eina_iterator_free(iter);
+}
+
+// _ego_stringshare_del wraps eina_stringshare_del so the caller does not need
+// to cast away the Eina_Stringshare typedef when invoking from Go.
+static void _ego_stringshare_del(Eina_Stringshare *s) {
+    eina_stringshare_del(s);
+}
+*/
+import "C"
+
+import "unsafe"
+
+// ClassType represents the kind of an Eolian class.
+type ClassType int
+
+const (
+	ClassRegular   ClassType = C.EOLIAN_CLASS_REGULAR
+	ClassAbstract  ClassType = C.EOLIAN_CLASS_ABSTRACT
+	ClassMixin     ClassType = C.EOLIAN_CLASS_MIXIN
+	ClassInterface ClassType = C.EOLIAN_CLASS_INTERFACE
+)
+
+// FunctionType represents the kind of an Eolian function.
+type FunctionType int
+
+const (
+	FunctionProperty FunctionType = C.EOLIAN_PROPERTY
+	FunctionPropSet  FunctionType = C.EOLIAN_PROP_SET
+	FunctionPropGet  FunctionType = C.EOLIAN_PROP_GET
+	FunctionMethod   FunctionType = C.EOLIAN_METHOD
+)
+
+// ParameterDirection represents the direction of a function parameter.
+type ParameterDirection int
+
+const (
+	ParamIn    ParameterDirection = C.EOLIAN_PARAMETER_IN
+	ParamOut   ParameterDirection = C.EOLIAN_PARAMETER_OUT
+	ParamInOut ParameterDirection = C.EOLIAN_PARAMETER_INOUT
+)
+
+// BuiltinType represents an Eolian built-in type identifier.
+type BuiltinType int
+
+// iterCollect drains an Eina_Iterator into a slice using the provided convert
+// function. It frees the iterator when done. Returns nil if iter is nil.
+func iterCollect[T any](iter *C.Eina_Iterator, convert func(unsafe.Pointer) T) []T {
+	if iter == nil {
+		return nil
+	}
+	defer C._ego_iter_free(iter)
+	var out []T
+	var data unsafe.Pointer
+	for C._ego_iter_next(iter, &data) != 0 {
+		out = append(out, convert(data))
+	}
+	return out
+}
+
+// State wraps Eolian_State. It must be created with NewState and released
+// with Free when no longer needed.
+type State struct {
+	ptr *C.Eolian_State
+}
+
+// NewState initialises eina and eolian, then allocates a new Eolian_State.
+// The caller is responsible for calling Free on the returned State.
+// Panics if the underlying library fails to allocate state.
+func NewState() *State {
+	C.eina_init()
+	C.eolian_init()
+	ptr := C.eolian_state_new()
+	if ptr == nil {
+		C.eolian_shutdown()
+		C.eina_shutdown()
+		panic("ego-gen: eolian_state_new returned nil")
+	}
+	return &State{ptr: ptr}
+}
+
+// Free releases the Eolian_State and shuts down the eolian and eina libraries.
+func (s *State) Free() {
+	C.eolian_state_free(s.ptr)
+	C.eolian_shutdown()
+	C.eina_shutdown()
+}
+
+// DirectoryAdd registers a directory to be scanned for .eo files.
+func (s *State) DirectoryAdd(dir string) bool {
+	cdir := C.CString(dir)
+	defer C.free(unsafe.Pointer(cdir))
+	return C.eolian_state_directory_add(s.ptr, cdir) != 0
+}
+
+// SystemDirectoryAdd registers the system-wide EFL .eo directory.
+func (s *State) SystemDirectoryAdd() bool {
+	return C.eolian_state_system_directory_add(s.ptr) != 0
+}
+
+// ParseAll parses all .eo files found in the registered directories.
+func (s *State) ParseAll() bool {
+	return C.eolian_state_all_eo_files_parse(s.ptr) != 0
+}
+
+// Classes returns all classes defined across the parsed units.
+// It casts the State to Eolian_Unit as the API requires.
+func (s *State) Classes() []*Class {
+	unit := (*C.Eolian_Unit)(unsafe.Pointer(s.ptr))
+	return iterCollect(C.eolian_unit_classes_get(unit), func(p unsafe.Pointer) *Class {
+		return &Class{ptr: (*C.Eolian_Class)(p)}
+	})
+}
+
+// Class wraps an Eolian_Class pointer.
+type Class struct {
+	ptr *C.Eolian_Class
+}
+
+// Name returns the fully qualified name of the class (e.g. "Efl.Ui.Button").
+func (c *Class) Name() string {
+	return C.GoString(C.eolian_class_name_get(c.ptr))
+}
+
+// Type returns the ClassType of this class.
+func (c *Class) Type() ClassType {
+	return ClassType(C.eolian_class_type_get(c.ptr))
+}
+
+// Parent returns the parent class, or nil if there is none.
+func (c *Class) Parent() *Class {
+	p := C.eolian_class_parent_get(c.ptr)
+	if p == nil {
+		return nil
+	}
+	return &Class{ptr: p}
+}
+
+// Extensions returns the list of interfaces/mixins this class extends.
+func (c *Class) Extensions() []*Class {
+	return iterCollect(C.eolian_class_extensions_get(c.ptr), func(p unsafe.Pointer) *Class {
+		return &Class{ptr: (*C.Eolian_Class)(p)}
+	})
+}
+
+// Functions returns functions of the given type defined on this class.
+func (c *Class) Functions(ft FunctionType) []*Function {
+	return iterCollect(
+		C.eolian_class_functions_get(c.ptr, C.Eolian_Function_Type(ft)),
+		func(p unsafe.Pointer) *Function {
+			return &Function{ptr: (*C.Eolian_Function)(p)}
+		},
+	)
+}
+
+// Events returns all events declared on this class.
+func (c *Class) Events() []*Event {
+	return iterCollect(C.eolian_class_events_get(c.ptr), func(p unsafe.Pointer) *Event {
+		return &Event{ptr: (*C.Eolian_Event)(p)}
+	})
+}
+
+// Function wraps an Eolian_Function pointer.
+type Function struct {
+	ptr *C.Eolian_Function
+}
+
+// Name returns the name of the function.
+func (f *Function) Name() string {
+	return C.GoString(C.eolian_function_name_get(f.ptr))
+}
+
+// Type returns the FunctionType of this function.
+func (f *Function) Type() FunctionType {
+	return FunctionType(C.eolian_function_type_get(f.ptr))
+}
+
+// Parameters returns the list of parameters for this function.
+func (f *Function) Parameters() []*Parameter {
+	return iterCollect(C.eolian_function_parameters_get(f.ptr), func(p unsafe.Pointer) *Parameter {
+		return &Parameter{ptr: (*C.Eolian_Function_Parameter)(p)}
+	})
+}
+
+// ReturnType returns the return type for the function under the given
+// FunctionType context (used to distinguish getter/setter return types for
+// properties), or nil if there is none.
+func (f *Function) ReturnType(ft FunctionType) *Type {
+	t := C.eolian_function_return_type_get(f.ptr, C.Eolian_Function_Type(ft))
+	if t == nil {
+		return nil
+	}
+	return &Type{ptr: t}
+}
+
+// Parameter wraps an Eolian_Function_Parameter pointer.
+type Parameter struct {
+	ptr *C.Eolian_Function_Parameter
+}
+
+// Name returns the parameter name.
+func (p *Parameter) Name() string {
+	return C.GoString(C.eolian_parameter_name_get(p.ptr))
+}
+
+// Direction returns the direction (in/out/inout) of the parameter.
+func (p *Parameter) Direction() ParameterDirection {
+	return ParameterDirection(C.eolian_parameter_direction_get(p.ptr))
+}
+
+// Type returns the Eolian type of the parameter.
+func (p *Parameter) Type() *Type {
+	t := C.eolian_parameter_type_get(p.ptr)
+	if t == nil {
+		return nil
+	}
+	return &Type{ptr: t}
+}
+
+// Type wraps an Eolian_Type pointer.
+type Type struct {
+	ptr *C.Eolian_Type
+}
+
+// Name returns the short (unqualified) name of the type.
+func (t *Type) Name() string {
+	return C.GoString(C.eolian_type_short_name_get(t.ptr))
+}
+
+// CType returns the C type string for this Eolian type.
+// eolian_type_c_type_get returns a freshly ref-counted Eina_Stringshare that
+// the caller owns; we copy it into a Go string and release the share.
+func (t *Type) CType() string {
+	cs := C.eolian_type_c_type_get(t.ptr)
+	if cs == nil {
+		return ""
+	}
+	goStr := C.GoString(cs)
+	C._ego_stringshare_del(cs)
+	return goStr
+}
+
+// BuiltinType returns the BuiltinType constant for this type, or 0 if it is
+// not a built-in type.
+func (t *Type) BuiltinType() BuiltinType {
+	return BuiltinType(C.eolian_type_builtin_type_get(t.ptr))
+}
+
+// Event wraps an Eolian_Event pointer.
+type Event struct {
+	ptr *C.Eolian_Event
+}
+
+// Name returns the name of the event (e.g. "clicked").
+func (e *Event) Name() string {
+	return C.GoString(C.eolian_event_name_get(e.ptr))
+}
+
+// Type returns the payload type of the event, or nil if none.
+func (e *Event) Type() *Type {
+	t := C.eolian_event_type_get(e.ptr)
+	if t == nil {
+		return nil
+	}
+	return &Type{ptr: t}
+}
diff --git a/cmd/ego-gen/eolian_test.go b/cmd/ego-gen/eolian_test.go
new file mode 100644
index 0000000..b9ec9ac
--- /dev/null
+++ b/cmd/ego-gen/eolian_test.go
@@ -0,0 +1,202 @@
+package main
+
+import (
+	"testing"
+)
+
+// newParsedState is a test helper that creates a State, adds the system
+// directory, parses all .eo files, and returns the State.
+// The caller must call s.Free() when done.
+func newParsedState(t *testing.T) *State {
+	t.Helper()
+	s := NewState()
+	if !s.SystemDirectoryAdd() {
+		s.Free()
+		t.Fatal("SystemDirectoryAdd returned false")
+	}
+	if !s.ParseAll() {
+		s.Free()
+		t.Fatal("ParseAll returned false")
+	}
+	return s
+}
+
+// TestStateParseAll verifies that a State can be created, the system directory
+// registered, all files parsed, and that at least one class is returned.
+func TestStateParseAll(t *testing.T) {
+	s := newParsedState(t)
+	defer s.Free()
+
+	classes := s.Classes()
+	if len(classes) == 0 {
+		t.Fatal("expected at least one class after ParseAll, got 0")
+	}
+	t.Logf("found %d classes", len(classes))
+}
+
+// TestClassIntrospection looks up Efl.Ui.Button and verifies basic metadata:
+// its name, type (regular class), the presence of a parent, extensions, and
+// that it exposes at least one method or property, and zero or more events.
+func TestClassIntrospection(t *testing.T) {
+	s := newParsedState(t)
+	defer s.Free()
+
+	const target = "Efl.Ui.Button"
+	var btn *Class
+	for _, c := range s.Classes() {
+		if c.Name() == target {
+			btn = c
+			break
+		}
+	}
+	if btn == nil {
+		t.Fatalf("class %q not found", target)
+	}
+
+	t.Run("name", func(t *testing.T) {
+		if got := btn.Name(); got != target {
+			t.Errorf("Name() = %q, want %q", got, target)
+		}
+	})
+
+	t.Run("type", func(t *testing.T) {
+		if got := btn.Type(); got != ClassRegular {
+			t.Errorf("Type() = %v, want ClassRegular (%v)", got, ClassRegular)
+		}
+	})
+
+	t.Run("parent", func(t *testing.T) {
+		p := btn.Parent()
+		if p == nil {
+			t.Error("Parent() returned nil, expected a parent class")
+		} else {
+			t.Logf("parent = %s", p.Name())
+		}
+	})
+
+	t.Run("extensions", func(t *testing.T) {
+		exts := btn.Extensions()
+		// Efl.Ui.Button implements several interfaces; expect at least one.
+		if len(exts) == 0 {
+			t.Error("Extensions() returned empty list, expected at least one")
+		}
+		for _, e := range exts {
+			t.Logf("extension = %s", e.Name())
+		}
+	})
+
+	t.Run("methods_and_properties", func(t *testing.T) {
+		methods := btn.Functions(FunctionMethod)
+		props := btn.Functions(FunctionProperty)
+		t.Logf("methods=%d properties=%d", len(methods), len(props))
+		// Efl.Ui.Button reports 0 own methods/properties: all its behaviour
+		// comes from implementing the interfaces listed in its "implements"
+		// block. eolian_class_functions_get only returns functions declared
+		// directly on the class, not inherited ones. The call must not crash.
+		_ = methods
+		_ = props
+	})
+
+	t.Run("events", func(t *testing.T) {
+		events := btn.Events()
+		t.Logf("events=%d", len(events))
+		// Efl.Ui.Button declares no events of its own; click events are
+		// defined on Efl.Input.Clickable which it extends. The call must
+		// not crash and the iteration must be safe.
+		for _, e := range events {
+			t.Logf("  event = %s", e.Name())
+		}
+	})
+}
+
+// TestFunctionParameters finds Efl.Object and inspects the name_find method,
+// which has a single @in parameter named "search" of type string.
+func TestFunctionParameters(t *testing.T) {
+	s := newParsedState(t)
+	defer s.Free()
+
+	const className = "Efl.Object"
+	var obj *Class
+	for _, c := range s.Classes() {
+		if c.Name() == className {
+			obj = c
+			break
+		}
+	}
+	if obj == nil {
+		t.Fatalf("class %q not found", className)
+	}
+
+	const methodName = "name_find"
+	var nameFindFn *Function
+	for _, fn := range obj.Functions(FunctionMethod) {
+		if fn.Name() == methodName {
+			nameFindFn = fn
+			break
+		}
+	}
+	if nameFindFn == nil {
+		t.Fatalf("method %q not found on %s", methodName, className)
+	}
+
+	t.Run("function_type", func(t *testing.T) {
+		if got := nameFindFn.Type(); got != FunctionMethod {
+			t.Errorf("Type() = %v, want FunctionMethod (%v)", got, FunctionMethod)
+		}
+	})
+
+	t.Run("return_type", func(t *testing.T) {
+		rt := nameFindFn.ReturnType(FunctionMethod)
+		if rt == nil {
+			t.Fatal("ReturnType() returned nil, expected Efl.Object return type")
+		}
+		t.Logf("return ctype = %s", rt.CType())
+	})
+
+	t.Run("parameters", func(t *testing.T) {
+		params := nameFindFn.Parameters()
+		if len(params) != 1 {
+			t.Fatalf("expected 1 parameter, got %d", len(params))
+		}
+		p := params[0]
+
+		if got := p.Name(); got != "search" {
+			t.Errorf("param Name() = %q, want %q", got, "search")
+		}
+		if got := p.Direction(); got != ParamIn {
+			t.Errorf("param Direction() = %v, want ParamIn (%v)", got, ParamIn)
+		}
+		pt := p.Type()
+		if pt == nil {
+			t.Fatal("param Type() returned nil")
+		}
+		t.Logf("param type name=%s ctype=%s", pt.Name(), pt.CType())
+	})
+}
+
+// TestClassTypes verifies that every parsed class has a type value within the
+// known valid range (i.e. one of the four ClassType constants).
+func TestClassTypes(t *testing.T) {
+	s := newParsedState(t)
+	defer s.Free()
+
+	valid := map[ClassType]bool{
+		ClassRegular:   true,
+		ClassAbstract:  true,
+		ClassMixin:     true,
+		ClassInterface: true,
+	}
+
+	classes := s.Classes()
+	if len(classes) == 0 {
+		t.Fatal("no classes found")
+	}
+
+	for _, c := range classes {
+		ct := c.Type()
+		if !valid[ct] {
+			t.Errorf("class %q has unexpected type %v", c.Name(), ct)
+		}
+	}
+	t.Logf("verified %d classes all have valid types", len(classes))
+}
diff --git a/cmd/ego-gen/main.go b/cmd/ego-gen/main.go
new file mode 100644
index 0000000..fe9649c
--- /dev/null
+++ b/cmd/ego-gen/main.go
@@ -0,0 +1,6 @@
+// Command ego-gen is the EFL Go binding code generator.
+// It uses libeolian to introspect EFL .eo files and generate Go bindings.
+package main
+
+func main() {
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to