JavaScriptでXPCOMを作る2005年06月13日 22時33分

FirefoxUIXULとJavaScriptで構成されていますが、JavaScriptの組み込みオブジェクトだけではファイル操作やウィンドウ操作などができません。そこで登場するのがXPCOM、これを用いることでOSに絡んだ操作やRDFの操作などが行えるようになります。

Firefoxに付随するXPCOMコンポーネントの多くはC++でかかれ、プラットフォーム別にコンパイル作業などが必要なのですが、XPCOMはJavaScriptを用いて作ることもでき、これならOSにあわせてコンパイルする必要はありません。そこでここでは実際にJavaScriptでXPCOMを作ってみることにします。

今回作成するXPCOMコンポーネント(nntPerson)は人物をあらわすもので、年齢を示すageプロパティと年齢を言うsayメソッドを持つものとします。また、以下ではWindowsで作成することを前提として話を進めていきますので、ほかのOSで作成する場合は適宜読み替えてください。

なお、ここではXPCOMコンポーネントの呼び出し方や拡張の作成については深くは触れません。XULチュートリアル(未完の邦訳)のXPCOMの章やFirefoxまとめサイトの「拡張の作成」を参考にしてください。

下準備

Gecko SDKの入手

Geckoに関する開発を進めるためのツール集がGecko SDKです。Mozilla 1.7開発版のFTPサイトからプラットフォームにあわせたGecko SDKを入手できます。ここではWindows用ということでgecko-sdk-i586-pc-msvc.zipをダウンロード。解凍してできたgecko-sdkディレクトリをCドライブ直下にコピーします。

libIDL-0.6.dll、glib-1.2.dllの入手

Gecko SDKのツールを使うためにいくつかライブラリを追加する必要が出てきます。プラットフォームによって異なるかもしれませんが、Windowsの場合はlibIDL-0.6.3-win32-bin.zipglib-19990228.zipを入手。解凍した中にあるlibIDL-0.6.dll、glib-1.2.dllをパスの通ったディレクトリに置きます。私は簡便のためC:\gecko-sdk\binにおきました。

UUIDの生成

拡張やインターフェースを識別するためにUUIDを生成する必要がでてきます。Web上でUUIDを生成してくれるサービスもありますが、私はMicrosoftの提供するPlatform SDKguidgen.exeを使用しました。

作業用ディレクトリの作成

ここではC:\JSPersonを作業用ディレクトリとします。

インターフェース記述ファイルの作成

作成するコンポーネントのインターフェース、すなわちどんなプロパティやメソッドを持つのかはあらかじめFirefoxに教えておかないといけません。そこで登場するのがIDLです。IDLを使用することでコンポーネントのプロパティやメソッド、継承関係などを記述することができます。

作業用ディレクトリにcomponentsディレクトリを作成し、その中に以下の内容でnntIPerson.idlを作ります。

#include "nsISupports.idl"

[scriptable, uuid(5E458528-CE7C-4610-9872-5177DCB32E7C)]
interface nntIPerson : nsISupports
{
    attribute int age;
    AString say();
};

nntIPersonインターフェースはnsISupportsインターフェースを継承し、ageプロパティとsayメソッドを持ちます。nsISupportsを継承しているのは、どんなインターフェースもnsISupportsを継承することになっているからです。

[]内にはインターフェースを識別するためのUUIDを指定します。また、キーワードscriptableをつけることで、作成するコンポーネントがスクリプトからアクセスできるようになります。

プロパティの宣言はattribute 型 プロパティ名とし、読み取り専用プロパティの場合はreadonlyをattributeの前に指定します。

メソッドの宣言はC/C++/Javaのそれとよく似ています。ただし、通常の引数はin 型 引数名の形で宣言します。

文字列型としては、string (ASCII文字のみ)やwstring (UTF-16文字列)ではなく、ACString (ASCII文字のみ)やAString (UTF-16文字列)を使ったほうが効率がいいらしいです。

タイプライブラリの作成

こうして作成したインターフェース記述ファイルですが、そのままでは使えません。Geckoがインターフェースを理解できるようコンパイルする必要があります。コンパイルに用いるのが準備で触れたGecko SDKに含まれるxpidlというツールです。コマンドプロンプトから以下のようにxpidlを実行し、nntIPerson.xptを生成してください。

> cd C:\JSPerson\components
> "C:\gecko-sdk\bin\xpidl" -m typelib -w -I "C:\gecko-sdk\idl" nntIPerson.idl

インターフェースの実装

インターフェースが決まったので実際にその処理の中身をJavaScriptで書いてみます。

componentsフォルダ内にnntPerson.jsを作成しましょう。

function nntPerson() { /* 初期化処理 */ }

nntPerson.prototype = {
    get age()         { return this._age; },
    set age(newValue) { return this._age = newValue; },

    say: function ()
    {
        return "Hello, I'm " + this._age + " years old.";
    },

    QueryInterface: function (iid)
    {
        if (iid.equals(Components.interfaces.nntIPerson) ||
            iid.equals(Components.interfaces.nsISupports))
            return this;

        Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE;
        return null;
    },

    _age: 0
}

プロパティのゲッタ、セッタはそれぞれキーワードgetsetを用いて指定します。

QueryInterfaceはnsISupportsインターフェースのメソッドです。コンポーネントが実装しているインターフェースを全て書くことになります。この場合ですとnntIPersonインターフェースはnsISupportsインターフェースを継承しているので nsISupportsも書きます。

var nntPersonModule = {
   _description: "JS Person Component";
   _CID: Components.ID("{5E458528-CE7C-4610-9872-5177DCB32E7C}"),
   _contractID: "@nanto.asablo.jp/jsperson;1",

   ...

   _factory: {
       createInstance: function (outer, iid)
       {
           if (outer != null)
               throw Components.results.NS_ERROR_NO_AGGREGATION;

           return (new nntPerson()).QueryInterface(iid);
       }
   },

   ...
};

function NSGetModule(compMgr, fileSpec) {
   return nntPersonModule;
}

上に書いたのが実装を登録するためのモジュールです。CIDにはIDLで指定したUUIDコンポーネントを識別する新たなUUIDを、Contract IDにはコンポーネントを識別するための文字列を指定します。Contract IDの書式についてはIBMのXPCOM作成に関する文書をごらんください。基本的には@<ドメイン名>/<モジュール名>;<バージョン>という形になります。

テストケースの作成

次にテストケースを作成します。作業フォルダに戻って、contentフォルダを作成、さらにその下にjspersonフォルダを作成します。その中にcontents.rdfjsperson.xulを作成します。

<?xml version="1.0" encoding="utf-8" ?>
<!-- jsperson.xul -->
<?xml-stylesheet type="text/css" href="chrome://global/skin/" ?>
<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

<script type="application/x-javascript">
<![CDATA[

var person = Components.classes["@nanto.asablo.jp/jsperson;1"]
               .createInstance(Components.interfaces.nntIPerson);

function setAge()
{
  person.age = parseInt(document.getElementById("text-age").value);
}

function sayAge()
{
  alert(person.say());
}

]]>
</script>

<vbox>
  <textbox id="text-age" value="5" />
  <hbox>
    <button label="Set age" oncommand="setAge();" />
    <button label="Say age" oncommand="sayAge();" />
  </hbox>
</vbox>

</page>

パッケージの作成

contentフォルダをZIP圧縮し、ファイル名をjsperson.jarに変更します。作業フォルダ内にchromeフォルダを作り、jsperson.jarをその中に移動します。さらに、作業フォルダ内にinstall.rdfを以下の内容で作成します。パッケージのUUIDは新たに取得します。

<?xml version="1.0" encoding="utf-8" ?>
<rdf:RDF
  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  xmlns:em="http://www.mozilla.org/2004/em-rdf#">

<rdf:Description rdf:about="urn:mozilla:install-manifest">
  <em:id>{34E20009-C0A0-49ad-82F6-5716E674C61B}</em:id>
  <em:name>JSPerson</em:name>
  <em:version>0.0.1</em:version>
  <em:description>JavaScriptによるXPCOM作成のテスト</em:description>
  <em:creator>TOYAMA Nao</em:creator>
  <em:homepageURL>http://nanto.asablo.jp/blog/</em:homepageURL>
  <em:targetApplication>
    <rdf:Description>
      <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
      <em:minVersion>1.0</em:minVersion>
      <em:maxVersion>1.0+</em:maxVersion>
    </rdf:Description>
  </em:targetApplication>
  <em:file>
    <rdf:Description rdf:about="urn:mozilla:extension:file:jsperson.jar">
      <em:package>content/jsperson/</em:package>
    </rdf:Description>
  </em:file>
</rdf:Description>

</rdf:RDF>

最後にchromeフォルダ、componentsフォルダ、install.rdfをZIP圧縮し、ファイル名をjsperson-0.0.1.xpiと変更すればインストールパッケージの完成です。

実行

あとは普通の拡張と同じようにインストールして、アドレスバーにchrome://jsperson/content/と入力してください。年齢を セットするさまが見られます。なお、Firefox 1.0.4およびDeer Park Alpha 1 RCにて動作を確認しました。

参考文献

この記事の作成にあたって以下のサイトを参考にさせていただきました。

seamonkey/xpcom/sample/ - seamonkey Cross Reference
XPCOMのサンプルが見られます。
IDL interface rules:
IDLの書き方について。
Mozilla String(文字列)ガイド
XPCOMで使う文字列型についてのガイドです。英語版のほうが内容が新しいのですが私は日本語版のほうを参考にしました。
xpidl - XPCOM インタフェース情報を生成するためのツール
XPIDLの使い方について。
dW : Components : XPCOM: 第2回: XPCOMコンポーネントの基本
Contract IDの書式について参考にしました。
拡張の作成 - Firefox まとめサイト
拡張のパッケージングについて参考にしました。
Element Reference - XULPlanet
テストケースの作成にあたって参考にしました。

大文字/小文字違いのURL2005年06月16日 15時28分

Firefoxは大文字か小文字かのみが異なるURLを同じものとして扱う。たとえばhttp://www.ne.jp/asahi/nanto/moon/2005/06/16/case.htmlhttp://www.ne.jp/asahi/nanto/moon/2005/06/16/CASE.htmlを続けてみると履歴には最後に見た方しか残らない。

……と思ったがDeer Park Alpha 1 RCで試してみたところちゃんと別物として扱われていた。Bug 99091 (修正済み)だそうな。