V8 (Google JavaScript Engine) を embed した感想とかあれこれ

なぜ CodeRepos に登録しないのか

べつだん深意や確執があるわけじゃなくて,華々しく 500 人めのコミッタになろうと思ったら,現在 461 人だったからです。ということで 38 人の方々,コミッタ登録してください ;P

冗談はともかくおまえの書いた汚いコードを早く添削したいんじゃという方がいらっしゃったら,代理でいれといて構いません。

Acme::JavaScript::V8Perl XS)を書くときに苦労したこと

New ていうのが XS での define 値だったので困りました。V8 側だと,new / delete するんじゃなくて Class::New() する流儀なので。

ですから,#undef New してあります。他のマクロで使われていたらマズいなぁと思いますが,動いたからよしとします。

エンベッダーズガイド

Handle<T> とか Local<T> とか Persistent<T> とか

Handle<T> とかっていうのは,いわゆるスマートポインタ的なものです。

なお Handle<T> が抽象クラスで,Local<T>Persistent<T> はそれらのサブクラスになります。だからたいていの場合,自作関数の戻り値は,Handle<T> にしとけばいいでしょう。

Local<T>HandleScope によって自分でスコープを指定できる auto_ptr 的なものです。実際には参照されているかどうかも加味してくれる(被参照がなくなれば GC される)ので,関数(スコープ)抜けたらなくなるのかー,とビビる必要はありません。たぶん。

Persistent<T> は,いちど V8 の世界を抜けてもそのハンドルを保持しとく必要がある場合に使うものであり,必要がなくなったら自分で Dispose() する必要があります。基本的に使う必要はありません。V8 のほとんどの API*::New() とか)の戻り値は Local<T> ベースですし。

Acme::JavaScript::V8 の場合だと,

  • コンテキスト生成時に Persistent<T> で保持,Perl インタプリタに制御を返す
  • そのコンテキストを利用してあれこれ(コンパイルしたり実行したり)するときに使う変数は,Local<T>

つまりほとんど Local<T> です。

mod_v8pp の場合だと,リクエストごとにコンテキストを生成,ソースコンパイル,実行,を行っているので Local<T> しか使っていません。

そのたあれこれ

  • そんな便利な Handle<T> 系テンプレートですが,自作クラスをその対象とすることはほぼ無理
    • プリミティブは V8 library が自力でヒープから取得して初期化したりしてる
    • それで GC の対象にしてる
  • コンパイル,というのは,字句的なコンパイルだけやってるわけではないっぽい

後者はどういうことか,というと,

var a = 1;
var b = a + c;      /* 'c' is not defined (compile time error) */
foo();              /* 'foo' is not defined (compile time error) */

これらがコンパイル時にエラーになります。同一コンテキストで,

var d = a + 5;

コンパイルする場合はエラーにはなりません(a は同一コンテキストで先ほど定義されたから)。

関数や変数が定義されているかどうかを実行フェーズで判断するインタプリタ処理系が多いんですけど(そのほうが実行時のダイナミズムを確保しやすいから),ちょっと意外でした。


実使用上は,コンテキスト生成時に指定するグローバル ObjectTemplateNamedPropertyGetter 等を利用すると,なんとかなるのかなぁ。

Object に内部的な値を格納する方法

v8::Objectに内部保持するnativeな値(例えばハンドルとか)を保持するには

    FILE* fp = fopen(...);
    v8::Local<v8::Object> obj = v8::Object::New();
    obj->Set(v8::String::New("value"), v8::External::New((void*)fp));

という様に、v8::Externalでメンバを追加してやれば良い。

Big Sky :: javascript v8エンジンでv8::Objectに内部的な値を格納する方法が分かった。

もちろんそれで動くんですけど,外にみせたくない内部値を扱うには,ObjectTemplate の InternalField を利用するほうが外にさらされないので better だと思います。XS の MAGIC EXT みたいな感じです。

ファイルハンドルを操るばあいの実例。ほんとは close の際に InternalField[0] を NULL にしたり,妥当性をチェックするべきなんですが,とりあえず。

#include <stdio.h>

#include <v8.h>
using namespace v8;

static Handle<Value>
_p(const Arguments& args)
{
    HandleScope scope;
    String::AsciiValue str(args[0]);
    fputs(*str, stdout);
    return Undefined();
}

static Handle<Value>
_open(const Arguments& args)
{
    HandleScope scope;

    if (args.Length () != 2)
        return ThrowException(String::New("usage: open(fname, mode)"));
    String::AsciiValue fname(args[0]);
    String::AsciiValue mode(args[1]);

    FILE *fh = fopen(*fname, *mode);
    if (! fh)
        return ThrowException(String::New("cannot open file"));

    args.This()->SetInternalField(0, External::New(fh));

    return args.This();
}

static Handle<Value>
_close(const Arguments& args)
{
    HandleScope scope;

    Local<Value> intl_field = args.This()->GetInternalField(0);
    FILE *fh
        = reinterpret_cast<FILE *>(Handle<External>::Cast(intl_field)->Value());

    fclose(fh);

    return True();
}

static Handle<Value>
_gets(const Arguments& args)
{
    HandleScope scope;
    static char buffer[1024];

    Local<Value> intl_field = args.This()->GetInternalField(0);
    FILE *fh
        = reinterpret_cast<FILE *>(Handle<External>::Cast(intl_field)->Value());

    if (! fgets(buffer, 1024, fh)) {
        return Undefined();
    }

    return String::New(buffer);
}

static Handle<FunctionTemplate>
create_file_object_template(void)
{
    HandleScope scope;

    Handle<FunctionTemplate> ft = FunctionTemplate::New();
    Handle<ObjectTemplate>   ot = ft->InstanceTemplate();

    ot->Set(String::New("open"),  FunctionTemplate::New(_open));
    ot->Set(String::New("close"), FunctionTemplate::New(_close));
    ot->Set(String::New("gets"),  FunctionTemplate::New(_gets));

    ot->SetInternalFieldCount(1);

    return ft;
}

int
main(int argc, char *argv[])
{
    HandleScope scope;
    TryCatch try_catch;

    Handle<ObjectTemplate> global
        = ObjectTemplate::New();

    // debugging purpose
    global->Set(String::New("p"), FunctionTemplate::New(_p));

    global->Set(
        String::New("File"),
        create_file_object_template(),
        PropertyAttribute(ReadOnly | DontDelete)
    );

    Handle<Context> context = Context::New(NULL, global);

    Context::Scope context_scope(context);

    Handle<String> source = String::New(
        "var f = new File();\n"
        "\n"
        "f.open(\"test.txt\", \"r\");\n"
        "var s;\n"
        "while (s = f.gets()) {\n"
        "    p(s);\n"
        "}\n"
        "f.close();\n"
    );
    Handle<Script> compiled = Script::Compile(source, Undefined());

    if (compiled.IsEmpty()) {
        String::AsciiValue error(try_catch.Exception());

        fprintf(stderr, "compile error: %s\n", *error);
    }
    else {
        Handle<Value> result = compiled->Run();

        if (result.IsEmpty()) {
            String::AsciiValue error(try_catch.Exception());

            fprintf(stderr, "execute error: %s\n", *error);
        }
        else {
        }
    }

    return 0;
}

destruction 時に自動的に close() するようにしたかったんですが,やりかたがわかりませんでした((Persistent<Object>::MakeWeak() するのもなんか違うような気が……。SetGlobalGCPrologueCallback() でなんとかするのかなぁ。それも違うよなぁ。))。とりあえず明示的に close() してください。

いったん FunctionTemplate を作ってそこの InstanceTemplate を利用している理由は,JavaScript 側で var f = new File(); みたいに関数オブジェクトを利用した new を利用させる(ほうが見目麗しい)からです。

new する必要がない場合,たとえば,すでに存在する外部リソースを JavaScript で扱う場合は,直接 ObjectTemplate::New() すればいいです(もちろん FunctionTemplate 経由でもいいんですけど)。

サンプルとして Apacheapr_table_t * 型の(ハッシュ)テーブルを V8 JavaScript で扱う例をあげます。ちまちま JavaScript Array を構築してもいいんですけど,ObjectTemplate を利用するとプロパティアクセサを自分で指定できるので,いい感じです。

static Handle<Value>
apr_table_property_getter(Local<String> property, const AccessorInfo& info)
{
    Handle<Value> intl_field = info.This()->GetInternalField(0);
    apr_table_t *table
        = (apr_table_t *) Handle<External>::Cast(intl_field)->Value();

    String::AsciiValue key(property);

    const char *value = apr_table_get(table, *key);

    if (value)
        return String::New(value);
    else
        return Undefined();
}

/******* snip snip snip *******/

static Handle<ObjectTemplate>
create_apr_table_ro_template(void)
{
    Handle<ObjectTemplate> ot = ObjectTemplate::New();

    ot->SetNamedPropertyHandler(apr_table_property_getter,
                                NULL,
                                apr_table_property_query,
                                NULL,
                                apr_table_property_enumerator);

    ot->SetInternalFieldCount(1);

    return ot;
}

static Handle<Object>
create_apr_table(Handle<ObjectTemplate>ot, apr_table_t *table)
{
    Handle<Object> object = ot->NewInstance();
    object->SetInternalField(0, External::New(table));

    return object;
}

/******* snip snip snip *******/

void sample(request_rec *r);
{
    apr_table_t *table = r->headers_in;

    Handle<ObjectTemplate> apr_table_ro_template
        = create_apr_table_ro_template();

    Handle<Value> headers_in
        = create_apr_table(apr_table_ro_template, table);

    /*
        これで headers_in を JavaScript で扱えるようになりました
     */
}

本筋とは関係ないですが,SetNamedPropertyHandler() するときに任意(といっても Handle<Value> 型)の Data をわたせます。が,あくまで対象は ObjectTemplate です。だから,あんまり用途はありません(いくつかの違う種類の class を同じ callback で利用する場合くらい)。各インスタンスごとの秘密データは,さきにあげたように InternalField 経由で設定しておくべき。