入り口に戻る

原文はこちら。

原文は CC-BY-SA-4.0 で公開されており、本サイトのこのページも同じ。

# D-Bus インターフェイスへアクセスする

本文書は、QtDBus を使用して D-Bus メソッドを呼び出したり、同じく QtDBus を使用して D-Bus シグナルへ接続したりする方法を段階的に説明していくものである。

### 目次

- 概要
- QDBusMessage を使う
  - D-Bus メソッドを呼び出す
  - 引数を設定する
  - リプライ(返答)を取得する
  - これは最良の方法なのか？
- QDBusInterface を使用する
  - 同期を取る呼び出し
  - 返答を処理する
  - 同期を取らないメソッド呼び出しとシグナル
  - もっと便利な方法は無いのか？
- D-Bus XML から生成されたクラスを使用する
- 内部調査(introspection)をしてみる

## 概要

アプリケーションは、D-Bus を使用することで自身の内部 API を外の世界に公開することができる。こうした API は、コマンド・ライン・アプリケーションを使用して、あるいは D-Bus ライブラリそのものや D-Bus バインディングそのものを使用して、実行時に D-Bus プロトコル経由でアクセスすることができる。このチュートリアルでは後者の方法を見ていくが、その際には読者が自身のアプリケーションの中で使用できるようなコードの例を示す。

## QDBusMessage を使う

QDBusMessage は、D-Bus メッセージ１つを表す。QDBusMessage は、バスを通して送信することができるし、バスから受信したメッセージを入れることもできる。各メッセージは次の４種類のいづれかであり、どの種類を使うかはメッセージの目的によって決まる。

- メソッド呼び出し
- シグナル
- リプライ(返答)
- エラー

これら４つを表す値を含む列挙型が QDBusMessage クラスの中で定義されている。メッセージの種類はメソッド `QDBusMessage::type` を通じて知ることができる。

### D-Bus メソッドを呼び出す

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);

上記の例の５行目では、現在のセッション・バスへ送信するメッセージをキューに追加している。戻り値として bool 型の値が返り、これによってキューへの追加が成功したか否かを知ることができる。

しかしながら、これだけでは次の２つの疑問が残る。

- メソッド呼び出しの際に引数を設定するにはどうすれば良いか。
- D-Bus メソッドに戻り値がある場合、戻ってくるメッセージをどうやって受け取れば良いか。

### 引数を設定する

メソッドの呼び出しに合わせて引数を送信する手順は、とても簡単なものである。まず QVariant オブジェクト(の群)の QList を作成する必要があり、次いで同オブジェクト群を読者の D-Bus メッセージに追加する。したがって、もし上記の ping メソッドが引数としてホスト名を取るのであれば、コードは次のように変更することになる(５行目から７行目に注目)。

> 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 の使用は避けるべきである。特にグラフィカル・アプリケーションでは使用しないようにするべきである。QDBusInterface のコンストラクタは、対象となる D-Bus サービスの内部を調査する(introspect)ために、表からは見えない形で停止有りの呼び出し(blocking call)を行う(QTBUG-14485)。この問題は、D-Bus の XML から生成されたクラス(後の節で説明)では起こらない。なぜかというと、この場合、必要な情報はコンパイルの時に手に入るからである。

このような停止有りの呼び出しは、必須でないサービスのインターフェイスを初期化するために使用すると、特に問題となる。なぜかというと、こうした呼び出しの１つ１つには、サービスを運営するプロセスへ行って帰って来る情報の往復(roundtrip)が必ず存在するからである。良くても、アプリケーションの読み込みの時間が僅かに増え、最悪の場合、ハングした(動かなくなった)プロセスにより、２５秒が経過して時間切れになるまで、読者のプロセスが止められた(blocked)ままになる。

QDBusInterface は、D-Bus の呼び出しを行ったり D-Bus のシグナルに接続したりするための単純で直接的なメソッドを備えている。

QDBusInterface オブジェクトは、特定の D-Bus インターフェイス１つを表す。コンストラクタは、次に挙げるものを挙げた通りの順番で引数として受け入れる。即ち、サービス名、オブジェクト・パス、インターフェイス(オプション)、どのバスを使用するか(例えば system か session か、これもオプション)である。バス(bus)が明示的に指定されなかった場合、セッション・バスをデフォルトとして使用する。インターフェイスが指定されなかった場合、返された QDBusInterface オブジェクトを使用して、バス上の全てのインターフェイスが呼び出される。

しかしながら、QDBusInterface のコンストラクタに対して明示的にインターフェイス名を渡すやり方が推奨される。引数として空のインターフェイスを渡した場合、QtDBus の内部構造により、遠隔アプリケーションにおいてどのメソッドが利用可能なのかを確認するべく、同アプリケーションへ行って帰ってくる情報の往復が常に発生する。これに対して、空でないインターフェイス名を渡した場合、QtDBus は以後の使用に備えて結果を保存(cache)することができる。

QDBusInterface は QObject なので、QDBusInterface (のコンストラクタ)に対して親オブジェクトを渡すこともできる。これによって、親オブジェクトが削除されたときに Qt が清掃作業を行えるようになり、新たな QDBusInterface オブジェクトの作成にまつわる帳簿管理が簡素化される。(訳註：QObject クラスでは、親オブジェクトのディストラクタ(destructor)は全ての子オブジェクトを破壊する。)

次のコードは QDBusInterface の使用例である。以下で、このコードを１行づつ見ていく。

> 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)));

### 同期を取る呼び出し

まず３行目で QDBusInterface を作成している。これは、上記の QDBusMessage の例でアクセスしていたのと同じオブジェクト(訳註：D-Bus の用語の「オブジェクト」)を表すものである。

次いで、そのオブジェクト宛てにいくつか D-Bus メソッドの呼び出しを行っており、その際にはいくつかの手法を用いている。９行目では、ping という名前のメソッドを引数無しでそのまま呼び出している。１０行目では、同一のメソッドを引数有りで呼び出している。引数のために `QList<QVariant>` を作成する必要は無いことに注意。このやり方で最大８個の引数を D-Bus メソッドに渡すことができる。

８個を超える数の引数を渡す必要がある場合、あるいは何らかの理由で `QList<QVariant>` の方が状況に合った手法である場合、代わりに上記のコードの１２行目から１４行目のようなやり方で `callWithArgumentList` メソッドを使うことができる。

### 返答を処理する

１６行目にてもう一度 ping メソッドを呼び出しているが、今度は返答(reply)を QDBusReply オブジェクトに保存している。返答が有効であること(例では、エラーが返らず、実際に `int` 型の値を得たこと)を確かめてから、戻ってきたデータを使用して情報表示ポップアップにメッセージを入れている。

### 同期を取らないメソッド呼び出しとシグナル

上の例のコードでは、ここまでは全ての呼び出しが同期を取る類のものであり、アプリケーションは返答を受信するまで停止(block)していた。例示のコードの中の QDBusInterface の最後の２つの使用例は、同期を取らない D-Bus の使い方を示しており、どちらの場合も Qt のシグナルとスロットの機構を利用している。

２９行目では、`callWithCallback` を使用して、通常の QObject スロットを準備している。この QObject スロットは D-Bus リプライ(返答)が返ってきた時に呼び出されるものである。このやり方の場合、`callWithCallback` は、バスに送信するメッセージをキューに追加した後、すぐに返るので、アプリケーションは停止(block)しない。`interfaceList` スロットは、後になってから呼び出されることになろう。この `callWithCallback` メソッドの場合、`QList<QVariant>` が必須であることに注意。今回は、抜け道は無い。

最後に、３３行目で D-Bus シグナルに接続している。QDBusInterface を使って D-Bus シグナルに接続するコードは、自分のアプリケーション(訳註：Qt アプリケーション)で通常のローカルなシグナルに接続するコードと全く同じ書き方で書かれている。しかも、標準の `QObject::connect` メソッドを使用している！ これは QDBusInterface の次の機能によって実現されている。即ち、Qt のメタ・オブジェクト・システムを用いて、D-Bus インターフェイスが公開(advertise)しているシグナルを動的に追加する機能である。上手い仕組みである。

### もっと便利な方法は無いのか？

QDBusInterface は、QDBusMessage よりもはるかに使いやすい。けれども、QDBusInterface を使用しても、依然として(コンパイラにやらせるのではなく自分で)実行時に対処しなければいけない面倒なことが残っている。例えば、インターフェイスの名前を知らなければならないこと、上で行ったように int 型のテンプレートを用いて正しい QDBusReply オブジェクトを設定すること、メソッド名の打ち間違いを発見・修正することなどである。そのため、QDBusInterface は、QDBusMessage よりも一段使いやすいものではあるが、まだ申し分無しとは言えない。

そして、ここでこそ `qdbusxml2cpp` が役に立つのである。

## D-Bus XML から生成されたクラスを使用する

サービスを表すローカル・オブジェクトをインスタンス化し、同ローカル・オブジェクトをすぐに使い始めることができれば、本当に素晴らしいことであろう。おそらく、コードは次のようなものになる。

> 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)に２つのファイル(`network_interface.h` と `network_interface.cpp`)が生成されることになる。この２つのファイルは、そのままコンパイルされるアプリケーションに追加される。そのため、単に次のコードを加えるだけで、生成されたクラスを上記の例のように使用することができる。

> #include "network_interface.h"

生成されたヘッダ・ファイルを調べると次のものがわかる。即ち、サービスの提供者からの情報によって、メソッド、シグナル、それらのシグネチャ、及び戻り値に何があるのかが正確にわかる。D-Bus XML から生成されたクラスを直接使用することで、メソッド呼び出しの型をコンパイラが検査してくれるようになるため、調査しなければならない実行時の破損を減らすことができる。

生成されたクラスは QDBusInterface と同じく QDBusAbstractInterface の下位クラスであるため、QDBusInterface を使ってできることは全て、生成されたクラスでも実行可能である。

使いやすさとコンパイル時の検査、この２つがあるため、複雑な D-Bus インターフェイスにアクセスするときに使用する手法としては、通常はこの手法(D-Bus XML からクラスを生成する手法)を用いるのが望ましい。

#### 豆知識

CMake によるインストールにおいて `${DBUS_INTERFACES_INSTALL_DIR}` が用意されない場合、忘れずに KDE ECM モジュールを `CMakeLists.txt` に追加する。

しかし、この手法には缺点もある。コンパイル時にアダプタ(訳註：)を生成するための XML ファイルが必要であるという点である。XML ファイルがシステムに存在しなければならないということは、次のことを意味する。即ち、まず先にこの XML ファイルを含むプロジェクトを構築(build)する必要があるということであり、コンパイル時の依存関係が増えるということでもある。ソースコードの中に XML ファイルを入れておけばこの問題が起きるのを避けることができるけれども、このやり方を採れるのは、実際のインターフェイスと同梱した XML ファイルとの間の同期を取ることが保証できる場合に限られる。

それぞれのやり方には長所と短所があり、好きな方法を選んで良いのだが、筆者は次の選択基準を提案する。

- 次の場合に限って `QDBusInterface` を使用する。即ち、呼び出しで停止すること(blocking)が問題にならならず(例えば、端末エミュレータで実行するユーティリティ・プログラム)、且つ遠隔のインターフェイスが十分に単純である場合である。
- 次の場合はシステムの XML ファイルに基くアダプタを使用する。即ち、XML ファイルが、先にコンパイルされる可能性の高いソフトウェアから得られる場合である。例えば、XML ファイルが `KWin` や `PowerDevil` やその他の主要な Plasma ソフトウェア・パッケージから得られる場合がそうである。
- 次の場合はアダプタを使用しても問題無い。それは XML ファイルが同一のプロジェクトによってインストールされている場合、即ち、プロジェクトが「デーモン」と「クライアント」から成り立っており、XML ファイルはデーモンから得られ、アダプタはクライアントが使用するという場合である。(XML ファイルが)同一のプロジェクトの中にあれば、生成されたアダプタは常に正しいものであることが保証される。
- 自身のプロジェクトに XML ファイルを入れておくことは、完璧ではないけれども、安定した API には向いているやり方である。また、このやり方は、これ以外のやり方を選んだ場合に不必要な依存関係をあまりにも多く作ってしまう場合や、システムの XML ファイルが存在しない場合(内部調査(introspection)によって独自の XML を作成することができる)にも、良い選択肢となり得る。
- 他の手法では対処できない複雑な状況であれば、`QDBusConnection` とともに、生の `QDBusMessage` を使用することもできる。

## 内部調査(introspection)をしてみる

あるサービスが利用可能かどうかを探り出したり、どのアプリケーションがそのサービスを提供しているのかを確認したりすることが有益な場面もある。QDBusConnectionInterface は、XML から生成されたクラスや QDBusInterface と同じく QDBusAbstractInterface の下位クラスであり、どのサービスが登録されているのかを問い合せたり誰がそのサービスを所有しているのかを問い合わせたりするメソッドを備えている。

サービス名を手に入れたら、QDBusInterface を使用してインターフェイス `org.freedesktop.DBus.Introspectable` を取得し、同インターフェイスに対して `Introspect` を呼び出すことができる。これによってオブジェクト群について記述している XML の塊が返るので、このオブジェクト群の内部調査(introspect)を行って同オブジェクト群が何を備えているのかを知ることができる。XML そのものは QDomDocument を用いて処理されるので、内部調査の手続きはきわめて単純なものになる。

Qt に同梱してあるアプリケーション `qdbus` には、これ(まさしく上記の手続き)を実行するコードが入っており、とても参考になる。該当コードは、Qt の配布版ソースコードの tools/qdbus/tools/qdbus/qdbus.cpp にある。

前の記事

D-Bus 入門

次の記事

中級の D-Bus

入り口に戻る