Development/Tutorials/D-Bus/Accessing Interfaces (de)

From KDE TechBase

Zusammenfassung

D-Bus ermöglicht es Anwendungen, ihr internes API, der Außenwelt zugänglich zu machen. Auf diese APIs kann zur Laufzeit über den D-Bus, unter Zuhilfenahme von Kommandozeilenprogrammen oder D-Bus Bibliotheken und Bindungen, zugegriffen werden. Dieses Tutorial befasst sich mit der letzteren Methode, mit praxisnahen Beispielen, die Sie in Ihren Anwendungen verwenden können.

QDBusMessage benutzen

QDBusMessage repräsentiert eine D-Bus Nachricht, die über einen bestimmten Bus gesendet oder empfangen wird. Jede Nachricht kann, abhängig von ihrem Einsatzzweck, von einem der folgenden Typen sein:

  • Methodenaufruf (method call)
  • Signal (signal)
  • Antwort (reply)
  • Fehler (error)

Ein Aufzählungstyp (enum), der diese 4 Typen beinhaltet, wird in QDBusMessage definiert. Auf einen Nachrichtentyp kann über die QDBusMessage::type() Methode zugegriffen werden.

Der Aufruf einer D-Bus Methode

Eine QDBusMessage kann direkt mit der statischen Methode QDBusMessage::createMethodCall( const QString & service, const QString & path, const QString & interface, const QString & method ) benutzt werden, um Methoden in D-Bus Diensten aufzurufen. Der Rückgabewert ist ein QDBusMessage Objekt, welches dann für den eigentlichen Aufruf benutzt wird.

Der interface Parameter ist optional und nur erforderlich, wenn die Methode nicht eindeutig identifizierbar im, mit dem Pfad assoziierten, Objekt ist.Dies kann passieren, wenn das Objekt mehrere Schnittstellen implementiert, die Methoden, mit den gleichen Namen haben. In solchen (seltenen) Fällen, gibt es keine Garantie darüber, welche Methode tatsächlich aufgerufen wird, wenn man die gewünschte Schnittstelle nicht explizit definiert. Allerdings kann man in der Regel einfach eine leere Zeichenfolge (z.B. ""), als Argument für interface übergeben.

Als Beispiel für den Zugriff auf einen Dienst der (fiktiven) ping-Methode für das /network object in der org.foo.bar, könnte man dies zu tun:

 
QDBusMessage m = QDBusMessage::createMethodCall("org.foo.bar", 
    "/network", 
    "",
    "ping");
bool queued = QDBusConnection::sessionBus().send(m);

In Zeile 5 im obigen Beispiel haben wir die Nachrichten-Warteschlange für das Senden über den aktuelle Session-Bus. Der Rückgabewert vom Typ bool lässt uns wissen, ob das Einstellen in die Warteschlange erfolgreich war, oder nicht.

Dies lässt noch zwei Fragen offen:

  • Wie werden die Parameter für einen Methodenaufruf korrekt gesetzt?
  • Wie kommt man an die Rückmeldung im Falle der D-Bus-Methoden, die einen Rückgabewert haben?

Parameter setzen

Argumente zusammen mit dem Aufruf der Methode zu senden, ist ganz einfach. Zunächst muss eine QList of QVariant objects erzeugt, und diese dann der D-Bus-Nachricht hinzugefügt werden. Wenn man also der ping Methode in der obigen Deklaration einen Hostnamen als Parameter übergibt, könnte man den Code folgenderweise abändern (beachte Zeile 5 bis 7):


QDBusMessage m = QDBusMessage::createMethodCall("org.foo.bar",

                                             "/network",
                                             "",
                                             "ping");

QList<QVariant> args; args.append("kde.org"); m.setArguments(args); bool queued = QDBusConnection::sessionBus().send(m);

Alternativ bietet QDBusMessage mit der "operator<<" Funktion eine bequeme Methode zum Anhängen von Parametern an eine Nachricht. Das obige Beispiel wird zu:

QDBusMessage m = QDBusMessage::createMethodCall("org.foo.bar",

                                             "/network",
                                             "",
                                             "ping");

m << "kde.org"; bool queued = QDBusConnection::sessionBus().send(m);

Note
Die Argumente müssen in der QList in der gleichen Reihenfolge, wie von der aufgerufenen D-Bus Methode erwartet, erscheinen.


Antworten erhalten

Wenn man Informationen wieder von der D-Bus Methode zurückbekommen möchte, wendet man statt dessen die Methode QDBusConnection::call an. Diese blockiert, bis eine Antwort oder ein Timeout eintritt. Wenn unsere ping Methode Informationen über den im Parameter übergebenen Host zurückgibt, könnten wir den Code folgendermaßen abändern, um an diese Informationen zu gelangen:

QDBusMessage m = QDBusMessage::createMethodCall("org.foo.bar",

                                              "/network",
                                              "",
                                              "ping");

m << "kde.org"; QDBusMessage response = QDBusConnection::sessionBus().call(m);

Die response (Antwort) ist von einem der Typen: QDBusMessage::ReplyMessage oder QDBusMessage::ErrorMessage, je nachdem ob es erfolgreich war oder nicht. Man kann die zurückgegebenen Werte durchsehen, indem man die Argumente mit der QDBusMessage::arguments() Methode holt, die einen QList<QVariant>. zurückgibt.

Ist das der beste Weg?

QDBusMessage direkt auf diese Weise zu benutzen, um D-Bus Methoden aufzurufen, ist nicht der einfachste, beste, oder empfohlene Weg. Werfen wir einen Blick auf die weitaus bequemer zu handhabende Klasse QDBusInterface um dann den Remote-Zugriff auf D-Bus Schnittstellen zu betrachten, als wären sie lokale Methoden, die automatisch aus XML heraus generierte Proxy-Klassen benutzen.

QDBusInterface anwenden

QDBusInterface bietet eine einfache und direkte Methode, um D-Bus Aufrufe und Verbindungen zu D-Bus Signalen herzustellen.

Ein QDBusInterface Objekt stellt eine gegebene D-Bus Schnittstelle dar. Der Konstruktor erwartet als Parameter (in dieser Reihenfolge) einen Dienst-Namen, einen Objekt-Pfad, eine optionale Schnittstelle und ebenfalls optional, welchen Bus (z.B. system oder session) es benutzen soll. Wenn kein Bus explizit angegeben ist, wird standardmäßig der Session-Bus benutzt. Wenn keine Schnittstelle angegeben ist, wird das zurückgegebene Objekt alle Schnittstellen auf dem Bus ansprechen.

Es wird empfohlen, explizit eine Schnittstelle zu benennen, wenn man den Konstruktor von QDBusInterface aufruft. Aufgrund der Interna des QtDBus, wird bei Leerlassen des Schnittstellen-Parameters eine Methode der Remote-Anwendung aufgerufen, die prüft, welche Methoden überhaupt verfügbar sind. Dies ist vermeidbarer Overhead. Andererseits kann QTDBus bei Übergabe eines Schnittstellennamens, das Ergebnis für weiteren Gebrauch zwischenspeichern.


Da QDBusInterface ein QObject ist, kann man ihm ein Elternobjekt übergeben. Dies erleichtert die Verwaltungsarbeit, die mit dem Erzeugen neuer QDBusInterface Objekte einhergeht, indem man QT das Aufräumen überlässt, wenn das Elternobjekt zerstört wird.

Hier ist ein Beispiel der Benutzung von QDBusInterface, das wir Zeile für Zeile durchgehen werden:

QString hostname("kde.org"); QDBusConnection bus = QDBusConnection::sessionBus(); QDBusInterface *interface = new QDBusInterface("org.foo.bar",

                                              "/network",
                                              "org.foo.bar.network", 
                                              bus,
                                              this); 

interface->call("ping"); interface->call("ping", hostname);

QList<QVariant> args; args.append("kde.org"); interface->callWithArgumentList("ping", args);

QDBusReply<int> reply = interface->call("ping",

                                       hostname);

if (reply.isValid()) {

    KMessageBox::information(winId(), 
                             i18n("Ping to %1 took %2s")
                              .arg(hostname)
                              .arg(reply.value()),
                             i18n("Pinging %1")
                              .arg(hostname));

}

args.clear(); interface->callWithCallback("listInterfaces", args,

                           this,
                           SLOT(interfaceList(QDBusMessage));

connect(interface, SIGNAL(interfaceUp(QString)),

       this, SLOT(interfaceUp(QString)));

Synchronous Calls

The first thing we did was create a QDBusInterface on line 3 that represents the same object we were accessing in the QDBusMessage examples above.

We then called several D-Bus methods on that object using a few different techniques. On line 9 we make a simple call to a method called ping without any arguments. On line 10, we call the same method but with a parameter. Note that we didn't have to create a QList<QVariant> for the arguments. We can pass up to 8 arguments to a D-Bus method this way.

If you need to pass more than 8 arguments or for some other reason a QList<QVariant> is simply a better approach for the circumstances, then you may use the callWithArgumentList method instead as seen on lines 12-14 above.

Handling Replies

On line 16 we call the ping method yet again, but this time save the reply in a QDBusReply object. We check to make sure the reply was valid (e.g. no errors were returned and we did indeed get an int back) and then use the returned data to populate a message in an informational popup.

Asynchronous Method Calls and Signals

Up to this point in the example all of the calls made were synchronous and the application would block until a reply was received. The last two uses of QDBusInterface in the example show asynchronous usage of D-Bus, and in both cases we rely on Qt's signal and slot mechanism.

On line 29 we use callWithCallback and provide a regular QObject slot to be called when the D-Bus reply returns. This way the application will not block as callWithCallback returns immediately after queueing the message to be sent on the bus. Later, the interfaceList slot would get called. Note that this method requires a QList<QVariant>; there is no shortcut for us this time.

Finally, on line 33 we connect to a D-Bus signal. Using QDBusInterface to do this looks exactly like connecting to a regular, local signal in our own application. We even use the standard QObject::connect method! This is accomplished by QDBusInterface using Qt's meta object system to dynamically add the signals the D-Bus interface advertises. Very slick!

Is This the Best Way?

This ease of use over QDBusMessage does come with some prices, however. First, since QDBusInterface is a QObject it carries the overhead that implies. It also will perform at least one round-trip to the requested D-Bus object on creation to set up the interface object with things such as the available signals. Often this additional overhead is negligible in the larger scheme of things and well made up for by the convenience it provides.

There are still some annoyances we have to deal with, however, such as having to know the name of the interface, setting up the correct QDBusReply object such as we did above by templating it with an int and having to debug method name typos and the like at runtime versus letting the compiler do it for us. So while it's an improvement over QDBusMessage, it's still not perfect.

And that's precisely where qdbusxml2cpp comes to our rescue.

Using Classes Generated From D-Bus XML

What would be truly great is if we could simply instantiate a local object that represented a given service and start using it right away. Perhaps something like this:

org::foo::bar::network *interface =

   new org::foo::bar::network("org.foo.bar", "/network",
                           QDBusConnection::sessionBus(),
                           this);

interface->ping("kde.org");

Fortunately for us, this is precisely what Qt allows us to do. The only requirement is an XML file describing the D-Bus service. Such files are installed in the D-Bus prefix in the interfaces directory.

Tip
The D-Bus prefix can be found by issuing the following command in a terminal:
pkg-config dbus-1 --variable=prefix


We can also create our own XML files from the C++ header files and use those directly. This is covered in the next tutorial, Creating D-Bus Interfaces.

With the path to the XML in hand, we then add something like this to our CMakeLists.txt:

PKGCONFIG_GETVAR(dbus-1 prefix DBUS_PREFIX) set(network_xml ${DBUS_PREFIX}/interfaces/org.foo.bar.xml) qt4_add_dbus_interface(myapp_SRCS ${network_xml} network_interface )

This will generate two files at build time, network_interface.h and network_interface.cpp, and add them to the compiled application. We can then simply #include "network_interface.h" and use the generated class as seen in the example above.

Examining the generated header file, we can see exactly what methods, signals as well as their signatures and return values are according to the provider of the service. Using the class directly will let the compiler do type checking on method calls leaving fewer run-time breakages to track down.

Due to the generated class being a subclass of QDBusAbstractInterface just as QDBusInterface is, anything we can do with QDBusInterface is also available to us.

Due to this combination of ease of use and compile-time checking, this is generally the preferred mechanism to use when accessing D-Bus interfaces.

Tip
If your CMake installation does not provide the PKGCONFIG_GETVAR, you can add this cmake module to your project.


Doing A Little Introspection

It may also be helpful to find out if a given service is available or to check which application is providing it. Another QDBusAbstractInterface subclass, QDBusConnectionInterface, provides methods to query for such information as which services are registered and who owns them.

Once you have a service name, you can then use QDBusInterface to get the org.freedesktop.DBus.Introspectable interface and call Introspect on it. This will return an XML block describing the objects, which can in turn be introspected for what they provide. The XML itself can be processed using QDomDocument, making it a fairly simple process.

The qdbus application that ships with Qt4 provides a nice example of code doing exactly this. It can be found in tools/qdbus/tools/qdbus/qdbus.cpp in the Qt4 source distribution.