Home > Latest topics

Latest topics 近況報告

たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。

萌えるふぉくす子さんだば子本制作プロジェクトの動向はもえじら組ブログで。

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

宣伝2。Firefox Hacks Rebooted発売中。本書の1/3を使って、再起動不要なアドオンの作り方のテクニックや非同期処理の効率のいい書き方などを解説しています。既刊のFirefox 3 Hacks拡張機能開発チュートリアルと併せてどうぞ。

Firefox Hacks Rebooted ―Mozillaテクノロジ徹底活用テクニック
浅井 智也 池田 譲治 小山田 昌史 五味渕 大賀 下田 洋志 寺田 真 松澤 太郎
オライリージャパン

Page 10/239: « 6 7 8 9 10 11 12 13 14 »

画面の描画内容を一時的にロックしておいて、裏であれこれして最後にまとめて描画させる方法 - Dec 18, 2009

ツリー型タブと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日追記。この方法には重大な弊害があることが分かりました。使用を検討している人はより安全な方法を使うようにして下さい。

以下は、これに辿り着いた経緯のお話。

続きを表示する ...

拡張機能におけるeval()の5つの間違った使い方 - Nov 28, 2009

以下、Adblock Plus and (a little) more: Five wrong reasons to use eval() in an extensionのいいかげんな訳です。XUL/Migemoのバージョンアップ時のエディタによるレビューのコメントで「今回は公開を承認するけど、次からはeval()はなるべく減らすように。詳しくはこれを読んで。」と指摘されたので、自分が読むために訳してみました。誤訳があったら指摘して。一部のサンプルコードは見やすさのためにインデントを勝手に加えてます。

ちなみに、僕は5番目の点(こういう用途でeval()を使うなという話)については反対の立場です。拡張機能同士を協調して動作させたいなら、むしろeval()を使って関数を書き換えるやり方を使う方が望ましいとすら考えています。なので参考のために、似たような立場と思われるSimon氏・Dorando氏のコメントも訳しています。

2010年2月8日追記。このエントリで述べられている内容に対する反論を公開しました。できればそちらも併せてご覧下さい。


過剰に使われているJavaScriptの機能のひとつに、eval()関数があります。私はそれが非常に多くの拡張機能で利用されているのを見てきましたが、そのうちのごく一部だけが正しい使われ方をしています。ですので、eval()のすべての間違った使い方について説明したいと思います。

1. JSON形式のデータのパースのため

今日において、JSONはデータを保存するためのポピュラーな形式となっています。その最大の特長は、パースが非常に簡単であるということです。単に data = eval(json) という風に書けば、それだけで事足ります。

うまい話には裏があるんじゃないの? その通り。このjsonという変数は {foo: "bar" + alert(Components.classes)} のような内容が含まれるかもしれず、このようなJavaScriptのコードを実行してしまうと、あなたが意図していなかった結果になってしまうでしょう。このように、信頼できない情報源からやってきたデータをJSONとしてパースする用途にはeval()は全く不向きです。それがFirefoxの拡張機能であるなら、どんなWebサーバから送られてくるデータも信頼できません。もしそれがあなたのWebサーバであっても、ハックされて(※訳註:原文ではhacked)いるかもしれませんし、ユーザへの通信経路上で情報が改竄されているかもしれません(特に、暗号化されていない接続では)。あなたは、ユーザを危険な状況に晒したくはないでしょう。

それだけではありません。そのデータが拡張機能自身によって(例えば、ブラウザ終了時の状態を保存するためなどの目的で)書き出されたデータであっても、常に信頼できるとは限りません。その中にはひょっとしたら、Webから受け取ったデータが含まれるかもしれません。もしJSONを書き出す処理にバグがあって、JavaScriptの文字列として書き出すべき物が文字列になっていなければ、それをJSONとして解釈しようとした時、知らない間にJavaScriptのコードとして実行されてしまうでしょう。これが、JSON処理専用の機能を必ず使うようにした方が良い理由です。JSON処理専用の機能は、不正なデータを受け取った時にもJavaScriptのコードとして実行してしまわないので安全です。

2. プロパティ名が動的に変化する時に、そのオブジェクトのプロパティにアクセスするため

obj.fooNNnという変数の値である、というプロパティにアクセスしたいとき、どんなコードを書けばよいでしょうか? これは、あなたがアクセスしなければいけないプロパティの名前が事前には分からなくて、動的に決定されるものであるという場合のことです。拡張機能の中には、これを eval("obj.foo" + n) のようなやり方で解決しているものがあります。この時、その拡張機能はnの値の中に危険な内容が含まれていないかを検証する必要があるでしょう――でも、どうやって?

幸いにも、この質問の答えを考える必要はありません。もっといい方法があります。JavaScriptではすべてのオブジェクトが連想配列である(※訳註:原文ではassociative arrays)ことを思い出してください。言い換えると、obj.fooobj["foo"] は全く同じ意味で、すべてのプロパティは配列の要素としてアクセスできるのです。ですから、前述のような問題を解決するには単に obj["foo" + n] とだけ書けばよく、この操作は、何も他の余計なことをすることなく常にそのプロパティにアクセスするでしょう。

では、メソッド(関数)の場合は? JavaScriptではメソッドも、値が関数オブジェクトであるという違いがあるだけのただのプロパティです。その関数を this が正しい値を示すようにして呼び出すために、Function.call() というメソッドが利用できます:

var method = obj["foo" + n];
method.call(obj, param1, param2);

あるいは簡潔にこう書くこともできます:

obj["foo" + n](param1, param2);

同じアプローチが、グローバル変数やグローバルな関数に対しても使えます。「グローバルオブジェクト」のすべてのプロパティはwindowのプロパティとして参照できます。window.foowindow["foo"] は、グローバル変数fooの値を返すでしょう。(※訳註:JavaScriptコードモジュールなどwindowが使えない変数スコープでも、 (function() { return this; })() とすればその実行時の変数スコープのグローバルオブジェクトを取得できます。)

3. 関数に対して、その関数が処理を終えたあとに何をするべきかを示すため

私が時々見かけるひとつのパターンは、このような関数の呼び出し方です:

foo("window.close()");

その関数は他の場面で、異なるJavaScriptのコードをパラメータとして渡されていました。そして関数が処理を終えた後で、パラメータとして渡された内容を動作の指定として eval() で実行するようになっていました。

どう見ても、ここにはセキュリティ上の問題はありません(※訳註:もちろん、パラメータで渡す内容にWebから取ってきたデータが含まれる可能性がある時は問題外ですよ!)。では、このアプローチの一体どこが間違っているのでしょうか? 実際には、以下のような問題があります:

  • このコードはeval()が呼ばれるまでコンパイルされないでしょう。これは、それ以外の部分のコードについてはスクリプトが読み込まれた時にすぐにJavaScriptインタープリタが文法エラーを報告するのに対して、関数のパラメータとして渡されたコードの文法エラーは後になってからしか報告されないため、そのコードが実行されるような経路を辿る操作をあなたがテストしなかった場合に、問題が見過ごされてしまうだろうということを意味します。
  • もう1つの問題は、そのコードの中で起こったエラーに対して、JavaScriptインタープリタが正しいソースファイルと行番号を報告することができず、どこで問題が起こったのかを知ることができないという点です。このようなエラーのデバッグはとても面倒です。
  • 付け加えると、foo()に対して実行して欲しいコードをパラメータとして渡すというのは普通のやり方ではなく、見苦しい回避方法を色々と必要とします。(※訳註:ダブルクォーテーションをエスケープしないといけない、など。)

幸いにも、クロージャを使うことによってそれらの問題は解決できます(※訳註:この例はクロージャではなく関数リテラルとか関数オブジェクトとかその辺の話だと思うんですが……)。以下は、前述のコードを少し書き換えた例です:

foo(function(error)
{
  alert(error);
  window.close();
});

そしてfoo()という関数の内容は以下のようになるでしょう:

function foo(callback)
{
  ...
  callback("Full success");
}

4. HTMLやXULにインラインで記述されたイベントハンドラを実行するため

以下のようなボタンがあると仮定しましょう:

<button id="button" oncommand="doSomething();"/>

このイベントハンドラを実行するために eval(document.getElementById("button").getAttribute("oncommand")) としてはいけないのは何故でしょうか? その要素が実際にクリックされたなどの場合以外の所でイベントハンドラを実行するための方法として、拡張機能の中ではしばしばこのようなやり方が用いられます。しかしながら実は、commandイベントを生成する方法のほうがもっと簡単で、しかもイベントハンドラがどのように定義されていようとも正常に動作することが期待できます:

document.getElementById("button").doCommand();

doCommand()というメソッドは、すべてのXUL要素で利用可能です。他のイベントに対しては、document.createEvent()を使って本当のイベントオブジェクトを生成する方がよいでしょう――何故なら、イベントハンドラがそれを期待しているでしょうから。例えば:

var event = document.createEvent("MouseEvents");
event.initMouseEvent("click", true, true, window,
                     0, 0, 0, 0, 0,
                     false, false, false, false,
                     0, null);
document.getElementById("button").dispatchEvent(event);

では、あなたが「onfooaction」という風な独自の属性を定義していて、それがいかなる実際のイベントとも関連付けられていない場合には? このような場面でも、eval()を使うのは最良の選択とは言えません。何故なら、eval()を呼び出した関数の実行コンテキストにおいてコードが実行されてしまうからです。もしそのイベントハンドラがfooというグローバル変数を参照したとして、あなたがそのイベントハンドラを呼んでいる関数の中にfooという名前のローカルな変数があったら――そのイベントハンドラは意図しないままにそのローカル変数にアクセスしてしまうでしょう。そしてもちろん、その時イベントハンドラに対してパラメータを渡すこともできません。よりベターな解決策は、そのイベントハンドラのための関数を作る事でしょう:

var handler = new Function("param1", "param2",
                           document.getElementById("button")
                                   .getAttribute("onfooaction"));
handler("foo", "bar");

このシナリオでは、このイベントハンドラは「foo」をparam1という名前の引数として、「bar」をparam2として受け取るでしょう。(これは、よくあるインラインで記述されたイベントハンドラに対してeventというパラメータを渡す時にも使えます。)

5. ブラウザの関数を書き換えるため

以下のようなことをしているコードをよく見かけます:

gBrowser.foo = eval(gBrowser.foo
                            .toString()
                            .replace("foo", "bar"));

このようなやり方でブラウザの関数を書き換えている人は、公の場所でおしりペンペンされることをお勧めします。それは、ブラウザの関数を新しい関数で単に置き換えてしまうだけの拡張機能に比べてほんのちょっとだけマシであるに過ぎません。どちらの場合も、書き換えられようとしているコードが変化しないことを前提にしていますが――しかし、もしそれが起こったら? 最良のケースでは、その拡張機能は大きな損害を与えることなく単に動作しなくなるでしょう。しかしひょっとすると、ブラウザを壊してしまうかもしれません。あるいは、ブラウザのその関数がセキュリティ上の問題の修正のために書き換えられたとしたら、その拡張機能は同じセキュリティ上の問題をまた持ち込んでしまうかもしれません。

言い換えると――この使い方はしてはいけません。ほとんどの場合で、このアイデアはブラウザの関数の動作を変えるのではなく、その関数の前後に追加の処理を挿入するために使われています。幸いなことに、それをするためのもっと危険でないやり方として、ここでもクロージャを使えば元の関数を単純にあなたの関数で包み込むことができます:

var origFoo = gBrowser.foo;
gBrowser.foo = function(param1, param2)
{
  if (param1 == "top secret")
    doSomethingBeforeFoo();
  var result = origFoo.apply(this, arguments);
  if (result == null)
    doSomethingAfterFoo();
  return result;
}

元の関数にすべてのパラメータを渡すためにFunction.apply()を使うことに注意してください。その関数が今現在2つだけパラメータを受け取っているとしても、将来のバージョンのブラウザでは変わるかもしれません。あなたの拡張機能は新しいパラメータに対して何をすればよいのか知っているかもしれませんが、元の関数の動作を壊さないために、それらの新しいパラメータはそのまま元の関数へ引き渡しましょう。

eval()の正しい使い方とは?

私は、eval()関数の有効な使い方はそれほど無いと思っています。拡張機能の中にはユーザに対して、評価可能なJavaScriptのコードを入力させることを許しているものもあります。そのスクリプトが関数のパラメータとして値を受け取る必要があり、関数を作り変数を渡すのにFunction()コンストラクタを使うことが依然として望ましいとしても、これはeval()の妥当な使い方でしょう。

もう1つのeval()の使い方として、状況に応じて定数を宣言するためにも使えます:

if (typeof MY_CONSTANT == "undefined")
  eval("const MY_CONSTANT = 'foo'");

こうすることによって、もし他のスクリプトで同じ名前の定数が定義されていても、あなたは文法エラーを目にしなくて済むでしょう。しかしながら、私はこれはその場しのぎのやり方だと思います。もし同じ名前空間で実行される未知のスクリプトと衝突することを恐れているのなら、あなたは他のスクリプトが使わないような一意な名前を定数(グローバル変数も)に与えるように気をつけるべきです。また、あなた自身のスクリプトについても、定数宣言を含んでいるスクリプトを複数回読み込まないように気をつけてください。

最後に、実行時にそれ自身のコードを生成するためにeval()を大量に使う、分かりにくい・「圧縮」されたスクリプトもあります。Webにおいては「圧縮された」スクリプトに価値があることは認めますが、拡張機能の中で同じ事をやることにはほとんど意味がありません。拡張機能は1度だけしかダウンロードされませんから、ダウンロードにかかる時間をたった2秒だけ節約できても、誰も喜ばないでしょう。また、「圧縮」されたスクリプトはロードされ実行される度に毎回、処理に余計な時間を食うことでしょう。

エントリにつけられたコメント

1. Simonによるコメント

5番目について。関数の中のコードに手を入れる(単にコードの前後に処理を付け加えるだけでは実現不可能で、関数全体を書き換えないといけないような場合)方法として、どんなやり方なら勧められるというのでしょうか? また、ある1つの拡張機能があなたの推奨しているやり方を実行したら、関数の中身を書き換えるタイプの他のすべての拡張機能の動作が妨げられ、それらの拡張機能はすべてのコードを書き直すことを強いられるので、拡張機能同士が円満に共存できなくなるでしょう。

私は元々はあなたが勧めているようなやり方を取っていましたが、しかしeval()を使うことによってしか解決できないような非互換性の問題に何度か遭遇してきました。より良いやり方があるのなら私は喜んでそれを採用するつもりですが、残念ながら、私にはあなたが勧めているやり方が問題を解決してくれるようには見えません……

4番目について。new Function("some code")eval("some code")と比べてどのように安全なのでしょうか? bug 477380であなたはeval()を禁止することを提案していますが、new Function()も禁止しないと無意味なのではないでしょうか。

Wladimir Palantによる返信:

あなたが書いたわけではない関数の中身に対して変更を行う事は、一般的に言ってよいアイデアではありません。あなたがどんな風にそれをやったとしても、必ず酷い結果になるでしょう。他の手段(例えばObject.watch()など)であなたがやりたいことを実現できないのであれば、多分あなたはそれをするべきではないのでしょう。

new Function()について。それはeval()に比べて本質的に安全であるとは言えませんが、それはたいていの場合静的なコードのために使われ、それほど多くの問題を持ち込みません。これは、トラブルを抱え込まないようにするための良いコーディングの習慣ということです。

2. Mookによるコメント

特権が無いWebページの場合でも同じ事が言えるのでしょうか? Gecko 1.9.1はグローバルなJSONオブジェクトを実装するそうですが、evalInSandboxと同等のものはあるのでしょうか?

evalInSandboxを使うためにUniversalXPConnect権限が必要なのは困ります……

Wladimir Palantによる返信:

クロスサイトスクリプティングを防ぐことについて話しているのだと思いますが――スクリプトのためのサンドボックスは十分な対策とは言えません。これについてはbug 341604(※訳註:IEの独自拡張であるiframe要素のsecurity属性の実装についてのバグ)とWHATWGのWeb Appsの仕様のiframe要素のsandbox属性を参照してください。

5. Dorandoによるコメント

5番目について、例えば元の関数 gBrowser.foo について以下の場合を仮定しましょう(似たようなコードはMozillaのコードベースから簡単に見つけられます。例えばtabbrowser.xmlで「.isTrusted」や「permitUnload」を検索してみてください):

gBrowser.foo = function(param1, param2)
{
  if(!gBrowser.isItSaveToDoSomethingBeforeFoo())
    return;

  if (param1 == "do_nothing")
    return;

  var color = "red";
  doSomething(color, param2.length);

  if (param2 == "some_extensions_want_to_prevent_this")
    doSomethingAnnoying("dark"+color);
}
  1. 関数内でdoSomething()が実行された時に、常にdoSomethingAfterFoo()を実行するようにしたい、という時はどうすればいいのでしょうか?
  2. 関数内でdoSomething()が実行されるであろう時に、常にdoSomethingBeforeFoo()を実行するようにしたい、という時はどうすればいいのでしょうか? 元のコードのうちいくらかを複製することはできますが、それは既に修正されたセキュリティの問題をまた持ち込みかねず、関数を単に書き換えるだけにしない事のメリットを打ち消してしまいかねません。関数に対して行われる変更を常に参照し続けることは、いくつかのシチュエーションでは上手く行くでしょうが、私に言わせてもらうとそれは大変すぎます。(そしておそらくコードをいくらか遅くさせるでしょう。)
  3. gBrowser.foodoSomethingAnnoying()を実行するのを食い止めたい時はどうすればいいのでしょうか?
  4. gBrowser.fooが使う色をgreenに変えたい時はどうすればいいのでしょうか? 追加の関数を置き換えてその呼び出し元関数を確認するようにするという手もありますが、私にとってはそれは何かを壊してしまう可能性を増大させるものでしかないです。関数に本来の処理を行わせた後でその変更を後から取り消すというやり方も、可能ではありますが、良いユーザ体験をもたらすものとは思えません。

どちらの場合も、書き換えられようとしているコードが変化しないことを前提にしていますが――しかし、もしそれが起こったら?

maxVersionはそのために存在しています。

あるいは、ブラウザのその関数がセキュリティ上の問題の修正のために書き換えられたとしたら、その拡張機能は同じセキュリティ上の問題をまた持ち込んでしまうかもしれません。

isItSaveToDoSomethingBeforeFooが導入されて、doSomethingBeforeFoo();eval()でパッチを当てられた場合と同様の内容を含むようになったとしたら、(※訳註:ここで推奨されているやり方の方の)関数の置き換えでも同じ問題が起こり得ます。

その一方で、拡張機能の作者はeval()によって、(セキュリティに関するものも含めて)バグがFirefox本体側で修正されるよりも前にパッチを適用することができます。そのバグが本体側で修正された後は、書き換え対象のコードが見つからなくなるので、eval()によるパッチ適用は望ましい形で失敗する(※訳註:何の変化ももたらさず悪影響も及ぼさないということ)でしょう。

あなたが書いたわけではない関数の中身に対して変更を行う事は、一般的に言ってよいアイデアではありません。

もちろんです、それは組み込みのどんな機能の挙動を変えることについても常に言えることです。しかし拡張機能の作者にとっては、やりたいことを実現するための組み込みのインターフェースや関数が存在しない時に、それを実現するための唯一の手段がこれであるという場合もあります。

Simonが既に指摘していることの繰り返しになりますが、関数へパッチを当てるのではなく関数を丸ごと置き換えるという(※訳註:ここで推奨されている)やり方は、私が述べたようなことをやろうとしているすべての拡張機能に対してそれを完全に妨げてしまいます。ですからどうか、あなたが代替になる方法を提案できないのなら、このような手法を推奨しないでください。

あなたがどんな風にそれをやったとしても、必ず酷い結果になるでしょう。

(以下翻訳中)

今まで私がみてきた限りでは、ほとんどの最小の変更がそれを壊しうるという事実にもかかわらず、(APIの変更を含む)他の種類の変更によってコードが動かなくなるケースの方が、eval()によるパッチの適用が原因で動かなくなるケースよりも多く見られました。

Wladimir Palantによる返信:
  1. あなたはgBrowser.foowindow.doSomethingの両方を拡張する必要があります。gBrowser.fooの開始時にフラグ変数をfalseにセットして、window.doSomethingがそれをtrueにする、doSomethingAfterFooはそのフラグ変数がtrueになった時にだけ呼ばれる、という具合です。
  2. 私はあなたが、予知能力を持ったモジュールを必要としているとは思いません――あなたがやりたいのは、gBrowser.foowindow.doSomethingを呼んだ時にdoSomethingBeforeFooを呼ぶ、という事でしょう(window.doSomethingが他の場所から呼ばれた場合を除いて)。このような場合、やはり両方の関数を拡張するべきです。gBrowser.fooの開始時にフラグ変数をtrueにセットして、終了時にそれをfalseにセットする。window.doSomethingの開始時に、そのフラグ変数の値がtrueであるならdoSomethingBeforeFooを実行する。という具合です。
  3. bのやり方と同じ手法でgBrowser.fooを拡張し、doSomethingAnnoyingについてもそのフラグ変数がtrueな時は何もしないように拡張することで、可能でしょう。ただし、あなたが本当にそれをする必要があって、それが何をどのように壊すのかについては、改めてよく考えてください。
  4. bのやり方と同じ手法でgBrowser.fooを拡張し、doSomethingについてもそのフラグ変数がtrueで且つarguments[0]"red"である場合にarguments[0]"green"に置き換えるように拡張すれば、それも可能でしょう。ただしその場合も、やる前にその影響をよく考えてください。

maxVersionの仕組みについては、大抵の拡張機能は「バージョン3.0.*に対して互換性がある」という風に指定されているので、セキュリティの修正やマイナーリリースでの変更をキャッチアップしてくれません。

その一方で、拡張機能の作者はeval()によって、(セキュリティに関するものも含めて)バグがFirefox本体側で修正されるよりも前にパッチを適用することができます。――このような方法でバグにパッチを当てるのは、実に宜しくないアイデアです。Bugzillaにバグを報告して、ちゃんとしたパッチを書いてください。それが確かに明らかにバグで、安全に修正できるのであれば、その修正はマイナーリリースに取り込まれるでしょう。もしあなたが間違ったことをしていれば、フィードバックを得られるでしょう――それは、誰にも何も尋ねずに「パッチ適用」をしてブラウザの機能を壊してしまうよりも良いことです。

6. Dorandoによるコメント

doSomething(あるいは他の関数)は、 Components.classes[""].createInstance().doSomething(); という風な(※訳註:置き換え不能なネイティブのメソッドを呼ぶ)コードかもしれませんし、関数ではなくべた書きされたコードのブロックかもしれませんし、あるいは、その処理がとても頻繁に呼ばれる機能である場合は置き換え後の関数によって度し難いほどの余計な処理時間がかかるかもしれません(特に、複数の拡張機能がそういうことをするのなら)。

maxVersionの仕組みについては、大抵の拡張機能は「バージョン3.0.*に対して互換性がある」という風に指定されているので、セキュリティの修正やマイナーリリースでの変更をキャッチアップしてくれません。

これは「必ず酷い結果になる」という未来予測への反論ですよ。あなたが言っているのは実際には拡張機能の作者の過ちです。また、私に言わせれば、既存のコードを動かなくしてしまうような変更がMozillaのセキュリティアップデートで行われることがあまりに多すぎます(私が知っている最近の例はBug 442333(※訳註:eval()の第2引数の廃止を決定したバグ)です)。

このような方法でバグにパッチを当てるのは、実に宜しくないアイデアです。Bugzillaにバグを報告して、ちゃんとしたパッチを書いてください。

既にBugzillaに報告されていたり、次のメジャーリリースに修正が取り込まれることが確定していたりするなら、同じコードを使えるでしょう。そのバグが外部の(※訳註:拡張機能などの)コードに対してのみ影響を与えるものである場合は特に、(パッチが提出されていても)そのバグが実際に修正されるまでには何ヶ月もかかることがあります。

それが確かに明らかにバグで、安全に修正できるのであれば、その修正はマイナーリリースに取り込まれるでしょう。

その修正が次のマイナーリリースに取り込まれたと仮定しても、それには最低でも1ヶ月はかかります。その修正内容をバックポートすることはそれほど害を及ぼさないでしょう。

~誰にも何も尋ねずに「パッチ適用」をしてブラウザの機能を壊してしまうよりも良いことです。

もしその拡張機能の意図するところが、そのシチュエーションにおいて行われる通常の挙動を変更する事なのであれば、それは初期状態の挙動を壊してしまおうとするもののように見えるでしょう。

7. Simonのコメント

ちなみに、異なる実行コンテキストでコードを動的に実行することは、eval()のもう1つの妥当な使い方です。

eval("code to inject", context);

あるいは

domWindow.eval("code to inject");

これらは、コードを一時ファイルに書き出して、サブスクリプトローダーで読み込ませ、一時ファイルを削除する、という風なやり方によってのみ置き換えることができます。なぜなら、サブスクリプトローダーは依然としてdata: URIの読み込みを許可していないからです。(この回避策は実に面倒です。テンポラリファイルを使うことは他の問題を引き起こしかねませんから。)

Wladimir Palantによる返信:

「異なる実行コンテキスト」というのは、「特権がないコンテキスト」という意味ですか? それは別物です、そのコードはChrome権限を得ることはないでしょう。

8. Simonのコメント

「異なる実行コンテキスト」というのは、「特権がないコンテキスト」という意味ですか?

そうとは限りません。nsIWindowMediatorによって返されたDOMWindowのコンテキストは、Chrome権限のあるオブジェクトと同じになり得ます。(サブスクリプトローダーを使って読み込ませたスクリプトや、XBLのバインディングの中に書かれたスクリプトのようにwindowオブジェクトを汚染することなく動的に使われるスクリプトにおいては。)

最初の影響を見るには、以下のコードをエラーコンソールで実行してみてください:

Components.classes["@mozilla.org/appshell/window-mediator;1"]
          .getService(Components.interfaces.nsIWindowMediator)
          .getMostRecentWindow("navigator:browser")
          .eval("location");

クリップボード監視のJetpack版を作ってみた - Nov 06, 2009

テキストリンクに続いてClipboard ObserverJetpackに移植してみた

テキストリンク同様に、普通のアドオンのClipboard ObserverのコードをJetpackで動くように手直しして、最後にJetpack用のコードを付け加えただけ……という感じ。今回は、テキストリンクでは使わなかったステータスバーへの項目追加のAPIを使ってる。

以下、詰まった点。

  • APIリファレンスが貧弱で、何ができるのかまるで分からない。
  • jetpack.storageの使い方が分からない、というか、Web上に書かれてる情報が古くて役に立たない。APIリファレンスに書いてある書き方を試したら「この方法は古いです、もうすぐサポートしなくなります」なんてメッセージが出る。
    • なので、起動する度に「Observe Clipboard」のチェックが入った状態になる。
  • ステータスバーに追加する内容の幅は、自動的には調整してくれない。ピクセル数できちんと指定してやる必要がある。これ、フォントが違ったら表示崩れるよねえ?

とにかくドキュメント不足が深刻です。

追記。アウトラインリスト生成のやつでstorageの使い方がやっと分かったので、チェック状態を保存するようにした。

テキストリンクのJetpack版を作ってみた - Nov 06, 2009

いわゆる軽量アドオンであるところのJetpackについて今度の日曜のFirefox Developers ConferenceでAza氏とトークショーじゃなかったトークセッションを行うにあたって、「全くJetpackを触らないまま行くのはさすがに失礼すぎるだろ常識的に考えて」と思ったので、テキストリンクをJetpack featureとして移植してみた

以下、詰まった点。

  • Firebug使ってないからデバッグが面倒だった。
  • クリックされた箇所のURIっぽい文字列、のRangeを取得するにあたり、nsIFind(などのXPCOMコンポーネント)の支援を受けられないので、JavaScriptとDOM Rangeだけで強引に解決するのに手間取った。
  • XPCNativeWrapper同士を==で比較すると、ラップされてるノードが同じノードでもfalseになる。===!==で比較すればちゃんと意図通りに判定される。これは盲点だった。(普通にアドオンを作る時だと==で期待通りに判定されるので)今Jetpack 0.6で試したら問題なく動いた。アルェー?
  • APIリファレンスが貧弱で、何ができるのかまるで分からない。

やっつけ移植なので、URLっぽい文字列をダブルクリックしたら新しいタブを開く、という機能しかないです。そのくせ25KBもありやがる。

スクリプトの中身はぶっちゃけ普通のテキストリンクのコードのコピペです。というか、Jetpackで動かないであろう部分を削っていって残ったのがコレなんで。最後の方にちょこっとだけ、JetpackのAPIを使ってページの読み込み完了を監視する処理が入ってて、そこが本題です。

いいな、と思った点。

  • これは有り物の移植だからあんまり参考にならないけど、ゼロからスクラッチする場合を考えると、アドオンの時に比べて事前の準備が要らないので、確かに楽ではあるだろうなあ。
  • about:jetpackからスクリプトの管理を行えるんだけど、削除・再インストール・リフレッシュ(スクリプトを配布元から再取得する)まで行える。開発中は、仮登録→テスト→編集→リフレッシュ→テスト→... というサイクルになる。
    • 一旦アンインストールした物も、about:jetpack内にリストが残る。「消したけど、やっぱり使いたい」という風な心変わりがあっても大丈夫。
  • スクリプトの登録時に、自動更新のためのチェックボックスが表示される。多分自動アップデートできるということなんだろうけど、この機能はまだ試してない。(でも電子署名とか何も無いし、第三者攻撃に対する安全はどうやって確保するんだ?)
  • Firefoxの再起動無しで入れたり外したりできるのは楽でよい。気軽に試せる。

SSTabRestoring/SSTabRestoredイベントが、「ウィンドウ全体の復元」の時の物か「個別のタブの復元(または複製)」の時の物かを判別する - Oct 27, 2009

Firefoxでは、タブがセッション情報も伴って復元された時に、SSTabRestoringSSTabRestoredという2つのイベントが発行される。イベントのoriginalTargetはいずれも復元されたタブの要素で、SSTabRestoringはセッション復元処理が走ったけれどもタブの読み込みは完了していない段階、SSTabRestoredはタブの読み込みが完了した段階で発行される。

これらのイベントが発行される場面は、3つある。

  1. ウィンドウが復元された時(起動時のセッション復元、「最近閉じたウィンドウ」からの復元など)
  2. タブが個別に復元された時(「閉じたタブを元に戻す」など)
  3. タブが複製された時

この3つの場合を、特に1とそれ以外とを判別したい、その方法を考えてみたよ、というのがこのエントリの主題です。

どういう場面で必要か

例えばツリー型タブでは、タブの親子関係が変更された時のインデントの変更やツリーの折りたたみ時にアニメーション効果を適用している。しかしながら、Firefoxのセッション復元時に複数のタブのツリー構造を一気に変更する場合には、いちいちアニメーションしてたら重くてしょうがない。なので、この時だけはアニメーション効果を適用しないようにしたいと僕は思った。

しかしながら、前述のSSTabRestoringSSTabRestoredからは、そのイベントが発行されたのが上記の3つの場面のうちいずれなのかが分からない。どの場面でイベントが発行されたのかを判別するには、他の情報も参照する必要がある。

判別できそうで判別できない理由

実は1の場合については、セッションが復元される時にObserverServiceに登録したオブザーバに対してsessionstore-windows-restoredというメッセージが通知される。なので、これを監視すればいいんじゃないか?と、最初のうちは考えてた。

でも、話はそう単純には済まなかった。実際にイベントを監視してみると、セッション復元時には以下のような順番でイベントが発行されていることが分かった。

  1. 現在のタブのSSTabRestoringイベントが発行される。
  2. sessionstore-windows-restoredが通知される。
  3. 残りのタブのSSTabRestoringイベントが順番に発行される。
  4. それぞれのタブのSSTabRestoredイベントが読み込みの終わった物から発行される。

3と4はこの通りにならないこともあって、あるタブのSSTabRestoringが発行されてすぐに読み込みが完了したタブについては、次のタブのSSTabRestoringが発行される前にSSTabRestoredが発行される場合もある。

一番致命的なのは、複数タブのセッション復元が始まったことは通知されるのに、全部のタブのセッション復元が終わったことは通知されないという点だ。

  • 最初にsessionstore-windows-restoredが通知されたらその後に発行されたSSTabRestoringは全部ウィンドウ単位のセッション復元の一部なんだな、と判断してしまうと、例えば10個のタブを開いたウィンドウのセッションを復元した後、1つタブを開いて、閉じて、開き直した時、そのタブまで「ウィンドウ単位のセッション復元の一部」と誤爆してしまう。
  • SSTabRestoredが発行されるまでの間にSSTabRestoringが発行されたタブ」がウィンドウ単位のセッション復元なんだな、と判断してしまうと、後続のタブのSSTabRestoringよりも前にSSTabRestoredが発行された時に、セッション復元が終わってないのに終わったと見なされてしまう。

という具合で、「ウィンドウ単位でのセッション復元の終わり」がいつなのかが分からないと、誤爆しまくりで全然役に立たない。

また、重ねて困ったことに、sessionstore-windows-restoredの通知はsubjectがnullなので、どのウィンドウでセッション復元が開始されたのかすらオブザーバ側からは分からない。別のウィンドウでセッション復元がはじまった時に来た通知を見て「これからこのウィンドウで開き直されるタブは全部、ウィンドウ単位のセッション復元の一部なんだな」と判断してしまってはいけない。

強引に判別する

dump()をコードの中に埋め込んで調べたところ、nsSessionStore.js内の各処理とイベントは、以下のような順番で起こっているらしいということが分かった。

  1. nsSessionStore::restoreWindow()
    • ウィンドウの復元を開始
  2. nsSessionStore::restoreHistoryPrecursor()
    • 各タブの復元を開始
  3. nsSessionStore::restoreHistory() (選択されたタブ)
    • SSTabRestoringが発火
  4. sessionstore-windows-restoredが通知される
  5. nsSessionStore::restoreHistory() ×タブの個数分(他のタブ)
    • SSTabRestoringが発火×タブの個数分
  6. nsSessionStore::restoreDocument_proxy() ×タブの個数分
    • SSTabRestoredが発火×タブの個数分
    • 最後に復元されたタブのSSTabRestoredが発火したら、すべてのタブの復元が完了

このうちnsSessionStore::restoreHistoryPrecursor()やnsSessionStore::restoreHistory()やnsSessionStore::restoreDocument_proxy()は、タブを1つ復元するだけの時にも使われてる。 よくコードをよく読むと、nsSessionStore::restoreHistoryPrecursor()の中で「復元するタブの数だけ新しくタブを開く」「それらのタブのtab.linkedBrowser.parentNode.__SS_data._tabStillLoadingtrueにセットする」という処理が行われて、その後でsessionstore-windows-restoredがオブザーバに通知されていた。(このtab.linkedBrowser.parentNode.__SS_data._tabStillLoadingは、タブの復元が完了した段階でundefinedになる。)

ということは、sessionstore-windows-restoredが通知された時にウィンドウ内の全部のタブを調べて、tab.linkedBrowser.parentNode._SSdata._tabStillLoadingtrueであるタブが2つ以上ある(復元待ちのタブが複数ある)なら、そのウィンドウでこれからウィンドウ単位のセッション復元が行われようとしている、と考えてよいわけだ。

で、SSTabRestoredが発行される度に復元待ちのタブの数を確認して、復元待ちのタブの数が0になったら、ウィンドウ単位でのセッション復元が終わったと判断できる。

厳密には、「ウィンドウ単位でのセッションの復元中に別途タブを1個だけ復元した」という場合にもそのタブが「ウィンドウ単位のセッション復元の一環で復元された」と見なされてしまうので、この方法にも穴はある。たくさんのタブのツリー構造を一気に変更した時にアニメーションでクソ重くなるのを避けたい、という僕の目的ではこれで必要十分なので、これ以上は追求してない。

まとめ

以上をコンパクトにまとめると、こんな感じになる。

var ObserverService = Cc['@mozilla.org/observer-service;1']
                        .getService(Ci.nsIObserverService);

var observer = {
  restoringWindow : false,

  getRestoringTabsCount : function() {
    return Array.slice(gBrowser.mTabContainer.childNodes)
             .filter(function(aTab) {
               var owner = aTab.linkedBrowser;
               var data = owner.parentNode.__SS_data;
               return data && data._tabStillLoading;
             }).length;
  },

  observe : function(aSubject, aTopic, aData) {
    if (aTopic == 'sessionstore-windows-restored')
      this.restoringWindow = this.getRestoringTabsCount() > 1;
  },

  handleEvent : function(aEvent) {
    switch (aEvent.type) {
      case 'load':
        window.removeEventListener('load', this, false);
        window.addEventListener('unload', this, false);
        gBrowser.addEventListener('SSTabRestoring', this, false);
        gBrowser.addEventListener('SSTabRestored', this, false);
        return;
      case 'unload':
        ObserverService.removebserver(this, 'sessionstore-windows-restored');
        window.removeEventListener('unload', this, false);
        gBrowser.removeEventListener('SSTabRestoring', this, false);
        gBrowser.removeEventListener('SSTabRestored', this, false);
        return;

      case 'SSTabRestoring':
        this.onTabRestoring(aEvent);
        return;
      case 'SSTabRestored':
        this.onTabRestored(aEvent);
        return;
    }
  },

  onTabRestoring : function(aEvent) {
    var tab = aEvent.originalTarget;
    if (this.restoringWindow) {
      // ウィンドウ単位でのセッション復元時の処理
    }
    else {
      // タブが個別に開き直された時の処理
    }
  },

  onTabRestored : function(aEvent) {
    if (this.restoringWindow)
      this.restoringWindow = this.getRestoringTabsCount() > 0;
  }
};

window.addEventListener('load', observer, false);
ObserverService.addObserver(observer, 'sessionstore-windows-restored', false);

セッションストアAPIを使ってタブにいろんな情報を紐付けて、その情報に基づいてあれこれしたい人には、役に立つんじゃないでしょうか。(でもそんな変なことやってる人はほとんどいないんだろうなー)

ツリー型タブでタブバーの表示・非表示をキーボードショートカットで切り替えたい(How to show/hide the tab bar by a keyboard shortcut?) - Oct 26, 2009

Q

I would like to see a shortcut assigned to show/hide the tab bar with the next update. That would be very useful since I reckon, since I have to click every time I want to show/hide it, which every time I want to read some thing on the web, which is way too frequent.

次のアップデートで、タブバーの表示・非表示の切り替えのためのキーボードショートカットを追加して欲しいです。私が思うにそれはきっととても便利でしょう。Webで何かを読もうと思う度に、タブバーの表示・非表示を切り替えるために今は(タブバーを?)その都度クリックしていますが、この操作が頻繁すぎて面倒です。

A

I'm very sorry, currently I have no idea to add new shortcut to show/hide tab bar. Instead, if you press "Ctrl" key for a while, collapsed tab bar will be expanded while you press the key. I hope it helps you.

If you like, you can call internal methods to show/hide tab bar from keyboard shortcuts defined by keyconfig or KeySnail. For example:

// toggle mode shown <=> hidden (shown <=> shrunken)
TreeStyleTabBrowserAutoHide.toggleMode();

// set to "shown"
TreeStyleTabService.setTreePref('tabbar.autoHide.mode',
   TreeStyleTabBrowserAutoHide.prototype.kMODE_DISABLED);

// set to "hidden"
TreeStyleTabService.setTreePref('tabbar.autoHide.mode',
   TreeStyleTabBrowserAutoHide.prototype.kMODE_HIDE);

// set to "shrunken"
TreeStyleTabService.setTreePref('tabbar.autoHide.mode',
   TreeStyleTabBrowserAutoHide.prototype.kMODE_SHRINK);

Note: These examples work on Tree Style Tab 0.8.2009100101 or later.

すみません、今の所タブバーの表示・非表示を切り替えるショートカットを加える予定はないです。その代わり、Ctrlキーを長押しすると、非表示になっていたタブバーがCtrlキーを押している間表示されるという機能があります。これが助けになることを願っています。

お好みで、タブバーの表示・非表示を切り替える内部的な機能をkeyconfigKeySnailで定義されたショートカットから呼ぶこともできます。例は上記の通りです(ツリー型タブ0.8.2009100101以降で動作します)。

ツリー型タブのタブバーの背景を透明にしたい(How to make the tab bar transparent?) - Oct 26, 2009

Q

In Tree Style Tab extension, would it be possible to make the background (dark gray blank area) display an image? Maybe with a userstyle or script...

ツリー型タブで、タブの背景(暗いグレーの空白の領域)に画像を表示できませんか? ユーザースタイルシートかスクリプトを使えばできると思うんですが……

A

On Firefox 3.6, you'll be able to make the background color of the tabbar transparent, like following CSS in the userChrome.css:

tabbrowser[treestyletab-style][treestyletab-mode]
  .tabs-stack,
tabbrowser[treestyletab-style][treestyletab-mode]
  .tabbrowser-tabs,
tabbrowser[treestyletab-style][treestyletab-tabbar-position]
  .tabbrowser-tabs {
  background: transparent !important;
}

On Firefox 4:

:root[treestyletab-style][treestyletab-tabbar-position]
  #appcontent,
:root[treestyletab-style][treestyletab-tabbar-position]
  tabbrowser,
:root[treestyletab-style][treestyletab-tabbar-position]
  .tabbrowser-strip {
  background: transparent !important;
}

Another way, you can get transparent tab bar with a secret preference "extensions.treestyletab.tabbar.style.aero". Go to "about:config" and turn it to "true".

上記のようなCSSをuserChrome.cssに書くと、タブバーの背景色を透明にできます。1つ目はFirefox 3.6用、2つ目はFirefox 4用の指定です。

また別の方法として、隠し設定「extensions.treestyletab.tabbar.style.aero」でもタブバーを透明にできます。「about:config」を開いて、この設定の値を「true」に変更して下さい。

Webアプリケーションからも利用できるAPIを備えたXUL/Migemoをリリースしたよ - Oct 21, 2009

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上で使える。

情報化タブのプログレスバーをいじってみたよ - Oct 08, 2009

Tab Progress BarのプログレスバーがFirefox 3.7のモックアップ風なのを見て「羨ましい!」と思ったのでInformational Tabのデフォルトスタイルをそのように変えてみた。一応、設定で今まで通り(ラベルの下に表示)にも戻せる。

で、やるならとことんやってみっか!と一念発起して、モックアップにあるような光るプログレスバーを再現しようと頑張ってみた。 伸びるバーの部分は背景画像で、ぽわーんと光った感じは-moz-box-shadowを使ってるので、Firefox 3.5じゃないと期待通りには見えない。

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++のレイヤから呼び出したいような時だけだ。普通に開発する分には、こんな事で悩む必要は今や全くない。という事に気がついて、今更になって激しい徒労感を感じている。

Page 10/239: « 6 7 8 9 10 11 12 13 14 »

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のつぶやき

オススメ

Mozilla Firefox ブラウザ無料ダウンロード