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.