Qt
D-Bus
QtDay 19

QtでD-Busデーモンを作るよ

More than 1 year has passed since last update.

はじめに

この記事は、Qt Advent Calendar 2016 の19日目の記事です。
18日目はOhgochiさんの「 Android向けQt開発環境のセットアップ 2016年年末最新環境版」でした。

概要

Qtのdbusモジュールを使用してD-Bus APIを提供するアプリケーションと、それを利用するメインアプリケーションの実装サンプルを示します。Qtのdbusモジュールを使用してD-Busデーモンを作成する機会があったのですが、結構ハマるポイントが有ったので、ベストプラクティス的な何かを書ければいいな、と思って書き始めたのですが、時間が足りないので最低限のサンプルと自分がハマった注意点を書いてお茶を濁すことにしました。orz

D-Busを使う利点

アーキテクチャとか細かいことは正直よく分かっていないのでWikipediaさんにお任せすることにして、ここではアプリケーションの一部をD-Busデーモンとして設計する利点を紹介します。

バックグラウンド処理を別プロセスに分離できる

データベースの検索やWebへのアクセスなど、ある程度時間がかかる可能性のある処理を、別のプロセスに分離することができます。バックグラウンド処理が異常終了した場合でもメインプロセスは処理を継続することができます。
優先度やメモリの管理もプロセス単位でできるようになるため、特にメインのアプリケーションがタイムクリティカルなGUIを持っている場合(FPSゲームとかでしょうか)に、「ここは時間がかかるからQThreadを作ってmoveToThreadして...」というコードを書く必要が少なくなります。

モジュール単位のテストが楽になる

相手側のmockを作ることで、D-Busデーモン側とそれを使うメインアプリケーションのそれぞれを、プロダクトコードを変更することなくテストすることができます。

Qtのメタシステムが使える

単純にプロセスを分離してプロセス間通信する場合には標準入出力を使う方法やQLocalSocketを使う方法がありますが、それらに比べるとsignal/slotやpropertyが使える、Qtのメタタイプシステムが使える(バイナリとの相互変換コードが不要)、という利点があります。また、通信を受ける場所や目的のオブジェクトへ配信する方法などに、あまり悩まなくて済みます。

注意すること

とても便利なD-Busですが、注意すべき点もあります。

遅い

本の虫:Linus Torvalds、「dbusはマジでクソだ」にも書かれていますが、結構遅いです。頻繁にアクセスする関数がD-Busのインタフェースを参照していたりすると、メインアプリケーションの挙動に影響を及ぼす可能性があります。

不完全なインスタンス

これはQt内部の実装の問題だと思いますが、QDBusInterfaceのインスタンスを生成した時に、メソッドはコールできるがメタシステムは使えない(signal/slotのconnectができない、propertyがとれない)という不完全なインスタンスになることがあります。

ひとつ目はQDBusInterfaceのインスタンスを作った後でサービスが登録される場合で、サービスが登録された後はこのインスタンスのisValidはtrueを返すのですが、propertyは常にinvalidなQVariantを返し、connectは常に失敗します。これはQDBusInterfaceのインスタンスをdeleteして作り直すまで解消されません。

ふたつ目はQDBusInterfaceのインスタンスを作る時にデーモン側のメインスレッドで長時間処理をしている場合で、デーモン側のイベントループに処理が戻るまでいつまででもQDBusInterfaceクラスのコンストラクタがブロックされ、タイムアウトしているのでひとつ目同様メタシステムは使えず、そのくせコンストラクタから戻った直後からisValidはtrueを返します。

D-Busデーモンのスレッド管理

D-Busデーモン内部のD-Busの処理は、イベントループで行われます。このため、D-Busに応答するはずのスレッドで時間のかかる処理が行われていると、その処理が終わってイベントループに戻るまでメインアプリケーションの処理がブロックされます。

D-Busデーモンとメインアプリケーションの実装

実際にD-Busデーモンとメインアプリケーションを実装するコードのサンプルです。
github
エラー処理などは全然入っていません。

D-Busデーモンの実装 time_daemon

まずはD-Busデーモンを実装します。以下の手順でD-BusのAPIを公開できます。今回はsessionバスのみを使用しています。systemバスを利用する場合はもう少し手順が必要ですが、今回は時間が無くなってしまったので省略します。
以降、番号はソース内コメントの番号を示しています。

  1. D-Busのインタフェースになるクラスを実装します。
  2. D-Busのインタフェースクラスには、Q_CLASSINFOでD-BusのInterfaceを宣言しておきます。
  3. QObjectのpropertyは、後で出てくるQDBusInterfaceクラスのproperty関数で参照できます。
  4. time関数はtimeプロパティの実装ですが、Q_INVOKABLEをつけることでcallでも参照できるようにしています。
  5. slotはQ_NOREPLYをつけることで、call時にメインアプリケーションの処理がブロックされないようにしています。
  6. QDBusConnection::registerServiceでサービスを登録して
  7. QDBusConnection::registerObjectでオブジェクトのインスタンスを登録。
  8. 注意点1:メインスレッドで時間のかかる処理をすると、その間、メインスレッドからsignalが出なくなったり、メインアプリケーションからの接続がタイムアウトになったりします。
  9. 注意点2:D-Busに公開したクラスは、属しているスレッド以外のスレッドからsignalをemitしようとすると、QDebugにエラーを吐いてメインアプリケーションにはsignalが通知されません。

time_daemonをビルドして実行するとsessionバスにTimeServerクラスのAPIが公開されます。Qtのbinディレクトリにあるqdbusviewerで接続して確認することができます。

メインアプリケーションの実装 time_client

メインアプリケーション側は、QDBusInterfaceクラスをそのまま使うだけでも実装できますが、使いやすいようにsignal/slotやpropertyを実装したクラスでラップしています。

  1. ラッパークラスでD-Bus APIと同じsignal/slot, propertyを実装します。
  2. QDBusInterfaceクラスのインスタンスを、デーモン側で指定したservice, path, interface, バスを指定して作成します。
  3. TimeServerのsignalやslotはSIGNAL/SLOTマクロを使用してconnectできます。
  4. また、プロパティはQDBusInterface::property関数で取得できます。
  5. デーモンのプロパティは、changedシグナルを受けてメンバー変数を更新しておき、ラッパーのgetterではそのメンバー変数の値を返すのがいいと思います。D-Busのproperty参照が遅いからです。

最後に

色々書きたいことが有ったのですが、ぐだぐだになってしまいました。。。

明日はDonokonoさんでーす。