Cop versus Bus (D- edition)

[Disclaimer: Despite what you may have assumed from the title, this post does not discuss the obscure rock, paper, scissors variant “cop, bus, driver” (where driver controls bus, bus runs over cop, cop shoots driver). We apologize for any confusion.]

It has been remarked that the great thing about standards is that there’s so many to choose from. While working on adding support for Amarok to the next version of Music Applet, I ran into this first-hand.

GNOME and KDE, the two main desktop environments for Linux, each have a preferred IPC mechanism to provide a relatively easy way for programs to talk to one another. GNOME uses D-Bus, and several of the players currently supported by Music Applet use it. KDE, however, uses DCOP, and since Amarok is a KDE-based application, it provides a DCOP interface.

D-Bus and DCOP are, at a high level at least, pretty similar: they’re based on making method calls on objects exported by applications. They’re mutually incompatible with each other, of course, but they’re targeting the same niche. So, since Music Applet 2.3.0 will be using both of them to some extent, how do they shape up against each other?

I’m not going to go into the technical merits of each, or any of their implementation details. I’m only looking from the perspective of someone trying to write some Python code that interacts with other programs via D-Bus or DCOP.

Both D-Bus and DCOP provide a command-line interface for issuing individual method calls. While you wouldn’t normally use this interface in a program (unless you’re writing a shell script, of course), it is handy for quick testing, making sure the method you’re looking at does what you think it does.

Let’s say we want to ask our music player what song it’s currently playing. For Rhythmbox, which uses D-Bus, we need to do this:

$ dbus-send --print-reply --dest=org.gnome.Rhythmbox /org/gnome/Rhythmbox/Player  \
        org.gnome.Rhythmbox.Player.getPlayingUri
method return sender=:1.13 -> dest=:1.23 reply_serial=2
   string "file:///home/paul/music/Jonathon%20Coulton/Chiron%20Beta%20Prime.mp3"

You’ll note that I had to break the command across two lines to get it to fit in this column, and a lot of that command looks pretty redundant. Yes, yes, it avoids namespace pollution in case an application or an object supports multiple interfaces defined by different parties, but it’s a pain to type out. Also note that I had to include --print-reply so that the command would actually say what got returned. Also also, not pictured here, if you’re passing arguments to the method you’re calling, you have to explicitly say what the type of each argument is. Ick.

Now let’s see what the equivalent use of the DCOP command-line tool for calling the equivalent method on Amarok is:

$ dcop amarok player encodedURL
file:///home/paul/music/Jonathon%20Coulton/Chiron%20Beta%20Prime.mp3

Even though it’s saying the same sort of thing (listing an application, an object owned by that application, and a method on that object), this is much easier to type. And even better, if we leave out some of the arguments, the command will list what’s available. For example, if we instead did:

$ dcop amarok player

It would list all the methods available on the player object exported by amarok. Pretty nifty. Sure, there’s a D-Bus way to do that, but you need to remember what method to call on the org.freedesktop.whatever interface of the object — or is it a separate object that handles introspection? I’d have to look up the D-Bus documentation. But the DCOP command-line tool doesn’t make you do that.

When we turn to the Python interface, things improve a bit for D-Bus, even though we’re still stuck with those very long fully qualified names for everything. Here’s a quick Python program that does the same thing as the command-line example:

import dbus
 
bus = dbus.SessionBus ()
player_proxy = bus.get_object ("org.gnome.Rhythmbox", "/org/gnome/Rhythmbox/Player")
player = dbus.Interface (player_proxy, "org.gnome.Rhythmbox.Player")
url = player.getPlayingUri ()
print url

Of course, if we’re going to make multiple calls to the object, we only have to set things up once, and then reuse the object as many times as we want.

The DCOP equivalent once again looks simpler (at least for now):

import pcop
import pydcop
 
# Magic goes here, to be discussed later...
 
amarok = pydcop.anyAppCalled ("amarok")
url = amarok.player.encodedURL ()
print url

While this might seem like another win for DCOP, things don’t look so good once we move away from these toy examples. For starters, the Python DCOP bindings don’t provide any support for asynchronous calls. Instead, all calls are synchronous: once we make the call, the program sits there and does nothing until it receives a response. If that other program you’re talking to takes a long time to respond, that spells trouble for our program’s responsiveness to the user, especially in a GUI, where now our program won’t even be able to redraw itself.

The Python D-Bus bindings do provide a way to do asynchronous calls, by passing a function that should be invoked once a response comes back from the other program. In the meantime, our program can go on to do other things. This also lets our program issue multiple IPC calls all at once more efficiently: in the ideal case, the other program can send its responses to all our calls during a single timeslice. If we’re stuck with synchronous calls, each one gets serviced in separate timeslices, which will end up taking longer.

Technically, this lack of asynchronous calls isn’t a fault with DCOP per se, but rather with its Python bindings. The underlying C++ library supports asynchronous calls. Adding support for this for Python programs is listed in the TODO file.

However, there’s a more obnoxious problem with DCOP when compared to D-Bus. Although D-Bus is used by GNOME, it’s not actually part of GNOME; it’s a freedesktop.org project with minimal dependencies. So, if D-Bus isn’t already running (which in GNOME is pretty unlikely anyway), there isn’t that much the library has to do to start it up.

DCOP, on the other hand, is a part of KDE itself, and to assure that it’s running, it’s necessary to initialize at least the base set of KDE services. Remember that code in the Python DCOP example I didn’t show you before? Here’s what it is:

import kdecore
import sys
app = kdecore.KApplication (sys.argv, "program name")

That’s right, our program has to initialize a KDE application object. It doesn’t look like much code-wise, but that initialization will go ahead and start up DCOP and a handful of other KDE services before it returns, if they’re not already running. And if you’re running a GNOME-based desktop — and if you’re running Music Applet, you certainly are — you probably don’t have all the KDE services running yet.

On my laptop at least, this initialization takes several seconds. As a result, if you have Music Applet’s Amarok plugin enabled, this adds several seconds to start-up time before the applet appears in the panel.

To avoid this delay, it’ll be necessary to create a thread in the Amarok plugin just to do that initialization. Ick. Note that deferring initializing the plugin until the applet appears in the panel doesn’t really help, since the applet would then just become unresponsive while that initialization is taking place. And no, launching dcopserver via a call to os.system instead of initializing KDE doesn’t quite work. I mean, it does work just for DCOP, but when a proper KDE application starts, it will get confused about how only some of the KDE services are running and will start spitting out error messages and will behave a bit strangely.

Even worse, if the Python DCOP bindings try to connect to the DCOP server and fail, they will never try again. So you have to make sure the DCOP server is running before you try using it at all.

So, while DCOP does have simplicity going for it, D-Bus ends up providing a more robust Python interface and lower initialization overhead, which are both big plusses when you want your program to be small and responsive.

Fun fact: KDE4 will be ditching DCOP in favor of D-Bus. Unfortunately, I expect this will require having both DCOP and D-Bus in Music Applet’s Amarok plugin, so that it works with Amarok-for-KDE3 and Amarok-for-KDE4 seamlessly. Sort of like how, back in Music Applet’s C days, there was Rhythmbox-via-D-Bus and Rhythmbox-via-Bonobo. Thankfully no Music Applet users seem to care about compatibility with semi-ancient Rhythmbox pre-0.9 compatibility these days.