注意: この文書は DOM の専門家からの精査を受けていません。問題のある記述があるかもしません。最近になって DOMClassInfo コードが変更されたため、記述が古い可能性もあります。ご指摘をお待ちしています。
Mozilla を使うと、強力で完全な DOM を単に 使える ようになるというだけではなくて、これまでに培 (つちか) われてきた最も偉大なインターネット技術の世界レベルの実装を 作業できる ようになります。
Mozilla DOM のほとんどは C++ で書かれています。DOM を真剣にハックするには、C++ と Mozilla 独自のコンポーネントモデルである XPCOM に通暁している必要があります。私はこの文書を通じて、実装の中心的要点を解説することにします。まずは、DOM の中心に位置する Class Info 機構から初めて、さまざまなインターフェースやクラスの解説へと進むことにします。私自身も、その動作を勉強中ですので、不完全な資料であることを理解しておいてください。この文書のために時間や知識を割いていただけると、とても助かります!
対象となる読者: DOM の実装に興味がある人。C++ と XPCOM の知識があることが前提になります。「XPCOM は知らないけれどもこの文書を読みたい」という方は DOM のための XPCOM 入門 を読んでください。XPCOM について詳しくは XPCOM プロジェクトのページ (和訳) を参照してください。
Class Info と補助クラス
Class Info 入門
Class Info とは、「DOM の正しい動作はこうだ」という情報を XPConnect を通して DOM クラスに提供するものです。それは 5 月に起きた、あの有名な「XPCDOM の結論」の根底にあるものです。ここで XPConnect について詳しく触れておきますが、それは DOM にとって重要だからです。ここで言う「正しい動作」とは「仕様や、事実上の標準に合致した動作」のことです。Class Info が使われるのは、主に DOM レベル 0 が実行される時です。W3C DOM のほとんどは IDL に実装されています。Class Info の目的は「インターフェースの違いの吸収」と「IDL 単独では不可能な動作を実装すること」にあります。
JavaScript と XPConnect の簡単な紹介。
Class Info の説明に入る前に、JavaScript エンジンと XPConnect について簡単に触れておきましょう。JavaScript には、C++ には存在する「型」という概念がありません。例えば、ある機能を JSFunction に定義することもできますし JSObject とすることもできますし jsval にもできます。つまり、JavaScript で DOM を扱う分には、引数を渡すだけであって、その種類が渡されるわけではありません。ただし DOM は C++ で書かれているため、引数の種類が正しいことが前提になります。それが XPConnect の役割でもあります。XPConnect は、引数を C++ の関数が期待する種類に仕立ててくれるわけです。また、C++ 関数の戻り値の種類も JavaScript で安全に使えるように仕立ててくれます。
JavaScript で、DOM オブジェクトや DOM の関数を操作しようとする命令を実行すると、JS エンジンが XPConnect に対して、適切な C++ 関数を問い合わせます。例えば「document.getElementById("myID");」を実行すると、XPConnect が window オブジェクトの「document」、つまり nsIDOMWindow の GetDocument() を見つけるわけです。GetDocument() の戻り値は nsIDOMDocument なので、次にその nsIDOMDocument の GetElementById() を見つけることになります。
W3C DOM オブジェクトと関数を使う場合は、基本的には前述のとおりです。ただし、DOM レベル 0 のオブジェクトと関数の中には、違う動作をするものもあります。違う動作には二通りあって、一つは window.location オブジェクト (実際には document.location と同じ) です。window.location の値を変えることで、ウィンドウのアドレスを変えることができます。IDL の場合、location は読みとり専用として宣言されています。というのは SetLocation() の引数は、アドレスの文字列ではなくて nsIDOMLocation だからです。その代わり、window オブジェクトの補助クラス (nsWindowSH。次節参照) で GetProperty() メンバ関数が定義されています。GetProperty() というのは、オブジェクト (ここでは window) の未知の属性が設定される際に XPConnect が呼びだす関数です。GetProperty() では、設定されようとしている属性が「場所」になっているかどうかを確認します。場所なら nsIDOMLocation::SetHref() を呼びだします。実際には window.location を設定しているつもりでも window.location.href を設定しているのです。そうした自動的な変換ができるのも、すべては XPConnect と DOM との連携のおかげなのです。
もう一つの例は history オブジェクトです。よそのブラウザでは history オブジェクトを history[1] のように、配列として扱えます。IDL だけでは、そうした動作を実現できません。しかし XPConnect を使えば、JavaScript 内でクラスを配列として扱うことも可能になります。それは「スクリプト可能化フラグ」と呼ばれています。nsDOMClassInfo クラスの sIXPCScriptable (節を参照) 【訳注: どの節か明示されていません】 には、フラグがいくつかあり、そのうちの一つに WANT_GETPROPERTY フラグというものがあります。そのフラグを設定すれば nsHistorySH (history オブジェクトの補助クラス) に GetProperty() を定義できるので、配列として使えるという仕組みです。history[1] という記述は history.item(1) に変換されます。その書式なら IDL でのコード化も簡単です。その適切なコードは nsDOMClassInfo.cpp の 4,520 行付近にあります。
以上、二つの例で、XPConnect と JavaScript エンジンとの連携による DOM の威力が分かりました。可能性は無限大です。「今日はどんなコードを書きたいですか?」 ;-)
DOM クラスの nsDOMClassInfoID はすべて nsIDOMClassInfo.h に定義されています。定義されているものは DOM0、Core DOM、HTML、XML、XUL、XBL、range、css、通知などです。
sClassInfoData 配列 (nsDOMClassInfo.cpp に定義) で、各 DOM クラスが JavaScript の補助クラスとインターフェースに割りあてられています。その配列は nsDOMClassInfoData 型の配列で、nsIDOMClassInfo.h に定義されている構造です。その配列の要素の定義には NS_DEFINE_CLASSINFO_DATA
と NS_DEFINE_CLASSINFO_DATA_WITH_NAME
というマクロが使われています。一つ目のマクロは、二つ目のマクロを呼ぶためのものです。 NS_DEFINE_CLASSINFO_DATA_WITH_NAME
に渡される最初の引数 _class はデバッグ目的に使われます。二つ目の引数 _name は JavaScript 内で表記されるオブジェクト名です。三つ目の引数 _helper は、その DOM クラス用の補助クラスの名前です。補助クラスについては 1.3 節で解説します。四つ目の引数と最後の引数 _flags は nsIXPCScriptable フラグ用です。フラグ用のマクロは nsDOMClassInfo.cpp に定義されています。フラグを使うと、XPConnect に特別な動作を指定できます。1.9 節を参照してください。
sClassInfoData 配列を明示的に初期化すると、その中に nsDOMClassInfoData オブジェクトが用意されます。その構造体の構成を次に示します。
const char *mName
: マクロの二つ目の引数に渡される C 形式の文字列。DOM を通じてブラウザ内で利用可能な JavaScript オブジェクトの名前です。-
union { nsDOMClassInfoConstructorFnc mConstructorFptr; nsDOMClassInfoExternalConstructorFnc mExternalConstructorFptr; } u;
typedef 定義の関数アドレスの union です:typedef nsIClassInfo* (*nsDOMClassInfoConstructorFnc)(nsDOMClassInfoID aID);
あるいはtypedef nsIClassInfo* (*nsDOMClassInfoExternalConstructorFnc) (const char* aName);
マクロの三つ目の引数として渡される補助クラスの doCreate メンバ関数で初期化されます。 nsIClassInfo *mCachedClassInfo
: mCachedClassInfo は、関連する補助クラスのインスタンスに対する nsIClassInfo のアドレスを持ちます。const nsIID *mProtoChainInterface
: JavaScript クライアントで使える最初のインスタンスの IID のアドレス。XPConnect がメンバ関数を見つける時に、広域解決関数で使われます。const nsIID **mInterfaces
: 配列のアドレス。最初の要素は JS でこのクラスを操作する際に利用可能なインターフェースのアドレスです。PRUInt32 mScriptableFlags: 31;
: NS_DEFINE_CLASSINFO_DATA_WITH_NAME に渡される四つ目の引数です。PRBool mHasClassInterface: 1;
: 補助的なものといったところでしょうか。
mName と mConstructorFptr、mScriptableFlags と mHasInterface は NS_DEFINE_CLASSINFO_DATA_WITH_NAME で初期化されます。一方、mCachedClassInfo と mProtoChainInterface と mInterfaces は nsDOMClassInfo::Init() で初期化されます。1.5 節で解説します。
インターフェースの違いの吸収
XPConnect による DOM で、いちばん素晴らしくて、いちばん重要な機能は、インターフェースの違いを吸収してくれるということです。「インターフェースを気にしなくても、オブジェクトの関数を呼びだせます。」例えば、JavaScript なら document.getElementById() と書くこともできますし、document.addEventListener() と書くこともできます。ですが、内部で処理されているインターフェースは (DOMDocument と DOMEventTarget という別のものです。その機能によって、DOM が使いやすいものとなっていることは言うまでもないでしょう。
Mozilla がインターフェースの違いを吸収できるのは nsIClassInfo というインターフェースのおかげです。nsIClassInfo には、各オブジェクトに適用できるインターフェースの配列が格納されて、その配列は XPConnect が適切な関数を探すために使われます。
JS で扱えるインターフェースも、コードを見れば誰でも簡単に分かるところが素晴らしいところです。その面白い部分は nsDOMClassInfo::Init() に書かれています。そこには、マクロがずらりと並べられていて、DOM クラス ごとに分類されています。それを見れば、オブジェクトごとに、どのインターフェースが「違いを吸収されている」ものかが分かります。window オブジェクトの場合、次に挙げるインターフェースの関数全部を呼びだせます。nsIDOMWindow、nsIDOMWindowInternal、nsIDOMJSWindow、nsIDOMEventReciever、nsIDOMEventTarget、nsIDOMViewCSS、nsIDOMAbstractView。どの関数がどのインターフェースに実装されているかを気にする必要はありません。Init() について詳しくは 1.E 節を参照してください。
W3C DOM (レベル 1、2、3) のオブジェクトの場合、オブジェクトごとに「標準準拠」のインターフェースが一つあります。DOM レベル 0 では、W3C のものと同一で nsIDOM<オブジェクト名>.idl という名前で、mozilla 仕様に拡張されたインターフェースのものは nsIDOMNS<オブジェクト名>.idl です。例えば、HTML の「AREA」要素には nsIDOMHTMLAreaElement と nsIDOMNSHTMLAreaElement という「違いを吸収された」機構があります。
補助クラス
nsDOMClassInfo.h には、クラスがいくつか追加されています。それらのクラスには「Scriptable Helper」(スクリプト可能化補助) を意味する「SH」が末尾についています。nsWindowSH や nsElementSH です。一般に、それらのクラスを「補助クラス」と言います。補助クラスは、必ず nsDOMClassInfo クラスを継承しています。例として nsDOMClassInfo.h を見てください。補助クラス nsEventRecieverSH は nsDOMGenericSH を継承していることが分かります。
class nsEventRecieverSH : public nsDOMGenericSH
nsDOMGenericSH は nsDOMClassInfo 型として typedef されています。
typedef nsDOMClassInfo nsDOMGenericSH;
もう一つの例は nsWindowSH で、nsEventReceiverSH を継承しています。つまり nsDOMClassInfo も継承しているわけです。
sClassInfoData 配列の初期化中に、各 DOM クラスに対して補助クラスが割りあてられます。
各補助クラスには公開 (public) メンバ関数 doCreate があって、クラスの (メモリ上の) 実体を用意する必要がある場合に GetClassInfoInstance (1.6 節参照) から呼ばれます。ただし、実際には nsDOMClassInfoData 構造体の mConstructorFptr に設定されている名前の関数が呼ばれるということを覚えておいてください。補助クラスの実体が用意されるのはいつかというと、オブジェクトのインターフェースの「違いを吸収された」集まりを XPConnect が操作する最初の時です。補助クラスは、その後のために一時保存されます。
たいていの補助クラスには nsIXPCScriptable の関数が実装されています。XPConnect はその関数を使って、IDL では定義されていない情報を JavaScript から引きだします。例えば、GetProperty() が使われる時というのは、IDL で定義されていない属性を取得する時ですし NewResolve() は、未知の属性や関数を判明させようとする時に使われます。詳しくは nsIXPCScriptable interface を参照してください。
The nsDOMClassInfo クラス
Class Info の中心は nsDOMClassInfo クラスです。 nsDOMClassInfo.h に定義されています。そのクラスでは nsISupports の他にも nsIXPCScriptable と nsIClassInfo が実装されています。
nsIXPCScriptable を使う理由については、すでにお話しました。
nsIClassInfo は XPCOM インターフェースの一つです。Mike Shaver が nsIClassInfo の概要 で詳しく解説しています。そのインターフェースには普通、オブジェクトのインターフェースを見つける便利な関数があります。ここでは、「Class Info」が各インターフェースの一覧を設定することになります。Init() については 1.5 節を参照してください。インターフェースの違いの吸収については 1.2 節を参照してください。
1.3 節では、nsDOMClassInfo が補助クラスすべての基礎クラスになっていることを紹介しました。それでは、その構成を見てみましょう。まずは公開 (public) のインターフェースから見ていきましょう。
- 構築関数 (コンストラクタ): メンバ初期化リストを通して、用意された補助クラスごとに呼ばれます。単に引数の aID でメンバ変数 mID を初期化するだけです。
- nsIXPCScriptable と nsISupports と nsIClassInfo メンバ関数は NS_DECL_X マクロで宣言されます。
-
static nsIClassInfo* GetClassInfoInstance(nsDOMClassInfoID aID)
:
この補助関数は nsIClassInfo のアドレスを返します。返されるインターフェースの参照数はそのままです。渡された ID に対応する補助クラスの実体の nsIClassInfo* を返すわけです。この実装について詳しくは 1.6 節を参照してください。 -
static nsIClassInfo* GetClassInfoInstance(nsDOMClassInfoData* aData);
:
この補助関数は nsIClassInfo のアドレスを返します。返されるインターフェースの参照数はそのままです。渡された Data に対応する補助クラスの実体の nsIClassInfo* を返すわけです。この実装について詳しくは 1.6 節を参照してください。 -
static void ShutDown()
:
インターフェースのポインタを解放します。 -
static nsIClassInfo* doCreate(nsDOMClassInfoData* aData)
:
インライン関数で nsDOMClassInfo クラスの新しい実体の nsIClassInfo* を返します。 -
static nsresult WrapNative(...)
: XPConnect 関数です。ここでは無関係です。 -
static nsresult ThrowJSException(JSContext *cx, nsresult aResult);
:
どなたか解説をお願いします。 -
static nsresult InitDOMJSClass(JSContext *cx, JSObject *obj);
:
どなたか解説をお願いします。 -
static JSClass sDOMJSClass;
:
どなたか解説をお願いします。
Protected 節:
-
const nsDOMClassInfoData* mData;
: どなたか解説をお願いします。 -
static nsresult Init()
: 一度だけ呼びだされます。nsDOMClassInfoData 構造体の残りのメンバを初期化します。前述の通りです。呼びだされたら sIsInitialized を true に設定して、初期化されたというフラグを立てます。この実装については 1.E 節で解説します。 -
static nsresult RegisterClassName(PRInt32 aDOMClassInfoID)
: どなたか解説をお願いします。 -
static nsresult RegisterClassProtos(PRInt32 aDOMClassInfoID)
: どなたか解説をお願いします。 -
static nsresult RegisterExternalClasses();
: どなたか解説をお願いします。 -
nsresult ResolveConstructor(JSContext *cx, JSObject *obj, JSObject **objp);
: どなたか解説をお願いします。 -
static PRInt32 GetArrayIndexFromId(JSContext *cx, jsval id, PRBool *aIsNumber = nsnull)
:
JS 値が整数なら *aIsNumber に true が渡されて、整数値を返します。それ以外なら *aIsNumber は false で -1 を返します。 -
static inline PRBool IsReadonlyReplaceable(jsval id) { ... }
: どなたか解説をお願いします。 -
static inline PRBool IsWritableReplaceable(jsval id) { ... }
: どなたか解説をお願いします。 -
nsresult doCheckPropertyAccess(...)
: どなたか解説をお願いします。 (バグ 90757) -
static JSClass sDOMConstructorProtoClass
: XPConnect 関数で、DOM オブジェクトの構築関数を JavaScript に公開します。 -
static JSFunctionSpec sDOMJSClass_methods[];
: どなたか解説をお願いします。 (バグ 91557) -
static nsIXPConnect *sXPConnect
: nsIXPConnect 関数を呼ぶために使われます。Init() で初期化されます。 -
static nsIScriptSecurityManager *sSecMan
: DOM のセキュリティエンジンが使います。Init() で初期化されます。 -
static nsresult DefineStaticJSVals(JSContext *cx);
: nsDOMClassInfo の static 型 JSString メンバ変数すべてを定義するために使われます。 -
static PRBool sIsInitialized
:
Class Info が初期化済みかどうかを示すフラグです。Init() が複数回呼ばれないようにします。 -
static jsval *sX_id
: 広域解決関数で比較用に使われる文字列です。引数と一緒に渡されます。DOM の場合、特別な単語を意味します。DefineStaticJSVals() で初期化されます。 -
static const JSClass *sObjectClass
: どなたか解説をお願いします。 -
static PRBool sDoSecurityCheckInAddProperty;
: どなたか解説をお願いします。
nsDOMClassInfo::Init()
この関数は一度だけ呼ばれます。その目的はもちろん初期化で、いろんな処理が行われます。sClassInfoData 配列の要素を空 (から) にすることであったり、sXPConnect と sSecMan メンバ変数を初期化することであったり、JavaScript 環境を用意することであったり、JSString メンバ変数を設定することであったり、クラスの名前と原型を登録することであったりします。最後に sIsInitialized を true に設定します。次に DOM の動作について説明します。
まず CallGetService() を呼びだして sXPConnect を初期化します。次に、スクリプトセキュリティ管理機能 (Script Security Manager、sSecMan) を初期化します。GetSafeJSContext() によって、JavaScript のコードが優れた JS 環境下で実行されるようになります。ComponentRegistrar 部では、JavaScript の利点として、外部モジュール (ここでは XPath) が DOMClassInfo 内に含まれるようにもします。その後、sClassInfoData 配列を空 (から) に初期化します。
Class Info についての最初の説明を覚えているでしょうか。主要な配列 sClassInfoData があって、その要素には nsDOMClassInfoData 型のオブジェクトが設定されます。ただし、その配列が用意される時、その構造体の三つのメンバ変数 mCachedClassInfo と mProtoChainInterface と mInterfaces は NULL のままです。Init() では DOM_CLASSINFO_MAP マクロ群でそのメンバが設定されます。DOM クラスごとにそのマクロを使わなくてはなりません。そうしないと予期しないことが起きる「はず」です。ここでは Window クラスを例にしてマクロの使い方を示します。次に挙げるのは適切なコードの一部です。
DOM_CLASSINFO_MAP_BEGIN(Window, nsIDOMWindow) DOM_CLASSINFO_MAP_ENTRY(nsIDOMWindow) ... DOM_CLASSINFO_MAP_ENTRY(nsIDOMAbstractView) DOM_CLASSINFO_MAP_END
DOM_CLASSINFO_MAP_BEGIN(_class, _interface) は _DOM_CLASSINFO_MAP_BEGIN(_class, &NS_GET_IID(_interface), PR_TRUE) に展開されます。NS_GET_IID というのはマクロで、インターフェースの名前を渡すと IID に変換してくれます。nsIID オブジェクトのアドレスが次のマクロに渡されることになります。
#define _DOM_CLASSINFO_MAP_BEGIN(_class, _ifptr, _has_class_if) { nsDOMClassInfoData &d = sClassInfoData[eDOMClassInfo_##_class##_id]; d.mProtoChainInterface = _ifptr; d.mHasClassInterface = _has_class_if; static const nsIID *interface_list[] = {
マクロに出てくる「d」とは、マクロの引数 (クラス) に対応する sClassInfoData 配列の要素への参照です。mProtoChainInterface メンバは DOM_CLASSINFO_MAP_BEGIN の引数 (インターフェースの IID のアドレス) に設定されます。その後に、nsIID 型オブジェクトのアドレスが入った static 配列が宣言されます。その配列は DOM_CLASSINFO_MAP_ENTRY マクロ (次を見てください) で明示的に初期化されることになります。
似たマクロが二つあります。
#define DOM_CLASSINFO_MAP_BEGIN_NO_PRIMARY_INTERFACE(_class) _DOM_CLASSINFO_MAP_BEGIN(_class, nsnull, PR_TRUE)
このマクロが使われる時というのは、DOM クラス (例えば XMLHTTPRequest) にインターフェースが無い場合で、それでもそのオブジェクトを JavaScript から操作したい場合です。
#define DOM_CLASSINFO_MAP_BEGIN_NO_CLASS_IF(_class, _interface) _DOM_CLASSINFO_MAP_BEGIN(_class, &NS_GET_IID(_interface), PR_FALSE)
このマクロは DOM クラスに「末端」のインターフェースが無い場合に使われるべきでしょう。例えば、HTMLSpanElement は W3C DOM 仕様にはありません。そのため、SPAN 要素の型系列内で最初に見つかるインターフェースが HTMLElement です。でも、実際にはなんとかして HTMLSpanElement を使いたいわけです。そのマクロを使えば、HTMLSpanElement を使えるようになるというわけです。詳しくは バグ 92071 を参照してください。それでは、JavaScript で扱える特定の DOM クラスのインターフェースの指定方法を見ていくことにしましょう。
#define DOM_CLASSINFO_MAP_ENTRY(_if) &NS_GET_IID(_if),
アドレスの配列 interface_list の要素にはマクロの引数 (全てのインターフェースの IID のアドレス) が設定されます。Window を例にとると、そのインターフェースは nsIDOMWindow、nsIDOMJSWindow、nsIDOMWindowInternal、nsIDOMEventReciever、nsIDOMEventTarget、nsIDOMViewCSS、nsIDOMAbstractView です。それらのインターフェースを同じように扱える仕組みについては 1.2 節の説明を参照してください。クラスの初期化は DOM_CLASSINFO_MAP_END マクロで閉じることになります。
#define DOM_CLASSINFO_MAP_END nsnull }; d.mInterfaces = interface_list; }
interface_list 配列の末尾は NULL です。d.mInterfaces = interface_list の行で、mInterfaces に interface_list 配列の最初の要素のアドレスが設定されています。mInterfaces は nsIID 型オブジェクトのアドレスのアドレスになります。
Init() では、DefineStaticJSVals() が呼びだされて jsvals が定義されます。また、RegisterClassProtos が呼びだしでクラス名が登録され、RegisterClassNames の呼びだしでクラスの型が登録されます。中身の処理については今後の文書で触れるかもしれません。最終的に sIsInitialized が true に設定されます。問題が起きなければ Init() が NS_OK を返します。
nsDOMClassInfo::GetClassInfoInstance()
この関数には二通りあります。一つは引数に ID を取るもので、もう一つは引数に Data 構造体を取るものです。この関数は重要ですので、詳しく見ていくことにしましょう。関数定義は次のようになっています。
nsIClassInfo* nsDOMClassInfo::GetClassInfoInstance(nsDOMClassInfoID aID) { if(!sIsInitialized) { nsresult rv = Init(); } if(!sClassInfoData[aID].mCachedClassInfo) { nsDOMClassInfoData &data = sClassInfoData[aID]; data.mCachedClassInfo = data.u.mConstructorFptr(&data); NS_ADDREF(data.mCachedClassInfo); } return sClassInfoData[aID].mCachedClassInfo; }
まずは簡単に説明します。
この関数の戻り値は sClassInfoData 配列内の aID に対応する nsDOMClassInfoData の mCachedClassInfo メンバです。それは、戻り値となる構造体のメンバに値が設定済みの場合、つまり、この関数が呼ばれたのが二度目以降の場合です。最初の呼びだしでは mCachedClassInfo が NULL のままなので、適切な補助クラスの実体を用意して、そのアドレスを mCachedClassInfo に格納してからそのアドレスを返します。
詳しく知りたい人向けに、長めの説明。
GetClassInfoInstance() が最初に呼びだされた時点では、引数 aID 用のクラス mCachedClassInfo は NULL です。そのため if 文の本体が実行されます。「補助」したい DOM クラスの nsDOMClassInfoData オブジェクトの参照を「データ」として初期化します。次の行には data.mConstructorFptr(aID) への呼びだしがあります。Class Info の入門で説明しましたが、その呼びだしは、適切な補助クラスの doCreate static メンバ関数に変換されます。doCreate で補助クラスの実体が用意されて、nsIClassInfo インターフェースのアドレスが返されます。その後、mCachedClassInfo に変換されます。mCachedClassInfo の参照数を増やして (AddRef を呼びだして)、参照数を戻すまでは破棄されないようにします。最後にそのインターフェースのアドレスを返します。
同じ aID での二度目以降の呼びだしでは、同じ mCachedClassInfo を返すだけなので、補助クラスを改めて用意する必要がありません。
「GetClassInfoInstance はどこで使用するべきか。また、なぜ使うべきか」。良い質問です。一言で答えるなら「nsIClassInfo への QueryInterface を実装するため」です。nsIClassInfo への QueryInterface だけは、他のインターフェースと違って実装できないのです。マクロ嫌いな人は nsXMLElement.cpp にある QueryInterface の完全な実装を見てみてください。Mozilla で定義されている別の二つ (nsContentUtils クラスと nsDOMSOFactory クラス) の GetClassInfoInstance メンバ関数です。どちらも最後は nsDOMClassInfo::GetClassInfoInstance の呼びだしで終わっています。その事実がすべてを語っています。NS_INTERFACE_MAP_ENTRY_CONTENT_CLASSINFO マクロでは GetClassInfoInstance を呼びだすことで、 たいていの DOM クラス での nsIClassInfo への QueryInterface を実装しています。また NS_DOM_INTERFACE_MAP_ENTRY_CLASSINFO マクロでは、GetClassInfoInstance を呼びだして、たいていの 広域オブジェクトの属性 における nsIClassInfo への QueryInterface を実装しています。
この関数についてご説明しなければならない事はこれだけだと思います。「そうじゃないんだ」という考えをお持ちの方は、遠慮なく私に問い合わせてください。
nsWindowSH::GlobalResolve()
この節ではものすごいメンバ関数 GlobalResolve() を nsWindowSH 補助の関数例として詳しく解説します。これは気の小さい人には向きませんし、どうしても必要というわけでもありません。時間が無いならこの節を飛ばしてもかまいません (恐らくあなたには時間がないでしょう)。
Class Info の使い方
注意: この文書は DOM の専門家からの精査を受けていません。問題のある記述があるかもしれません。2002 年 4 月ごろには、特に変更があったため、以前可能できたことができなくなっているかもしれません。できるだけ早く、この文書を更新しようと思います。Fabian Guisset まで、ご意見をお寄せください。
DOMClassInfo を使っても良い時
- 既存の DOM オブジェクトにインターフェースを追加するため。
- 新しい DOM オブジェクトを JavaScript に公開するため。
- 新しい JS 外部の構築関数 (新しい Image() など) を追加するため。
- XPConnect の既定の動作を迂回させるため。
- 「置換可能」属性を実装するため。
- DOM オブジェクトの型をいじるため。
DOMClassInfo を利用して実装される機能の例:
- DOM オブジェクトの (広域) 構築関数。例 Node。
- それらの DOM オブジェクトの独自型の設定。
- 新しい Image()、新しい Option()。
- window.history[index]
- document.<formName>
既存の DOM オブジェクトにインターフェースを追加するには
この節では DOMImplementation オブジェクトの簡単な例を使って説明します。バグ 33871 (この文書の執筆時には、そのパッチはまだチェックインされていません) の解決のために使われた実例です。それでは問題です。DOMImplementation オブジェクトに新しく HTMLDOMImplementation インターフェースを追加する必要があるものとします。JS では DOMImplementation オブジェクトは document.implementation
のように使われるものとします。そのオブジェクトには DOMImplementation というインターフェースが既にありますが、DOM2 HTML では HTMLDOMImplementation というインターフェースも実装すべきだということになっているものとします。それでは始めましょう。その C++ の実装は nsDocument.cpp にあります。
最初の手順はもちろん、新しいインターフェースを C++ で実装することですよね。それは XPCOM 入門 にも書かれています。
nsDOMImplementation には nsIDOMHTMLDOMImplementation というインターフェース が実装されているものと仮定します (実装法を知りたい人はバグ 33871 を簡単に調べてみてください)。ここでは、そのインターフェースを JavaScript に公開したいわけです (公開しなければ、単に XPCOM の呼びだし元がそのインターフェースを操作できるというだけのことでしかありません)。公開するには、そのインターフェースを DOMImplementation オブジェクトの DOMClassInfo に追加する必要があります。
利点
- HTMLDOMImplementation インターフェースが JavaScript から利用できるようになります。
- HTMLDOMImplementation インターフェースの関数を
document.implementation
オブジェクトで扱えるようになります (最終目的)。nsDOMClassInfo と XPConnect
による自動的なインターフェースの違いの吸収によって扱えるようになるわけです。
-
document.implementation instanceof HTMLDOMImplementation
が動作します (true が返ってくる)。 -
HTMLDOMImplementation.prototype
を操作可能で変更可能です。 - その他いろいろ。
手順
- nsDOMClassInfo.cpp で新しいインターフェースの定義を include してください。
#include "nsIDOMHTMLDOMImplementation.h"
.
それを適切な場所に記述してください。 - 適切な DOM オブジェクトで、必要なインターフェースすべてを実装しているコードの箇所を探してください。nsDOMClassInfo::Init() の中のどこかにあります。
- DOMImplementation の場合は 1220 行あたりです (この文書の執筆時では)。
1224 DOM_CLASSINFO_MAP_BEGIN(DOMImplementation, nsIDOMDOMImplementation)
これが、DOMImplementation オブジェクトが nsIDOMDOMImplementation というインターフェースを実装している行です。 - DOMClassInfo 定義に新しいインターフェースを追加してください。ここでは、次のようにします。
1225 DOM_CLASSINFO_MAP_ENTRY(nsIDOMHTMLDOMImplementation)
- makefiles や manifests に新しいインターフェースを追加してください。
- 再コンパイルしてください。
- 最適化ビルドをしたい場合は components.reg を変更してください。
- DOMClassInfo の美しさを目の当たりにしてください。
JavaScript に新しい DOM オブジェクトを公開するには
それでは次に移りましょう。オブジェクトにインターフェースを追加できるようになりましたが、それだけでは不十分です。まったく新しいオブジェクトを JavaScript に公開したいこともあります。DOMClassInfo がたいていのことはやってくれます。独自のオブジェクトで、既定の ToString() も実装してくれます。
ここでまた DOMImplementation オブジェクトの例を使います。 document.implementation
で操作できます。それは W3C DOM レベル 1 の中心仕様に定義されています。広域構築関数 DOMImplementation
を操作できなくてはなりません。DOMImplementation の ToString() が「DOMImplementation」を返す必要もあります。hasFeature() (DOM1) と createDocumentType() と createDocument() (DOM2) を実装する必要もあります。
手順
- 自前のオブジェクトを C++ で実装します。そのコードについては、ここで解説する範囲を超えています。いちばん良いのは、既存のコードをコピーすることでしょう。DOM オブジェクトというのは DOMClassInfo を伴った簡単な XPCOM オブジェクトとも言えます。ここでの実装クラスは (nsDocument.cpp の) nsDOMImplementationです。そのオブジェクトは nsIDOMDOMImplementation インターフェース (前述のとおり、三つのメンバ関数をもたせます) を実装するものです。
- 自前の XPCOM オブジェクトの QueryInterface の実装を改変して DOMClassInfo データを含めてください。QueryInterface の実装の最後に次の行を追加してください。
NS_INTERFACE_MAP_ENTRY_CONTENT_CLASSINFO(dom_object_name)
DOMImplementation オブジェクトでは、次のよう記述することになります。NS_INTERFACE_MAP_ENTRY_CONTENT_CLASSINFO(DOMImplementation)
何をするための記述かというと、nsIClassInfo を問い合わせるためのもので、内部的に XPConnect から必要とされます。そう記述することで、DOM オブジェクト用のスクリプト可能化補助クラスの実体が用意されるはずです。それ以上のことは、後述します。 - sClassInfoData 配列に DOMClassInfo オブジェクトを追加してください (nsDOMClassInfo.cpp)。
NS_DEFINE_CLASSINFO_DATA(dom_object_name, scriptable_helper_class, scriptable_flags)
DOMImplementation オブジェクトの場合は、次のようになるはずです。
NS_DEFINE_CLASSINFO_DATA(DOMImplementation, nsDOMGenericSH, DOM_DEFAULT_SCRIPTABLE_FLAGS)
配列のどこに DOMClassInfo を追加するのかは決まりきったことです。「それは違う」という方は <a href="mailto:[email protected]">Johnny Stenback</a> に問い合わせてみてください。 - nsDOMClassInfo::Init() 内で DOMClassInfo オブジェクトを追加してください (nsDOMClassInfo.cpp)。
DOM_CLASSINFO_MAP_BEGIN(dom_object_name, dom_object_main_interface) DOM_CLASSINFO_MAP_ENTRY(interface1) DOM_CLASSINFO_MAP_ENTRY(interface2) ... DOM_CLASSINFO_MAP_END
DOMImplementation オブジェクトの場合は次のようになるはずです。
DOM_CLASSINFO_MAP_BEGIN(DOMImplementation, nsIDOMDOMImplementation) DOM_CLASSINFO_MAP_ENTRY(nsIDOMDOMImplementation) DOM_CLASSINFO_MAP_END
interface1
、interface2
…… の引数は DOM オブジェクトが実装するインターフェース名であって、かつ、JavaScript に公開されるものだけです。内部のインターフェースをそのリストに入れてはいけません。 - 適切なファイルを #include して、makefile を変更するなどしてビルドしてください。どんな環境でもビルドできるようにしてくださいね。:-P
- 既存のスクリプト可能化補助クラスを使っていた場合は、(ビルド最適化を使っていた場合は components.reg も変更して) すべてをビルドしなおして実行する必要があります。うまくいくはずです。
- 新しくスクリプト可能化補助クラスを使いたい場合も、同じように実装する必要があります。
DOM オブジェクトで XPConnect の既定の動作を変更するには
XPConnect は、XPCOM オブジェクトに対して一般に既定の動作を実装します。DOM オブジェクトに対しては特にその傾向が強くなります。DOMClassInfo を使うと、実装者が nsIXPCScriptable インターフェースを利用して、既定の動作を無効にできます。作業に入る前に nsIXPCScriptable.idl ファイルに目を通してください。「スクリプト可能化フラグ」と言われる定数と関数 (NewResolve()、SetProperty() など) が定義されています。各フラグが一つの関数に対応します。例えば、nsIXPCScriptable::WANT_NEWRESOLVE は「NewResolve() を実装したい」ということを意味します。大切なことは、存在する DOM オブジェクトの動作と各関数が対応するということです。例えば、JS で DOM オブジェクトの属性が設定されようとすると、XPConnect が自動的に SetProperty() を呼びだします。それが「そのオブジェクトでその属性を設定する」という既定の XPConnect の動作を無効にできる方法です。各 nsIXPCScriptable 関数について詳しくは nsIXPCScriptable の文書を参照してください。
nsIXPCScriptable と scriptable の補助関数の使い方を説明するために、window オブジェクトの「location」属性を例に挙げます。 window.location
は「Location」型の DOM オブジェクトです。しかし、 window.location = "https://mozilla.org"
という使われ方がよくされます。正しくは window.location.href = "https://mozilla.org"
と書かれるべきです。そこで「window オブジェクトの location 属性を設定する」という既定の動作を変更する必要があります。既定の動作は「XPConnect が nsIDOMLocation オブジェクトを期待する」ということです。しかし、実際には JS 文字列が渡されることもあります。その時、変換の例外が発生します。
nsIXPCScriptable インターフェースの実装を見ていく前に、実装には次の情報が必要になります。
- どの DOM オブジェクトが関係しているか。
- どの動作を無効にしたいか。
- 何が起きるべきか。
ここでは window オブジェクトであって、動作は属性の設定です。.location を設定すると、.location.href が設定されるべきです。情報が揃ったところで、コードを書き始めましょう。
手順
- sClassInfoData 配列に ClassInfo オブジェクトを設定します。ここでは Window オブジェクトです。以前の節で説明したように、マクロに渡される引数は三つです。DOM オブジェクトの名前、スクリプト可能化補助クラス、スクリプト可能化フラグです。
- スクリプト可能化フラグによって、その DOM オブジェクトに nsIXPCScriptable というインターフェースが実装されていることが分かります。必要なフラグが既にある場合は、次の手順に進んでください。まだ無ければ、フラグのリストに追加してください。
- そのオブジェクト用のスクリプト可能化補助クラスの名前を覚えておいてください。たいていのオブジェクトでは、nsDOMGenericSH クラスで、nsDOMClassInfo クラス用の typedef です。DOM オブジェクトに特別なものを求めない場合は、そのオブジェクト用のスクリプト可能化補助クラスは nsDOMGenericSH になります。特別なものが必要な場合は補助クラスの実装をスクロールしてください。
ここでの例では nsWindowSH クラスです。 - 補助クラスに必要な nsIXPCScriptable 関数を実装済みの場合は次の手順に移ってください。未実装なら nsIXPCScriptable インターフェース内に記述される引数で、その新しいメンバ関数を実装してください。
- やっと面白みがあるところに来ました。スクリプト可能化補助クラス全部を記述することは、まずできないので、ご自分のプログラム技術と既存コードからのコピーを併用する必要があります。ただし、ここではコピーせずに window.location 属性の実装を記述することにします。
window.location の実装
属性の設定を無効にするために必要なスクリプト可能化フラグは WANT_NEWRESOLVE と WANT_SETPROPERTY です。NewResolve() という関数が、そのオブジェクトでの属性を JS API で定義します。二つ目のフラグは .location を .location.href に変換するためのものです。nsWindowSH::NewResolve() 内のコードは次のようになります (nsDOMClassInfo.cpp)。
3553 if (flags & JSRESOLVE_ASSIGNING) { // 設定されていれば、単に属性を定義します。 3554 if (str == sLocation_id) { // location 属性を設定。 3555 nsCOMPtr<nsIDOMWindowInternal> window(do_QueryInterface(native)); 3556 NS_ENSURE_TRUE(window, NS_ERROR_UNEXPECTED); 3557 3558 nsCOMPtr<nsIDOMLocation> location; 3559 rv = window->GetLocation(getter_AddRefs(location)); 3560 NS_ENSURE_SUCCESS(rv, rv); // DOM を使って、window オブジェクトの Location オブジェクトを取得します。 3561 3562 jsval v; 3563 3564 rv = WrapNative(cx, obj, location, NS_GET_IID(nsIDOMLocation), &v); // この XPConnect 関数で Location オブジェクトの入れ物が Window // オブジェクト上に用意されます。 3565 NS_ENSURE_SUCCESS(rv, rv); 3566 3567 if (!::JS_DefineUCProperty(cx, obj, ::JS_GetStringChars(str), 3568 ::JS_GetStringLength(str), v, nsnull, 3569 nsnull, 0)) { 3570 return NS_ERROR_FAILURE; 3571 } // この JS API 呼びだしで、window オブジェクトの「location」属性を定義します。 // その値は Location オブジェクトの XPConnect 容器になっています。 3572 3573 *objp = obj; 3574 3575 return NS_OK; 3576 }
以上が最初の手順です。同じように、.location 作業用の取得関数を用意する必要がありますが、それはまた別の手順になります。次の手順では nsWindowSH::SetProperty() 内で .location を .location.href へと変換します。
2894 if (str == sLocation_id) { // location 属性を設定します。 2895 JSString *val = ::JS_ValueToString(cx, *vp); 2896 NS_ENSURE_TRUE(val, NS_ERROR_UNEXPECTED); // location (つまり url) に設定されている値を JSString に変換します。 2897 2898 nsCOMPtr<nsISupports> native; 2899 wrapper->GetNative(getter_AddRefs(native)); // 格納先となっている容器オブジェクトのアドレスを取得します。 2900 2901 nsCOMPtr<nsIDOMWindowInternal> window(do_QueryInterface(native)); 2902 NS_ENSURE_TRUE(window, NS_ERROR_UNEXPECTED); // QueryInterface して nsIDOMWindowInternal のアドレスを取得して // その GetLocation() を呼びます。 2903 2904 nsCOMPtr<nsIDOMLocation> location; 2905 nsresult rv = window->GetLocation(getter_AddRefs(location)); 2906 NS_ENSURE_SUCCESS(rv, rv); // そのウィンドウの Location オブジェクトを取得します。 2907 2908 nsDependentString href(NS_REINTERPRET_CAST(PRUnichar *, 2909 ::JS_GetStringChars(val)), 2910 ::JS_GetStringLength(val)); // SetHref() へ渡せように JSString を文字列に変換します。 2911 2912 rv = location->SetHref(href); 2913 NS_ENSURE_SUCCESS(rv, rv); // この後の .location から .location.href への変換は簡単です。 2914 2915 return WrapNative(cx, obj, location, NS_GET_IID(nsIDOMLocation), vp); // 値 vp (url) をもつ location オブジェクト用の入れ物を用意します。 2916 }
簡単で可能性は無限大。
関連情報
スクリプト可能化補助フラグ
この章は未執筆です。手伝ってくださる方は連絡してください。
セキュリティ機能の実装
この章は未執筆です。手伝ってくださる方は連絡してください。
原文書の情報
- 著者: Fabian Guisset
- 最終更新日: September 27, 2007
- 著作権: Portions of this content are © 1998–2007 by individual mozilla.org contributors; content available under a Creative Commons license | 詳細