Understand 'Just Enough' GObject For Code Reading
Without referring to GObject
documentation or any online posts
For the record, I can not claim I have never been exposed to GObject
materials. The best I can say is that I haven’t systematically learnt GObject
., understand just enough knowledge about GObject
for the
purpose of reading related code. The process is question-driven and
code-inspired.
Reading code is essentially an activity in which readers trace the control flow
(i.e. function calls), state transitions (i.e. variable values) from interested
points
Say the point where an error in the log is printed. Or main
function, public API entries and etc. to a point where readers
claim sufficient understanding is attained
Say points where a bug has been identified or mis-configured environment has been confirmed..
To understand GObject
for reading code is to be able to trace method calls,
property changes. This is a problem for GObject
because some C tricks (most
are intricate macros) are employed to implement some OOP features and they are
not at all obvious. Further, for the purpose of reading GObject
code, I do not
need to understand how the macro magic work.
For the Impatience
This section was added after the main body was finished. The whole argument below is quite lengthy and murky. Maybe it’s better to summarize what I’ve learnt through the process beforehand.
The procedure I would form with this just enough GObject
knowledge is as
follows:
Look for class hierarchical info through file dependencies (includes), and
parent_XXX
members. Locate the exact method used in the polymorphism by
looking at explicit reference of the real class type, usually this happens in
the abstract, parent class. As instantiation is poorly comprehended, state
changes (updates to variable member) can be hard to track. Pay special attention
to naming, signals and callbacks. Overall, with the OOP
experience from other
languages as direction, naming as hints, take bold jumps of faith
to form a
useful understanding.
Explain the Terms
Question Driven
question-driven is to start with questions about GObject
. Being an OO
implementation for C, basically we’re asking about how Object-Oriented features
are achieved in GObject
:
- Where&How a class is defined?
- How the class inheritance is defined?
- What are the common implicit members?
- How an instance is
new
-ed? - How
polymorphism
is implemented? (or howthis
pointer is understood.) - What are those
signals
?
Code Inspired
Read GObject
code directly and use the code snippet to figure out answers to
above questions.
In the following, I’m using code from gnome-session-3.20.2
as example.
Just Enough GObject
Where&How a class is defined?
Where is relatively simple as Class is meant for reuse and thus the declaration must be some headers.
Technically, we should be looking at the block between G_BEGIN_DECLS
and
G_END_DECLS
. However these two are not helpful in identifying the class we’re
interested in. Together with boilerplates like GSM_TYPE_APP
, GSM_APP_CLASS
and etc., these are intuitively understood as the GObject
magic and should
be skimmed over when reading the code.
/**
* SP2_GNOME_SESSION:/gnome-session/gsm-app.h
*/
typedef struct _GsmApp GsmApp;
typedef struct _GsmAppClass GsmAppClass;
typedef struct _GsmAppPrivate GsmAppPrivate;
Even from above, the verbosity of Object
in C is obvious. Class
and its
Instance
have their separate types. Data Hiding
is achieved by explicitly
putting internal details into a XXXPrivate
type.
/**
* SP2_GNOME_SESSION:/gnome-session/gsm-app.h
*/
struct _GsmApp
{
GObject parent;
GsmAppPrivate *priv;
};
struct _GsmAppClass
{
GObjectClass parent_class;
/* signals */
void (*exited) (GsmApp *app,
guchar exit_code);
// ... more
/* virtual methods */
gboolean (*impl_start) (GsmApp *app,
GError **error);
// ... many more
gboolean (*impl_is_conditionally_disabled) (GsmApp *app);
};
// ...
gboolean gsm_app_start (GsmApp *app,
GError **error);
// ...
gboolean gsm_app_peek_is_conditionally_disabled (GsmApp *app);
// ...
As priv
of type GsmAppPrivate *
is only a member of _GsmApp
, XXXPrivate
must be a collection of state variables for instances. The naming parent_class
and parent
member has suggested that they are what marks the inheritance
relationship.
Two things are of more considerations: the “virtual methods” comment and
consistent naming of functions following the _GsmAppClass
declaration.
Intuitively, I thought methods of an object are pointers to functions. Later,
we’ll see it turns out that methods are mere functions with the first argument
being the object itself.
Maybe this is a way to avoid duplications. Or more importantly, it’s so because of the lack of auto this
pointer for methods in C. Indeed, obj->method(obj, arg1, arg2)
feels more verbose and confusing than type_method(obj, arg1, arg2)
as in the former the term obj
shows up twice.
How is the class inheritance defined?
As seen above, the parent
and parent_class
are what define the inheritance
relationship among classes. Since the C
has no awareness of class and
objects, there is no easy UML
diagram to be drawn. However, by inspecting the
dependencies (include/includeby) within the source, we can still easily see
this relationship in graph as
this diagram
show.
Another interesting point revealed in the diagram is the empty dependency from gsm-app
to gsm-client
, i.e. gsm-client.h
is included in gsm-app.h
but nothing is used from gsm-client.h
. Though this might be a mere historical residual, it suggests some connection between client
and app
. Indeed, app
once started is a client
. (And there can be more interconnections.)
From the dependency graph, we see clearly that gsm-autostart-app
is a
subclass of gsm-app
, which is also shown in the code:
/**
* SP2_GNOME_SESSION:/gnome-session/gsm-autostart-app.h
*/
struct _GsmAutostartApp
{
GsmApp parent;
GsmAutostartAppPrivate *priv;
};
struct _GsmAutostartAppClass
{
GsmAppClass parent_class;
/* signals */
void (*condition_changed) (GsmApp *app,
gboolean condition);
};
What are the common implicit members?
By implicit
, I mean members and methods created, used by the GObject
macro
magic. They show up naturally when we trace some GObject
functions and find
their declarations/usages are nowhere within the source tree. XXX_init
,
XXX_contructor
, XXX_dispose
are most noticed ones. Though it’s relatively
easy to pick these magic elements out, it proved to be hard to see the exact
interconnections between them.
How an instance is new
-ed?
The arguments made in this section is much less plausible than others. It turned out to be quite hard to formalize the guesswork done during the analysis. Nevertheless, to complete the project of this post, I proceeded to write out my thoughts, with best efforts.
The instantiation of objects turns out to be a hard procedure to get to the exact details. Much of the difficulty stems from the mystical interactions among implicit members. However, with some jumps (hypothesis), we might still be able to to understand the code.
First, we notice many methods impl_XXX
of gsm-app
is set to NULL
. With the
lack of new
-ish functions, let’s assume gsm-app
is abstract and can not
have any instances.
Second, let’s turn to gsm-autostart-app
. There is gsm_autostart_app_new
,
which seems to be the only generator of gsm-autostart-app
externally.
/**
* SP2_GNOME_SESSION:/gnome-session/gsm-manager.c
*/
static gboolean
add_autostart_app_internal (GsmManager *manager,
const char *path,
const char *provides,
gboolean is_required)
{
GsmApp *app;
// ...
app = gsm_autostart_app_new (path, &error);
// ...
}
Third, the implementation of gsm_autostart_app_new
looks bizarre for our
untrained eyes.
/**
* SP2_GNOME_SESSION:/gnome-session/gsm-autostart-app.c
*/
GsmApp *
gsm_autostart_app_new (const char *desktop_file,
GError **error)
{
return (GsmApp*) g_initable_new (GSM_TYPE_AUTOSTART_APP, NULL, error,
"desktop-filename", desktop_file,
NULL);
}
The doc on g_initable_new
says something about
“failable object initialization”.
The declaration of the function itself looks like:
/**
* https://developer.gnome.org/gio/stable/GInitable.html#g-initable-new
*/
gpointer
g_initable_new (GType object_type,
GCancellable *cancellable,
GError **error,
const gchar *first_property_name,
...);
// The '...' means that the value if the first property, followed by and other
// property value pairs, and ended by NULL.
The ending varargs
of key-value pairs are of particular interests. The
following gsm_autostart_app_initable_init
has already started to use
desktop-filename
property.
/**
* SP2_GNOME_SESSION:/gnome-session/gsm-autostart-app.c
*/
static gboolean
gsm_autostart_app_initable_init (GInitable *initable,
GCancellable *cancellable,
GError **error)
{
GsmAutostartApp *app = GSM_AUTOSTART_APP (initable);
g_assert (app->priv->desktop_filename != NULL);
app->priv->app_info = g_desktop_app_info_new_from_filename (app->priv->desktop_filename);
// ...
}
Where the property desktop-filename
is installed:
gsm_autostart_app_class_init (GsmAutostartAppClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
// ...
g_object_class_install_property (object_class,
PROP_DESKTOP_FILENAME,
g_param_spec_string ("desktop-filename",
"Desktop filename",
"Freedesktop .desktop file",
NULL,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
// ...
}
I’ve assumed desktop-filename
is the same as app->priv->desktop_filename
.
From these, it’s reasonable to say g_initable_new
call above is the very call
that instantializes an object.
Finally, Similar arguments can also be made about other parts of the
instantiation process. From the various function names and comparison with
similar functions in gsm-app
, the educated guess we can have about the
instantiation process is as follows:
XXX_new
is the method to start instantializing an object. It’s likely there are other types ofnew
offered byGObject
other thang_initable_new
.XXX_class_init
will get called if the class data have not initialized. This may happen only once as all instance share the same class data.XXX_constructor
,XXX_init
will get called afterXXX_class_init
.
How polymorphism
is implemented?
This is already shown in previous code snippets. In short, polymorphism
is
achieved explicitly with boilerplate code.
Take method impl_start
as example.
/**
* SP2_GNOME_SESSION:/gnome-session/gsm-app.h
*/
struct _GsmAppClass
{
// ...
/* virtual methods */
gboolean (*impl_start) (GsmApp *app,
GError **error);
// ...
};
// ...
gboolean gsm_app_start (GsmApp *app,
GError **error);
// ...
/**
* SP2_GNOME_SESSION:/gnome-session/gsm-app.c
*/
static void
gsm_app_class_init (GsmAppClass *klass)
{
// ...
klass->impl_start = NULL;
// ...
}
gboolean
gsm_app_start (GsmApp *app,
GError **error)
{
g_debug ("Starting app: %s", app->priv->id);
return GSM_APP_GET_CLASS (app)->impl_start (app, error);
}
The definition of gsm_app_start
shows the explicit reference to the real
class of app
using GSM_APP_GET_CLASS
macro. gsm-app
is abstract and have
impl_start
as NULL
. And for gsm-autostart-app
, it indeed sets its own
impl_start
:
/**
* SP2_GNOME_SESSION:/gnome-session/gsm-autostart-app.c
*/
static void
gsm_autostart_app_class_init (GsmAutostartAppClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GsmAppClass *app_class = GSM_APP_CLASS (klass);
// ...
app_class->impl_start = gsm_autostart_app_start;
// ...
}
So the public API for consumers of gsm-XXX-app
is gsm_app_start
and this can
be verified by looking up its references within the source tree.
What are those signals
?
This is a question we can ask only after reading the code.
From above snippets, we’ve seen a lot of signal
declaration, which is not
typical among other OOP
languages. For the purpose of understanding, most of
time they are no different than other event-driven pattern, and can be handled
as such safely. We only need to pay attention to property names, callbacks and etc,
just like the above desktop-filename
.
After Thoughts
While writing down the above process, I frequently doubted the usefulness of
doing them at all. Whatever the little understanding I claim to achieve above
is abysmal compared to what I would have gained in, say 2 hours of, reading
GObject
documentation. However, I still completed the post for two reasons:
- The guesswork process, inaccurate as it is, is very typical for code
reading. And we’re usually not that fortunate to have a well-polished
documentation as that of
GObject
. Forming the gut feelings in the guess has proved to be arduous but fruitful. - The possible enlightenment I’ll have when I learn
GObject
systematically. The reward might be some great revelation about theguesswork
process that can improve my code reading skill a lot.
∎