Wireless・のおと

D-Bus のはなし

ブログ
規格 技術解説 Linux Bluetooth

久しぶりに Bluetooth の話題が出たところで、BlueZ がらみで何度か言及してきた「D-Bus」について解説してみます。IOT プロトコル規格の一つである AllJoyn についてもちょっぴり言及します。

D-Bus とは

D-Busは1台のコンピュータ上で動作する複数のプログラムの間で情報を交換するシステム(IPC:Inter Process Communication)です。2000 年代に勢いのあったオープンソース化の潮流のなか、それまで各種混ぜこぜに使われていた IPC(※註) を統一化するため freedesktop.org プロジェクトによって開発されました。開発当初は GNOME など GUI ウィンドウシステムの制御を目的としていましたが、今では GUI に限らず Linux 上のサービスインターフェースとして次第に応用範囲を広げています。

※註:パイプ、名前付きパイプ、シグナル、共有メモリ、Unix ソケット、ループバックソケット等です。D-Bus もリンク層は Unix ソケットで動作していますが、手順とフォーマット(プレゼンテーション層)が既定されていることが「生の」Unix ソケットとは異なります。

D-Bus については「OS や言語に依存しない」「柔軟で汎用性が高い」「シンプルで実行効率が良い」などと紹介されますが、前の2つはともかく、後の2つについて個人的には同意しかねます。また D-Bus には「仕様がわかりにくい」うえに「解説が少ない(特に日本語では!)」という欠点もあり、D-Bus ベースで動作する Linux サービスを使おうとするといきなり「とにかく何が何だかわからない」事態に直面すると思います。

まずとにかく、Linux 上で次のコマンドを実行してみてください。

dbus-send --print-reply --system --dest=org.freedesktop.DBus / --type=method_call org.freedesktop.DBus.ListNames

すると、次のような返答が表示されるはずです。

method return sender=org.freedesktop.DBus -> dest=:1.5391 reply_serial=2
  array [
    string "org.freedesktop.DBus"
    string ":1.128"
    string ":1.7"
    string ":1.129"
    string ":1.107"
    string ":1.8"
    string ":1.9"
    string ":1.109"
    string "org.freedesktop.ModemManager1"
    string "org.freedesktop.NetworkManager"
    string ":1.709"
    string ":1.4080"
    string ":1.4081"
    :
    :

これは最も単純な D-Bus の使用例ですが、いきなり呪文みたいなコマンドに呪文みたいな返答になりました。これが「D-Bus のわかりにくさ・取っつきにくさ」です。

D-Bus の基礎

そもそも D-Bus は『「メッセージ」を「オブジェクト」に届ける仕組み』です。「メッセージ」は要するにデータであり、「オブジェクト」はデータの受け手であるプログラムです。「どのプログラムの」「どの機能部分に」「何という名前のデータを伝えるか」が D-Bus である、と書いてもいいかも知れません。わかりにくいのは、その「名前」のところに同じような文字列が何度も反復して記述されるところです。ゴチャゴチャと余計なオマケがついて迷いやすいのですが、最終的には「メッセージをオブジェクトに届ける仕組み」であることは折に触れて思い出してください。

D-Bus の「メッセージ」には手続き呼び出し(METHOD_CALL)、手続き返値(METHOD_RETURN)、エラー(ERROR)、シグナル(SIGNAL)の4種類があります。「手続き呼び出し」と「手続き返値」はペアで使われ、これをもって「他プロセスが提供するサービス機能を、あたかもサブルーチン呼び出しのように使える」という一種の RPC (Remote Procedure Call)として使うことができます(※註)。これに対して「シグナル」は片道の一斉通知として使うことができ、Linux では「ネットワークが落ちた」とか「USB デバイス検出」「再起動」などの通知メカニズムとして多用しています。

※註:ただし D-Bus は同一マシン上の IPC に特化しており、本当の意味での RPC...ネットワークを介した異種マシン間の通信は原則としてサポートしていません。

一方、「オブジェクト」の定義は厄介です。D-Bus における「オブジェクト」は「サービス」に内包された「操作対象を指す名前」なので、オブジェクトの前にまず「サービス」を説明しなければなりません。

(図) D-Bus メッセージ伝達

(図) D-Bus メッセージ伝達

D-Bus におけるサービス名は、サービス提供プロセスが D-Bus サーバーに接続するときに登録する名前(※註)です。これには "org.freedesktop.DBus" とか "org.bluez" のように "." 区切りの文字列(慣習としてはサービスプログラム開発元の公式ドメイン)が使われます。

※註:この「名前」は「サービス名」のほか「バス名」「コネクション名」など、出典あるいは使われ方によって違う名前で呼ばれます。D-Bus の仕様としては『「バス名(Bus name)」が正式であり、名前を明示指定したバス名を「サービス名」と呼ぶこともある』のですが、日本語表記では「バス」と「パス」を誤読しやすいこともあり、この記事では一貫して「サービス名」と呼ぶことにします!

プロセスはサービス名を明示せずに D-Bus サーバーに接続することも可能で、この場合は ":1.128" のように D-Bus サーバーが適当に生成した数字の羅列が割り当てられます。冒頭に示した「dbus-send ListNames」の例は、D-Bus サーバーに対し「持っているサービス名の一覧を問い合わせ」という手続きメッセージを発行し、その返答メッセージとして「サービス名一覧の文字列配列」が返される様子を示していたわけです。

D-Bus のオブジェクト

さて、オブジェクトは「サービスに対して送られるメッセージ」に含まれる「そのサービス内におけるメッセージの宛先」であり、/ から始まる / 区切り文字列で表記されます。これを「オブジェクトパス」あるいは単純に「パス名」「パス」と呼ぶこともあります。
慣習として、パス名はサービス名の "." を "/" に置き換えた接頭辞(プレフィックス)を持たせます。例えば BlueZ の bluetooth デーモンはサービス名 "org.bluez" を持ちますが、BlueZ のオブジェクトは "/org/bluez/hci0" のような名前になります。HCI インターフェースを複数持つシステムならば、"/org/bluez/hci1" "/org/bluez/hci2" のようなオブジェクト名によって「どのインターフェースに対するメッセージか」を識別することになります。

サービスが持つオブジェクトの一覧は、「イントロスペクト(Introspect)」という機能によって潜ってゆくことができます。例えば BlueZ 5.39 のサービスに対し

dbus-send --print-reply --system --dest=org.bluez / --type=method_call org.freedesktop.DBus.Introspectable.Introspect

を発行すると

method return sender=:1.14 -> dest=:1.481 reply_serial=2
string "<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object
Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd>
<node><interface name="org.freedesktop.DBus.Introspectable"><method
name="Introspect"><arg name="xml" type="s" direction="out"/>
</method></interface><interface
name="org.freedesktop.DBus.ObjectManager"><method
name="GetManagedObjects"><arg name="objects" type=
"a{oa{sa{sv}}}" direction="out"/>
</method><signal name="InterfacesAdded"><arg name="object" type="o"/>
<arg name="interfaces" type="a{sa{sv}}"/>
</signal>
<signal name="InterfacesRemoved"><arg name="object" type="o"/>
<arg name="interfaces" type="as"/>
</signal>
</interface><node name="org"/></node>"

というのが返ってきます。これまた呪文みたいな XML でひどく読みにくいのですが、ここではとりあえず <node name="org"/> にだけ注目してください。これは

「サービス "org.bluez" のオブジェクト "/" に対し」
「"org.freedesktop.DBus.Introspectable.Introspect" という名前の手続きメッセージを送ったら」
「"/" の下には "org" というオブジェクトがあると返された」

ことを意味しています。この調子で、次は

dbus-send --print-reply --system --dest=org.bluez /org --type=method_call org.freedesktop.DBus.Introspectable.Introspect

を発行すると /org/bluez があることがわかり、/org/bluez に対して Introspect をかけると /org/bluez/hci0 があることがわかるはずです。ls /proc/ とか ls /sys/ みたいな疑似ファイルシステムに比べると何とも大袈裟で回りくどいですが、とりあえず D-Bus においてオブジェクト階層構造を閲覧する手段は Introspect メッセージとして標準化されているわけです。

※註:デスクトップ Linux のディストリビューションにはいまだに BlueZ 4.x 系を使っているものがあります。BlueZ の D-Bus API 仕様は 4.x 系と 5.x 系で全く互換性が無いので注意してください。バージョンは "bluetoothd --version" で確認できるはずです。もっと古い bluetoothd にはバージョン表示が無いかも知れませんが、その場合は hcitool のバージョンを見てみてください。
 

D-Bus のメッセージ

先ほどから "org.freedesktop.DBus.ListNames" だとか "org.freedesktop.DBus.Introspectable.Introspect" を「手続きメッセージ」の例として使ってきました。これらは freedesktop.org で既定された「標準メッセージ」の一部ですが、実装者は各サービス毎に独自のメッセージを実装することもでき、例えば BlueZ 5 で HCI インターフェースを検索可能状態にするメッセージは次のようになります。

dbus-send --print-reply --system --dest=org.bluez /org/bluez/hci0 --type=method_call org.bluez.Adapter1.StartDiscovery

メッセージ名の前半部分が "org.bluez.Adapter1" になりましたが、この前半部分を「インターフェース名」と呼び、見てのとおりインターフェース名の前半はサービス名と同じ(慣習的には開発元の公式ドメイン名)で、その後ろにグループ名("Adapter1")が付く場合が多いです(※註)。

※註:ただしこれはあくまで「慣習」であって、D-Bus 仕様として強制力のあるものではありません。また「グループ名」という呼称は著者が勝手に付けたもので、D-Bus の仕様では両者ひっくるめて単に「インターフェース名」です。

D-Busにおける「インターフェース」はオブジェクト指向の考えに基づいており、同じ「インターフェース」に属する手続きは異なるオブジェクト(サービス)間でも同じ機能として動作します。例えば既に述べたイントロスペクト機能の org.freedesktop.DBus.Introspectable.Introspect という手続きメッセージは、org.freedesktop.DBus サービスの / オブジェクトに対しても org.bluez サービスの /org/bluez/hci0 オブジェクトに対しても同じように動作します。
 

D-Bus のプロパティ

もうひとつ、多用される org.freedesktop.DBus の標準インターフェースとして「プロパティ」があります。プロパティは要するに「名前+値」のセットで、これによってオブジェクトの持つ状態や設定などを共通の枠組みで扱うことができます。
プロパティのインターフェースは org.freedesktop.DBus.Properties であり、Get, GetAll, Set のメソッドが定義されています。たとえば

dbus-send --print-reply --system --dest=org.bluez /org/bluez/hci0 --type=method_call org.freedesktop.DBus.Properties.GetAll string:org.bluez.Adapter1

とすれば、サービス org.bluez、オブジェクト /org/bluez/hci0 の持つプロパティの一覧が表示されます。

method return sender=:1.13 -> dest=:1.110 reply_serial=2
   array [
      dict entry(
         string "Address"
         variant             string "00:80:92:12:34:56"
      )
      dict entry(
         string "Name"
         variant             string "BlueZ 5.39"
      )
      dict entry(
         string "Alias"
         variant             string "silex123456"
      )
      dict entry(
         string "Class"
         variant             uint32 524288
      )
      dict entry(
         string "Powered"
         variant             boolean true
      )
      dict entry(
         string "Discoverable"
         variant             boolean false
      )
      dict entry(
         string "DiscoverableTimeout"
         variant             uint32 180
      )
      dict entry(
         string "Pairable"
         variant             boolean true
      )
      dict entry(
         string "PairableTimeout"
         variant             uint32 0
      )
      dict entry(
         string "Discovering"
         variant             boolean false
      )
      dict entry(
         string "UUIDs"
         variant             array [
               string "00001801-0000-1000-8000-00805f9b34fb"
               string "00001200-0000-1000-8000-00805f9b34fb"
               string "00001800-0000-1000-8000-00805f9b34fb"
            ]
      )
   ]

...はい、もうだいぶワケがわかんなくなってきたところでしょうね。まず、dbus-send コマンドの引数の意味を1つ1つ解説してみます。

dbus-send --print-reply \
--system --dest=org.bluez \ ★バスタイプ(system)とサービス名(org.bluez0)
/org/bluez/hci0 \ ★メッセージ宛先のオブジェクト名
--type=method_call \ ★メッセージタイプ(手続き呼び出し)
org.freedesktop.DBus.Properties.GetAll \ ★メッセージのインターフェース名(org.freedesktop.DBus.Properties)とメッセージ名(GetAll)
string:org.bluez.Adapter1 ★メッセージ引数1の型(string)と値(org.bluez.Adapter1)=プロパティのインターフェース名

プロパティの手続き Get, GetAll, Set とも第1引数は「プロパティのインターフェース名」を指定します。org.bluez サービスの /org/bluez/hci0 オブジェクトには "Address" "Name" "Powered" "Discoverable" などのプロパティがあるのですが、これらには全て「org.bluez.Adapter1」という枕言葉(インターフェース名)が付くのです。

(図) D-Bus オブジェクトとプロパティの関係

(図) D-Bus オブジェクトとプロパティの関係

D-Bus プロパティ GetAll メッセージの構造

D-Bus プロパティ GetAll メッセージの構造

ここが BlueZ の「仕様がわかりにくい」ところの真髄です。C++ 風に書けば hci0.org::bluez::Adapter1::Name であり、「hci0 というオブジェクトに所属する Name というメンバーであり、Name の名前空間が org::bluez::Adapter1 である」という意味のですが、幸か不幸か D-Bus ではインターフェース名の名前空間階層もドメイン名と同じ "." 区切りですし、一方オブジェクトの識別名は / 区切りの「オブジェクトパス」なので、パッと見は何がなんだか訳がわかりません。

更に Set が出てくるとますます混乱が深まります。hci0 の電源状態を "ON" にする dbus-send コマンドは次のようになります。

dbus-send --print-reply --system --dest=org.bluez /org/bluez/hci0 --type=method_call org.freedesktop.DBus.Properties.Set string:org.bluez.Adapter1.Powered variant:boolean:true

Set メッセージは Get や GetAll と異なり2個の引数を取ります。variant:boolean:true というのは D-Bus の型システムにおける入れ子構造の表記で、「variant という汎用型のなかに boolean が定義されておりその値は true である」ということを意味しています。
何で boolean:true をそのまま渡せないかというと、D-Bus は妙なところで型に厳密な表記を採用しており、Set メソッドを (string, boolean) として定義してしまうと boolean 以外の型が渡せなくなるためです。variant は C で言うところの void* のようなもので、C 言語風に書くなら

int Set(char *name, void *value);

という手続きに対し

bool b = true;
Set("org.bluez.Adapter1.Powered", (void*)&b);

として呼び出しているようなものです。より D-Bus の仕様に近づけた表記にしようとすると、Set という手続き名の手前にインターフェース名前空間が入り、その送り付け先が org.bluez サービスの /org/bluez/hci0 オブジェクトですから、C++ のネームスペース記法を使って似せて書くと

bool b = true;
org::bluez::hci *hci_ptr = GetObjectFromService("org.bluez", "/org/bluez/hci0");
hci_ptr->org::freedesktop::DBus::Properties::Set("org.bluez.Adapter1.Powered", (void*)&b);

みたいな格好になるわけです。これでも充分にわかりにくいですが、これを dbus-send の表記にすると

dbus-send --print-reply --system --dest=org.bluez /org/bluez/hci0 --type=method_call org.freedesktop.DBus.Properties.Set string:org.bluez.Adapter1.Powered variant:boolean:true

になって、ますます「同じような文字列が何度も反復して出てくる」「パッと見ただけでは何が何だかわからない」有様になります。

D-Bus プロパティ Set メッセージの構造

D-Bus プロパティ Set メッセージの構造

D-Bus はもともとがプログラム同士(Machine-to-Macihne)の通信手続きであり、人間が直接関わることは大して考慮されていない仕様なので読みにくいのも仕方がないところもありますが、サービス名・オブジェクト名・メッセージ名・インターフェース名にそれぞれ「ドメイン名のようなもの」が付いて、同じ文字列がアホみたいに重複しているような気がするところには、当時流行っていた「URI と XML で世界を記述し直すんだ!」という気運の影響が(良くも悪くも)感じられます。
 

D-Bus のプログラミング

D-Bus は Machine-to-Macihne 手続きというだけあって、各種のプログラミング言語用の D-Bus ライブラリが用意されています。これを言語バインデイング(language binding)と呼びます。
D-Bus の制御によく利用されるのが Python 言語で、BlueZ では test/ ディレクトリ下に各種の Python ソースが事実上の仕様書がわりになっています。たとえば「hci0 の電源状態を "ON" にする」処理を Python で書くと次のようになります。

#!/usr/bin/python
import sys
import dbus
bus = dbus.SystemBus()
obj = bus.get_object('org.bluez', '/org/bluez/hci0');
interface = dbus.Interface(obj, 'org.freedesktop.DBus.Properties');
b = dbus.Boolean(1);
interface.Set('org.bluez.Adapter1', 'Powered', b);

やっていることは dbus-send と同じなのですが、dbus-send では一緒になっていたメッセージの「インターフェース名」と「手続き名」が別扱いになっていること、variant:boolean:値 の扱いが「dbus.Boolean(値)」になっているところが違います。前者については「D-Bus プロキシ」という考え方で、D-Bus インターフェースを「その言語の持つクラス呼び出し仕様」に合わせることで、D-Bus を介した IPC 手続きライブラリ関数と同じように記述できる、という仕組みにのっとっているものです。

D-Bus の低レベルインターフェースは C 言語のライブラリ(libdbus-1)として記述されており、ANSI-C で D-Bus プログラムを書くことも不可能ではありません。しかし libdbus-1 は本当に「低レベル」で 、例えば variant 型や dictionary 型に対して最低限の API (型取得、名前取得、値取得)しか提供していないので、配列を持つvariant 型のようなデータ(現実の D-Bus サービスでは頻繁に出てきます)を処理しようとすると if と switch-case とループが何段もネストすることになります(※註)。Python なら1行で済むことを何十行もかけて書かねばならず、物凄く大変なのでお勧めはしません

※註:冒頭で「D-Bus がシンプルで実行効率が良いという話には同意しかねる」と書いた理由がこれです。

もう少しマシなのが GLIB を使った GDBUS (libdbus-glib ※註1) です。GLIB というのは D-Bus と同時期に開発された(※註2)高機能ライブラリで、これを使うと GDBusProxy や GVariant という型を使ったプログラムができ、素の ANSI-C よりは多少楽になります。とはいえ GLIB を使った「オブジェクト指向もどき」のプログラミングはそれ自体がわけのわからんローカルルールに満ちた苦行の世界なので、やはりお勧めはしかねます。

※註1:ライブラリファイル名が libgdbus ではなく libdbus-glib なのは、GDBUS 以前に使われていた DBUS-GLIB の名残です。もはや現在では互換性すら無くなっているので、うっかり DBUS-GLIB 仕様を参考にしないよう注意してください。

※註2:GLIB もD-Bus 同様 freedesktop.org のプロジェクトで、GLIB の G はもともと GNOME GUI のために開発された名残りともいいます。GCC の標準ライブラリである glibc とは直接関係ありません。libgdbusとlibdbus-glibは似て非なるもので、libglibとglibcは似ても似つかない別物...何だか無駄に紛らわしいのですが、「そういうもの」なので諦めてください。

実用的な D-Bus プログラミングには少なくとも言語レベルでオブジェクト指向をサポートしたものが必要で、最低限 C++ は必要でしょう。C++ のバインディングは DBUS-C++ (libdbus-c++)と呼ばれており、特徴的なのは D-Bus のインターフェース定義ファイル(XML)から C++ のプロキシ・クラス(のヘッダファイル)を自動生成するコンパイラ・コンパイラ(dbusxx-xml2cpp)が提供されていることです。やっぱりここにも、D-Bus が設計された時代の「XML 万能主義」の匂いを感じます。
プロキシクラス自動生成は便利といえば便利なのですが、これが思わぬ罠になることもあります。特に emerge や yocto のような自動ビルドシステムを使っていると、ビルドは /tmp ディレクトリ下で行われてコンパイラ・コンパイラの吐く中間ファイルはビルドが終わると自動削除されてしまうので、xml2cpp の存在を知らないと「全ソース grep しても何処にもクラスやメソッドが定義されていない」と悩むことになります(私は悩みました!)。

D-Bus と RPC

冒頭で「D-Bus はネットワークを介した異種マシン間の通信は原則としてサポートしていません」と書きましたが、Linux 上の D-Bus は Unix ソケット(/var/run/dbus/system_bus_socket)をリンク層として動作しているので、これをネットワークソケットに変えるだけで RPC にも使えるんじゃないか?とは誰もが考えそうなことです。しかし少なくとも 2016 年現在、freedesktop.org の D-Bus はネットワーク上の動作をサポートしていません。
ただし、サードパーティで「勝手に」D-Bus を拡張してネットワーク対応したものは存在します。中でも比較的大きな勢力だったのは携帯電話の巨人 Qualcomm 社が「来るべき IOT 時代の標準プロトコル」として 2011 年に発表した「AllJoyn / AllSeen」でしょう。AllJoyn Standard Core Spec (※註)には、このプロトコルが D-Bus の通信範囲を「ローカルマシンの外」にまで拡大したものであることが記されています。

※註:https://allseenalliance.org/framework/documentation/learn/core/standard-core

AllJoyn について深入りしてはいないので、ここで詳しくは記しません。「だった」という過去形で書いたのも、2016 年 2 月付けで AllJoyn / AllSeen は Open Connectivity Foundation(OCF)への統合を発表したからです。
OCF はもともと Intel, Broadcom, Samsung らによって 2014 年に設立された Open Interconnect Consortium (OIC) が母体となっており、その通信プロトコルは COAP (Constrained Application Protocol) と呼ばれる TLV (Type-Length-Value) 形式のバイナリフォーマットで、D-Bus 拡張の AllJoyn とは似て非なる別物です。OIC は発足が遅かったわりには勢いが大きく、UPnP Forum など他団体のプロジェクトを吸収しながら拡大発展し遂には先行する AllJoyn も吸収した格好ですが、似て非なる2種類のプロトコルをどう統合するつもりなのか、私にはわからないし興味もありません(※註)。

※註: OCF は IoTivity というオープンソースプロジェクトも立ち上げおり、AllJoyn は IoTivity に吸収されるという話もありますが、個人的に「落ち付くまで手は付けないから、勝手にすれば?」と思っています。この手の規格団体がくっついたの分裂したのという「ソープ・オペラ」は UWB をやったときに嫌というほど体験しました。

まとめ

以上、D-Bus について簡単に解説してみました。無愛想が売りの BlueZ project と異なり、FreeDesktop.org にはかなりの量のドキュメント(英文)が公開されているのですが、量が多すぎて何処から手を付けていいのか戸惑います。「簡単な実例」から手掛けてみようにも実例はあまり載っておらず、実例らしきものを見つけても「アホみたいに重複したドメイン名のようなものの羅列」で、その一つ一つが何であるのかいちいち解説がないと、パッと見ただけでは何が何だかわかりません。少なくとも私はそんな状態から始めて、とりあえずこんな解説が書けるまでにはなりました。
今回は D-Bus に付いて回る「XML 万能主義」への皮肉が少々多くなりました。このシリーズを通読されている読者の方なら、私が過去の記事で OSI プロジェクトを「バベルの塔」と揶揄したり、「Java 革命」や「IPv6 の掲げた理想」についても同様にシニカルな態度を取っていることを覚えておられるかも知れません。この業界に長く居て、新規格が出るたびに「今度こそ全世界共通の統合規格になる」と騒がれ、その理想が数年で瓦解して「沢山ある規格のひとつ」がまた1つ増える、というのを何度も何度も見ていると、どうしても冷めた気持ちになってしまいます。AllJoyn を筆頭に雨後のタケノコのごとく湧いて出てきた「自称 IOT 時代の標準規格」の有象無象から「意図的に距離を取ってきた」のも、まぁだいたいそういう理由です。

関連リンク

関連記事

製品のご購入・サービスカスタマイズ・資料請求など
お気軽にお問い合わせください