XPCOM
XPCOM は、Microsoft COM に似た、クロスプラットフォームのコンポーネントオブジェクトモデルです。
これは、XPCOM のページからの引用です。
Firefox は、2 つの階層から構成されていることが分かります。2 つの内の大きな方は、大部分が C++ で書かれ、コンパイルされたプラットフォームです。その上に、大部分が XML や JavaScript、CSS で書かれた chrome が横たわっています。事実、2 つに分けることができます。私たちは度々、他の "Mozilla ベースのアプリケーション" に言及します。これらは、簡単に言えば、基礎をなすプラットフォームにいくつかの変更と機能追加をして独自の chrome の階層部分を書いたアプリケーションです。この下層部分は XULRunner と呼ばれています。これは、とても強力なプラットフォームであり、ウェブ利用可能でクロスプラットフォームなアプリケーションのための頑丈な開発ベースを提供します。OS に依存しないアプリケーションを簡単に作成できることは、XULRunner の大きなセールスポイントです。
XPCOM は、2 つの階層 (XULRunner と chrome) を橋渡しする方法です。下層のほとんどのオブジェクトや関数は、chrome に隠されています。これらは、XPCOM コンポーネントとインタフェースを通して露出される必要があります。XPCOM を Firefox の下層で利用可能なすべての権限への参照として考えてください。
XPCOM コンポーネントを使用することは、前回のセクションの例のように、比較的簡単です。
this.obsService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
Cc オブジェクト (Components.classes) は、XPCOM と通して利用可能な静的オブジェクトと class 定義のインデックスです。角括弧内の文字列はただの識別子です。この場合は Observer サービスに相当します。コードの例やドキュメントを読めば、どの文字列を使用するか分かってくるでしょう。これらの総合的なリストは (私たちが知る限り) ありません。もしあれば、アドオンによって拡張することもできるため、このリストはとても長くなります。現在インストールされた Firefox のこのリストを表示したいときは、エラーコンソールで次のコードを実行してください:
var str = ""; for (var i in Components.classes) { str += i + "\n" }; str
Firefox 3.6.2 にいくつかの拡張機能をインストールした環境では、876 行の文字列が出力されました。とても多いですね。幸運なことに、拡張機能開発のために知っておく必要のあるものは一握りだけです。@mozilla.org/ プレフィックスは、名前空間に保持しておくために付いています。私たちが独自のコンポーネントを作成する時は、@xulschool.com/ のような名前を使います。
コンポーネントは、JavaScript で扱っているオブジェクトと同じような、サービス (静的オブジェクト) とクラスのインスタンスのどちらかです。Cc["some-string"] で呼び出せるメソッドは、問い合わせるものによって、getService と createInstance のどちらかになります。多くの場合、どちらを呼び出すかがはっきりと区別できます。そうでない場合は、これらのドキュメントを参照してください。これら 2 つのメソッドは、常にインタフェース識別子を引数として受け取ります。
Cc と似た Ci (Components.interfaces) は、利用可能なインタフェースのインデックスです。上記のコードスニペットを少し変更すれば、利用可能なインタフェースの長いリストを得られます。コンポーネント識別子と同じように、nsI はインタフェースを整理するためのものです。"NS" は、Mozilla の前のアプリケーションである Netscape を意味し、"I" は、インタフェースを意味しています。私たちのインタフェース名は、xsIHello のようなプレフィックスで始めます。
インタフェースは、オブジェクトに実装されている属性とメソッドのセットの定義でしかありません。XPCOM コンポーネントは複数のインタフェースを実装できます (多くの場合、複数実装されています)。この例として、Preference サービスを見てみましょう。このドキュメントを、XUL Planet と呼ばれるとても古い XUL のサイトで見ていきます。このサイトのすべてのドキュメントは MDC へ移行されましたが、完了する前に XUL Planet が閉鎖されてしまったようです。これらの XPCOM ドキュメントはコンポーネントとインタフェースの関係を見るには良いドキュメントなので、私たちはこれを利用します。
もう一つの役立つリファレンスは、この XPCOM リファレンスです。これはソースコードから生成されており、自動的に更新されています。このリファレンスは、コンポーネントとインタフェースの関係を表示しますが、これは、ドキュメント参照というよりもソースブラウザと言った方がよいでしょう。
タイムマシンに乗り込んで、Preferences サービスコンポーネントのページを見ましょう。右側の上部には、実装されているインタフェースのリストがあり、各ドキュメントのページへリンクされています。その次に、このオブジェクトのすべてのメンバのリストと説明があります。この部分は特に重要です。コンポーネントのすべてのメンバと、メンバが定義されているインタフェースが書かれています。getBranch メソッドのリンクをクリックすると、nsIPrefService のドキュメントのページへ移動します。ここには、インタフェースとメソッドについての詳細があります。また、このインタフェースを実装しているコンポーネントのリストもあります。これらのドキュメントはすべて、Firefox のソースファイルから生成されているため、生成された当時のドキュメントとして完成されています。しかし、残念なことに XUL Planet はもう存在しません。
インタフェースの扱いは不便なことがあります。メソッドの呼び出しやコンポーネントのインタフェース X の属性を使用したいときは、最初にコンポーネントをインタフェース X に "キャスト" する (割り当てる) 必要があります。これは、すべての XPCOM コンポーネントに含まれている QueryInterface メソッドを通して行います。
this._prefService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); this._prefValue = this._prefService.getBoolPref("somePreferenceName"); this._prefService.QueryInterface(Ci.nsIPrefBranch2); this._prefService.addObserver("somePreferenceName", this, false); this._prefService.QueryInterface(Ci.nsIPrefBranch);
これは、環境設定に関するコンポーネントや JSM を初期化する一般的なコード片です。コードの 4 行目で行っているように、環境設定の値を取得、設定するために Preferences サービスを利用します。これらのメソッドは nsIPrefBranch インタフェース内にあります。getService メソッドと createInstance メソッドは、インタフェースにすでにセットされているコンポーネントを取得します。多くの場合、使用する必要のあるインタフェースは一つだけであり、QueryInterface について心配する必要はありません。しかし、この例の場合は、環境設定のオブザーバを追加するメソッドが含まれた nsIPrefBranch2 へインタフェースを変更する必要があります。オブザーバを追加した後は、環境設定の値の取得と設定だけが必要なため、インタフェースをこれらのメソッドが含まれた nsIPrefBranch に戻しています。
引数を渡す
XPCOM メソッドへ引数を渡すことは、いくつかの例外を除き、他の JavaScript オブジェクトの場合と違いはありません。一般的には、JavaScript による値の型変換に頼ることができますが、最初の場所で正しい型を渡すことが最善です。このセクションは、XPCOM ドキュメントの読み方についてのクイックガイドです。ここでは、XPCOM インタフェースで使用される言語の XPIDL の構文が理解できるようにします。
MDC では、次のように書かれています:
void setCharPref(in string aPrefName, in string aValue);
ここで注目すべき最も重要な点の一つは、両方の引数に in キーワードが使用されていることです。このキーワードは、これらの引数が入力パラメータであり、値はメソッドがその動作のために使用することを指定します。どのような場合に in でない引数になるでしょうか? 一部のメソッドでは、実際の値を返す引数のために out キーワードが使用されます。これは、IDL 内で、戻り値として正しくない型指定された配列などの特定の型に対して使用されます。
void getChildList(in string aStartingAt, out unsigned long aCount,[array, size_is(aCount), retval] out string aChildArray);
このメソッドは文字列の配列を返します。1 番目の引数は、メソッドに文字列を見始める場所を教える入力パラメータです。2 番目の引数は、返された配列の長さを保持し、3 番目の引数は、その配列自身を保持します。各括弧内にメタデータが含まれていることに注意してください。これは、引数が配列であり、そのサイズが aCount パラメータによって決定されていることを示しています。次の例は、このメソッドの使用例の一つです:
let childArrayObj = new Object(); let childArray; this._prefService.getChildList("", {}, childArrayObj); // .value は実際の配列を保持しています。 childArray = childArrayObj.value;
一般的な out の引数の使い方は、引数に空のオブジェクトを渡し、メソッドを呼び出した後にこのオブジェクトの value 属性へアクセスすることによって値を取得します。value 属性はメソッドがセットします。また、JavaScript 配列には、その長さを取得する length 属性があるため、2 番目の引数を使用する必要はありません。そのため、使用されない空のオブジェクトを渡しています。2 番目の引数は、高レベルの配列の代わりにポインタを使用する C++ コードから呼び出される場合に必要になります。
一部のよく使用される XPCOM メソッドは、引数として他の XPCOM の型を必要とします。例えば、nsIPrefBranch2 の addObserver メソッドがあります。
void addObserver(in string aDomain, in nsIObserver aObserver, in boolean aHoldWeak);
幸運なことに、JavaScript オブジェクトを preference オブザーバとして登録したいときは、何も特別なことをする必要はありません。nsIObserver には単独の observe メソッドがあるため、必要なことは、あなたのオブジェクト内で observe メソッドを記述することだけです。
XULSchool.PrefObserver = { init: function() { this._prefService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch2); // nsIOvserver を実装するかのように 'this' を渡します this._prefService.addObserver( "extensions.xulschoolhello.somePref", this, false); }, observe : function(aSubject, aTopic, aData) { // オブザーブ処理 } };
最後に、次の表は、最もよく遭遇する XPCOM インタフェースの型の要約とその扱い方の表です:
JS の型 | IDL の型 | 補足 |
---|---|---|
Strings | AUTF8String, string, wstring, char*, others | 歴史的に、XPCOM にはいくつかの文字列型があります。現在最も使用されているのは AUTF8String です。これらの文字列型の詳細については、XPCOM 文字列ガイドをお読みください。 |
Integers | short, unsigned short, long, unsigned long, PRInt32, PRUInt32 | PRInt32 は、long と等価です。ほとんどの PR* 型は等価の型を簡単に読めるため、これらを使うことが望ましいです。 |
Floating point | float | |
Boolean | boolean, PRBool | |
Void | void | |
Timestamps | PRTime | この型は、Javascript Date オブジェクトの getTime() メソッドの出力のような、ミリ秒単位のタイムスタンプを渡すために使用されます。 |
XPIDL についての詳細は、XPDIL 構文の定義を参照してください。
独自のコンポーネントの作成
JavaScript で書かれた XPCOM コンポーネント
以前にも言ったように、できるだけ JSM を使用することをお勧めします。しかし、特定の機能を追加するために XPCOM コンポーネントを作成するしかない場合もあります。このような場合は、C++ で書かれコンパイルされた XPCOM コンポーネントと JavaScript XPCOM コンポーネントのどちらかを選べます。作成や管理がそれほど複雑でない後者を選ぶとよいでしょう。
多くの場合、JavaScript XPCOM コンポーネントには、IDL インタフェースファイルと機能を実装する JavaScript ファイルの 2 個のソースファイルが必要です。最終的な拡張機能の XPI には、JavaScript の実装ファイルと IDL ファイルをコンパイルした XPT ファイルを含める必要があります。コンポーネントが既存の Firefox のインタフェースだけを利用する場合は、IDL ファイルや XPT ファイルは必要ありません。このような場合は、JSM と XPCOMUtils モジュールを使用して簡単にコンポーネントを実装できることが分かるでしょう。
XPCOM を含めたバージョンの Hello World プロジェクトをダウンロードして、XPCOM ファイルがどのように構成され、ビルドされるかを確認してください。(おそらく、あなたのビルドは完了しないでしょう。このことは後で取り上げます。)
components ディレクトリの xsIHelloCounter.idl ファイルは次の内容です:
#include "nsISupports.idl" /** * Counter for the Hello World extension. Keeps track of how many times the * hello world message has been shown. */ [scriptable, uuid(BD46F689-6C1D-47D0-BC07-BB52B546B8B5)] interface xsIHelloCounter : nsISupports { /* The maximum allowed count. */ const short MAX_COUNT = 100; /* The current count. */ readonly attribute short count; /** * Increments the display count and returns the new count. * @return the incremented count. */ short increment(); };
nsISupports についての部分は、ほとんどの XPCOM インタフェース定義で共通です。nsISupports は、すべてのインタフェースの基礎となるインタフェースです。そのため、他のインタフェースを拡張する場合を除き、常に含まれていなければなりません。他のインタフェースを拡張する場合は、nsISupports の部分をそのインタフェースに置き換えるだけです。また、複数のインタフェースをカンマ区切りで記述して、それらのインタフェースから拡張することもできます。
[scriptable, uuid(BD46F689-6C1D-47D0-BC07-BB52B546B8B5)]
scriptable 限定子は、このコンポーネントが JavaScript コードからアクセスされることを宣言します。これはまた、Firefox のインタフェースの一部に見られるメソッドの基底ごとに指定されます。しかし、独自のコンポーネントでは行わなくてもよいでしょう。2 番目の部分は、インタフェースの UUID を定義します。各インタフェースに一つ、新しいものを生成しなければなりません。また、インタフェースが変更されるごとに毎回 UUID も変更すべきです。この部分は、拡張機能の ID に使用されるようなメールアドレス形式では動作しないため、UUID 形式で記述しなければなりません。
IDL ファイルには定数と属性、メソッドの 3 つの例を含めましたが、これは、簡単なカウンタにしておくには明らかに作りこみ過ぎです。
IDL ファイルでは、数値と真偽値の定数が定義できますが、文字列の定数は定義できません。これは、XPIDL の既知の制限です。簡単な回避策は、代わりに readonly 属性 を定義することです。それでも、実装ファイルでは getter を定義しなければなりません。コンポーネントの参照を通じて、またはインタフェースから直接、定数へアクセスできます:
// これらは等価です max = Ci.xsIHelloCounter.MAX_COUNT; max = counterReference.MAX_COUNT;
実装ファイルの xsHelloCounter.js はとても長いので、部分ごとに解説していきます。
const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const Ce = Components.Exception;
このコードは分かりますね。ここでは、さらに Components.results と Components.Exception の 2 行が追加されています。これらは、さらに後で使用されます。
const CLASS_ID = Components.ID("{37ED5D2A-E223-4386-9854-B64FD38932BF}"); const CLASS_NAME = "Hello World Counter"; const CONTRACT_ID = "@xulschool.com/counter;1";
これらの定数は、下のコンポーネント登録コード内で使用されます。これらは、ユニーク UUID (IDL の UUID とは異なるものを生成しなければなりません)、このコンポーネントを表す名前 (この名前はどこにも使用されないようです)、コンポーネントへの参照を得るために使用される文字列の contract ID など、コンポーネントの詳細を定義しています。contract ID 文字列の末尾の ";1" は、コンポーネントのバージョンを示すためのものですが、あまり変更すべきではありません。これは、複数の互換性のないバージョンのコンポーネントが同時にインストールされている場合に役立ちます。
実装オブジェクトそのものは、簡単に理解できるでしょう。ここで注意すべきことは、これらのメソッドや属性の名前を IDL で定義したものと一致させなければならないことと、QueryInterface メソッドを実装しなければならないことだけです:
QueryInterface : function(aIID) { if (!aIID.equals(Ci.xsIHelloCounter) && !aIID.equals(Ci.nsISupports)) { throw Cr.NS_ERROR_NO_INTERFACE; } return this; }
メソッドはとても簡単です。呼び出し元がサポートされたインタフェースを要求しているか検証し、そうでなければ例外を投げます。
コードの残りは、長く複雑に見えますが、すべてのコンポーネントで同じ内容であるため心配することはありません。他のコンポーネントからコピーしていくつかの名前を変更するだけです。これらのコードの目的は、コンポーネントを登録し、他の Firefox のコンポーネントのように参照を得られるようにすることです。ここは、下から上へ向かって読むのがよいでしょう。
function NSGetModule(aCompMgr, aFileSpec) { return CounterModule; }
このコードは、Firefox が components ディレクトリ内のすべての実装ファイルから探す最初の関数です。これは、単にコンポーネントに先立つオブジェクトを返します。
var CounterModule = { // registerSelf, unregisterSelf, getClassObject, canUnload };
ここで変更する必要があるのは、Category Manager を使用する必要がある場合だけです。Category Manager は、既存または独自のカテゴリにコンポーネントを登録できるサービスです。また、このサービスは、カテゴリに登録されたすべてのコンポーネントを得ることができ、そのコンポーネント上のメソッドを呼び出すことができます。このサービスは一般的に、コンポーネントを Content Policy として登録するために使用します。こうすると、URL の読み込みを検知してフィルタにかけることができます。これについては、別のセクションで取り上げます。
Category Manager への add と delete は、registerSelf メソッドと unregisterSelf メソッド内で行われます:
registerSelf : function(aCompMgr, aLocation, aLoaderStr, aType) { let categoryManager = Cc[@mozilla.org/categorymanager;1].getService(Ci.nsICategoryManager); aCompMgr.QueryInterface(Ci.nsIComponentRegistrar); aCompMgr.registerFactoryLocation( CLASS_ID, CLASS_NAME, CONTRACT_ID, aLocation, aLoaderStr, aType); categoryManager.addCategoryEntry( "content-policy", "XULSchool Hello World", CONTRACT_ID, true, true); },
この場合、コンポーネントは nsIContentPolicy を実装する必要があるでしょう。
最後は factory オブジェクトです。
var CounterFactory = { /* Single instance of the component. */ _singletonObj: null, createInstance: function(aOuter, aIID) { if (aOuter != null) { throw Cr.NS_ERROR_NO_AGGREGATION; } // in this case we need a unique instance of the service. if (!this._singletonObj) { this._singletonObj = MessageCounter; } return this._singletonObj.QueryInterface(aIID); } };
単独のサービスを使用する代わりに具体的なクラスに置き換えたい場合、Factory は次のようになります:
var CounterFactory = { createInstance: function(aOuter, aIID) { if (aOuter != null) { throw Cr.NS_ERROR_NO_AGGREGATION; } return (new Counter()).QueryInterface(aIID); } };
IDL ファイルをビルドする手順は、このチュートリアルの『開発環境のセットアップ』のセクションに書かれています。
C++ XPCOM コンポーネント
これは使いたくないでしょう。あなたは本当に使うことはありません。
この種類のコンポーネントを使用するのは、いくつかの理由があります。一つは、Firefox がネイティブでサポートしていない機能を追加するためです。この場合、この機能をすべてのプラットフォーム向けに実装するか、拡張機能の互換性をサポートするプラットフォームだけに限定する必要があるでしょう。また、Windows 用の DLL、Mac (Intel, PPC) 用の dylib、Linux または互換 OS 用の .so ライブラリをそれぞれビルドする必要があります。
この詳細については、チュートリアルに相応しくないため取り上げません。bkrausz 氏のブログ記事に、XPCOM ビルドのセットアップについての詳細が書かれています。また、これがどのように行われるか理解するため、ビルドについてのドキュメントも読む必要があるでしょう。
This tutorial was kindly donated to Mozilla by Appcoast.