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

From KDE TechBase
Revision as of 20:31, 29 June 2011 by Neverendingo (talk | contribs) (Text replace - "<code cppqt n>" to "<syntaxhighlight lang="cpp-qt" line>")


Development/Tutorials/D-Bus/Accessing_Interfaces


Zugriff auf D-Bus Schnittstellen
Tutorial Series   D-Bus
Previous   Einführung
What's Next   Creating Interfaces, Intermediate D-Bus
Further Reading   n/a

Zusammenfassung

D-Bus ermöglicht es, das interne API von Anwendungen, für die 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:

<syntaxhighlight lang="cpp-qt" line> 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):


<syntaxhighlight lang="cpp-qt" line>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:

<syntaxhighlight lang="cpp-qt" line>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:

<syntaxhighlight lang="cpp-qt" line>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:

<syntaxhighlight lang="cpp-qt" line> 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)));

Synchrone Aufrufe

Zuallererst wird in Zeile 3 ein QDBusInterface erzeugt, welches desselbe Objekt wie in den obigen QDBusMessage Beispielen anspricht.

Dann werden mit einigen unterschiedlichen Techniken, verschiedene D-Bus Methoden aufgerufen. In Zeile 9 erfolgt ein einfacher Aufruf einer Methode namens ping, ohne weitere Argumente. In Zeile 10 wird die selbe Methode mit einem Parameter angesprochen. Zu beachten ist der Umstand, dass keine QList<QVariant> für die Argumentübergabe erzeugt werden muss. Man kann auf diese Weise bis zu 8 Argumente an eine D-Bus Methode übergeben.

Wenn mehr als 8 Argumente benötigt werden, um einen Zustand auszudrücken, oder aus anderen Gründen eine QList<QVariant> der bessere Ansatz ist, etwas zu bewerkstelligen, dann sollte man stattdessen eine callWithArgumentList Methode, wie in Zeile 12-14 zu sehen, einsetzen.

Der Umgang mit Antworten

In Zeile 16 wird die ping Methode erneut aufgerufen, aber diesmal der Rückgabewert in einer Objektvariablen vom Typ QDBusReply festgehalten. Es folgt die Prüfung auf gültige Werte (dass z.B. keine Fehler (error) zurückgeliefert wurden und der Rückgabetyp ein int ist), und die anschließende Verwertung der Rückgabedaten in einem "Pop-Up"-Fenster (Zeile 23).

Asynchrone Methodenaufrufe und Signale

Bis zu diesem Punkt im Beispiel sind alle Aufrufe synchron und die Anwendung blockiert, bis eine Antwort empfangen wurde. Die letzten beiden Verwendungen von QDBusInterface im Beispiel, zeigen die asynchrone Nutzung des D-Bus und in beiden Fällen ist es der Qt-Signal-und Slot-Mechanismus auf den man sich verlässt.

In Zeile 29 wird ein callWithCallback benutzt und dabei ein regulärer QObject Slot angegeben, der aufgerufen wird, wenn die D-Bus Antwort zurückkommt. Auf diese Weise blockiert die Anwendung nicht, da callWithCallback, sofort nach Einstellen der Nachricht in die bus Warteschlange zum senden, wieder zurrückkehrt. Der interfaceList Slot wird dann später aufgerufen. Zu beachten ist, dass diese Methode ein QList<QVariant> erfordert; da führt erstmal kein Weg drumherum.

Abschließend wird in Zeile 33 und 34 der Slot mit einem D-Bus Signal verbunden. Die Verwendung von QDBusInterface sieht genauso aus, wie das Verbinden zu einem regulärem lokalen Signal, in QT Programmen. Es wird einfach die Standard QObject::connect Methode angewandt. Dies wird von QDBusInterface bewerkstelligt, indem es Qt's meta object system benutzt um dynamisch die Signale, die die D-Bus Schnittstellen anbieten, hinzuzufügen. Ziemlich ausgefuchst!

Ist dies der beste Weg?

Diese einfache Bedienung über QDBusMessage hat allerdings ihren Preis. Da QDBusInterface ein QObject ist, schleppt es den entsprechenden Overhead mit. Es generiert bei seiner Erzeugung, auch eine Abfrage an das angeforderten D-Bus Objekt, um die Schnittstelle mit Dingen, wie den verfügbaren Signalen, zu initialisieren. Oft wird dieser zusätzliche Overhead, im größeren Zusammenhang, zu vernachlässigen sein und durch die gebotene Bequemlichkeit ausgeglichen.

Es gibt allerdings noch ein paar Ärgernisse zu bewältigen. Wie z.B., dass der Name der Schnittstelle bekannt sein muss, das QDBusReply Objekt muss, so wie oben durch ein Template mit int, korrekt gesetzt sein und nach wie vor bleibt das Debuggen von Schreibfehlern in Methodennamen und die Entscheidung etwas zur Laufzeit, oder zur Kompilierzeit zu tun. Auch wenn es mit QDBusMessage ein Fortschritt ist, es ist noch nicht perfekt.

Und genau hier kommt mit qdbusxml2cpp, die Rettung.

D-Bus XML generierte Klassen benutzen

Am besten wäre es, wenn man lokale Objekte, die einen gegebenen Dienst repräsentieren, einfach erzeugen und gleich benutzen könnte. Ungefähr so wie:

<syntaxhighlight lang="cpp-qt" line> org::foo::bar::network *interface =

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

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

Glücklicherweise ist dies genau das, was QT uns erlaubt. Die einzige Vorraussetzung ist eine XML Datei, die den D-Bus Dienst beschreibt. Solche Dateien werden im D-Bus prefix im interfaces Verzeichnis installiert.

Tip
Das D-Bus prefix findet man heraus, indem man den folgenden Befehl auf der Kommandozeile eingibt:
pkg-config dbus-1 --variable=prefix


Man kann seine XML Dateien auch aus C++ Header Dateien erzeugen. Dies wird im nächsten Tutorial Creating D-Bus Interfaces erläutert.

Mit dem Pfad zur XML Datei zur Hand, kann man dann so etwas wie den folgenden Code zu seiner CMakeLists.txt hinzufügen:

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 )

Dies erzeugt zwei Dateien während des Build-Prozess, network_interface.h und network_interface.cpp, und fügt sie der kompilierten Anwendung hinzu. Man kann einfach #include "network_interface.h" angeben und die erzeugte Klasse, wie im oben gezeigten Beispiel, benutzen.

Wenn man die erzeugte Header Datei betrachtet, sieht man genau, welche Methoden, Signale, so wie ihre Signaturen und Rückgabewerte dem Anbieter des Dienstes entsprechen.Benutzt man die Klasse direkt, wird der Compiler eine Typ-Prüfung auf Methodenaufrufe durchführen, was weniger Laufzeit Brüche übriglässt, die es aufzuspüren gilt.

Da die erzeugte Klasse eine Unterklasse von QDBusAbstractInterface ist, so wie auch QDBusInterface, ist alles was QDBusInterface bietet, ebenfalls verfügbar.

Durch diese Kombination aus einfacher Benutzung und Kompilierzeit-Typprüfung, ist dies der allgemein zu bevorzugende Mechanismus, um auf den D-Bus zuzugreifen.

Tip
Wenn die verwendete CMake Installation PKGCONFIG_GETVAR nicht anbietet, kann man this cmake module zu seinemm Projekt hinzufügen.


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.