たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。
の動向はもえじら組ブログで。
宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
(原著:David Teller, 2020年8月20日、CC BY-NC 4.0で公開されている内容の全訳。Qiitaにもクロスポストしています。)
要約:Firefoxはかつて、XULとXPCOMに基づく偉大な拡張機能の仕組みを持っていました。この仕組みは長い間私達によく尽くしてくれました。しかし、Firefox開発者と拡張機能開発者の両方にとって、メンテナンスコストは増大し続けるばかりでした。ある面では、増大していくコストは、Firefoxをセキュアにしたり、高速化したり、新しい事を試したりするための努力を、少しずつ破壊していきました。また別の面では、増大していくコストはアドオン開発者のコミュニティを少しずつ破壊していきました。最終的に、古いアドオンの仕組みを守ろうとして数年を過ごした後、Mozillaは、この拡張機能の仕組みを廃止し、それより拡張性は劣るもののメンテナンスしやすいWebExtensions APIに置き換えるという、難しい決断をしました。この選択のおかげで、Firefox開発者は再び、セキュリティや安全性やスピードを改善するために必要な変更を行えるようになったのです。
ここ数日、私はFirefoxのユーザーと会話して、2020年8月のMozillaによるレイオフの結果に関する噂と事実を区別しようとしていました。その過程で何度か持ち出されたのが、Firefox Quantumへの移行におけるXULベースのアドオンの廃止のことでした。私は非常に驚きました。もう何年も前に起こったことについて、この選択に感情を害されたと感じている人が、コミュニティにまだいるということにです。
そして、誰かがredditで指摘していたとおり、私は、何故XULベースのアドオンの廃止以外に選択の余地が無かったかについて、私達が深いところを説明する機会をまだ持っていなかったということに気付きました。
そこで、アドオンとGeckoの内部事情の話に飛び込む準備ができている人向けに、これを機にもう少し詳しい話をしてみようと思います。
Firefox上でいくつかのサブウィンドウを開いていて、それらのウィンドウすべてがワンセットで他のウィンドウより前に出てきていて欲しい場面、というのがある。
例えば、GIMPはツールボックス等が複数のウィンドウにばらけている。これがもし、画像を編集するウィンドウにフォーカスしたらツールボックスのウィンドウがその背後に隠れてしまうという風になっていると、まるで作業にならないだろう(古いバージョンのGIMPをWindowsで使った時にはそんな風になってて頭を抱えた記憶がある)。
また、そういうワンセットで表示されていて欲しいウィンドウ達が、同時に起動している他のアプリケーションのウィンドウの前と後ろにそれぞればらけてしまうというのも、使う時に地味にうざい。
Firefoxの上で、拡張機能が開くウィンドウでGIMPのウィンドウ群のような振る舞いをさせるにはどうすればよいのか。このエントリでは2つの方法を紹介する。
まず1つ目。nsIXULWindowインターフェースのzLevelプロパティを使うと、Firefoxのプロセスが開くウィンドウの重ね合わせの順序をある程度制御する事ができる。一番単純なやり方は、ウィンドウを一時的に最前面表示に切り替えて、その後すぐに元に戻す、という方法だろう。
var XULWindow = window .top .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShellTreeItem) .treeOwner .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIXULWindow); var originalZIndex = XULWindow.zLevel; XULWindow.zLevel = Ci.nsIXULWindow.highestZ; window.setTimeout(function() { XULWindow.zLevel = originalZIndex; }, 0);
ただ、仕様上の制限のため、この方法はWindowsでしか使えない。少なくともUbuntu 10.04のGnomeでは機能しなかった。
代わりに考えられるもう1つのやり方が、フォーカスを使うやり方だ。単純に考えても、window.focus()
でウィンドウにフォーカスを与えると、強制的にそのウィンドウを最前面に持ってくる事ができる。
しかしこの方法にも問題がある。普通にwindow.focus()
すると、例えばそのウィンドウの中の特定のテキストボックスにフォーカスしていたとしても、そのフォーカスが失われてしまう事になる。
この問題を回避するには、Gecko 1.9.2/Firefox 3.6から導入されたフォーカスマネージャを使わないといけない。具体的には以下のようにする。
var FocusManager = Cc['@mozilla.org/focus-manager;1'] .getService(Ci.nsIFocusManager); // 現在フォーカスされている要素を取得する。 // 第1引数:検索する最上位のDOMWindow // 第2引数:再帰的な検索を行うかどうかのフラグ(trueを渡す) // 第3引数:その要素が含まれているフレームのDOMWindowを // 受け取るためのスロットとなるオブジェクト var focusedWindow = {}; var focusedElement = FocusManager.getFocusedElementForWindow(window, true, focusedWindow); // 現在のフォーカスが何によって与えられたかの情報を取得する。 var reason = FocusManager.getLastFocusMethod(focusedWindow.value); // フレームにフォーカスする。 focusedWindow.value.focus(); // フォーカスされていた要素がある時は、その要素にもフォーカスする。 if (focusedElement) { // フォーカスを与える時に、スクロール状態等に変更を加えないように指定する let flags = Ci.nsIFocusManager.FLAG_RAISE | Ci.nsIFocusManager.FLAG_NOSCROLL | Ci.nsIFocusManager.FLAG_NOSWITCHFRAME | reason; FocusManager.setFocus(focusedElement, flags); }
こうすれば、Linuxでも要素のフォーカス状態を失わせずにウィンドウを最前面に持って来れる。ウィンドウのフォーカスを動的に切り替えるため、その都度各ウィンドウが画面上でぺかぺか点滅してしまう(一瞬だけフォーカスされて、その直後にフォーカスが失われるため)、というデメリットはあるが。
js-ctypesはFirefox 3.6から利用できるMozillaの独自の機能で、平たく言うとC言語の実装の中で定義された関数をJavaScriptから呼べるようにするという物。Pythonにctypesという機能があって、それのJavaScript版がjs-ctypes。
Firefox 3.6(Gecko 1.9.2)ではできる事の制限が厳しかったので使えるケースがあんまり無かったようなんだけど、Firefox 4(Gecko 2.0)では構造体がサポートされたので一気に使える場面が増えた。らしい。
システムモニターをFirefox 4に対応させなきゃねと思ってたんだけど、Compartmentがどうとか色々変更があったのを全部調べてたら絶対自分の手に負えん!!と思ったので、いっそのことjs-ctypesで実装すりゃいいんじゃね? と思って、試行錯誤しながらやってみてる。試行錯誤の様子はリポジトリを見るとバレバレです。
js-ctypesのいい所:
困った所:
parseInt()
する、みたいな癖を付けとくとハマらなくていいと思う。JavaScriptでFirefoxをクラッシュさせたかったらjs-ctypesでメモリ破壊とかやると手っ取り早いですよ! と、数え切れないほどFirefoxをクラッシュさせて思いました。
openWindow()
って何ですかアドオンで新しいChromeウィンドウ(特権付きのウィンドウ、XPCOMとか自由に使えるやつ。ブラウザウィンドウ等、Firefoxのアプリケーションとしてのウィンドウはだいたいそう。)を開く方法はいくつかある。
window.openDialog()
を使うopenWindow()
を使う普通にXULドキュメントの中で読み込まれてるスクリプトからやるのなら、1の方法でいい。
でも、JavaScriptコードモジュールやXPCOMコンポーネントのスクリプトのように、グローバルオブジェクトがDOMWindowじゃない場面ではこの方法が使えない。特にFirefoxが起動した直後でまだブラウザウィンドウすら開かれていないという場面では、nsIWindowMediatorのgetMostRecentWindow()
でDOMWindowを取得してそのメソッドを呼ぶ、というようなこともできない。なのでこういう時は2の方法を使わないといけない。
window.openDialog()
の引数は window.open()
と同じだ。
'_blank'
決めうち)'chrome,all,dialog=no'
とか'chrome,all,modal'
とかそういうの)window.openDialog()
の場合はこの後に続いてさらに引数を指定することができて、第4引数以降に渡した物がそのまま、開かれたウィンドウの中でwindow.arguments
として参照できるようになっている。例えば
var w = window.openDialog(url, '_blank', 'chrome,all',
arg0, arg1, arg2);
こう指定して開かれたウィンドウでは
window.arguments[0] // => arg0の値
window.arguments[1] // => arg1の値
window.arguments[2] // => arg2の値
となる。Firefoxのブラウザウィンドウは、コマンドライン引数で渡されたURI等をこうやって受け取っている。
これと同じことをnsIWindowWatcherのopenWindow()
でやろうとして、うまくいかなかった。
openWindow()
の引数は
'_blank'
決めうち)となっていて、最初の引数が加わったことを除けば2~4はwindow.openDialog()
の第1~第3引数と同じ。問題は、開かれるウィンドウに引数を渡す方法が違うということ。window.openDialog()
と同じ感覚で引数を渡しても、期待通りに受け渡されない。
var WW = Cc['@mozilla.org/embedcomp/window-watcher;1']
.getService(Ci.nsIWindowWatcher);
// これはダメ
WW.openWindow(null, url, '_blank', 'chrome,all',
arg1, arg2, arg3);
// これもダメ
WW.openWindow(null, url, '_blank', 'chrome,all',
[arg1, arg2, arg3]);
// これは場合によってはOK
WW.openWindow(null, url, '_blank', 'chrome,all',
arg1);
Function.prototype.call()
では引数を普通に並べてFunction.prototype.apply()
では配列で渡す、という様式を真似てJavaScriptの配列を渡してみても、WindowWatcherは「なんですかコレ」って感じでこっちの意図を読み取ってはくれない。開かれたウィンドウの方でwindow.arguments
を見てみても、長さ1の配列になってて、その要素はなんだかよく分からないnsISupportsのオブジェクトになってる。
引数を1つだけ渡す場合だとうまくいくことがあるというのは、このメソッドの第5引数の型がnsISupportsだからだ。nsISupportsをを実装してるオブジェクトならWindowWatcherはちゃんと受け取ってくれて、開かれた側のウィンドウでもwindow.arguments[0].QueryInterface()
して使うことができる。
でもやりたいことはあくまで、複数の引数をウィンドウに渡して、開かれたウィンドウの側でwindow.arguments
で配列の要素としてそれぞれを受け取ることなのですよ。というか、開かれる側のウィンドウがそういう実装になっているため、どうしてもその様式で引数を渡さないといかんのです。
XPCOMの世界で配列を扱わないといけない場面でたまーにnsISupportsArrayというのが出てくる。これは名前の通り配列のような性質を持つインターフェースで、openWindow()
の第5引数はこのインターフェースを実装してるオブジェクトも受け付けるという風にIDL定義には書いてある。
似たような感じでプリミティブ値に対応するようなXPCOMのインターフェースがいくつかあって、nsISupportsStringとかnsISupportsPRBoolとかnsISupportsPRUInt64とかそういうのがいっぱいある。これらのインスタンスをnsISupportsArrayに格納してopenWindow()
に渡してやれば、どうやら、開かれた方のウィンドウでは対応するJavaScriptのプリミティブ値としてそれらを受け取れるらしい。
が、こんな事真面目にやってらんないですよね。値の型に合わせてインターフェースを使い分けてインスタンスを作って……とか、めんどくさすぎる。
幸い、XPCOMにはnsIVariantという便利な物があって、これのsetFromVariant()
にJavaScriptの値を適当に渡してやると、あとはnsIVariant君がよろしくやってくれるのです。これを使わない手はない。
var JSArray = ['string', true, 29];
var array = Cc['@mozilla.org/supports-array;1']
.createInstance(Ci.nsISupportsArray);
JSArray.forEach(function(aItem) {
var variant = Cc['@mozilla.org/variant;1']
.createInstance(Ci.nsIVariant)
.QueryInterface(Ci.nsIWritableVariant);
variant.setFromVariant(aItem);
array.AppendElement(variant);
});
WW.openWindow(null, url, '_blank', 'chrome,all', array);
これでめでたく、複数の引数をWindowWatcherからも渡せるようになりました、と。
これで一件落着と思ってたんだけど、この方法が使えない場合があることが分かった。nsIVariantはプリミティブ値でもXPCOMのオブジェクトでも何でも受け渡せる万能なヤツなんだけど、nsISupportsArrayと組み合わせてWindowWatcherに渡す時は、XPCOMのインターフェースを持ってないJavaScriptのネイティブのオブジェクトはこの方法では渡せなかった。
var variant = Cc['@mozilla.org/variant;1']
.createInstance(Ci.nsIVariant)
.QueryInterface(Ci.nsIWritableVariant);
variant.setFromVariant({ prop : value });
こうやって値を設定した物をnsISupportsArrayに渡しても、開かれたウィンドウの方ではよく分からんnsISupportsのオブジェクトになってしまって、元のオブジェクトが持ってた情報を取り出せなかった。
回避方法として思いつくのは
JSON.stringify()
して渡して、受け取り側でJSON.parse()
するくらい。JSONにできない物は2の方法でやるしかないと思う。
var bag = Cc['@mozilla.org/hash-property-bag;1']
.createInstance(Ci.nsIWritablePropertyBag);
for (var i in hash)
{
if (hash.hasOwnProperty(i))
bag.setProperty(i, hash[i]);
}
で渡して、受け取った側で
var hash = {};
var enum = window.arguments[0]
.QueryInterface(Ci.nsIPropertyBag)
.enumerator;
while (enum.hasMoreElements())
{
let item = enum.getNext()
.QueryInterface(Ci.nsIProperty);
hash[item.name] = item.value;
}
という風にしてハッシュに戻す。とか。
ウィンドウ間の情報の引き渡しはマジ鬼門。
あと、上に書いた諸々の処理をライブラリとしてまとめておいたので、同じような事をしようとしてる人は使うといいよ。
SCRAPBLOG : JavaScript 製 XPCOM で配列構造・列挙構造のデータをメソッドの戻り値にする
独自に開発したXPCOMコンポーネントに対して配列を渡したり、あるいは戻り値を配列で受け取ったり、ということをやる方法はいくつかある。上記エントリではコメント欄も含めると4つの方法が紹介されていて、そのうちコメント欄にある2つはJavaScriptの配列をそのまま受け渡せるという点で有用だ。特にnsIVariantインターフェースを使うやり方は、戻り値に使う時に余計な引数を定義しなくていいので、実際にそのXPCOMコンポーネントをJavaScriptから使う時にとても使い勝手がいい。
ということでXUL/Migemoでは積極的にnsIVariantを使ってたんだけど、これがMinefield(検証したバージョンは3.7a4pre)で動かなくなってた。
結論から言うと、これはnsIVariantインターフェースのIIDが6c9eb060-8c6a-11d5-90f3-0010a4e73d9aから81e4c2de-acac-4ad6-901a-b5fb1b851a0dに変更されたせいで起こっている問題で、nsIDOMRangeのIIDが変更された時に起こった問題と同様の物だ。
変更が入ったのは昨年9月で、HTML5の新しい仕様に対応するための作業の一環として、何らかの必要があってインターフェースに機能を加えると同時にIIDも変わったらしい。nsIVariantはFROZENなインターフェースじゃないから、nsIDOMRangeの時のようにIIDが元に戻されることは多分あり得ない。よって、考えられる対策は以下のいずれかということになる 。
JavaScriptコードモジュールにするデメリットは、Thunderbird 2で利用できなくなってしまう点と、APIが変わってしまう点。バイナリを分けるデメリットは、リリースの時の作業がめんどくさくなる(XPIファイルが2つになるので)という点。どっちを選んでも大変なのは変わらない……
APIが変わってしまうことは避けたかったので、結局、後者の方で対処することにした。
前から使ってるXPI生成用シェルスクリプトに起動オプションでサフィックスを指定できるようにして、
こんなショボいスクリプトを作って、前出のスクリプトと一緒に
call xpidl.bat xulrunner-sdk-1.9.2
bash makexpi.sh -n xulmigemo -v 0 -s "1.9.2"
call xpidl.bat xulrunner-sdk-central
bash makexpi.sh -n xulmigemo -v 0 -s "central"
てな感じで実行するようにして(make.bat / make.sh)、MozillaのFTPサイトからXULRunner SDKのファイル一式を入手して、
という感じにファイルを配置するようにした。
XPIを作りたい時にはXULRunner SDKが必要になってしまうけど、スクリプトいっこ走らせれば xulmigemo-mozilla-1.9.2.xpi と xulmigemo-mozilla-central.xpi という風に複数のXPIを出力できるようになったので、リリースにかかる手間は少しは軽減された……のかな……
追記。Gomitaさんのコメントを見て、IDLファイルからincludeの行を消して試してみたら、それでちゃんとコンパイルできた。なんでだ……!!!
えーと。ずっと勘違いしてたんだけど、#include "nsISupports.idl"
みたいな行は、interface xmIXMigemoFileAccess : nsISupports
てな感じでインターフェース定義の継承元に別のインターフェースを使う場合にだけ必要で、戻り値や引数に使う分には単に interface nsIVariant;
とだけ書いておけばいいみたいですね……そうすると、コンパイル時には余計なIIDが含まれなくなって、Firefox 3.6まででも3.7以降でも問題なく使えるXPTファイルが作られるみたい。
まとめ。
interface インターフェース名;
と書く。interface インターフェース名;
だけ書く。そんなわけで、xmIXMigemo.idlの頭の所はずいぶんスッキリしました。
#include "nsISupports.idl"
#include "nsIObserver.idl"
interface nsIObserver;
interface nsIFile;
interface nsIVariant;
interface nsIDOMWindow;
interface nsIDOMDocument;
interface nsIDOMRange;
interface nsIDOMElement;
interface nsIDOMNode;
/* Utilities: You can use them for your language without additional implementation. */
[scriptable, uuid(4aca3120-ae38-11de-8a39-0800200c9a66)]
interface xmIXMigemoFileAccess : nsISupports
{
(以下略)
画面の描画を一時停止する方法を先日書いたけど、案の定というかやっぱりというか、重大な弊害があることが分かった。また、その弊害にぶち当たらない安全なやり方も見つけることができた。
安全に画面の描画を一時停止・再開する方法は、以下の通り。
var baseWindow = window.top
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.QueryInterface(Ci.nsIBaseWindow);
baseWindow.setPosition(window.innerWidth, window.innerHeight); // これで画面の描画が止まる
gBrowser.addTab(); // これによって起こる変化は画面上に現れない
gBrowser.addTab(); // この変化も画面上に現れない
gBrowser.addTab(); // 同上
baseWindow.setPosition(0, 0); // ここでやっと描画が再開される
以下、前のエントリに書いたやり方にどういう弊害があるのか、および、このエントリで紹介するやり方の方がどのくらい安全なのかについて詳しく説明する。
ツリー型タブとJetpackが同時にインストールされているとコンテンツ表示領域に何も表示されなくなってしまう、という問題の原因がやっと分かった。そこからさらに調査をして、表題のような「画面の再描画を任意に停止・再開させる」方法が見つかった。
先にやり方だけ書いとくと、こうするとできる。
var rootContentViewer = window.top
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.contentViewer;
rootContentViewer.hide(); // これで画面の描画が止まる
gBrowser.addTab(); // これによって起こる変化は画面上に現れない
gBrowser.addTab(); // この変化も画面上に現れない
gBrowser.addTab(); // 同上
rootContentViewer.show(); // ここでやっと描画が再開される
24日追記。この方法には重大な弊害があることが分かりました。使用を検討している人はより安全な方法を使うようにして下さい。
以下は、これに辿り着いた経緯のお話。
XUL/Migemo 0.12.xで、機能を他のアドオンとかから呼び出すためのAPIを刷新してみたよ。
古いAPI(はてなブックマーク拡張とかが使ってくれてるやつ)は僕自身が色々よく分からないまま作った物だったために、引数を文字列で渡さないといけないとか戻り値も正規表現オブジェクトではなく文字列だとか、色々と使い勝手が悪かったと思う。メインウィンドウ以外では呼び出す時にいちいちXPConnect使わないといかんし。
新しいAPIは、それに比べたらものっそシンプルになった。XPConnect使わなくてもmigemo.getRegExp('hoge')
とか書くだけで使えるし、戻り値もすぐに使える正規表現オブジェクトが返ってくるし。互換性を保つために旧APIはそのまま残してあるので、旧APIを使ってるアドオンが動かなくなるということはないけど、今後は使うなら新APIの方を使うのがお勧めです。
で、このAPIはWebページ内のスクリプトでもCPUの使用率を取得できるようにするAPIを提供する例のアドオンの技術の応用なので、Webページ内のスクリプトからもXUL/Migemoの機能を利用できてしまいます。
ただしスクリプトの実行権限の関係で、上記のmigemo.getRegExp('hoge')
のような、正規表現オブジェクトを直に受け取る機能は使えません。代わりにJavaScript/Migemo互換の、正規表現のソース文字列を返すAPI migemo.query('hoge')
などを使う必要があります。
XUL/Migemo 0.12.2以降が入ってる環境でこのページを表示してれば、以下のデモを試せるはず。
ページ内検索系のメソッドもあるんだけど、多分使いでがなさそうなので解説は用意してないです。
すでに上でもリンクしてるけど、エンジンごとページ内に埋め込めるJavaScript/Migemoという実装もあるから、XUL/Migemoを入れてるFirefoxユーザでないと使えないこのAPIってなんか意味あんの?と言われそう。深くはツッコまないでください。
以下、補足事項。
当初このエントリでは、「Webページ上のスクリプトからもmigemo.getRegExp('hoge')
のようにして正規表現を取得して利用できる」という風な書き方をしてたけど、これは大間違いだった。戻り値の正規表現オブジェクトが生成された実行コンテキストがUniversalXPConnect特権のある場所なのに対して、呼び出し元は特権のない普通のWebページ上であるため、それぞれの権限が違うので本来ならその正規表現の各メソッドは実行できないのが正しい。
ただ、Gecko 1.9.1(Firefox 3.5)以前のバージョンにはバグがあって、上記のようなセキュリティのチェックが働かないために、戻り値の正規表現の各メソッドを呼べてしまう状態になっていた。
このバグはGecko 1.9.2(Firefox 3.6)以降ではすでに修正済みで、実際、Trunk等で戻り値の正規表現オブジェクトのメソッドを呼ぼうとすると、その場で処理が中断されて Error: RegExp.prototype.toString called on incompatible ChromeObjectWrapper というエラーがエラーコンソール上に出力される。
ということで、JavaScript/Migemo互換のAPIとして正規表現オブジェクトのソース文字列だけを返すような機能を0.12.2で新たに加えた。文字列や数値などのプリミティブ値に対してはセキュリティのチェックが行われないようなので、こっちはGecko 1.9.2以降でもWeb上で使える。
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++のレイヤから呼び出したいような時だけだ。普通に開発する分には、こんな事で悩む必要は今や全くない。という事に気がついて、今更になって激しい徒労感を感じている。
var file = Cc['@mozilla.org/file/local;1']
.createInstance(Ci.nsILocalFile);
file.initWithPath('C:\\temp\\target.jpg');
var fileStream = Cc['@mozilla.org/network/file-input-stream;1']
.createInstance(Ci.nsIFileInputStream);
fileStream.init(file, 1, 0, false);
var binaryStream = Cc['@mozilla.org/binaryinputstream;1']
.createInstance(Ci.nsIBinaryInputStream);
binaryStream.setInputStream(fileStream);
var bytes = binaryStream.readBytes(fileStream.available());
binaryStream.close();
fileStream.close();
var base64 = btoa(bytes);
Bug 364586 - nsXmlRpcCLient.js conversion to base64 is slowで「JavaScriptでBase64エンコードするの遅いから組み込みの関数使えやゴラァ」と提出されていたパッチを見て知った。バイナリファイルの内容をバイト列の配列として読んでゴニョゴニョしなくても、単純に、バイナリのインプットストリームからreadBytes()
でバイト列を文字列として取得してbtoa()
にかければOK、だそうだ。試してみたら確かにちゃんといけた。