Shiki’s Weblog

ESウェブブラウザ通信 - ESウェブブラウザでのWeb IDLの利用

2012/04/04 #ESウェブブラウザ通信

今回はそろそろW3Cから勧告候補になりそうなWeb IDLが、ESウェブ ブラウザでどんな風に使われているか紹介していきたいと思います。

メッセージ

ESウェブ ブラウザはC++でもObjectというクラスを基本に構築しているのでオブジェクト指向の設計のように見えるかもしれません。けれども一番基本的な要素は、オブジェクト間でやりとりされる『メッセージ』の方です。
ESウェブ ブラウザでは、すべてのオブジェクトに対して、

    object.message_(selector, id, argc, argv);

というスタイルでメッセージを送ることができます。メッセージの応答は、Anyクラスの値として返されます。AnyクラスはWeb IDLで定義されているany型に対応するクラスです。(Web IDLで定義されているどんな型の値でも保持できるという意味でのAnyで、boost::anyのようにC++のどんな値でもとれるわけではありません。)

名前 説明
uint32_t selector idから自動的に生成されるハッシュ値。
const char* id メッセージの名前。ハッシュ値が被らなければ、0で構いません。
int argc 引数argvの要素数。
Any* argv Any型の配列で表されるメッセージの引数。

基本はこれだけです。例えば、DOM4の、

    Node appendChild(Node node);

というオペレーションをあるobjectに適用したい場合には、

    Any param(node);

    obect.message_(hash("appendChild"), "appendChild", 1, &param);

と言った具合にメッセージを送ると、objectがNodeであれば子ノードにnodeが追加されます。
「objectがNodeであれば」と書いたのは、ESウェブ ブラウザではクラスとかプロトタイプといったものは本質的なものとしては扱っていなくて、どんなオブジェクトに対しても"appendChild"という名前のメッセージを送りたければ送ることができるからです。C++で動的にダック・タイピングしているようなイメージです。

Web IDLの言語バインディング

原理的にはメッセージを送るだけ、と言っても何もかも、

    obect.message_(hash("appendChild"), "appendChild", 1, &param);

のように書かないといけない、というのではちょっとプログラミングが大変です。JavaScriptでは、この処理は、

    object.appendChild(node);

と書けます。そういうことを規程しているのが、Web IDLの仕様書の"ECMAScript binding"です。W3Cの仕様書の中などでWeb IDLで規定されているインターフェイスが、プログラミング言語上ではどんな風に見えるか、ということを規程したものは『バインディング』と呼ばれています。
ESウェブ ブラウザでは、ECMAScriptのバインディングに倣って、C++11のバインディングを独自に定義して、C++でも、

    object.appendChild(node);

のように書けるようにしています。この中で何をするかは、もうほとんど明らかですね。

    Node Node::appendChild(Node newChild)
    {
        Any arguments_[1];

        arguments_[0] = newChild;
        return message_(0x139bc932, "appendChild", 1, arguments_).toObject();
    }

こういった関数をひとつひとつ手で書いていてはキリがないので、Web IDLの定義ファイルから自動でコードを生成するツールを用意しています。それが『Web IDLコンパイラ』と呼ばれているもので、ESウェブ ブラウザで利用しているWeb IDLコンパイラはesidl(リンク先の記載はかなり前のバージョンのものなので、そのうち改訂します)と呼んでいます。コンパイラと名前は付いているものの、実際に出力するのはこのようなC++のソースファイルです。

ブリッジ

全般的にいまのところウェブ ブラウザはJavaScript以外の言語を使って実装されている部分が多いので、 JavaScriptで何かする場合にはJavaScriptからの呼び出しをC++の呼び出しに変換したり、あるいはC++からの呼び出しをJavaScriptの関数の呼び出しに変換したり、といった処理が必要になります。こういった処理を行う部分を『ブリッジ』と呼んでいます。C++とJavaScriptの間に橋を架けて、相互に行き来できるようにするわけです。
ESウェブ ブラウザではメッセージが基本になっているので、ブリッジの役割は、いきなり複数の言語間をつなげるのではなくて、各言語に合わせてメッセージを変換する、ということになります。
C++の関数呼び出しをメッセージに変換する部分は既に見ました。逆に受信したメッセージをC++のメンバー関数呼び出しにする部分はこのような感じになっています。

template <class IMP>
static Any dispatch(IMP* self, unsigned selector, const char* id, int argumentCount, Any* arguments)
{
    switch (selector) {
・・・snip・・・
     case 0x139bc932:
        if (argumentCount == 1)
            return self->appendChild(arguments[0].toObject());
・・・snip・・・

もちろんこのようなコードもWeb IDLコンパイラが自動生成するので、直接手で書く必要はありません。ESウェブブラウザのNodeの実装は単純に、

    class NodeImp : ObjectMixin<NodeImp, EventTargetImp>
    {
    ・・・snip・・・

        Node appendChild(Node newChild) {

    ・・・snip・・・

といった具合になるだけで、特にメッセージのことは意識する必要はありません。NodeImpとなっていることからも想像が付くように、Web IDLコンパイラが生成するのは基本的にはpimplイディオムの外側のようなものです。ObjectMixin<>って何、という話はいつかまた別の機会に。いまは、

    class NodeImp : public EventTargetImp

と書くかわりにObjectMixin<>を挟むという約束だと思っておいてください。

JavaScript用のブリッジ

C++側でメッセージに変換したり、メッセージからメンバー関数呼び出しに変換する部分の概要は説明したので、次はJavaScriptとメッセージを交換する部分です。
SpiderMonkeyV8といったJavaScriptエンジンでは、C/C++言語のプログラムから操作出きるようにそれぞれJSAPIV8 APIというAPIを提供しています。ですのでJavaScript用のブリッジという場合にはメッセージとこれらのAPIを繋ぐことになります。
JSAPIであれば、メッセージからJavaScriptの関数の呼び出しは、

    Any ProxyObject::message_(uint32_t selector, const char* id, int argc, Any* argv) {
    ・・・snip・・・
            jsval arguments[0 < argc ? argc : 1];

            for (int i = 0; i < argc; ++i)
                arguments[i] = convert(jscontext, argv[i]);

            if (JS_CallFunctionName(jscontext, jsobject, id, argc, arguments, &result) == JS_TRUE)
                return convert(jscontext, result);
    ・・・snip・・・

といった具合に、Any型の値をJSAPIのany型に相当するjsvalに変換して、JS_CallFunctionNameを呼び出すことで実現できます。
逆に、JavaScript側からC++のコードを呼び出す場合には、JSClassのcall関数で、

    JSBool operation(JSContext* cx, uintN argc, jsval* vp) {
        if (JSObject* obj = JS_THIS_OBJECT(cx, vp)) {
            ObjectImp* native = static_cast<ObjectImp*>(JS_GetPrivate(cx, obj));
            Any arguments[argc];
            for (unsigned i = 0; i < argc; ++i)
                arguments[i] = convert(cx, JS_ARGV(cx, vp)[i]);
            Any result = native->message_(NativeClass::getHash(cx, vp, N), 0, argc, arguments);
            JS_SET_RVAL(cx, vp, convert(cx, result));
            return JS_TRUE;
        }
        return JS_FALSE;
     }

といった具合に、今度はjsvalの値をAnyの値に変換して、message_でメッセージを送信することで実現できます。
V8 APIの場合も考え方はまったく同様で、v8のHandle<Value> とAnyを相互に変換してFunction::Callやmessage_を呼び出すだけです。

まとめ

今回は簡単にES ウェブ ブラウザのメッセージング アーキテクチャ、Web IDLの言語バインディングの話題、それからC++およびJavaScript用のブリッジの実装についてまとめてみました。
Web IDLは、ようやくW3Cの勧告の候補版ができようとしているところで、CSS 2.1のようなテストスイートなどもまだ用意されていません。仕様書からただ実装しただけでは実際の動きは実装したベンダーによってバラバラになってしまう、という点はCSSでも実証済みだと思うので、実際に仕様が落ち着くまでにはまだ少し時間がかかりそうな気もします。それでも既に非常に便利なものになっているのは間違いありません
ES ウェブ ブラウザのC++11バインディングも、まだWeb IDLの最新の仕様のすべては取りこめていなくて、またパフォーマンス面でも修正していきたい部分がだいぶ残っています。ES ウェブ ブラウザでは、ソースツリー全体をまるごと修正、といった作業も特に問題なく進められてしまいますので、こうしたらもっといい、といった提案なども、もしあればぜひお知らせください。

2012/4/10 追記

ESウェブブラウザ内のJSAPIブリッジの実装はbridge.cppに、V8ブリッジの実装はbridgeV8.cppになります。V8を組み込んだES ウェブブラウザのバイナリ ファイルは ScriptV8.test という名前になっています。

ScriptV8.test (r2599)ScriptV8.test (r2599)

なお、bridgeV8.cppで利用しているV8 APIのCallAsFunction関数はV8のバージョン3.3.5で追加された関数のようで、ubuntuで最初から利用可能になるのはprecise(12.04)からのようです。もうすぐリリースみたいですね。