Telepathy

Telepathy1 は音声・動画・テキスト・ファイルなどを転送するためのリアルタイム通信フレームワークです。Telepathy がユニークなのは様々なインスタントメッセージプロトコルを抽象化する点ではなく、サービスとしての通信 (communications as a service) というアイデアを採用する点です。これは印刷がサービスであるのとちょうど同じで、様々な応用を可能にします。Telepathy は D-Bus メッセージングとモジュール化された設計を大いに利用してこれを達成しています。

サービスとしての通信を使えば通信をアプリケーションから分離できるので、このアイデアは非常に強力です。興味深い様々なユースケースがこれによって可能になります: 電子メールアプリケーションで連絡相手が在席中であるかを確認したり、ファイルブラウザからファイル転送処理を直接開始したり、アプリケーション内でネットワーク越しの協調動作を行ったりできます (Telepathy ではこの協調動作を Tube と呼びます)。

Telepathy は Robert McQueen によって 2005 年に作成され、その後いくつかの企業と個人コントリビューターによって開発・保守されてきました。McQueen が共同創設者の Collabora 社もそういった企業の一つです。

D-Bus メッセージバス

D-Bus はプロセス間通信のための非同期メッセージバスであり、GNOME や KDE などのデスクトップ環境を含む多くの GNU/Linux システムにおいて重要な役割を果たします。D-Bus は基本的に共有バスアーキテクチャで設計されています: アプリケーションはソケットアドレスを使ってバスに接続し、バス上の他のアプリケーションに向けてメッセージを送ったり、バス上のメンバー全員にメッセージをブロードキャストしたりできます。バスに接続したアプリケーションには IP アドレスに似たバスアドレスが割り当てられ、よく使われるアドレスには DNS のように名前が付いています (org.freedesktop.Telepathy.AccountManager など)。全てのプロセスは D-Bus デーモンを通して通信し、デーモンがメッセージの受け渡しや名前の登録を行います。

ユーザーから見ると、どんなシステムにも利用可能なバスが二つ存在します。一つはシステムバスで、これはシステムに関するコンポーネント (プリンタ、bluetooth、ハードウェア管理など) との通信を行います。もう一つはセッションバスで、これはログインユーザーごとに一つずつ作成され、ユーザーアプリケーション同士の通信に使われます。アプリケーションがバスを通した通信を大量に行う場合には、dbus-deamon を持たない専用の P2P バス、あるいは決められたアプリケーションだけが使うプライベートなバスを作ることができます。

D-Bus プロトコルは libdbus, GDBus, QtDBus, python-dbus といったライブラリで実装されており、これらを使えば D-Bus デーモンとの通信が可能です。こういったライブラリは D-Bus メッセージの送受信、言語の型システムから D-Bus の型フォーマットへの変換 (マーシャリング)、オブジェクトのバスへの送信といった処理を行います。普通は便利な API もライブラリから提供され、接続されたアプリケーションや起動可能なアプリケーションの一覧を取得したり、バスに登録された名前をリクエストしたりできます。D-Bus のレベルでは、こういった操作は全て dbus-deamon 自身が公開するオブジェクトに対するメソッド呼び出しを行います。

D-Bus についてさらに詳しくは http://www.freedesktop.org/wiki/Software/dbus を参照してください。

Telepathy フレームワークのコンポーネント

Telepathy はモジュール化されており、各モジュールは D-Bus のメッセージングバス (通常はユーザーのセッションバス) を通じて通信を行います。この通信の詳細は Telepathy の仕様書2で定義されます。Telepathy のコンポーネントの例を 図 20.1 に示します。

Telepathy の現在の実装では、Account Manager と Channel Dispatcher を実行するのは Mission Control と呼ばれる単一のプロセスです。

Telepathy のコンポーネントの例
図 20.1. Telepathy のコンポーネントの例

このモジュール化されたデザインは「一つのことをうまくやるプログラムを書け」という Doug McIlroy の哲学から来ています。この哲学にはいくつかの重要な利点があります:

Connection Manager はたくさんの Connection を管理します。ここで Connection は通信サービスへの論理的な接続の表現であり、設定されたアカウント一つにつき一つの Connection が存在します。Connection には複数の Channel が含まれ、この Channel が通信を実際に行うメカニズムを表します。例えば IM 通話、音声・ビデオ通話、ファイル転送、あるいはその他のステートフルな操作が Channel となります。Connection と Channel については 20.3 節 で詳しく触れます。

Telepathy からの D-Bus の利用

Telepathy のコンポーネントは D-Bus メッセージングバス (通常はユーザーのセッションバス) を使って通信を行います。D-Bus は多くの IPS システムで見られる機能を提供します。サービスは /org/freedesktop/Telepathy/AccountManager3 のような細かく区分けされた名前空間にオブジェクトを公開し、このオブジェクトが様々なインターフェースを実装します。インターフェースも org.freedesktop.DBus.PropertiesofdT.Connection といった細かく区分けされた名前空間に定義され、ユーザーから利用可能なメソッド、シグナル、プロパティを提供します。

D-Bus サービスによって公開されるオブジェクトの概念図
図 20.2. D-Bus サービスによって公開されるオブジェクトの概念図
D-Bus オブジェクトの公開

D-Bus オブジェクトの公開は D-Bus ライブラリが全て行います。そのため D-Bus ライブラリは D-Bus オブジェクトのパスからそのインターフェースを実装するソフトウェアオブジェクトへのマッピングだと言えます。サービスが公開するオブジェクトのパスは org.freedesktop.DBus.Introspectable インターフェースから取得できます。

オブジェクトのメソッド呼び出しには目的パス (/ofdT/AccountManager など) が付いています。サービスが呼び出しを受け取ると、D-Bus ライブラリがそのオブジェクトを提供するソフトウェアを検索し、そのオブジェクトに対して適切なメソッド呼び出しを行います。

Telepathy が提供するインターフェース、メソッド、シグナル、プロパティは、XML をベースとして多く情報を持てるように拡張された D-Bus IDL で提供されます。この仕様書をパースしてドキュメントと言語バインディングが生成することもできます。

Telepathy サービスはバスにたくさんのオブジェクトを公開します。例えば Mission Control は Account Manager と Channel Dispatcher を公開し、他のオブジェクトがそのサービスにアクセスできるようにします。またクライアントは Client オブジェクトを公開し、Channel Dispatcher はこのオブジェクトにアクセスします。さらに Connection Manager は Account Manager が新しい接続のリクエストするときに使うオブジェクトを接続およびチャンネルごとに一つずつ作成します。

D-Bus オブジェクトはインターフェースを持つだけで型を持ちませんが、Telepathy はいくつかの方法を使って型をシミュレートします。まずオブジェクトのパスを見ればオブジェクトが接続か、チャンネルか、クライアントかといったことが分かります (ただしオブジェクトにプロキシをリクエストするユーザーはこれを知っています)。全てのオブジェクトはこの型に対する基底インターフェース (例えば ofdT.ConnectionofdT.Channel) を実装します。チャンネルではこれは抽象基底クラスのようになり、Channel オブジェクトは自身のチャンネル型を定義する具象クラスを持ちます。ここでもこのクラスは D-Bus インターフェースによって表されます。チャンネル型は Channel インターフェースの ChannelType プロパティを読むことで確認できます。

最後に、各オブジェクトはオプショナルなインターフェースをいくつか実装します。どれを実装するかはプロトコルと Connection Manager の機能によります。利用可能なインターフェースはオブジェクトの基底クラスの Interfaces プロパティを通して確認できます。

例えば odfT.Connection 型のオブジェクトのオプショナルなインターフェースには、ofdT.Connection.Interface.Avatars (プロトコルにアバターという概念がある場合)、odfT.Connection.Interface.ContactList (プロトコルが連絡先名簿を提供する場合 ――提供しないものもある)、そして odfT.Connection.Interface.Location (プロトコルが位置情報を提供する場合) などがあります。ofdT.Channel 型を持つ Channel オブジェクトが持つ具象クラスは ofdT.Channel.Type.Text, odfT.Channel.Type.Call, odfT.Channel.Type.FileTransfer といった名前のインターフェースを持ちます。Connection と同様に、オプショナルなインターフェースには odfT.Channel.Interface.Messages (チャンネルがテキストメッセージを送受信できる場合)、odfT.Channel.Interface.Group (チャンネルがマルチユーザーチャットなどの複数人をまとめるグループである場合) といった名前をしています。そのため例えばテキストチャンネルは最低でも ofdT.ChannelofdT.Channel.Type.TextChannel.Interface.Messages の三つを実装します。チャンネルがマルチユーザーのチャットであれば、これに加えて odfT.Channel.Interface.Group も実装します。

D-Bus イントロスぺクションではなくて Interfaces プロパティを使うのはなぜ?

利用可能なインターフェースを列挙するときにそれぞれの基底クラスが Interfaces プロパティを実装するのを見て、どうして D-Bus のイントロスぺクション機能を使わないのだろう、と思うかもしれません。答えは「Channel オブジェクトや Connection オブジェクトが機能によって異なるインターフェースを持つことがあるにもかかわらず、D-Bus のイントロスぺクションはあるクラスのオブジェクトが全て同一のインターフェースを持つことを仮定するから」です。例えば telepathy-glib ではクラスが実装するオブジェクトインターフェースから D-Bus イントロスぺクションを使って D-Bus インターフェースが取得されますが、この値はコンパイル時に静的に決まります。このワークアラウンドは D-Bus イントロスぺクションにオブジェクトが持ちうる全てのデータを持たせ、Interfaces プロパティで実際に持つのはどれかを示すという方法で行われます。

Connection オブジェクトが接続に関するインターフェースしか持っていないという確認を D-Bus 自身が行うことはありません (D-Bus は名前の付いたインターフェースであり、には型という概念が無いためです)。しかし Telepathy の仕様書に含まれる情報を使って Telepathy の言語バインディングをチェックすることはできます。

なぜ、どうやって仕様書のための IDL を拡張したか

元々使われていた D-Bus 仕様書用 IDL では、名前、引数、アクセス制限、メソッド・プロパティ・シグナルの D-Bus 型シグネチャが定義可能でした。ドキュメントやバインディングのヒント、あるいは名前付き型はサポートされていませんでした。

この制限を撤廃するために、必要な情報を提供するための新しい XML 名前空間が追加されました。この名前空間は他の D-Bus API からも使えるよう一般的になっており、インラインのドキュメント、関連事項、紹介文書、廃止バージョン、メソッドが送出する例外などを表現するための要素が追加されています。

D-Bus 型シグネチャはシリアライズされてバスに送られるデータの型の低レベルな表記です。D-Bus 型シグネチャは (ii) のような形をしており、これは二つの int32 からなるデータ構造を表します。あるいはもっと複雑な a{sa(usuu)} が表すのは、string から uint32, string, uint32, uint32 というデータ構造の配列へのマップです (図 20.3)。こういった型はデータフォーマットを表しますが、その型の情報が持つ意味を伝達しません。

型をプログラマが理解しやすいように、そして言語バインディングの型付けを強化するために、単純な型、構造体、マップ、列挙型、フラグのための要素が追加されました。型に加えてドキュメントを指定することもできます。また D-Bus オブジェクトの継承関係を表現するための要素もあります。

D-Bus 型 (ii) と a{sa(usuu)}
図 20.3. D-Bus 型 (ii)a{sa(usuu)}

ハンドル

Telepathy はハンドルを使って識別子 (連絡先とルームの名前) を表します。識別子は Connection Manager が割り当てる符号無し整数であり、これを使えば連絡先あるいはルームが (Connection, handle type, handle) というタプルによって曖昧さなく表せます。

識別子の利用方法 (大文字と小文字の区別やリソースを取り扱い方) は通信プロトコルごとに異なるので、二つの識別子が同じかどうかを判定する方法がハンドルによってクライアントに提供されます。二つの識別子に対するハンドルをリクエストし、ハンドルの番号が合致すれば、識別子は同じ連絡先またはルームを指しています。

識別子の正規化規則はプロトコルごとに異なるので、クライアントで識別子やその文字列を比較するのは間違いです。例えば escher@tuxedo.cat/bedescher@tuxedo.cat/litterbox は XMPP プロトコルにおいて escher@tuxedo.cat という同じ連絡先を指すので、同じハンドルを持ちます。クライアントがチャンネルをリクエストするときには識別子とハンドルの両方が使えますが、比較に使えるのはハンドルだけです。

Telepathy サービスの検索

Account Manager や Channel Dispatcher などの常に存在するサービスの名前は Telepathy の仕様書で定められており、変わることはありません。しかし Connection Manager やクライアントの名前は定められておらず、検索して見つける必要があります。

実行中の Connection Manager やクライアントの名前を記録するサービスは Telepathy に存在しません。その代わり D-Bus に接続して新しいサービスの通知を待つことになります。D-Bus デーモンは新しく名前の付いた D-Bus サービスがバス上に表れるとシグナルを送ります。Connection Manager とクライアントの名前は仕様で定まった接頭語で始まるので、新しい名前が表れるたびにそれをテストします。

この設計の利点は、内部状態が完全に無くなることです。Telepathy のコンポーネントは起動時に現在実行中のサービスの一覧をバスデーモンに尋ねます (バスデーモンにはオープンな接続に基づいた完全なリストがあります)。例えば Account Manager がクラッシュしたときには、実行中の接続の名前を調べればアカウントオブジェクトの関係を再構築できます。

接続もサービス

Connection Manager に加えて接続自身も D-Bus サービスとなります。こうすると理論上は Connection Manager は接続を分離されたプロセスとしてフォークできますが、実際にこうした Connection Manager は今まで一つもありません。より実際的な使い方としては、D-Bus デーモンに odfT.Connection で始まるサービスを尋ねて実行中の接続を取得するというのがあります。

Channel Dispatcher もこの方法で Telepathy クライアントを検索します。クライアントは ofdT.Client で始まり、ofdT.Client.logger のような名前をしています。

D-Bus トラフィックの削減

初期バージョンの Telepathy 仕様ではコンシューマーが必要な情報をメソッド呼び出しで取得するようになっており、D-Bus トラフィックを大量に消費していました。Telepathy の後のバージョンでは様々な最適化によってこの問題が解決されています。

まず、メソッド呼び出しは可能な限り D-Bus プロパティに置き換えられました。初期バージョンの仕様では GetInterfaces, GetChannelType といったオブジェクトプロパティの取得がメソッド呼び出しになっており、例えばオブジェクトの全てのプロパティを取得する処理が複数のメソッド呼び出しを使っていました。このような場合に D-Bus プロパティを使えば、標準的な GetAll メソッドを使って全ての情報を一度に取得できます。

さらに、チャンネルのプロパティの多くは最初から最後まで変化しません。例えばチャンネルの種類やインターフェース、接続先、リクエスト先などの情報がそうです。あるいはファイル転送チャンネルであれば、ファイルサイズやファイルの種類といった情報も変化しません。

そこでチャンネルの作成を予告するためのシグナルが作られました。このシグナルには新しいチャンネルの生涯変更されないプロパティを表すハッシュテーブルが含まれます。このハッシュテーブルを (20.4 節 で説明される) チャンネルプロキシのコンストラクタに渡せば、クライアントをこの情報の処理から解放できます。

ユーザーのアバターがバスを通して送られるときにはバイト配列として送られます。Telepathy はアバターを参照するのにトークンを使ってクライアントが不必要なアバターをダウンロードしないようにしているのですが、これはクライアント側から個別にリクエストを出す必要があることを意味します。つまりクライアントがアバターを個別にリクエストするときには RequestAvatar メソッドを使い、その返答としてアバターを取得します。このため、とある連絡先がアバターを更新し、そのことを Connection Manager がシグナルした場合には、そのアバターへのリクエストが複数回行われ、同じアバターのデータがバス上を複数回通ることになります。

この問題はアバターを直接には返さない新しいメソッドを作ることで解決されました。つまり、アバターを送り返す代わりにリクエストキューに入れておくということです。アバターをネットワークから取得したときに AvataRetrieved というシグナルが発生するようにしておき、クライアントはこのシグナルを待ちます。こうするとアバターのデータの転送は一度で済み、取得しようとしているクライアント全てで利用可能になります。クライアントのリクエストがキューにある場合には、AvatarRetrieved シグナルが発生するまで他のクライアントからのリクエストは無視されます。

連絡先名簿の読み込むなどして多数の連絡先を読み込むと、別名・アバター・機能・所属グループ・位置・住所・電話番号といった大量の情報がリクエストされます。以前の Telepathy ではこれが (GetAliases をはじめ多くの API が連絡先のリストを引数として取るために) 情報グループごとに情報を取得していたので、半ダースかそれ以上のメソッド呼び出しが発生していました。

この問題を解決するために、Contacts インターフェースが導入されました。これを使うと複数のインターフェースから集めた情報を一度のメソッド呼び出しで返すことができます。これに伴い Telepathy の仕様は拡張され、GetContactAttributes メソッドが返す名前空間で包まれたプロパティを表すための Contact Attributes が追加されました。クライアントは GetContactAttributes を連絡先のリストと所望のインターフェースと共に呼び出し、連絡先から「連絡先の属性から値へのマップ」へのマップを取得します。

コードを使うと分かりやすいでしょう。リクエストは次のような形をしています:

connection[CONNECTION_INTERFACE_CONTACTS].GetContactAttributes(
  [ 1, 2, 3 ], # 連絡先のハンドル
  [ "ofdT.Connection.Interface.Aliasing",
    "ofdT.Connection.Interface.Avatars",
    "ofdT.Connection.Interface.ContactGroups",
    "ofdT.Connection.Interface.Location"
  ],
  False # 連絡先への参照を保持しない
)

返るのは次の形をしたデータです:

{ 1: { 'ofdT.Connection.Interface.Aliasing/alias': 'Harvey Cat',
       'ofdT.Connection.Interface.Avatars/token': hex string,
       'ofdT.Connection.Interface.Location/location': location,
       'ofdT.Connection.Interface.ContactGroups/groups': [ 'Squid House' ],
       'ofdT.Connection/contact-id': 'harvey@nom.cat'
     },
  2: { 'ofdT.Connection.Interface.Aliasing/alias': 'Escher Cat',
       'ofdT.Connection.Interface.Avatars/token': hex string,
       'ofdT.Connection.Interface.Location/location': location,
       'ofdT.Connection.Interface.ContactGroups/groups': [],
       'ofdT.Connection/contact-id': 'escher@tuxedo.cat'
     },
  3: { 'ofdT.Connection.Interface.Aliasing/alias': 'Cami Cat',
        ...    ...    ...
     }
}

Connection, Channel, クライアント

Connection

Connection は Connection Manager によって作成され、一つのプロトコル/アカウントを使った接続を確立します。例えば XMPP アカウント escher@tuxedo.catcami@egg.cat に接続すると Connection が二つ作成され、それぞれが D-Bus オブジェクトとして表されます。Connection は通常 Account Manager によって現在有効化されているアカウントに対して作られます。

Connection は接続の管理・監視とチャンネルのリクエストのために必須の機能をいくつか提供します。その他にもプロトコルに応じて提供されるオプショナルな機能が多数あります。これらはオプショナルの D-Bus 機能として提供されるので、Connection の Interface プロパティに一覧となっています (一つ前の節で説明した通りです)。

Connection は Account Manager によって管理されることが多く、そのときには対応するアカウントのプロパティが使われます。Account Manager はアカウントごとにユーザーの着席状況を対応する接続と同期させることもできますし、アカウントに対する接続パスを提供することもできます。

Channel

Channel は通信を行う仕組みを表します。通常は IM 通話、音声通話、ビデオ通話、ファイル転送を表しますが、サーバーとのステートフルな通信 (例えばチャットルームや連絡先の検索) の提供に Channel を使うこともできます。各チャンネルは一つの D-Bus オブジェクトで表されます。

Channel はあなたが参加する二人以上のユーザーの間の通信を表します。Channnel にはターゲットの識別子が含まれ、これは一対一の通信の場合には相手の連絡先、複数ユーザー通信 (例えばチャットルーム) の場合にはルーム識別子となります。複数ユーザーの Channel は参加しているユーザーの連絡先を記録する Group インターフェースを公開します。

Channel は Connection に属し、Connection Manager によって (通常は) Channel Dispatcher を通してリクエストされます。あるいは、ネットワークイベント (チャットの受信など) を受け取った Connection によって作成され、その後 Channnel Dispatcher に渡されるということもあります。

チャンネルのタイプはその ChannelType プロパティによって定義され、チャンネルが必要とするコア機能、メソッド、プロパティ、シグナルが D-Bus インターフェース Channel.Type で定義されます (例えば Channel.Type.Text)。暗号化などのオプショナルな追加機能はチャンネルの Interfaces プロパティに追加インターフェースとして登録されます。ユーザーをマルチユーザーのチャットルームに接続するテキストチャンネルが持つインターフェースの例を 表 20.1 に示します。

名前 説明
odfT.Channel 全てのチャンネルが持つ機能
odfT.Channel.Type.Text テキストチャンネルに共通の機能
odfT.Channel.Interface.Messages リッチテキストメッセージング
odfT.Channel.Interface.Group このチャンネルのメンバーの一覧表示、追跡、招待
odfT.Channel.Interface.Room チャットルームの件名といったプロパティの読み書き
表 20.1 テキストチャンネルの例
連絡先リストチャンネル: 失敗

初期バージョンの Telepathy 仕様では、連絡先リストがチャンネルのタイプになっていました。サーバーで定義される連絡先リスト (購読ユーザー、公開先ユーザー、ブロック済みユーザーなど) がいくつかあり、Connection がこれをリクエストするという仕組みです。マルチユーザーのチャットと同様にリストのメンバーが Group インターフェースを使って検索されます。

元々この仕組みはチャンネルの作成を連絡先リストを取得したときの一度だけとするために作られました。連絡先リストの取得は時間のかかる処理であるためです。クライアントはチャンネルを好きなタイミングでリクエストでき、準備ができ次第答えが送られます。しかしこうすると、たくさんの連絡先を持つユーザーのリクエストは頻繁にタイムアウトすることになります。クライアントの購読/公開/ブロック済みユーザーを取得するのには三つのチャンネルを確認する処理であるためです。

連絡先グループ (例えば友達) もまたチャンネルとして公開され、一つのグループごとに一つのチャンネルが作成されました。しかしこれはクライアント開発者にとって非常に扱いものでした。グループのリストを取得するといった操作にかなりの量のクライアントコードが必要になったからです。さらにグループに関する情報がチャンネルからしか入手できないので、連絡先のグループや購読状況といったプロパティを Contact インターフェースで公開できませんでした。

両方のチャンネルタイプはその後 Connection のインターフェースに置き換えられ、連絡先の購読状態・連絡先が属するグループ・グループに属する連絡先といった連絡先リストに関する情報はクライアント開発者にとって使いやすい方法で Channel から公開されることになりました。シグナルを使って連絡先リストの準備が整ったことを知らせるという方法です。

チャンネル、チャンネルプロパティ、ディスパッチのリクエスト

チャンネルをリクエストするときには、作成するチャンネルが持つべきプロパティを収めたマップを渡します。通常のリクエストに含まれるのはチャンネルタイプ、ターゲットのハンドルタイプ (連絡先またはルーム)、そしてターゲットです。加えて転送するファイルの名前とサイズ、最初に音声や映像を表示するかどうか、会議通話に参加するチャンネル、連絡先の検索を行うべき連絡先サーバーといった情報も含めることができます。

チャンネルリクエストのプロパティは Telepathy の仕様で定められるインターフェースで定義されます。表 20.2 に示す ChannelType プロパティなどがその例です。プロパティは定義されたインターフェースの名前空間で修飾され、チャンネルリクエストに追加できるプロパティには Telepathy の仕様で requestable という印が付いています。

プロパティ
ofdT.Channel.ChannelType ofdT.Channel.Type.Text
ofdT.Channel.TargetHandleType Handle_Type_Contact (1)
ofdT.Channel.TargetID escher@tuxedo.cat
表 20.2 チャンネルリクエストの例

表 20.3 に示すもっと複雑な例は、ファイル転送チャンネルをリクエストします。リクエストするプロパティがインターフェースの名前で修飾されている点に注目してください (簡単のためプロパティをいくつか省略しています)。

プロパティ
ofdT.Channel.ChannelType ofdT.Channel.Type.FileTransfer
ofdT.Channel.TargetHandleType Handle_Type_Contact (1)
ofdT.Channel.TargetID escher@tuxedo.cat
ofdT.Channel.Type.FileTransfer.Filename meow.jpg
ofdT.Channel.Type.FileTransfer.ContentType image/jpeg
表 20.3 ファイル転送チャンネルのリクエストの例

チャンネルは作成 (create) または確保 (ensure) できます。チャンネルを確保するとは、存在しない場合に限って作成するということです。チャンネルの作成を行うと、全く新しい個別のチャンネルが作られるか、あるいは複数のチャンネルを作ることはできないというエラーが発生します。例えばテキストあるいは通話チャンネルは確保し (つまり、ある人物との通話は最大でも一つしか開けないということです。多くのプロトコルはそもそも同じ連絡先との多重の会話をサポートしません)、ファイル転送やステートフルな通信のチャンネルは作成することになるでしょう。

新しく作られるチャンネルは (リクエストされたかどうかに関わらず) Connection からのシグナルによって通知されます。このシグナルにはチャンネルの不変プロパティを表すマップが含まれます。このマップに含まれるプロパティはチャンネルが破壊されるまで変更されないと保証されています。不変とみなされるプロパティは Telepathy の仕様で決まっており、例えばチャンネルの種類、ターゲットハンドルタイプ、ターゲット、イニシエーター (チャンネルを作ったオブジェクト)、インターフェースなどが通常含まれます。チャンネルの状態といったプロパティはもちろん含まれません。

古いチャンネルリクエスト

初期バージョンではチャンネルのリクエストにタイプ、ハンドルタイプ、ターゲットハンドルが使っていました。しかしこのやり方は柔軟性に欠けます。全てのチャンネルがターゲットを持つわけではなく (連絡先探索チャンネル)、リクエスト時に追加情報が必要となるチャンネルもある (ファイル転送、ボイスメールのリクエスト、SMS 送信チャンネル) からです 。

加えて、チャンネルをリクエストしたときに行う可能性のある動作が二つある (ユニークなチャンネルを作成する、あるいは既存のチャンネルの存在を確認する) ことも判明したので、Connection がどちらの動作を行うかを判断するようになりました。こういった理由により古い方法は柔軟性の高い明示的な方法で置き換えられました。

チャンネルの作成・確保時にチャンネルの不変プロパティを返すことで、そのチャンネルに対するプロキシオブジェクトが素早く作成できるようになります。情報のリクエストが不必要になるからです。表 20.4 に (表 20.3 のようにリクエストした) テキストチャンネルの不変プロパティを示します。TargetHandleInitiatorHandle といったプロパティは省略しました。

プロパティ
ofdT.Channel.ChannelType Channel.Type.Text
ofdT.Channel.Interfaces [Channel.Interface.Messages, Channel.Interface.Destroyable, Channel.Interface.ChatState]
ofdT.Channel.TargetHandleType Handle_Type_Contact (1)
ofdT.Channel.TargetID escher@tuxedo.cat
ofdT.Channel.InitiatorID danielle.madeley@collabora.co.uk
ofdT.Channel.Requested True
ofdT.Channel.Interface.Messages.SupportedContentTypes [text/html, text/plain]
表 20.4 新しいチャンネルの不変プロパティの例

プログラムは通常 Channel Dispatcher に向けてチャンネルのリクエストを行います。そのときはリクエストをしているアカウントとチャンネルリクエストを渡し、加えてハンドラの名前を渡すこともできます (プログラムからチャンネルを処理する場合に便利です)。接続ではなくアカウントの名前を渡しているので、必要な場合には Channel Dispatcher が Account Manager にアカウントをオンラインにするよう指示できるようになります。

リクエストが完了すると、Channel Dispatcher はチャンネルを名前の付いた Handler に渡すか、そうでなければ適切な Handler を検索します (Handler と他のクライアントについては以降で説明します)。Handler の名前を省略可能にすることで、最初のリクエスト以外にチャンネルと対話を行わないプログラムはチャンネルの処理を適切なプログラムに任せられるようになります (電子メールクライアントからテキストチャットを起動するなど)。

Channel のリクエストとディスパッチ
図 20.4. Channel のリクエストとディスパッチ

プログラムがチャンネルリクエストを Channel Dispatcher に送ると、Channel Dispatcher はそのリクエストを適切な Connection に転送します。Connection は NewChannels シグナルを送出し、これを受信した Channel Dispatcher がチャンネルを処理するクライアントを探します。外からやってきたリクエストされていないチャンネルも同様にディスパッチされ、Channel Dispatcher が送り出した先の Connection からのシグナルで処理が進みます。ただしこの場合には一番最初のプログラムからのリクエストはもちろんありません。

クライアント

クライアントは通信を送受信するためのチャンネルの管理・監視を行います。Channel Dispatcher に登録されるものであれば何でもクライアントと呼ばれます。クライアントには次の三種類があります (ただし一つのクライアントが同時に二つあるいは三つの種類を兼ねることもできます):

クライアントが D-Bus サービスを提供するときには、Client.Observer, Client.Approver, Alient.Handler という三つのインターフェースのどれかを使います。各インターフェースが提供するメソッドは Channel Dispatcher から呼び出され、observe, approve, handle すべきチャンネルに関する情報がクライアントへ伝達されます。

Channel Dispatcher はチャンネルをクライアントのグループに順番にディスパッチします。まずチャンネルは Observer に送られ、それらが全てが値を返してから Approver に送られます。その後 Approver のいずれかがチャンネルを受理または拒否すると、そのことが全ての Approver にそのことが伝わり、チャンネルが Handler にディスパッチされます。チャンネルのディスパッチが段階的に行われるのは、Handler がチャンネルの処理を始める前に Observer がセットアップの時間を必要とする場合があるためです。

クライアントはチャンネルフィルターというプロパティを公開します。Channel Dispatcher はこのプロパティを読み、クライアントがどのようなチャンネルを要求しているかを判断します。フィルターは少なくとも一つのチャンネルタイプとターゲットハンドルタイプ (連絡先あるいはルーム) を含む必要がありますが、追加で他のプロパティを持つこともできます。マッチングはチャンネルの不変プロパティを使って単純な比較で行われます。表 20.5 に一対一のテキストチャンネル全てとマッチするフィルターを示します。

プロパティ
ofdT.Channel.ChannelType Channel.Type.Text
ofdT.Channel.TargetHandleType Handle_Type_Contact (1)
表 20.5 チャンネルフィルターの例

クライアントのサービスは cfdT.Client という接頭語を持つ名前で公開される (例えば ofdT.Client.Empathy.Chat) ので、D-Bus を通じて検索できます。さらにクライアントはチャンネルフィルターを指定するファイルをインストールして、Channel Dispatcher にそれを読ませることも可能です。こうするとクライアントが実行されていない場合に Channel Dispatcher からクライアントを起動できるようになります。このようにクライアントを検索することで、Telepathy の他の部分を置き換えずともユーザーインターフェースが設定・変更可能になっています。

オール・オア・ナッシング

全てのチャンネルを受理するフィルターを作ることも可能ですが、使い道は Observer の例として示すことぐらいでしょう。現実のクライアントはあるチャンネルタイプに特有のコードを持ちます。

空のフィルターは Handler がどんなチャンネルも受理しないことを表しますが、名前を知っていればこの Handler にチャンネルをディスパッチが可能です。特定のチャンネルを処理するために作成される一時的な Handler はこのフィルターを使います。

言語バインディングの役割

Telepathy は D-Bus API であり、したがって D-Bus をサポートする任意のプログラミング言語から利用できます。Telepathy には言語バインディングが必要ありませんが、利用を簡単にするバインディングを提供することはできます。

言語バインディングは二つの部分に分かれます。一つは低レベルバインディングで、ここには仕様書から生成されたコード、定数、メソッド名などが含まれます。もう一つは高レベルバインディングで、ここにはプログラマーが Telepathy を簡単に使えるようにするための手で書かれたコードが含まれます。高レベルバインディングの例は GLib や Qt4 バインディングであり、低レベルバインディングの例は Python バインディングとオリジナルの libtelepathy C バインディングです。ただし GLib と Qt4 には低レベルバインディングも含まれます。

非同期プログラミング

言語バインディングでは、D-Bus 越しのリクエストを行うメソッド呼び出しは全て非同期となります。メソッドを呼ぶとリクエストが作られ、返答はコールバック関数に返されます。これが必要なのは D-Bus 自身が非同期であるためです。

ネットワークやユーザーインターフェースのプログラミングと同様、D-Bus もイベントループを使って受信したシグナルやメソッドの返り値に対するコールバックをディスパッチします。D-Bus は GTK+ や Qt ツールキットで使用される GLib のメインループと上手く噛み合います。

D-Bus 言語バインディングの中には (dbus-glib のように) メソッドの返答が返るまでメインループがブロックする疑似的な同期 API を提供するものもあります。かつては telepathy-glib API バインディングも同様の API を公開していましたが、残念ながら疑似的な同期 API を使うと問題が起こることが判明したので、telepathy-glib からは削除されました。

疑似的な同期 D-Bus 呼び出しが上手く行かない理由

dbus-glib などの D-Bus バインディングで提供される疑似的な同期インターフェースは、request-and-block というテクニックで実装されています。ブロックしている間に新たな I/O を poll されるのは D-Bus ソケットだけであり、考えているリクエストへの返答でない D-Bus メッセージは全てキューに送られ後回しにされます。

こうすると避けられない重大な問題がいくつか生じます:

  • 呼び出し側はリクエストが返るのを待つ間ブロックするので、もしユーザーインターフェースがあるならそれごと全く反応しなくなる。リクエストがネットワークへのアクセスを必要とするなら、さらに時間がかかる。呼び出された関数が固まった場合には、タイムアウトするまで何もできなくなる。スレッドを使っても呼び出しが同期的になことは変わらないので、問題は解決しない。代わりにメソッドを非同期に呼び出して応答をイベントループを通じて受け取った方が理にかなっている。

  • メッセージの順序が変わる可能性がある。待っているメッセージよりも先に来たメッセージは全てキューに入れられ、現在の処理が終わってからクライアントに届けられるためである。これによって状態の変更 (例えばオブジェクトの破棄) を伝えるシグナル来たにもかかわらずそのオブジェクトへメソッド呼び出しを行って (UnknownMethod エラーで) 失敗してしまう、という状況が生じる。この場合ユーザーに表示すべきエラーが選びにくい。一方でシグナルを最初に処理していれば、処理待ちの D-Bus メソッド呼び出しをキャンセルする、あるいは返答を無視するという処理が可能になる。

  • 二つのプロセスが疑似的な同期呼び出しを互いに対して行った場合、それぞれがもう一方からの返答を待ってデッドロックする可能性がある。このシナリオは D-Bus サービスでありながら他の D-Bus サービスを呼び出すプロセス (例えば Telepathy クライアント) で起こりうる。Channel Dispatcher がチャンネルをディスパッチするためにクライアントのメソッドを呼び出したときに、クライアントが新しいチャンネルを開くために Channel Dispatcher を呼び出すような状況である (あるいは Channel Dispatcher と同じプロセス実行される Account Manager への呼び出しでもデッドロックが起こる)。

Telepathy の C バインディングの (仕様から生成される) メソッド呼び出しは、最初 typedef によるコールバック関数を使っていました。ユーザーはこの型シグネチャを持ったコールバック関数を実装します。

typedef void (*tp_conn_get_self_handle_reply) (
    DBusGProxy *proxy,
    guint handle,
    GError *error,
    gpointer userdata
);

このアイデアは単純であり、C で上手く行きます。そのためその後のバインディングでもこの方法が使われ続けました。

最近になって、JavaScript や Python あるいは C# ライクな Vala といった GLib/GObject ベースの API を使う言語から GObject-Introspection というツールを通して Telepathy を使う人々が現れました。残念ながら上述のコールバックを他の言語に再バインドするのは非常に難しかったので、新しいバインディングが設計されました。新しいバインディングでは言語と GLib が提供する非同期コールバックの機能を利用するようになっています。

オブジェクトのレディネス

低レベル Telepathy バインディングのようなシンプルな D-Bus API では、D-Bus オブジェクトに対してメソッド呼び出しやシグナルの受信を始めるときに必要なのはそのオブジェクトに対するプロキシオブジェクトの作成だけです。オブジェクトのパスとインターフェースの名前を指定すればそれだけで使えるようになります。

しかし Telepathy の高レベルではプロキシオブジェクトが利用可能なインターフェースを知っておくべきなので、オブジェクトタイプの共通プロパティ (チャンネルタイプ、ターゲット、イニシエーター) を取得できることが望ましくなります。またオブジェクトの状態 (接続状態など) も取得・追跡できると便利です。

そのため、レディネス (readiness) という概念が全てのプロキシオブジェクトに対して存在します。プロキシオブジェクトに対してメソッド呼び出しを行って非同期的にそのオブジェクトの状態を取得し、状態が伝わってオブジェクトが準備完了になったときに通知を受けることができます。

全てのクライアントがオブジェクトの持つ全ての機能を実装するわけでもなければ知りたいわけでもないので、オブジェクトタイプのレディネスは機能ごとに分けられます。全てのオブジェクトが実装する機能はコア機能と呼ばれます。この機能はオブジェクトに関する重要な情報 (Interfaces プロパティおよび基礎的な状態など) を準備します。これ以外の状態に関してはオプショナルな機能がたくさん用意されており、例えば状態あるいはプロパティの追跡などが含まれます。プロキシから取得できる追加機能の具体例としては、連絡先の情報、(オブジェクトの) 機能、位置情報、チャットの状態 (「Escher がタイプ中...」など)、ユーザーのアバターがあります。

例えば、Connection オブジェクトのプロキシは次の機能を持ちます:

プログラマがリクエストを行うときには、使う機能のリストとその機能が全て利用可能になったら呼び出すコールバック関数を渡します。機能がその時点で利用可能である場合にはコールバック関数がすぐに呼ばれ、そうでなければ機能が取得されてから呼ばれます。

頑健性

Telepathy の大きな強みの一つが頑健性です。コンポーネントはモジュール化されており、一つがクラッシュしてもシステム全体が落ちることはありません。Telepathy を頑健にしている機能をここにあげます:

Telepathy の拡張: Sidecar

Telepathy の仕様は通信プロトコルが持つ機能の多くをカバーしていますが、プロトコル自身が拡張可能である4場合もあります。Telepathy では仕様を変更せずともこういった拡張機能を使った通信が可能です。Sidecar と呼ばれる機能を使います。

Sidecar は通常 Connection Manager のプラグインとして実装され、指定された D-Bus インターフェースを実装する Sidecar をリクエストするメソッドがクライアントから呼び出されます。例えば XEP-0016 プライバシーリストの実装では com.example.PrivacyLists というインターフェースが実装されるでしょう。このメソッドはプラグインによって提供される D-Bus オブジェクトを返し、このメソッドがインターフェース (など) を実装します。このオブジェクトはメインの Connection オブジェクトとは別に存在します (Sidecar (側車) と呼ばれるのはこのためです)。

Sidecar の歴史

Telepathy が生まれて間もないころ、One Laptop Per Child というプロジェクトがデバイス間で情報を共有するための XMPP の独自拡張 (XEP) を必要としていました。この拡張は Telepathy-Gabble (XMPP の Connection Manager) に直接追加され、Connection オブジェクトのドキュメントされていないインターフェースを通して公開されました。その後たくさんの開発者が他の通信プロトコルに無い機能を持った独自 XEP のサポートを求めたこともあり、より一般的なプラグインインターフェースが必要だということで意見が一致しました。

Connection Manager 内部の概観

ほとんどの Connection Manager は C/GLib 言語バインディングを使って書かれており、Connection Manager の開発を簡単にするための基底ベースクラスも用意されます。前述したように、D-Bus オブジェクトを公開するのは D-Bus インターフェースに対応するソフトウェアインターフェースを実装したオブジェクトです。Telepathy-GLib は Connection Manager, Connection, Channel オブジェクトを実装するための基底オブジェクト、および Channel Manager を実装するためのインターフェースを実装します。Channel Manager は BaseConnection から利用するファクトリーであり、バスへ公開するチャンネルオブジェクトの初期化と管理を行います。

言語バインディングはミックスイン (mixin) も利用します。ミックスインをクラスに追加すると、追加機能の提供、API 仕様の抽象化、新しいあるいは廃止されたバージョンの API との互換性の提供といったことが単一のメカニズムを通して可能になります。最もよく使われるミックスインはオブジェクトに D-Bus プロパティインターフェースを追加するものです。他には ofdT.Connection.Interface.ContactsofdT.Channel.Interface.Group といったインターフェースを実装するミックスイン、現在バージョンに無いインターフェースを実装するミックスイン、古いまたは新しいテキストメッセージインターフェースを既存のメソッドを使って実装するミックスインがあります。

Connection Manager のアーキテクチャの例
図 20.5. Connection Manager のアーキテクチャの例
ミックスインを使って API の間違いを直す

ミックスインを使って Telepathy の仕様の問題を解決した例が TpPresenceMixin です。Telepathy が最初に公開したインターフェース (odfT.Connection.Interface.Presence) は非常に複雑で、Connection と Client の両方にとって実装が困難であり、さらに通信プロトコルが普通持っていない、あるいはめったに使われない機能を公開していました。このインターフェースはずっとシンプルな (odfT.Connection.Interface.SimplePresence) に置き換えられました。これはユーザーが必要とする機能と Connection Manager で実装されたことのある機能を全て公開するものです。

TpPresenceMixin が両方のインターフェースを Connection で実装するので、単純になったインターフェースを使いながらもこれまでのクライアントが動作を続けることが可能になりました。

教訓

Telepathy は D-Bus を使った柔軟でモジュラーな API の好例であり、拡張可能で疎結合なフレームワークを D-Bus の上に構築する方法を示しています。デーモンによる中央管理を必要とせず、コンポーネントが再起動可能であり、そのときにデータが消失することもありません。Telepathy はバスのトラフィックを最小化して D-Bus を効率的に使う方法も示しています。

Telepathy の開発は反復的であり、D-Bus の使い方を少しずつ改善してきました。間違ったこともありましたが、その度に教訓を学びました。Telepathy のアーキテクチャの設計を通して私たちが重要だと実感したことをここにまとめます:


  1. http://telepathy.freedesktop.org/ あるいは開発者マニュアル http://telepathy.freedesktop.org/doc/book/ を参照してください。[return]

  2. http://telepathy.freedesktop.org/spec/[return]

  3. これ以降 /org/freedesktop/Telepathyorg.freedesktop.TelepathyodfT と省略して書くことにします。[return]

  4. Extensible Messaging and Presence Protocol (XMPP) などがその例です。[return]