XPIDLで自作コンポーネントのインターフェースを定義する時に気をつけないといけないこと宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。2009XUL/MigemoがTrunk(3.7a1pre)で動かなくなっていた件について原因を調べてた。エラーの原因エラーメッセージに見覚えがあるなあと思って検索したら、前に書いたエントリがヒットした。一個だけ躓いた所として、XPCOMコンポーネントの新しいメソッドをIDL定義に追加して引数にnsISelectionController型のオブジェクトを渡すようにしていた所、XPIDLでのコンパイルは通るんだけど実際に使う時にNS_ERROR_XPC_CANT_GET_PARAM_IFACE_INFOというエラーが出てにっちもさっちもいかなくなってしまった。ダメ元で、引数の型をnsISupportsにして受け取り側でQueryInterfaceするようにしてみたところ、ちゃんと動いてくれた。何だったんだろうこれは。ああ、前にも詰まってたところだったか......この時は結局「理由」が分からないままで、とりあえず動くようにはなったからということでそれ以上は調べなかったんだよね。検索してみたら、似たような問題にぶち当たった人がいたようだった。何が問題の原因だったのかが分かった。新しいXPCOMコンポーネントを定義する時に、インターフェースも新しく定義する場合、XPIDLを使ってインターフェースを定義してやらないといけない。encoding);};この時、メソッドの引数や返り値の型として、AStringやlongのようなプリミティブ型(?)だけでなく、nsIFileのように他のインターフェースを使うこともできる。この時気をつけないといけないのが、インターフェースには2つの識別子があるということ。uuid(4aca3120-ae38-11de-8a39-0800200c9a66)]という部分で定義されているインターフェースID(IID)「4aca3120-ae38-11de-8a39-0800200c9a66」だ。今回は、XUL/Migemoのコンポーネントの機能のうち、nsIDOMRangeやnsIDOMWindowを値の型として使っていた部分でエラーが起こっていた。(略)具体的にはこの辺。3.5から3.7a1preまでの間のどこかの時点で、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が動くようになるはず。3.5以下で動かなくなる。無難な解決策は、値の型として使うインターフェースを、古いFirefoxから新しいFirefoxまでの間でずっと変わっていないインターフェースにするということ。nsISupports(すべてのXPCOMの基底インターフェース)ならほぼ確実に使える。(略)このようにインターフェースの定義を変えた上で、実装の方で受け取った値をQueryInterface()してやる。(略)こうすると、メソッドを呼ぶ時に、DOMのRangeオブジェクトをそのまま引数に渡せる状態を維持できる。JavaScriptのレイヤからはIIDではなくヒューマン・リーダブルなインターフェース名だけを使うのが一般的なのだけれども、こうしておけばXPCOMのフレームワークが自動的に「新しいIIDのnsIDOMRangeインターフェース」を参照してくれる。実際にインターフェースで定義されている内容には変更が起こっているかもしれないから、確実な動作は保証できなくなるけれども、当該のインターフェースが「既にある機能はなくなったり変更されたりせず、新しい機能が追加されていくだけ」という傾向があるのなら、おおむね問題なく動作し続けてくれると考えられる。FROZENと書かれていて、本来であれば、IIDが変わることもなければメソッドやプロパティなどのインターフェース定義の内容が変わることもない、安心して永続的に使えるインターフェースだったはず......のようだ。Rangeに提出されたパッチでメソッドが追加されると同時にIIDも変更されてしまった。本当は、これはあってはならない事態らしい。当該バグのコメントでもnsIDOMRangeのIIDは元に戻して、変更はnsIDOMNSRange(Geckoの独自拡張の機能が色々定義されているインターフェース)に対して行うべきと書かれている。近いうちに、nsIDOMRangeのIIDは元の物に戻されて、XUL/Migemoが動かなくなってしまった問題も解消されるものと思われる。ぶっちゃけ、「安心して使えるAPI」なんてのはMozillaの世界じゃリップサービスに過ぎないと思ってる。(とぴあさんには、それはプロジェクトのマネジメントがマズイという別のレイヤの問題だよねと言われた。1.9以降)では、自分で新しくインターフェースを定義してXPCOMコンポーネントを作る必要はほとんど無いと言っていい。JavaScriptでコンポーネントを定義してJavaScriptだけから使うのであれば、JavaScriptコードモジュールを使えばよくなった。起動直後に処理を行うような場合なんかには相変わらずXPCOMコンポーネントの定義が必要だけど、それは既存のインターフェース(nsISupportsやnsIObserver)だけでも事足りる。XPIDLが必要になるのは、JavaScriptで書かれた機能をC++のレイヤから呼び出したいような時だけだ。普通に開発する分には、こんな事で悩む必要は今や全くない。いう事に気がついて、今更になって激しい徒労感を感じている。確かに回避策としては動く可能性があるものではありますが、互換性を確保するためにinterfaceを変更する時にはuuidを変えているので、好ましくは無いと思います。aRange.startContainer;この時点で、startContainerがnsIDOMRangeにある保証が全くないので。title>互換性を確保するためにinterfaceを変更する時にはuuidを変えているすみませんが、ここの意味が分かりません。いうことでしょうか?>この時点で、startContainerがnsIDOMRangeにある保証が全くないので。すみませんが、ここの意味が分かりません。いうことでしょうか?uuidが同じままなら、その開発時のinterfaceに存在した内容にはアクセスできることが保証される、ということです(一応、ほとんど行われませんが、uuid変更無しにinterfaceの末尾にメソッドが追加されている可能性はありますが、この場合は互換性に問題はありません)。逆に、uuidが別物になっているというのはどのような変更があったか全く分からない、ということです。>>この時点で、startContainerがnsIDOMRangeにある保証が全くないので。そんなことを言い出したら複数のバージョンのFirefoxに対応するアドオンなんて一個も作れません......それは違うでしょう。複数のバージョンに対応したXPCOMコンポーネントが(シンプルには)作れないだけだと思います(そして多分、本来やるべきじゃないと思います)。uuidが変わっている、というのはそれぐらい何があったか全く分からない状況ので、例のようなアクセスは不適当だと思います。try-catchでどうにかするなり、メソッド等の存在確認をするのが適当ではないかと。ちなみに、branch上では通常のインターフェースのuuidは一切変更されません。(一応、ほとんど行われませんが、uuid変更無しにinterfaceの末尾にメソッドが追加されている可能性はありますが、この場合は互換性に問題はありません)。いや、それは、新しい、メソッドの追加されたインターフェースを期待しているのに、古いインターフェースも使えてしまうのでやはり問題はあるでしょう。呼べばいい」ということではないかと。それぞれ呼んで、結果が帰ってきた方を使えばいいのではないかと思います。(一応、ほとんど行われませんが、uuid変更無しにinterfaceの末尾にメソッドが追加されている可能性はありますが、この場合は互換性に問題はありません)。いや、それは、新しい、メソッドの追加されたインターフェースを期待しているのに、古いインターフェースも使えてしまうのでやはり問題はあるでしょう。それは確かにそうですね、ただ、Mozillaとしては古いバージョンとの互換性は開発者が知っているべき、と考えているのかもしれません。今は互換のあるバージョンも示さなくてはいけませんし。推奨されない方法ですが、一応容認はされている、という話です。ちなみに、普通は末尾に追加するのみの場合でもuuidを変更します。変えているので、ああ、そういえば、nsIDOM*ですね、これ。title本文に追記しました。>この時点で、startContainerがnsIDOMRangeにある保証が全くないので。そこだけ厳密にして『互換性が保証される』なんて、詭弁もいいとこじゃね?」と思ったというのがその理由だったのだと思います。変更しなくてはいけません。既存のインターフェースを期待した呼び出し側と、新しいインターフェースを持った呼び出された側の間で齟齬が発生してしまいます。末尾に関数を追加する場合は、普通は既存のコードとの互換性は保たれているので、まあ気にしない、という考え方は一応あります。その場合も古いAPIに対して新しい関数を呼ぼうとしたら(そしてそれは、バージョン判定など別の手法を使わない限りは普通にできてしまいます)同様に齟齬が発生します。どちらも場合によってはクラッシュすることになるので、インターフェースが変わったらuuidを変える、というのは、C++の世界ではとても大事な話なのです。atの末尾に2018年6月21日時点の日本の首相のファミリーネーム(ローマ字で回答)を繋げて下さい。「noda」なら、「2009-10-01_interface.trackbacknoda」です。これは機械的なトラックバックスパムを防止するための措置です。