Yet another C object model - but better

It has been a while since I posted anything about E or EFL. I have been neglectful and it's time to change that. I want to do a quick intro on something really cool and new in EFL. It's part of a long-term move towards EFL 2.0. And that something is... Eo.

So the "G" world (GLib, GTK+, GNOME etc.) has GObject. We couldn't let them have all the fun, so We've made our own. We call it Eo (EFL Objects). It gives you objects in plain-old-C. Not just plain objects, but a host of related features. What do you get?

  • Inheritance
  • Interfaces
  • Mixins
  • Multiple inheritance
  • Method / property overriding
  • Properties
  • All object internals are opaque
  • Reference counting
  • Callbacks (all objects)
  • Cross references
  • Parent / child object hierarchy
  • Weak references
  • Key / value attachment on all objects
  • Code generation / maintenance to avoid boilerplate
    • Define your classes in Eo files cleanly
  • Multiple language support (beyond C) for bindings
    • Done by code generation and base type support
    • Currently C++ and Lua
      • C++ bindings use C ABI, not C++ ABI (fewer problems)
    • Plans to add Python and JS (v8)
  • Runtime type safety for objects
  • Runtime method / property call safety (if not supported becomes NOOP)
  • Object indirection to remove pointer bugs
  • Multi-call per object de-reference (lowers object access overhead)
  • Construction finalizers allowing calls during construction for lower setup overheads

And I'm sure I missed a few things. Why are we doing this? Why re-invent wheels? Why not just port to C++? Oh so many questions.

First - we like C. It just needs some extra filling out. Eo does just that. Changing our API to C++ would instantly shut out a lot of C developers and limit our audience, instead we are doing the inverse. We are adding a full and proper C++ API to EFL that is a direct 1:1 mapping to our Eo classes, properties and so on.

We can't feasibly use GObject without bringing in all of the GLib world with it, and GObject doesn't do many of the above things, so we're choosing a home-grown solution. In fact if you look at Eo a bit, you'll see it resembles JS, Python or Lua in terms of objects more than C++. In fact our object pointer indirection alone means we need to do it ourselves. But really - it's more fun to make your own object system in C.

But really it's a by-product of trying to clean up our libraries and their existing ad-hoc object model. This is basically what GObject did for GTK+ and we are now doing the same. We have a lot of interfaces that are duplicated in normal C functions because that becomes a necessity. Like:

evas_object_image_file_set(obj, "blah.png", "key");
edje_object_file_set(obj, "blah.edj", "group");

Or

evas_object_del(obj);
ecore_timer_del(obj);
ecore_animator_del(obj);

There are 100's of such examples across APIs in EFL. We are moving to clean this up and have "one class to rule them all ... and in the darkness bind them":

eo_do(obj, efl_file_set("blah.file", "key"));

Or

eo_del(obj);

Nice and simple now. So how do you make a new class? It's not hard. Make an Eo file like this:

class Tst (Eo.Base)
{
   data: Tst_Data;
   properties {
      name {
         set { /*@ This sets the name of the tst object */
         }
         get { /*@ This gets the name of the tst object if set */
         }
         values {
            const(char) *name; /*@ The name of the tst object as a C string */
         }
      }
      size {
         set { /*@ This sets the size of the object, and on success
                * returns EINA_TRUE, and on failure EINA_FALSE */
            return Eina_Bool;
         }
         get { /*@ This gets the size set */
         }
         values {
            int size; /*@ The size in pixels */
         }
      }
   }
   methods {
      activate { /*@ This method will activate the tst object, and when
                  * called, any events listening to activated will be
                  * triggered */
         params {
            @in int number; /*@ The number of pixels to activate */
            @in const(char) *string; /*@ A label to display on activation */
         }
         return Eina_Bool; /* If activation succeeds, return EINA_TRUE */
      }
      disable { /*@ This disables the tst object to the level indicated */
         params {
            @in int level; /*@ This is the disabling level to use */
         }
      }
   }
   implements {
      class.constructor;
      class.destructor;
   }
   events {
      activated; /*@ When the tst object has been activated */
      disabled; /*@ When the tst object has been disabled */
   }
}

Now to use the eolian code generators to fill in your class nuts and bolts. Let's assume you saved the above as tst.eo

eolian_gen -I /usr/local/share/eolian/include/eo-1 --gc --eo -o tst.eo.c tst.eo
eolian_gen -I /usr/local/share/eolian/include/eo-1 --gh --eo -o tst.eo.h tst.eo
eolian_gen -I /usr/local/share/eolian/include/eo-1 --gi --eo -o tst.c tst.eo

This generates your class eo header (tst.eo.h) and the body of the code (tst.eo.c) and the actual class implementation in tst.c. You never have to touch tst.eo.c and tst.eo.h as these are fully generated and have no need for human maintenance. So let's look at tst.c:

#define EFL_BETA_API_SUPPORT
#include <Eo.h>
#include "tst.eo.h"

typedef struct
{

} Tst_Data;

EOLIAN static void
_tst_name_set(Eo *obj, Tst_Data *pd, const char *name)
{

}

EOLIAN static const char *
_tst_name_get(Eo *obj, Tst_Data *pd)
{

}

EOLIAN static Eina_Bool
_tst_size_set(Eo *obj, Tst_Data *pd, int size)
{

}

EOLIAN static int
_tst_size_get(Eo *obj, Tst_Data *pd)
{

}

EOLIAN static Eina_Bool
_tst_activate(Eo *obj, Tst_Data *pd, int number, const char *string)
{

}

EOLIAN static void
_tst_disable(Eo *obj, Tst_Data *pd, int level)
{

}

EOLIAN static void
_tst_eo_base_constructor(Eo *obj, Tst_Data *pd)
{
   eo_do_super(obj, TST_CLASS, eo_constructor());
}

EOLIAN static void
_tst_eo_base_destructor(Eo *obj, Tst_Data *pd)
{
   eo_do_super(obj, TST_CLASS, eo_destructor());
}

#include "tst.eo.c"

This is basically an empty useless class that does nothing, BUT almost all of the boring boilerplate code is done for you. We have no code to USE our test class, so let's make some in main.c:

#define EFL_BETA_API_SUPPORT
#include <Eo.h>
#include "tst.eo.h"

static Eina_Bool
on_activated(void *data, Eo *obj, const Eo_Event_Description *desc, void *event_info)
{
   printf("activated callback for %p\n", obj);
   return EINA_TRUE; // pass event on to next cb
}

static Eina_Bool
on_disabled(void *data, Eo *obj, const Eo_Event_Description *desc, void *event_info)
{
   printf("disabled callback for %p\n", obj);
   return EINA_TRUE; // pass event on to next cb
}

int main(int argc, char **argv)
{
   eo_init(); // init eo
   
   Eo *obj = eo_add(TST_CLASS, NULL);
   eo_do(obj,
         tst_name_set("Smelly"),
         tst_size_set(100),
         eo_event_callback_add(TST_EVENT_ACTIVATED, on_activated, NULL),
         eo_event_callback_add(TST_EVENT_DISABLED, on_disabled, NULL));
   eo_do(obj, tst_activate(37, "Chickens"));
   eo_do(obj, tst_disable(99));
   eo_del(obj);
   return 0;
}

And we shall compile all of this into a small test binary with:

gcc tst.c main.c -o tst `pkg-config --cflags --libs eo eina`

And voila. A tst" binary. Let's now go and fill in our class implementation and update tst.c to be like:

#define EFL_BETA_API_SUPPORT
#include <Eo.h>
#include "tst.eo.h"

typedef struct
{
   int size;
   char *name;
   Eina_Bool activated : 1;
   Eina_Bool disabled : 1;
} Tst_Data;

EOLIAN static void
_tst_name_set(Eo *obj, Tst_Data *pd, const char *name)
{
   printf("set %s\n", name);
   if (pd->name) free(pd->name);
   pd->name = strdup(name);
}

EOLIAN static const char *
_tst_name_get(Eo *obj, Tst_Data *pd)
{
   return pd->name;
}

EOLIAN static Eina_Bool
_tst_size_set(Eo *obj, Tst_Data *pd, int size)
{
   printf("size set %i -> %i\n", pd->size, size);
   pd->size = size;
   return EINA_TRUE;
}

EOLIAN static int
_tst_size_get(Eo *obj, Tst_Data *pd)
{
   return pd->size;
}

EOLIAN static Eina_Bool
_tst_activate(Eo *obj, Tst_Data *pd, int number, const char *string)
{
   printf("activate! %i '%s'\n", number, string);
   pd->activated = EINA_TRUE;
   // strictly correct...
   eo_do(obj, eo_event_callback_call(TST_EVENT_ACTIVATED, NULL));
}

EOLIAN static void
_tst_disable(Eo *obj, Tst_Data *pd, int level)
{
   printf("disable\n");
   pd->disabled = EINA_TRUE;
   //this is just fine. it's a local call within  an eo_do() already
   // due to eo_do handling its own stack. this works with any method
   // actually that this object inherits/supports
   eo_event_callback_call(TST_EVENT_DISABLED, NULL);
}

EOLIAN static void
_tst_eo_base_constructor(Eo *obj, Tst_Data *pd)
{
   eo_do_super(obj, TST_CLASS, eo_constructor());
   printf("constructor...\n");
   pd->size = 77;
}

EOLIAN static void
_tst_eo_base_destructor(Eo *obj, Tst_Data *pd)
{
   printf("destructor...\n");
   free(pd->name);
   eo_do_super(obj, TST_CLASS, eo_destructor());
}

#include "tst.eo.c"

And now things run and do something when we run ./tst:

constructor...
set Smelly
size set 77 -> 100
activate! 37 'Chickens'
activated callback for 0x8000000000000001
disable
disabled callback for 0x8000000000000001
destructor...

Of course the above eolian_gen commands would become part of your Makefiles or build setup. it will even "maintain" the implementation file for you (tst.c in the case above) and add methods as you add methods and properties in your class. It won't remove old ones - you'll have to do that, and if parameters change, you'll need to do that yourself, but a lot of the manual footwork has been removed by automated code generation.

Now what if i want a C++ binding to my new class? Well that's easy:

eolian_cxx -I /usr/local/share/eolian/include/eo-1 tst.eo

This will add the base Eo include dir to scan for Eo files and generate bindings for the class in tst.eo and out will pop tst.eo.hh. I won't paste the .hh file here as it is large and complex template-based C++, but you now could write some C++ code to use the same C class such as:

#define EFL_BETA_API_SUPPORT
#include <Eo.hh>
#include "tst.eo.hh"

int main()
{
   efl::eo::eo_init eo_;

   ::tst obj;
   obj.name_set("Smelly");
   obj.size_set(100);
   obj.callback_activated_add(std::bind([&]
    {
       std::cout << "activated callback for " << obj._eo_ptr() << std::endl;
       return true; // pass event on to next cb
    }));
   obj.callback_disabled_add(std::bind([&]
    {
       std::cout << "disabled callback for " << obj._eo_ptr() << std::endl;
       return true; // pass event on to next cb
    }));
   obj.activate(37, "Chickens");
   obj.disable(99);
   return 0;
}

Just compile it like so:

gcc -c tst.c -o tst.o `pkg-config --cflags eo eina`
g++ tst.o main.cc -o tstcc -std=c++11 `pkg-config --cflags --libs eo-cxx eina-cxx`

This ensures you build the pure C object file to tst.o then link it in as part of compiling main.cc.

So Eo now makes doing objects in C a breeze. It removes a lot of the footwork in C that would make doing objects painful (and which often drives developers to C++ or some other language). It allows you to build simple C APIs that are usable not just from C in an OO way, but in C++ as if they are native C++ APIs, and in other languages too. We're fleshing out the Lua support at the moment, and other languages will appear as time goes on. This isn't just useful in EFL, but for anyone wanting to make simple OO code in C, be it in embedded devices, phones, desktops, laptops or servers. We happen to be doing it so we can offer far nicer APIs for EFL in future as well as clean up lots of internals, provide far more call safety at runtime, but I am sure this may sole some other problems people have out there.

We invite people to test this out and if you're interested, provide a bindings generator for your favorite language or runtime. This then allows such a language to be a "first class citizen" when using EFL, and any other APIs written using Eo. Such binding generation is then automatic with no human intervention. That is the only sane way to support bindings for languages as manual binding eventually fails.

Now what is object indirection? Well like GObject and older GTK+ before it, Qt and others, our objects before Eo were pointers to memory where the object data is stored. This allows programmers to "do bad things" (by accident) and pass in invalid pointers. We used to solve this by checking the pointer is NULL or not, and if not NULL, de-referencing it and checking for some magic numbers in the memory it points to to ensure the object is there and the right type. No more. Now what look like pointers are actually object IDs. They are not able to be de-referenced. They are broken up into separate fields (different bits in the pointer now have different meanings), and looked up in a table. The table is managed in memory away from normal heap allocations and managed by Eo core. This table in turn holds locations in memory of the objects. This means that an invalid pointer is always caught as it fails table range checks or the table entry is NULL. It even tries to detect false positives with generation count checks. This makes object addressing far more robust and basically almost impossible to get to crash. Eo brings this along with it to EFL, improving stability and correctness even when we don't find the bug and a user unfortunately has to encounter it.

This extra object reference checking leads us to the multi-call capability in Eo. This is where you do something like this:

eo_do(obj,
      tst_name_set("Smelly"),
      tst_size_set(100),
      eo_event_callback_add(TST_EVENT_ACTIVATED, on_activated, NULL),
      eo_event_callback_add(TST_EVENT_DISABLED, on_disabled, NULL));

You can call multiple methods on an object with a single object de-reference/check. This amortizes the cost of such checks over more calls, thus eventually negating them as we now do less checking that we did before on average per call.

Eo also does a check on every method call. If the object does not support that interface/class the call is turned into a no-op and safely returns. This is now guaranteed for every single method call at the C level, so making mistakes and calling the wrong method on the wrong object simply results in some complaints to stderr, and safely marching on.

So much is added and improved with the addition of Eo. Even the existing "legacy" C calls are auto-generated and wrapped on top of Eo calls, so in using EFL today, you are already using Eo, just with a very simple wrapper function on top for compatibility. Every object in EFL is now an Eo object, so you can even mix-and-match old style C calls and newer Eo ones. Eo is a huge step forward for EFL and hopefully is useful to others as well.