Minimalistic example of the GLib’s GBoxedType usage

Introduction

As I’ve explained in one of the previous posts, it is possible to use
advantages of the GObject introspection even with a plain C non-GObject
code. It is okay to write C functions taking arguments and returning values,
call g-ir-scanner and g-ir-compile on them and then call them from
Python or any other language supporting GObject introspection. However, that’s
not entirely true as it per se only works with elementary types like numbers and
strings and arrays of such values plus structs with no pointer fields.

So what if some functions need to take or return complex values not only numbers
or strings? And why it’s only structs with no pointer fields? Let’s start with
the second question. Imagine the following situation: caller (e.g. Python) calls
a function that returns a struct containing a number, a string and a pointer to
another struct and the ownership transfer (extra metadata for GObject
introspection) is set to full which means the caller takes the ownership of
the returned value. What if the caller wants to copy or delete such value? In
case of number or string or array of such values it is simple. The same applies
to a simple struct with no pointers (the introspection data documents struct’s
fields and their types).

GBoxedType declaration example

So the problem is missing code for copying and freeing the complex values and
the first question coming to mind is: "Can’t I simply tell the caller how to
copy and free such values?"
And that’s what GLib’s GBoxedType is all
about. It is a wrapper type around plain C structs which provides information
how to copy and free such values. Let’s have a look at a minimalistic example
showing how such type can be declared:

#include <glib-object.h>
#include <glib.h>

#define TEST_TYPE_DATA (test_data_get_type ())
GType test_data_get_type ();

typedef struct _TestData TestData;

struct _TestData {
    gchar *item1;
    gchar *item2;
};

/**
  * test_data_copy: (skip)
  *
  * Creates a copy of @data.
  */

TestData* test_data_copy (TestData *data);

/**
 * test_data_free: (skip)
 *
 * Free's @data.
 */
void test_data_free (TestData *data);

First the glib-object.h and glib.h header files need to be included
because they define types and functions necessary for a definition of a new
GBoxedType. Then a macro and a function for getting type of the new GBoxedType
need to be declared for the type system to work with the type. Of course, there
has to be a definition of the actual struct holding the data. It can be done in
two steps as in the above example or in one step as:

typedef struct TestData {
    type1 field1;
    type2 field2;
    ...
} TestData;

defining the struct type and "non-struct" type [1] both at once, but GLib coding
style recommends the two-steps definition. And the core are the two functions
for creating a new copy and freeing a value of the new GBoxedType with not much
surprising signatures.

[1] in C these are two different type namespaces

With definitions of the functions above it will be possible to call functions
that return a TestData* value and get the values of the item1 and
item2 fields. It would also be possible to create a new TestData object
and passing values to its fields. However, it is often useful to declare and
define one more function:

/**
 * test_data_new: (constructor)
 * @str1: string to become the .item1 field
 * @str2: string to become the .item2 field
 *
 * Returns: (transfer full): new data
 */
TestData* test_data_new (gchar *str1, gchar *str2);

It is a constructor function that, given values of the fields, returns a new
object of type TestData. It is only a convenience function here where it
should just passing the values to the struct’s fields, but as you can imagine, it
can do a lot more if needed.

GObject definition example

The implementation of the functions declared above is really
straightforward. The only exception is the test_data_get_type function that
creates and registers the type in the type system:

GType test_data_get_type (void) {
    static GType type = 0;

    if (G_UNLIKELY (!type))
        type = g_boxed_type_register_static ("TestData",
                                             (GBoxedCopyFunc) test_data_copy,
                                             (GBoxedFreeFunc) test_data_free);

    return type;
}

It defines a global variable type of type GType and if it is not set
(i.e. set to 0), it assigns it a new value created by the
g_boxed_type_register_static with arguments that are quite clear, I’d
say. The use of G_UNLIKELY macro tells the compiler that this condition will
hardly ever be evaluated to TRUE which is a simple but useful optimization.

Compilation

With the functions and types declared in the test_data.h and defined in the
test_data.c files the working introspectable library can be created with the
following commands:

$ gcc -c -o test_data.o -fPIC `pkg-config --cflags glib-2.0 gobject-2.0` test_data.c
$ gcc -shared -o libtest_data.so test_data.o
$ LD_LIBRARY_PATH=. g-ir-scanner `pkg-config --libs --cflags glib-2.0 gobject-2.0` --identifier-prefix=Test --symbol-prefix=test --namespace Test --nsversion=1.0 --library test_data --warn-all -o Test-1.0.gir test_data.c test_data.h
$ g-ir-compiler Test-1.0.gir > Test-1.0.typelib

The first two call gcc to produce the libtest_data.so shared dynamic
library that can be then loaded e.g. by Python. The third line is the invocation
of the g-ir-scanner utility that produces an XML containing the
introspection (meta)data. It gets compiler and linker flags for the libraries
required by the libtest_data.so, prefixes for identifiers (like types,
constants,…) and symbols (functions), namespace name and version, the name of
the library that should be scanned and paths to the sources that should be
scanned and -o Test-1.0.gir option that specifies the output file name. Name
of the file should match the namespace-nsversion.gir pattern. And finally
the last command compiles the Test-1.0.gir file to its binary representation
that is expected to match the same name pattern with the .typelib extension.
If you are reproducing the steps above, feel free to have a look at the produced
Test-1.0.gir file as it is quite easily readable and understandable, I’d
say. And if you are hardcore hacker, feel free to have a look at the
.typelib file too, of course. Just remember that running cat on it may
"nicely" change your terminal’s runtime configuration [2].

[2] use reset to get the defaults back in such cases

Testing

Having the definitions, declarations, introspection (meta)data available both in
the XML and binary forms, it’s time to test the result. The easiest way is
running ipython as it provides a TAB-TAB completion. It just have to be
told where to find the .typelib file and of course the libtest_data.so
library that it needs to load. Both are in the current directory so:

$ GI_TYPELIB_PATH=. LD_LIBRARY_PATH=. ipython

Runs the ipython in the properly set up environment. To test the library and
newly defined struct/class/object type it has to be loaded from the
gi.repository. Then it can be instantiated with the constructor or without it
and fields can be introspected (TAB-TAB) and used:

In [1]: from gi.repository import Test
In [2]: td = Test.Data()
In [3]: td.item1 = "ahoj"
In [4]: td.item2 = "cau"

In [5]: td.item1
Out[5]: 'ahoj'

In [6]: td.item2
Out[6]: 'cau'

In [7]: td2 = Test.Data.new("nazdar", "zdar")

In [8]: td2. # hit TAB-TAB
td2.copy   td2.item1  td2.item2  td2.new

In [8]: td2.item2
Out[8]: 'zdar'

Conclusion

That’s not entirely bad, is it? One doesn’t get an introspectable struct
completely for free if it is not trivial, but defining three (copy, free, new)
of the four functions defined above is a good practice anyway. So in the end
it’s all about adding one more function and two declarations (the TYPE macro
and the get_type function prototype) and calling two utilities producing the
introspection data. Quite easy if I think about writing language-tailored
bindings for any language that comes to my mind. And with these constructs one
gets bindings for all the languages supporting GObject introspection. To define
a new type’s method, it just needs to have a test_data prefix and take the
TestData* value as the first argument. Let me know in the comments if there
is anything unclear. If I know the answer, I’ll reply ASAP and possibly update
the post with such information.