本文書は、QtDBus を使用して D-Bus メソッドを呼び出したり、同じく QtDBus を使用して D-Bus シグナルへ接続したりする方法を段階的に説明していくものである。
アプリケーションは、D-Bus を使用することで自身の内部 API を外の世界に公開することができる。こうした API は、コマンド・ライン・アプリケーションを使用して、あるいは D-Bus ライブラリそのものや D-Bus バインディングそのものを使用して、実行時に D-Bus プロトコル経由でアクセスすることができる。このチュートリアルでは後者の方法を見ていくが、その際には読者が自身のアプリケーションの中で使用できるようなコードの例を示す。
QDBusMessage は、D-Bus メッセージ1つを表す。QDBusMessage は、バスを通して送信することができるし、バスから受信したメッセージを入れることもできる。各メッセージは次の4種類のいづれかであり、どの種類を使うかはメッセージの目的によって決まる。
これら4つを表す値を含む列挙型が QDBusMessage クラスの中で定義されている。メッセージの種類はメソッド QDBusMessage::type
を通じて知ることができる。
QDBusMessage を直接使用して次の静的メソッドを用いることで、D-Bus サービスにあるメソッドを呼び出すことができる。
QDBusMessage::createMethodCall(const QString &service, const QString &path, const QString &interface, const QString &method)
この静的メソッドは QDBusMessage オブジェクトを返す。返ってきた QDBusMessage オブジェクトを使って実際の呼び出しを行うことができる。
引数 interface
は、オプションであり(空でも良い)、呼び出すメソッド(の名前)が引数 path
に結び付けられているオブジェクト内で唯一のものでない場合に限って必要になるものである。そのような事態が起こるのは、同じ名前のメソッドを持つインタフェイスが同オブジェクト(path
に結び付けられているオブジェクト)に複数実装されている場合である。このような(珍しい)状況では、使用するインタフェイスを明示的に指定しなかった場合、実際に呼び出されるのがどのメソッドになるかについて、一切の保証が存在しない。しかしながら、通常は、空の文字列(たとえば、"")を引数 interface
に渡すだけでも問題は生じない。
例を挙げる。サービス org.foo.bar
の中にあるオブジェクト /network
にある(架空の)メソッド ping にアクセスするには、次のようにする。
QDBusMessage m = QDBusMessage::createMethodCall("org.foo.bar", "/network", "", "ping"); bool queued = QDBusConnection::sessionBus().send(m);
上記の例の5行目では、現在のセッション・バスへ送信するメッセージをキューに追加している。戻り値として bool 型の値が返り、これによってキューへの追加が成功したか否かを知ることができる。
しかしながら、これだけでは次の2つの疑問が残る。
メソッドの呼び出しに合わせて引数を送信する手順は、とても簡単なものである。まず QVariant オブジェクト(の群)の QList を作成する必要があり、次いで同オブジェクト群を読者の D-Bus メッセージに追加する。したがって、もし上記の ping メソッドが引数としてホスト名を取るのであれば、コードは次のように変更することになる(5行目から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);
QDBusMessage クラスには、メッセージに引数を付け加える便利な方法が備わっており、同クラスの演算子 <<
を用いて上記の例と同じことができる。この演算子を使うと、上の例は次のようになる。
QDBusMessage m = QDBusMessage::createMethodCall("org.foo.bar", "/network", "", "ping"); m << "kde.org"; bool queued = QDBusConnection::sessionBus().send(m);
引数は、その D-Bus メソッドの呼び出しで期待されているのと同じ順番で QList の中に並んでいなければならない。
D-Bus メソッドから戻ってくる情報を受け取りたい場合、QDBusConnection::send の代わりに QDBusConnection::call
メソッドを使用する。このメソッドは、返答が返るか呼び出しが時間切れになるまで停止(block)する。もし我々の ping メソッドが上記の引数で指定したホスト("kde.org")に関する情報を返すのであれば、上記のコードを次のように変更する。
QDBusMessage m = QDBusMessage::createMethodCall("org.foo.bar", "/network", "", "ping"); m << "kde.org"; QDBusMessage response = QDBusConnection::sessionBus().call(m);
メソッドの呼び出しが成功したか否かに応じて、返答の型は QDBusMessage::ReplyMessage
型または QDBusMessage::ErrorMessage
型のどちらかになる。qdbusmessage::arguments()
を用いて引数を取り出すことで、戻り値を調べることができる。qdbusmessage::arguments()
が返すのは QList<QVariant>
である。
QDBusMessage をこのように直接使用して遠隔の D-Bus メソッドを呼び出すやり方は、最も簡単なもの、最も良いものとは言えず、推奨されるものでもない。ここからは、もっと便利な QDBusInterface クラスについて学ぶ。そして、遠隔の D-Bus インタフェイスに対して、まるで同インタフェイスがローカル・メソッドであるかのようにアクセスする方法を学ぶ(この時、XML から自動生成されたプロキシ・クラスを使用する)。
この節は学習のためだけに書かれたものであり、QDBusInterface の使用は避けるべきである。特にグラフィカル・アプリケーションでは使用しないようにするべきである。QDBusInterface のコンストラクタは、対象となる D-Bus サービスの内部を調査する(introspect)ために、表からは見えない形で停止有りの呼び出し(blocking call)を行う(QTBUG-14485)。この問題は、D-Bus の XML から生成されたクラス(後の節で説明)では起こらない。なぜかというと、この場合、必要な情報はコンパイルの時に手に入るからである。
このような停止有りの呼び出しは、必須でないサービスのインタフェイスを初期化するために使用すると、特に問題となる。なぜかというと、こうした呼び出しの1つ1つには、サービスを運営するプロセスへ行って帰って来る情報の往復(roundtrip)が必ず存在するからである。良くても、アプリケーションの読み込みの時間が僅かに増え、最悪の場合、ハングした(動かなくなった)プロセスにより、25秒が経過して時間切れになるまで、読者のプロセスが止められた(blocked)ままになる。
QDBusInterface は、D-Bus の呼び出しを行ったり D-Bus のシグナルに接続したりするための単純で直接的なメソッドを備えている。
QDBusInterface オブジェクトは、特定の D-Bus インタフェイス1つを表す。コンストラクタは、次に挙げるものを挙げた通りの順番で引数として受け入れる。即ち、サービス名、オブジェクト・パス、インタフェイス(オプション)、どのバスを使用するか(例えば system か session か、これもオプション)である。バス(bus)が明示的に指定されなかった場合、セッション・バスをデフォルトとして使用する。インタフェイスが指定されなかった場合、返された QDBusInterface オブジェクトを使用して、バス上の全てのインタフェイスが呼び出される。
しかしながら、QDBusInterface のコンストラクタに対して明示的にインタフェイス名を渡すやり方が推奨される。引数として空のインタフェイスを渡した場合、QtDBus の内部構造により、遠隔アプリケーションにおいてどのメソッドが利用可能なのかを確認するべく、同アプリケーションへ行って帰ってくる情報の往復が常に発生する。これに対して、空でないインタフェイス名を渡した場合、QtDBus は以後の使用に備えて結果を保存(cache)することができる。
QDBusInterface は QObject なので、QDBusInterface (のコンストラクタ)に対して親オブジェクトを渡すこともできる。これによって、親オブジェクトが削除されたときに Qt が清掃作業を行えるようになり、新たな QDBusInterface オブジェクトの作成にまつわる帳簿管理が簡素化される。(訳註:QObject クラスでは、親オブジェクトのディストラクタ(destructor)は全ての子オブジェクトを破壊する。)
次のコードは QDBusInterface の使用例である。以下で、このコードを1行づつ見ていく。
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)));
まず3行目で QDBusInterface を作成している。これは、上記の QDBusMessage の例でアクセスしていたのと同じオブジェクト(訳註:D-Bus の用語の「オブジェクト」)を表すものである。
次いで、そのオブジェクト宛てにいくつか D-Bus メソッドの呼び出しを行っており、その際にはいくつかの手法を用いている。9行目では、ping という名前のメソッドを引数無しでそのまま呼び出している。10行目では、同一のメソッドを引数有りで呼び出している。引数のために QList<QVariant>
を作成する必要は無いことに注意。このやり方で最大8個の引数を D-Bus メソッドに渡すことができる。
8個を超える数の引数を渡す必要がある場合、あるいは何らかの理由で QList<QVariant>
の方が状況に合った手法である場合、代わりに上記のコードの12行目から14行目のようなやり方で callWithArgumentList
メソッドを使うことができる。
16行目にてもう一度 ping メソッドを呼び出しているが、今度は返答(reply)を QDBusReply オブジェクトに保存している。返答が有効であること(例では、エラーが返らず、実際に int
型の値を得たこと)を確かめてから、戻ってきたデータを使用して情報表示ポップアップにメッセージを入れている。
上の例のコードでは、ここまでは全ての呼び出しが同期を取る類のものであり、アプリケーションは返答を受信するまで停止(block)していた。例示のコードの中の QDBusInterface の最後の2つの使用例は、同期を取らない D-Bus の使い方を示しており、どちらの場合も Qt のシグナルとスロットの機構を利用している。
29行目では、callWithCallback
を使用して、通常の QObject スロットを準備している。この QObject スロットは D-Bus リプライ(返答)が返ってきた時に呼び出されるものである。このやり方の場合、callWithCallback
は、バスに送信するメッセージをキューに追加した後、すぐに返るので、アプリケーションは停止(block)しない。interfaceList
スロットは、後になってから呼び出されることになろう。この callWithCallback
メソッドの場合、QList<QVariant>
が必須であることに注意。今回は、抜け道は無い。
最後に、33行目で D-Bus シグナルに接続している。QDBusInterface を使って D-Bus シグナルに接続するコードは、自分のアプリケーション(訳註:Qt アプリケーション)で通常のローカルなシグナルに接続するコードと全く同じ書き方で書かれている。しかも、標準の QObject::connect
メソッドを使用している! これは QDBusInterface の次の機能によって実現されている。即ち、Qt のメタ・オブジェクト・システムを用いて、D-Bus インタフェイスが公開(advertise)しているシグナルを動的に追加する機能である。上手い仕組みである。
QDBusInterface は、QDBusMessage よりもはるかに使いやすい。けれども、QDBusInterface を使用しても、依然として(コンパイラにやらせるのではなく自分で)実行時に対処しなければいけない面倒なことが残っている。例えば、インタフェイスの名前を知らなければならないこと、上で行ったように int 型のテンプレートを用いて正しい QDBusReply オブジェクトを設定すること、メソッド名の打ち間違いを発見・修正することなどである。そのため、QDBusInterface は、QDBusMessage よりも一段使いやすいものではあるが、まだ申し分無しとは言えない。
そして、ここでこそ qdbusxml2cpp
が役に立つのである。
サービスを表すローカル・オブジェクトをインスタンス化し、同ローカル・オブジェクトをすぐに使い始めることができれば、本当に素晴らしいことであろう。おそらく、コードは次のようなものになる。
org::foo::bar::network *interface = new org::foo::bar::network("org.foo.bar", "/network", QDBusConnection::sessionBus(), this); interface->ping("kde.org");
幸運にも、このコードは Qt でそのまま実行できるものなのである。必要なものは、D-Bus サービスについて記述した XML ファイルのみである。こうしたファイルは、D-Bus prefix の指すディレクトリの中に(大抵は interfaces ディレクトリの中に)インストールされる。
D-Bus prefix は次のディレクトリを指す。
${CMAKE_INSTALL_PREFIX}/share/dbus-1/interfaces
また、${CMAKE_INSTALL_PREFIX}
の値は、端末エミュレータで次の命令を実行して知ることができる。
pkg-config dbus-1 --variable=prefix
また、C++ のヘッダ・ファイルから独自の XML ファイルを作成し、同 XML ファイルを直接使用することもできる。これについては、次のチュートリアルの「D-Bus インタフェイスを作成する」で説明する。
XML への経路(path)がわかったので、続いて以下のようなコードを自分の CMakeLists.txt
に書き加える。
set(network_xml ${CMAKE_INSTALL_PREFIX}/${DBUS_INTERFACES_INSTALL_DIR}/org.foo.bar.xml) qt5_add_dbus_interface(myapp_SRCS ${network_xml} network_interface )
これによって、構築時(build time)に2つのファイル(network_interface.h
と network_interface.cpp
)が生成されることになる。この2つのファイルは、そのままコンパイルされるアプリケーションに追加される。そのため、単に次のコードを加えるだけで、生成されたクラスを上記の例のように使用することができる。
#include "network_interface.h"
生成されたヘッダ・ファイルを調べると次のものがわかる。即ち、サービスの提供者からの情報によって、メソッド、シグナル、それらのシグネチャ、及び戻り値に何があるのかが正確にわかる。D-Bus XML から生成されたクラスを直接使用することで、メソッド呼び出しの型をコンパイラが検査してくれるようになるため、調査しなければならない実行時の破損を減らすことができる。
生成されたクラスは QDBusInterface と同じく QDBusAbstractInterface の下位クラスであるため、QDBusInterface を使ってできることは全て、生成されたクラスでも実行可能である。
使いやすさとコンパイル時の検査、この2つがあるため、複雑な D-Bus インタフェイスにアクセスするときに使用する手法としては、通常はこの手法(D-Bus XML からクラスを生成する手法)を用いるのが望ましい。
CMake によるインストールにおいて ${DBUS_INTERFACES_INSTALL_DIR}
が用意されない場合、忘れずに KDE ECM モジュールを CMakeLists.txt
に追加する。
しかし、この手法には缺点もある。コンパイル時にアダプタ(訳註:)を生成するための XML ファイルが必要であるという点である。XML ファイルがシステムに存在しなければならないということは、次のことを意味する。即ち、まず先にこの XML ファイルを含むプロジェクトを構築(build)する必要があるということであり、コンパイル時の依存関係が増えるということでもある。ソースコードの中に XML ファイルを入れておけばこの問題が起きるのを避けることができるけれども、このやり方を採れるのは、実際のインタフェイスと同梱した XML ファイルとの間の同期を取ることが保証できる場合に限られる。
それぞれのやり方には長所と短所があり、好きな方法を選んで良いのだが、筆者は次の選択基準を提案する。
QDBusInterface
を使用する。即ち、呼び出しで停止すること(blocking)が問題にならならず(例えば、端末エミュレータで実行するユーティリティ・プログラム)、且つ遠隔のインタフェイスが十分に単純である場合である。KWin
や PowerDevil
やその他の主要な Plasma ソフトウェア・パッケージから得られる場合がそうである。QDBusConnection
とともに、生の QDBusMessage
を使用することもできる。あるサービスが利用可能かどうかを探り出したり、どのアプリケーションがそのサービスを提供しているのかを確認したりすることが有益な場面もある。QDBusConnectionInterface は、XML から生成されたクラスや QDBusInterface と同じく QDBusAbstractInterface の下位クラスであり、どのサービスが登録されているのかを問い合せたり誰がそのサービスを所有しているのかを問い合わせたりするメソッドを備えている。
サービス名を手に入れたら、QDBusInterface を使用してインタフェイス org.freedesktop.DBus.Introspectable
を取得し、同インタフェイスに対して Introspect
を呼び出すことができる。これによってオブジェクト群について記述している XML の塊が返るので、このオブジェクト群の内部調査(introspect)を行って同オブジェクト群が何を備えているのかを知ることができる。XML そのものは QDomDocument を用いて処理されるので、内部調査の手続きはきわめて単純なものになる。
Qt に同梱してあるアプリケーション qdbus
には、これ(まさしく上記の手続き)を実行するコードが入っており、とても参考になる。該当コードは、Qt の配布版ソースコードの tools/qdbus/tools/qdbus/qdbus.cpp にある。