27. April 2010

Remote Procedure Calls über den DBus

Wozu dient der DBus? Und wie spricht man ihn an? Ein einfaches Beispiel in fünf verschiedenen Sprachen —- mit Java, OCaml, Python, C und der Shell!

DBus dient zur Interprozesskommunikation (IPC). Es ist ein Bussystem, über welches Prozesse mit Signalen oder Methodenaufrufe kommunizieren können. Eine Einführung gibt es auf der Entwicklerseite.

Um die Benutzung des DBus zu lernen, habe ich die Bindings zu verschiedenen Programmiersprachen ausprobiert (C, Python, Java, OCaml, sh). Als einfaches Beispiel möchte ich einem entferntem Prozess, der Objekte über den DBus exportiert, dazu veranlassen, die Hypotenuse in einem rechtwinkligen Dreieck auszurechnen, und das Ergebnis über den DBus zurückzusenden.

Jede Verbindung zum DBus-Dämon hat einen eindeutigen Namen. Zusätzlich können sich Verbindungen noch zusätzliche, lesbare Namen registrieren. In diesem Beispiel nennen wir die Verbindung /example/dbus/ServiceProvider.

Über den DBus kann man Objekte exportieren, welche eine bestimmte Schnittstelle anbieten. Im Modell des DBus hat ein Objekt folgende Eigenschaften:

  • object path – Jedes Objekt hat einen Pfad, der das Objekt identifiziert. Der Pfad ist ähnlich wie ein Dateipfad aufgebaut. In diesem Beispiel wählen wir für das (hier: einzige) exportierte Objekt den Pfad /example/dbus/hypotenuse.
  • interfaces – Jedes Objekt unterstützt mindestens eine Schnittstelle. Die Namen der Schnittstellen gleichen der Java-Konvention für vollständige Klassennamen. In unserem Beispiel wird das exportierte Objekt die Schnittstelle /example/dbus/HypotenuseService implementieren.
  • members – Jedes Objekt kann verschiedene Methoden und Signale anbieten. Im Beispiel ist dies nur die Methode hypotenuse.

Nun zum Beispiel. Ein Prozess wird über den DBus einen Dienst anbieten, der darin besteht, die Hypotenuse zu berechnen. Die Parameter und der Rückgabewert müssen mit dem DBus übermittelt werden. Diesen Prozess implementieren wir wie folgt in Java:

package example.dbus;

import org.freedesktop.dbus.DBusInterface;

public interface HypotenuseService extends DBusInterface {

    public double hypotenuse(double a, double b);

}

Die Java-DBus-Bindings kümmern sich teilweise um das Mappen des Java-Objektmodells auf das DBus-Objektmodell. Wir benötigen noch eine Implementation der Schnittstelle:

package example.dbus;

public class DefaultHypotenuseService implements HypotenuseService {

    public double hypotenuse(double a, double b) {
        System.out.println("Calculating hypotenuse for a=" + a + ", b=" + b);
        return Math.sqrt(a * a + b * b);
    }

    public boolean isRemote() {
        return false;
    }

}

Schließlich brauchen wir noch eine Klasse, die das ganze miteinander verklebt, die Verbindung mit dem Session-DBus-Dämon aufbaut, und ein Objekt exportiert.

package example.dbus;

import org.freedesktop.dbus.DBusConnection;

public class ServiceProvider {

    public static void main(String[] args) throws Exception {
        DBusConnection conn = DBusConnection.getConnection(DBusConnection.SESSION);
        conn.requestBusName("example.dbus.ServiceProvider");
        HypotenuseService service = new DefaultHypotenuseService();
        conn.exportObject("/example/dbus/hypotenuse", service);
    }
}

Die Klasse ServiceProvider starten wir nun in einem eigenen Prozess, der auf RPCs über den DBus wartet. Auf meinem Rechner hat dies mit

javac -classpath /usr/share/java/dbus.jar:. example/dbus/*.java
java -classpath /usr/share/java/dbus.jar:. example.dbus.ServiceProvider

recht gut funktioniert, vorausgesetzt libdbus-java ist installiert. Wenn es funktioniert, dann können wir mit dem Kommando ListDBus aus dem Paket dbus-java-bin überprüfen, ob der Java-Prozess sich erfolgreich mit dem DBus-Dämon verbunden hat. Ist dies geschehen, können wir uns daran machen, in verschiedenen Sprachen über den DBus mit dem Prozess zu kommunizieren. Als erstes betrachten wir die Implementierung eines Klienten mit Java:

package example.dbus;

import org.freedesktop.dbus.DBusConnection;

public class ServiceRequestor {

    public static void main(String[] args) throws Exception {
        DBusConnection conn = DBusConnection.getConnection(DBusConnection.SESSION);
        HypotenuseService service = conn.getRemoteObject(
                "example.dbus.ServiceProvider",
                "/example/dbus/hypotenuse",
                HypotenuseService.class);
        double a = 3, b = 4;
        double c = service.hypotenuse(a, b);
        System.out.println("a=" + a + ", b=" + b + ", c=" + c);
        conn.disconnect();
    }
}

Man sieht, dass man für den Zugriff auf das entfernte Objekt hauptsächlich vier Informationen benötigt, den Busnamen des entfernten Prozesses, den Objektpfad, den Namen der Schnittstelle (welcher bei den Java-Bindings aus dem Klassennamen der Java-Schnittstelle extrahiert wird) und den Namen der Methode (die Java-Bindings bieten hier eine sehr angenehme Proxy-Lösung).

Mit ebenfalls wenigen Zeilen und sehr ähnlich können wir in Python den gleichen Methodenaufruf über den DBus duchführen. Dazu muss mindestens das Paket python-dbus installiert sein.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import dbus
bus = dbus.SessionBus()
service_obj = bus.get_object('example.dbus.ServiceProvider', '/example/dbus/hypotenuse')
service = dbus.Interface(service_obj, dbus_interface='example.dbus.HypotenuseService')
a = 3.0
b = 4.0
print "a=%f, b=%f, c=%f" % (a, b, service.hypotenuse(a, b))

Zum Entwickeln mit OCaml benötigen wir mindestens die Pakete libdbus-ocaml und libdbus-ocaml-dev, hier wurde die Version (0.07-1build1) karmic verwendet. Mit den OCaml-Bindings können wir im Unterschied zu Java oder Python nicht mit Proxy-Objekten arbeiten, sondern konstruieren uns im Baukastenprinzip einen Methodenaufruf zusammen.

let bus_error = DBus.Error.init ();;
let session_bus = DBus.Bus.get DBus.Bus.Session bus_error;;

if DBus.Error.is_set bus_error then
begin
    print_endline "bus error";
    exit 1
end;;

let a = 3.0 and b = 4.0;;
let hypotenuse = DBus.Message.new_method_call 
    "example.dbus.ServiceProvider"
    "/example/dbus/hypotenuse"
    "example.dbus.HypotenuseService"
    "hypotenuse";;
DBus.Message.append hypotenuse [DBus.Double(a); DBus.Double(b)];;

let send_error = DBus.Error.init ();;
let ret_msg = DBus.Connection.send_with_reply_and_block session_bus hypotenuse (-1) send_error;; 

if DBus.Error.is_set send_error then
begin
    print_endline "send error";
    exit 1
end;;

match DBus.Message.get ret_msg with
        | (DBus.Double c)::xs -> Printf.printf "a=%f, b=%f, c=%f\n" a b c;
        | _ -> print_endline "return value error";;

Das Kompilieren des OCaml-Programms hat auf meinem Rechner erfolgreich geklappt mit

ocamlc -I /usr/lib/ocaml/dbus dBus.cma exampledbus.ml -o exampledbus 

Auch über die Shell können wir mit dem Kommando dbus-send Methoden aufrufen. Das Prinzip sollte nun wirklich nicht mehr überraschen.

#!/bin/sh
dbus-send --session \
          --dest=example.dbus.ServiceProvider \
          --type=method_call \
          --print-reply \
          /example/dbus/hypotenuse \
          example.dbus.HypotenuseService.hypotenuse \
          double:33.5 double:4 | grep "double" | grep -o -e "\([0-9\\.]*\)"

Zuletzt noch der Methodenaufruf in C über die GLib-Bindings. Hier besteht vor allem die Kunst darin, das Programm zu kompilieren. Das Finden der Pfade kann man sicherlich geschickter anstellen, ich war jedoch zufrieden, das C-Programm mit

gcc -Wall -Werror \
		-I /usr/include/glib-2.0 \
		-I /usr/lib/glib-2.0/include \
		-I /usr/include/dbus-1.0 \
		-lglib-2.0 -ldbus-glib-1 \
		exampledbus.c -o exampledbus

zum Kompilieren zu überreden. Hier ist der etwas modifizierte Beispiel-Code aus dem DBus-Tutorial, zugeschnitten auf unser Beispiel:

#include <stdlib.h>
#include <stdio.h>
#include <dbus/dbus-glib.h>

int main(int argc, char **argv) {
    DBusGConnection *conn;
    GError *error;
    DBusGProxy *proxy;
    gboolean ok;
    double a = 3.0;
    double b = 4.0;
    double c;

    g_type_init();

    error = NULL;
    conn = dbus_g_bus_get(DBUS_BUS_SESSION, &error);
    if (conn == NULL) {
        g_printerr("Failed to open connection to bus: %s\n", error->message);
        exit(1);
    }

    proxy = dbus_g_proxy_new_for_name(conn,
        "example.dbus.ServiceProvider",
        "/example/dbus/hypotenuse",
        "example.dbus.HypotenuseService");
    error = NULL;
    ok = dbus_g_proxy_call(proxy, "hypotenuse", &error,
            G_TYPE_DOUBLE, a,
            G_TYPE_DOUBLE, b,
            G_TYPE_INVALID,
            G_TYPE_DOUBLE, &c,
            G_TYPE_INVALID);
    if (!ok) {
        if (error->domain == DBUS_GERROR && error->code == DBUS_GERROR_REMOTE_EXCEPTION) {
            g_printerr("Caught remote method exception %s: %s",
                dbus_g_error_get_name(error),
                error->message);
        } else {
            g_printerr("Error: %s\n", error->message);
        }
        g_error_free(error);
        exit(1);
    }

    g_print("a=%f, b=%f, c=%f\n", a, b, c);

    return 0;
}

Alle obigen Beispiele verwenden nur die Möglichkeit des Remote Procedure Call. Über den DBus kann man prinzipiell auch Signale verschicken, per Broadcast an alle interessierten Prozesse, die mit dem DBus verbunden sind. Vielleicht ein andermal.