glib dbus bindings notes



Hi,

I sent an earlier draft of these notes to davidz, but I think would be
useful for other binding authors and for the glib team when looking at
eggdbus, so cleaned it up a bit for this mail. A lot of this is not
specific to glib binding and would apply to any binding.

Some biases I have in mind:

* dbus should be used as much if not more from other languages as it
is from C. Async IPC is _so_ much easier when you have 1) garbage
collection and 2) closures. I think using dbus is reason enough by
itself to have part of an app coded in gjs or seed or something,
personally. Anyway, I think a goal of dbus bindings in glib should be
to support mappings from dbus direct to other languages.

* I'm personally something of a generated-code hater. I know davidz is
a static-typing lover, and I can get into static typing, but I kind of
don't find it worth a ton of generated code. To me one lesson of the
CORBA/Bonobo excursion was that generated code is kinda bloated, kinda
inconvenient to mess with, etc. Anyway, I'm not against people using
statically-generated proxies, but even in C, I tend to like a generic
proxy with varargs convenience functions, sort of like
dbus_g_proxy_call(). Saves makefile hassle in the app, saves code
size, saves a bunch of headaches installing introspection data and idl
files, just simpler all around.

Those biases admitted to, here are my detailed notes. You can
s/glib/other-main-loop-providing-framework/ and it should largely
still apply.

Breaking down a binding into 6 pieces. The first 5 are generic to any
language binding using glib - pygtk, gjs, seed, etc. etc. The last one
(object mapping) is specific to C programs. However, notably, I think
the last one is responsible for 80% or more of the *code* in a glib
binding. I think it's also much more a "matter of taste" where
different programmers like to do the object mapping in different ways.
I would give serious consideration to keeping the object mapping
separate, or at least giving it more time to bake in the real world,
while focusing on getting the first 5 pieces right to start with.

First Piece: Main Loop Integration
===

The main loop integration between DBusConnection and GMainContext is
tricky, tedious, and annoying to get right. This should be in the GLib
binding for D-Bus. The code in dbus-glib for this is pretty much fine,
afaik.

Second Piece: Asynchronous DBusConnection
===

This is mostly just points for style right now, because
dbus_connection_open() does some blocking anyhow. But it's needed if
the API theoretically supports reconnecting to the bus when the bus
restarts, which might be nice. Also, a good way to structure
single-instance apps is to put essentially all of main() in a
callback.

by "put all of main in a callback" I mean something like this:
http://mail.gnome.org/archives/gtk-devel-list/2007-August/msg00039.html

Asynchronous DBusConnection should probably look like a
ConnectionWatcher object that has "connected"/"disconnected" signals,
since that's better for language bindings. However, in C a wrapper
like this is more convenient:

void (* BusConnectedCallback) (DBusConnection*, void *data);
void (* BusDisconnectedCallback(DBusConnection*, void *data);
void add_connection_listeners(BusType bus, BusConnectedCallback,
BusDisconnectedCallback, void *data);
void add_connection_listeners_with_address(const char *address,
BusConnectedCallback, BusDisconnectedCallback, void *data);
// also need a way to remove listeners of course

The lame implementation of this for now just queues a main loop idle,
which calls dbus_connection_open(). In future it could retry
connecting to the bus when the bus restarts. There could also be a
real async way to open a connection sometime.

This API is little-used because usually you simply ask to own or ask
to be told about bus names, implicitly also asking to be notified of
new bus connections. See below.


Third Piece: Asynchronous Name Ownership
===

This builds on Asynchronous DBusConnection, and allows an application
to asynchronously be notified when it owns or loses a bus name.
Generally, an app would enable or disable a particular area of
functionality according to which bus names it owns. If an app owns
only one bus name, it might exit when it loses that bus name.

Again this should probably look like an OwnershipWatcher object with
"owned" and "lost" signals, for ease of language binding. However the
convenient API in C might be something like:

void (* BusNameOwnedCallback) (DBusConnection*, const char
*well_known_name, void *data);
void (* BusNameLostCallback) (DBusConnection*, const char
*well_known_name, void *data);
enum OwnershipType { SINGLE_INSTANCE, OPTIONAL };
flags OwnershipFlags { NONE, REPLACE };
void own_bus_name(BusType bus, OwnershipType, OwnershipFlags,
BusNameOwnedCallback, BusNameLostCallback, void *data);
void own_bus_name_with_connection(DBusConnection *connection,
OwnershipType, OwnershipFlags, BusNameOwnedCallback,
BusNameLostCallback, void *data);
// and a way to remove the callbacks

Note that this API is intended to chain off the asynchronous
DBusConnection API (second piece). The version taking a BusType is a
convenience function that sets up async connection listeners in the
background and calls own_bus_name_with_connection() inside them.
Anytime there's a new bus, the process tries to own the name on that
bus.

This API also enables you to implement a --replace command line
option, which is handy for debugging. Just have your app gracefully
handle (possibly including exit) loss of the bus name.

Fourth Piece: Asynchronous Name Watching
===

Moving to the "client side" (even though many apps are both server and
client), a client often wants to watch for a bus name and take action
whenever it appears or disappears.

This works similarly to owning a name, and is also async of course.
And like the others, it should really be a NameWatcher object with
signals, the below custom callback API is really C convenience to show
the idea.

void (* BusNameAppearedCallback) (DBusConnection*, const char
*name_that_appeared, const char *name_owner, void *data);
void (* BusNameVanishedCallback) (DBusConnection*, const char
*name_that_appeared, const char *name_owner, void *data);
void watch_bus_name(BusType bus, const char *name,
BusNameAppearedCallback, BusNameVanishedCallback, void* data);
void watch_bus_name_with_connection(DBusConnection*, const char *name,
BusNameAppearedCallback, BusNameVanishedCallback, void* data);
// and a way to remove the callbacks

Of course this also chains off the asynchronous DBusConnection API,
meaning it is not usually necessary to use the DBusConnection API.

Note: because this always gives you the name owner (which is in the
NameOwnerChanged signal used to implement the name watching), it is
never necessary to do a blocking round trip to obtain the owner. It's
always known asynchronously.

This is extremely important because in the BusNameAppearedCallback you
normally create your proxy for objects inside the app owning the bus
name. These proxies can be created with the unique name owner, not the
well-known name; as a result, race conditions are avoided when the
remote app restarts. If you create proxies with the well-known name,
those proxies can refer to one process at moment 1, and another
process at moment 2. By using the unique name, the proxy always refers
to the same process. If the process exits, then
BusNameVanishedCallback should be invoked (destroy proxy here), and
BusNameAppearedCallback is then invoked on restart, where a new proxy
can be created.

The implementation of watch_bus_name would normally make an async call
to GetNameOwner when the watch is first added (if the name owner is
not already known). It calls GetNameOwner only after setting up a
match for NameOwnerChanged, otherwise there would be a race.

As with all of these callbacks, they should never be called
synchronously from the add() function; if the callback info such as
name owner is already known, an idle should be queued to notify the
app. Otherwise the app has to deal with two codepaths (one sync, one
deferred to main loop).

Fifth Piece: Asynchronous Signal Watching
===

This machinery is used to implement signals on proxies in the object
mapping. It is kept separate from any particular object mapping
because all language bindings need this functionality, and it is
fairly tricky to implement correctly.

This part of the binding has to call dbus_bus_add_match() with the
right arguments, and uses the tracked bus names from the fourth piece
to keep track of whether a signal connection is still relevant,
removing the match if it isn't.


Sixth Piece: The Object Mapping
===

All of the first five pieces can be shared among all object mappings -
they are GLib-specific because they depend on the main loop and are
probably best implemented with GObject, but they are not
binding-specific.

The final piece is the object mapping, which should be
language-specific. The object mapping's primary role: marshaling
method and signal arguments to and from some type system. Getting a
GValue-using object mapping in the way of other language object
mappings creates quite a bit of complexity and bloat. Instead, there
should be code to go directly from D-Bus types to Java, JavaScript,
Python, etc. rather than trying to convert from D-Bus then to GValue
then to another language's type system, while round-tripping exact
type signatures through these possibly not-quite-the-same systems.

An object mapping has two sides which should look different. The
"client" side has thin proxies for remote objects. The ideal proxy is
a very thin object using minimal memory, it just stores the bus name,
interface name, and object path to send method calls to. The "server"
side has object instances, which have to be registered with particular
object paths.

Proxies should almost always be created (or at least "rebound" to a
new unique bus name) in a BusNameAppearedCallback and destroyed in a
BusNameVanishedCallback. A little painful, yes, but flatly necessary
if you're going to handle the remote process restarting.

Exported instances from the server only have to exist once
BusNameOwnedCallback is called, but it's harmless if they exist
before.

An object mapping should omit entirely, or strongly discourage,
synchronous methods on proxies. In gjs D-Bus bindings, the proxy
methods are all called FooRemote(), BarRemote() and take an optional
callback as the last argument which gets the error or return value.
Proxies have no synchronous methods in gjs.

Object instances exported by servers need two modes of operation:
"synchronous" returns a value immediately, sending a dbus reply
immediately; "asynchronous" allows the server instance to defer the
reply while it does something that takes time.

The object mapping should support running with no bus daemon (bus name
of NULL). This is for peer-to-peer mode.

A secondary role of the object mapping: hiding libdbus. I don't think
there should be a stack like libdbus -> libdbus-wrapper-thing -> all
the object mappings. The reason is that *most* of the libdbus API is
only useful for bindings - for writing an object mapping. So by
wrapping libdbus and *then* doing the object mapping, your libdbus
wrapper has to be 10x larger than it would be if the object mapping
itself was the thing hiding libdbus, while the object mapping used
libdbus directly.


Anyway, that's my take. You may notice that the above (except object
mapping) more or less describes hippo-dbus-helper.c, and also
describes a hippo-dbus-helper-like file I have in the gjs dbus
bindings. While hippo-dbus-helper is not a good API (the structs
should mostly be GObjects, callbacks should mostly be signals, among
other issues), I think it is meaningful that one 2500-line (<1000
semicolon-terminated lines) file can do pieces 1-5. The gjs bindings
are completely full-featured using only a small file like that, plus a
JavaScript object mapping.

So maybe my big picture thought on eggdbus is that by also doing
object mapping and wrapping enough of libdbus to support another
binding, it is ~12x larger (in terms of source code size) than it
would be just doing 1-5. Granted, a *nice* version of 1-5 using
gobject would be larger, but not that much larger. Does it truly make
sense, to have over 90% of the code in a glib dbus binding be a C-only
convenience API? Should this be split up somehow?

I'm not saying the C-only convenience API is bad, or even that it
should not be in GLib, just that there's a very logical split between
the object mapping (90% of the code) and the rest that's more generic
(10%).

I'll see if I can come up with a followup mail or two showing what the
gjs bindings are like and discussing some specifics about eggdbus.

Havoc


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]