Firefox Developers Conference Summer 20072007年06月19日 20時50分

Firefox Developers Conference Summer 2007 の「拡張機能作者による大ライトニングトーク」に発表者として参加させていただきました。以下が発表資料です。中身は JavaScript による XPCOM コンポーネント作成方法のダイジェストということで、あまり「拡張機能作者」とは関係ないものになってしまいました。また、補足として、XPCOM コンポーネントの置換を行う例を書きました。

プレゼンテーション機能は Firefox 2 以上でないと使えません (現在のTrunkでもエラーになる?)。その他のブラウザではデフォルトスタイルシートによる表示となります。また、Atom 版でプレゼンテーション機能を利用するには、Enhanced Feed Preview などを導入する必要があります。なお、プレゼンテーション機能は、解像度 1024 × 768、フルスクリーンでの利用を前提としているので、それ以外の環境では適宜文字サイズを拡大縮小してください。

皆さん興味深い話題が目白押しでしたが、それはほかの方のレポートに譲るとして、以下個人的に Gomita さんへしゃべったことを載せておきます。

  • UCJS Loader に、Overlay XUL を読み込む機能がないのは、私自身がその機能を使っていないから。
  • userChrome.js 0.8 への移行手段として示された、borwser.xul 以外なら例外を投げるという手段は、UCJS Loader のアプローチでは対応できない。
  • mozIJSSubScriptLoader や JavaScript コンポーネントが UTF-8 として読み込まれない (Latin-1 として読み込まれる?) のは困るが、まだ Bugzilla に登録などはしていない。(mozIJSSubScriptLoaderに関してはバグがたっていました。)
  • (userChrome.js 用スクリプトも、Greasemonkey 用スクリプトのように、メタ情報を解釈してほしいという要望が挙がっていることに関して) そういう機能を userChrome.js 本体に取り込んでしまうと、userChrome.js の利点である「軽さ」が犠牲になってしまうのではないか。

XPCOM コンポーネントの置換2007年06月19日 20時51分

Firefox Developers Conference Summer 2007 での発表の補足も兼ねて、実際に既存の XPCOM コンポーネントを自作のものに置き換える手順を説明していきたいと思います。ちょうど Mozilla でプログラミング (XUL) スレッドで about:blank を置き換える話が出ていたので、それを題材にとってみましょう。

Firefox で about:foo を表示するとき、内部的にはコントラクト ID が "@mozilla.org/network/protocol/about;1?what=foo" である XPCOM コンポーネントが作成されます。このコンポーネントは nsIAboutModule インターフェースを実装している必要があり、その newChannel メソッドで得られたチャンネルの内容がブラウザに表示されます。チャンネルというのはデータストリームの源であり、ある URI のページを Firefox で表示するときは、URI からチャンネルを作成、そのチャンネルからストリームを取得、ストリームの内容を解析して表示、という流れになります。

それでは早速コンポーネントの実装に入りましょう。実装するインターフェースは nsIAboutModule だけでいいので、新しくインターフェースを定義する (IDL を書く) 必要はありません。

function MyAboutBlank() {
}
MyAboutBlank.prototype = {
  newChannel:
  function MAB_newChannel(aURI) {
    var content =
      <html xmlns="http://www.w3.org/1999/xhtml">
        <head>
          <title>My About Blank</title>
        </head>
        <body>
          <p>Hello, about:blank world!</p>
        </body>
      </html>;
    var uriSpec = "data:application/xhtml+xml;charset=utf-8," +
                  encodeURI(content.toXMLString());
    var ios = Cc["@mozilla.org/network/io-service;1"].
              getService(Ci.nsIIOService);
    var channel = ios.newChannel(uriSpec, null, null);
    channel.originalURI = aURI;
    return channel;
  },

  getURIFlags:
  function MAB_getURIFlags(aURI) {
    return Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT;
  },

  QueryInterface:
  function MAB_QueryInterface(aIID) {
    if (aIID.equals(Ci.nsIAboutModule) ||
        aIID.equals(Ci.nsISupports))
      return this;
    throw Cr.NS_ERROR_NO_INTERFACE;
  }
};

newChannel メソッドの役割は上で述べたとおりです。ここでは data URI からチャネルを作成していますが、http URI や chrome URI からチャンネルを作成することもできます。

getURIFlags メソッドでは、その about URI に適用されるフラグを返します。URI_SAFE_FOR_UNTRUSTED_CONTENT を返すことで、一般の Web ページからの読み込み (たとえば、<iframe src="about:blank"> といった使い方) が可能になります。

コンポーネントの登録は通常と代わりありません。クラス ID には新しく生成した UUID を指定します。ただし、コントラクト ID は既存のものを指定します。こうすることで、既存のコントラクト ID に関連付けられたコンポーネントを、自作のものに置換することができます。

const MAB_CLASSID = Components.ID("{80b51603-d8b8-4bcf-aa0b-644f6a196ffe}");
const MAB_CLASSNAME = "My About Blank";
const MAB_CONTRACTID = "@mozilla.org/network/protocol/about;1?what=blank";

var MyAboutBlankFactory = {
  createInstance:
  function MABF_createInstance(aOuter, aIID) {
    if (aOuter)
      throw Cr.NS_ERROR_NO_AGGREGATION;
    return new MyAboutBlank().QueryInterface(aIID);
  }
};

var MyAboutBlankModule = {
  registerSelf:
  function MABM_registerSelf(aCompMgr, aFile, aLocation, aType) {
    var cr = aCompMgr.QueryInterface(Ci.nsIComponentRegistrar);
    cr.registerFactoryLocation(MAB_CLASSID, MAB_CLASSNAME, MAB_CONTRACTID,
                               aFile, aLocation, aType);
  },
  
  getClassObject:
  function MABM_getClassObject(aCompMgr, aCID, aIID) {
    if (!aIID.equals(Ci.nsIFactory))
      throw Cr.NS_ERROR_NOT_IMPLEMENTED;
    if (!aCID.equals(MAB_CLASSID))
      throw Cr.NS_ERROR_NO_INTERFACE;
    return MyAboutBlankFactory;
  },

  ...
};

以上を my-about-blank-0.0.1.xpi としてパッケージ化しました。導入することで about:blank が空白ページではなく、「Hello, about:blank world!」というメッセージを表示するものになります。ただし、このままではエラーコンソールでの評価がおかしくなるといった副作用もあります。

なお、Enhanced Feed Preview の場合は、なぜか起動のたびに関連付けが元に戻されてしまうので、browser.xul へのオーバーレイ内で以下のような処理をし、毎回登録を行っています。

var gEnhancedFeedPreview = {
  EXTENSION_ID: "{347b1bed-0fe7-4cdc-921a-51c49e690864}",
  FC_CLASSID: Components.ID("{81adca60-0b9a-405b-a514-6f2cf548fc4b}"),
  FC_CLASSNAME: "Enhanced Feed Stream Converter",
  FC_CONTRACTID: "@mozilla.org/streamconv;1" +
                 "?from=application/vnd.mozilla.maybe.feed&to=*/*",

  init: function EFP_init() {
    ...

    // XXX On trunk, we have to register the component at every statup
    if (!Cc[this.FC_CONTRACTID].equals(this.FC_CLASSID))
      this.registerFeedConverter();
  },

  ...

  registerFeedConverter: function EFP_registerFeedConverter() {
    var cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
    var em = Cc["@mozilla.org/extensions/manager;1"].
             getService(Ci.nsIExtensionManager);
    var file = em.getInstallLocation(this.EXTENSION_ID)
                 .getItemFile(this.EXTENSION_ID, "components/FeedConverter.js");

    cr.registerFactoryLocation(this.FC_CLASSID, this.FC_CLASSNAME,
                               this.FC_CONTRACTID, file,
                               "abs:" + file.path, "text/javascript");
  }
};

gEnhancedFeedPreview.init();