Home > Latest topics

Latest topics > XPIDLで自作コンポーネントのインターフェースを定義する時に気をつけないといけないこと

宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能! シス管系女子って何!? - 「シス管系女子」特設サイト

XPIDLで自作コンポーネントのインターフェースを定義する時に気をつけないといけないこと - Oct 01, 2009

XUL/MigemoがTrunk(3.7a1pre)で動かなくなっていた件について原因を調べてた。

エラーの原因

エラーメッセージに見覚えがあるなあと思って検索したら、前に書いたエントリがヒットした。

一個だけ躓いた所として、XPCOMコンポーネントの新しいメソッドをIDL定義に追加して引数にnsISelectionController型のオブジェクトを渡すようにしていた所、XPIDLでのコンパイルは通るんだけど実際に使う時にNS_ERROR_XPC_CANT_GET_PARAM_IFACE_INFOというエラーが出てにっちもさっちもいかなくなってしまった。ダメ元で、引数の型をnsISupportsにして受け取り側でQueryInterfaceするようにしてみたところ、ちゃんと動いてくれた。一体何だったんだろうこれは。

ああ、前にも詰まってたところだったか……この時は結局「理由」が分からないままで、とりあえず動くようにはなったからということでそれ以上は調べなかったんだよね。

改めて検索してみたら、似たような問題にぶち当たった人がいたようだった。で、やっと何が問題の原因だったのかが分かった。

新しいXPCOMコンポーネントを定義する時に、インターフェースも新しく定義する場合、XPIDLを使ってインターフェースを定義してやらないといけない。

[scriptable, uuid(4aca3120-ae38-11de-8a39-0800200c9a66)]
interface xmIXMigemoFileAccess : nsISupports
{
   AString getAbsolutePath(in AString filePath);
   AString getRelativePath(in AString filePath);
   AString getExistingPath(in AString absoluteOrRelativePath);
   AString readFrom(in nsIFile file, in ACString encoding);
   nsIFile writeTo(in nsIFile file, in AString content, in ACString encoding);
};

この時、メソッドの引数や返り値の型として、AStringやlongのようなプリミティブ型(?)だけでなく、nsIFileのように他のインターフェースを使うこともできる。

この時気をつけないといけないのが、インターフェースには2つの識別子があるということ。1つは、上記の例でいえばinterface xmIXMigemoFileAccessという部分で定義されているインターフェース名「xmIXMigemoFileAccess」、もう一つは[scriptable, uuid(4aca3120-ae38-11de-8a39-0800200c9a66)]という部分で定義されているインターフェースID(IID)「4aca3120-ae38-11de-8a39-0800200c9a66」だ。

今回は、XUL/Migemoのコンポーネントの機能のうち、nsIDOMRangeやnsIDOMWindowを値の型として使っていた部分でエラーが起こっていた。

interface xmIXMigemoTextUtils : nsISupports
{
   (略)
   AString range2Text(in nsIDOMRange range);
   (略)

具体的にはこの辺。どうも、Firefox 3.5から3.7a1preまでの間のどこかの時点で、nsIDOMRangeやnsIDOMWindowのIIDが変更されたらしい。XPIDLのコンパイル(.xptファイルの生成)には成功しても、そのあと3.7a1preのFirefoxが.xptを解釈する時に、IIDの方でnsIDOMRangeやnsIDOMWindowのインターフェース定義を探すために、「こんなIIDのインターフェースは定義されてないよ! インターフェースの情報が見つからないよ!」というエラーになっていたようだ。

とぴあさんに色々教えてもらった。元々、XPCOMの元になった(?)COMの世界つまりC++の世界では、インターフェースの識別にはIIDを使うのが原則というかIIDこそが本来の識別子で、nsIDOMRangeとかの名前はそれへの参照に過ぎないということなのだそうな。

インターフェースの内容が変化した時(メソッドの追加等)には、「古いインターフェースの定義が消されて、別のIIDで新たなインターフェースが定義された」というような扱いになるようだ。「IIDが変わった」と前述しているけれども、プログラム的には「IIDが変わっただけで同じインターフェース」なのではなく「全くの別物」という扱いだから、全く互換性は保証されない……というわけ。

なお、互換性を維持したままインターフェースに新しい機能を追加するためには、現在使われているインターフェースの定義はそのまま残して、それを継承した新しいインターフェースを定義する必要があるということになる。「nsIPrefBranch2」とか「nsIGlobalHistory3」などがそれにあたる。

回避策

nsIDOMRangeやnsIDOMWindowのIIDが3.7a1preのものと同じになっている新しいSDKを使って.xptファイルを作り直してやれば、3.7a1preでもXUL/Migemoが動くようになるはず。でも、そうすると今度はFirefox 3.5以下で動かなくなる。それは困る。

無難な解決策は、値の型として使うインターフェースを、古いFirefoxから新しいFirefoxまでの間でずっと変わっていないインターフェースにするということ。nsISupports(すべてのXPCOMの基底インターフェース)ならほぼ確実に使える。

interface xmIXMigemoTextUtils : nsISupports
{
   (略)
   AString range2Text(in nsISupports range);
   (略)

このようにインターフェースの定義を変えた上で、実装の方で受け取った値をQueryInterface()してやる。

range2Text : function(aRange) {
   aRange.QueryInterface(Ci.nsIDOMRange);
   var doc = aRange.startContainer;
   (略)

こうすると、メソッドを呼ぶ時に、DOMのRangeオブジェクトをそのまま引数に渡せる状態を維持できる。

JavaScriptのレイヤからはIIDではなくヒューマン・リーダブルなインターフェース名だけを使うのが一般的なのだけれども、こうしておけばXPCOMのフレームワークが自動的に「新しいIIDのnsIDOMRangeインターフェース」を参照してくれる。実際にインターフェースで定義されている内容には変更が起こっているかもしれないから、確実な動作は保証できなくなるけれども、当該のインターフェースが「既にある機能はなくなったり変更されたりせず、新しい機能が追加されていくだけ」という傾向があるのなら、おおむね問題なく動作し続けてくれると考えられる。

そもそも……

とぴあさんが調べてくれたのだけれども、今回のトラブルの原因になったnsIDOMRangeは定義の頭の方に@status FROZENと書かれていて、本来であれば、IIDが変わることもなければメソッドやプロパティなどのインターフェース定義の内容が変わることもない、安心して永続的に使えるインターフェースだったはず……のようだ。

それが、Bug 396392 – Support for getClientRects and getBoundingClientRect in DOM Rangeに提出されたパッチでメソッドが追加されると同時にIIDも変更されてしまった。本当は、これはあってはならない事態らしい。当該バグのコメントでもnsIDOMRangeのIIDは元に戻して、変更はnsIDOMNSRange(Geckoの独自拡張の機能が色々定義されているインターフェース)に対して行うべきと書かれている。おそらく近いうちに、nsIDOMRangeのIIDは元の物に戻されて、XUL/Migemoが動かなくなってしまった問題も解消されるものと思われる。

個人的な感覚としては、インターフェースに変化が無くても実装が変わって挙動も変わりました……なんて事がMozillaではザラにあるので、インターフェースの部分でだけ「ちょっとでも変化があったらIIDは別の物! インターフェースとしても別物!」という風に厳密に区別しても意味なくね? と思う。ぶっちゃけ、「安心して使えるAPI」なんてのはMozillaの世界じゃリップサービスに過ぎないと思ってる。(とぴあさんには、それはプロジェクトのマネジメントがマズイという別のレイヤの問題だよねと言われた。)

あと、現在のFirefox(Gecko 1.9以降)では、自分で新しくインターフェースを定義してXPCOMコンポーネントを作る必要はほとんど無いと言っていい。JavaScriptでコンポーネントを定義してJavaScriptだけから使うのであれば、JavaScriptコードモジュールを使えばよくなった。また、起動直後に処理を行うような場合なんかには相変わらずXPCOMコンポーネントの定義が必要だけど、それは既存のインターフェース(nsISupportsやnsIObserver)だけでも事足りる。XPIDLが必要になるのは、JavaScriptで書かれた機能をC++のレイヤから呼び出したいような時だけだ。普通に開発する分には、こんな事で悩む必要は今や全くない。という事に気がついて、今更になって激しい徒労感を感じている。

分類:Mozilla > XUL, , , , , , 時刻:13:31 | Comments/Trackbacks (7) | Edit

Comments/Trackbacks

no title

うーん。確かに回避策としては動く可能性があるものではありますが、互換性を確保するためにinterfaceを変更する時にはuuidを変えているので、好ましくは無いと思います。

> range2Text : function(aRange) {
> aRange.QueryInterface(Ci.nsIDOMRange);
> var doc = aRange.startContainer;

この時点で、startContainerがnsIDOMRangeにある保証が全くないので。

Commented by 中野雅之 at 2009/10/01 (Thu) 14:42:18

no title

>互換性を確保するためにinterfaceを変更する時にはuuidを変えている

すみませんが、ここの意味が分かりません。どういうことでしょうか?

>この時点で、startContainerがnsIDOMRangeにある保証が全くないので。

そんなことを言い出したら複数のバージョンのFirefoxに対応するアドオンなんて一個も作れません……

Commented by Piro at 2009/10/01 (Thu) 16:23:46

no title

>>互換性を確保するためにinterfaceを変更する時にはuuidを変えている
>
> すみませんが、ここの意味が分かりません。どういうことでしょうか?

uuidが同じままなら、その開発時のinterfaceに存在した内容にはアクセスできることが保証される、ということです(一応、ほとんど行われませんが、uuid変更無しにinterfaceの末尾にメソッドが追加されている可能性はありますが、この場合は互換性に問題はありません)。

逆に、uuidが別物になっているというのはどのような変更があったか全く分からない、ということです。

>>この時点で、startContainerがnsIDOMRangeにある保証が全くないので。
>
> そんなことを言い出したら複数のバージョンのFirefoxに対応するアドオンなんて一個も作れません……

それは違うでしょう。複数のバージョンに対応したXPCOMコンポーネントが(シンプルには)作れないだけだと思います(そして多分、本来やるべきじゃないと思います)。

uuidが変わっている、というのはそれぐらい何があったか全く分からない状況ので、例のようなアクセスは不適当だと思います。try-catchでどうにかするなり、メソッド等の存在確認をするのが適当ではないかと。

ちなみに、branch上では通常のインターフェースのuuidは一切変更されません。

Commented by 中野雅之 at 2009/10/01 (Thu) 20:48:08

no title

> (一応、ほとんど行われませんが、uuid変更無しにinterfaceの末尾にメソッドが追加されている可能性はありますが、この場合は互換性に問題はありません)。
いや、それは、新しい、メソッドの追加されたインターフェースを期待しているのに、古いインターフェースも使えてしまうのでやはり問題はあるでしょう。

今回の問題は、
https://bugzilla.mozilla.org/show_bug.cgi?id=396392
http://hg.mozilla.org/mozilla-central/diff/3262c0bd49ac/dom/interfaces/range/nsIDOMRange.idl
というところのようで、そもそも Frozen Interface に勝手にメソッドを追加して勝手に uuid を変えているので、そのうち直るような気もしますが、
「uuid が同じならインターフェースは同じである」はずで、つまり「メソッドが存在していると知っている uuid で QueryInterface を呼べばいい」ということではないかと。
ここから先は試し方がわからなかったので推測になりますが、
aRange.QueryInterface(Components.ID("{a6cf90ce-15b3-11d2-932e-00805f8add32}")) とかが出来るのであれば、新旧二つの uuid でそれぞれ呼んで、結果が帰ってきた方を使えばいいのではないかと思います。

Commented by とぴあ at 2009/10/01 (Thu) 22:09:19

no title

>> (一応、ほとんど行われませんが、uuid変更無しにinterfaceの末尾にメソッドが追加されている可能性はありますが、この場合は互換性に問題はありません)。
> いや、それは、新しい、メソッドの追加されたインターフェースを期待しているのに、古いインターフェースも使えてしまうのでやはり問題はあるでしょう。

む。それは確かにそうですね、ただ、Mozillaとしては古いバージョンとの互換性は開発者が知っているべき、と考えているのかもしれません。今は互換のあるバージョンも示さなくてはいけませんし。
# 推奨されない方法ですが、一応容認はされている、という話です。
# ちなみに、普通は末尾に追加するのみの場合でもuuidを変更します。

> そもそも Frozen Interface に勝手にメソッドを追加して勝手に uuid を変えているので、

ああ、そういえば、nsIDOM*ですね、これ。

Commented by 中野雅之 at 2009/10/01 (Thu) 22:31:44

no title

本文に追記しました。

>この時点で、startContainerがnsIDOMRangeにある保証が全くないので。

に対して

>そんなことを言い出したら複数のバージョンのFirefoxに対応するアドオンなんて一個も作れません……

と書いた理由が自分でもモヤモヤしていたのですが、今思えば、追記に書いた「インターフェースが同じでも実装が変わることがようあるから、インターフェースの部分でだけ厳密さを求めても意味なくね? そこだけ厳密にして『互換性が保証される』なんて、詭弁もいいとこじゃね?」と思ったというのがその理由だったのだと思います。

Commented by Piro at 2009/10/02 (Fri) 08:48:40

no title

本論とは関係がないのでコメントでちらっと補足するにとどめておきますが、
C++ の場合は引数のタイプとかを実行時に判定したりはしないので、インターフェースが変わったら、スタックに何をどういう順番で積むかとか、まあそういったところが変わるので、かならず uuid を変更しなくてはいけません。既存のインターフェースを期待した呼び出し側と、新しいインターフェースを持った呼び出された側の間で齟齬が発生してしまいます。
ただし、末尾に関数を追加する場合は、普通は既存のコードとの互換性は保たれているので、まあ気にしない、という考え方は一応あります。その場合も古いAPIに対して新しい関数を呼ぼうとしたら(そしてそれは、バージョン判定など別の手法を使わない限りは普通にできてしまいます)同様に齟齬が発生します。
どちらも場合によってはクラッシュすることになるので、インターフェースが変わったらuuidを変える、というのは、C++の世界ではとても大事な話なのです。

Commented by とぴあ at 2009/10/02 (Fri) 21:36:21

TrackBack ping me at


の末尾に2020年11月30日時点の日本の首相のファミリーネーム(ローマ字で回答)を繋げて下さい。例えば「noda」なら、「2009-10-01_interface.trackbacknoda」です。これは機械的なトラックバックスパムを防止するための措置です。

Post a comment

writeback message: Ready to post a comment.

2020年11月30日時点の日本の首相のファミリーネーム(ひらがなで回答)

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のコメント

最近のつぶやき