V8 で C++ から JS Object のプロパティを列挙したい

C++ で V8 を拡張する関数とか書いていると,JavaScript から Object(というか,今回のコンテキストではざっくりいうと Hash 的なもの)をわたしてあれこれしたい,という欲求がでてきます。たとえば Object から apr_table_t に変換したい,とかね。


もっと単純に,

var hash = {
    field1: false,
    field2: 1,
    field3: 'abc'
};

// show_props(hash);
//      /*
//          みたく C++ の関数 show_props を呼びたい;
//          以下のようなことをする関数ね
//       */

for (var key in hash) {
    System.out.println(key);
}

みたいなコードを動かしたいとします。

ところが v8.h での class Object のインタフェースを眺めてみても,プロパティの列挙に使えそうなインタフェースはありません。

うーむと思って,Issue リストを眺めてたら……

Reported by matt...@trebex.net, Sep 06 (5 days ago)

There doesn't seem to be a way of listing the properties of an object from
C++, without evaluating a for..in loop.

    • -

Comment 1 by christian.plesner.hansen, Sep 08 (3 days ago)

We should have an API function for enumerating properties.

33 - API method to enumerate properties - v8 - Monorail

ええ,まったくもって you should have でございます。


ただ,同情というかなるほどなぁと思ったのですが,プロパティの列挙に関して ECMA-262 の仕様を調べてみると,12.6.4 The for-in Statement でちらりとでてくるのみです。

生成規則 IterationStatement : for ( LeftHandSideExpression in Expression ) Statement は、次のように評価される:

  1. Expression を評価。
  2. GetValue(Result(1)) を呼出す。
  3. ToObject(Result(2)) を呼出す。
  4. V = empty とする。
  5. DontEnum 属性を持たない、 Result(3) の次のプロパティの名前を取得する。そのようなプロパティが存在しないならば、 ステップ 14 へ。

以下略

12.6.4 The for-in Statement

プロパティの列挙に関わるのは「Result(3) の次のプロパティの名前を取得する」という文しかないです。だから内部的な実装しかないというのは,まぁ理解できます*1


ともかく,ない袖は振れないので,http://d.hatena.ne.jp/tokuhirom/20080907/1220799397 で紹介されている Google グループ をもとに,プロパティを列挙する関数を JavaScript で書いて,それを C++ 側から呼び出すことにしました。

プロパティを列挙する関数といってもたいしたことはなくて,

function _enum_properties_(v) {
    var a = [];

    for (var k in v)
        a.push(k);

    return a;
}

こんな感じのものです。ENUMerable じゃないプロパティを取得できないとか,プロトタイプチェーンをたどってキーを列挙しちゃうよ(ですよね……たしか)とか問題はありますが,とりあえずこれでも C++ から呼べたら便利かな,と。


この JavaScript 関数をエンジンに登録するソースは以下のような感じです。

static Handle<Value> execute_source(Handle<Context>, const char *);

static const char *
enum_properties_source(void)
{
    return
        "function (v) {"                "\n"
        "	var a = [];"            "\n"
        "	for (var k in v)"       "\n"
        "		a.push(k);"     "\n"
        "	return a;"              "\n"
        "};"                            "\n"
        ;
}

static Handle<Value>
enum_properties_function(Handle<Context> context)
{
    return execute_source(context, enum_properties_source());
}

static bool
register_enum_properties(Handle<Context> context)
{
    Handle<Value> enum_properties
        = enum_properties_function(context);

    if (enum_properties.IsEmpty()) {
        fputs("failed to register enum_properties.\n", stderr);
        return false;
    }
    context->Global()->Set(String::New("_enum_properties_"), enum_properties);

    return true;
}

static Handle<Array>
call_enum_properties(Handle<Object> any, Handle<Value> target)
{
    Handle<Function> func
        = Handle<Function>::Cast(any->Get(String::New("_enum_properties_")));

    Handle<Value> argv[1] = { target };

    Handle<Value> result = func->Call(func, 1, argv);
    return Handle<Array>::Cast(result);
}

Context のグローバルオブジェクト(のテンプレート,ではないことにやや注意)に,_enum_properties_ としてさきほどの関数を登録しています。なので,この関数を呼び出すときは,「なんかしらのオブジェクト」から「_enum_properties_」を取得すれば,(結果的にプロトタイプチェーンのルートたるグローバルオブジェクトから)ひっぱってこれます。上記の引数では Handle<Object> any を要求しています。C++InvocationCallback から呼び出す場合,ArgumentsThis() あたりを与えてやればよろしい。

返り値は Handle<Array> なので Length() も使えるし,数値インデックスで値を Get() することもできます。これで C++ からまともに扱えるようになった,と。


残りのソースです。

#include <stdio.h>
#include <v8.h>
using namespace v8;

static Handle<Value>
execute_source(Handle<Context> context, const char *source)
{
    //HandleScope scope;
    TryCatch try_catch;

    Context::Scope context_scope(context);

    Handle<Script> script
        = Script::Compile(String::New(source), Undefined());

    if (script.IsEmpty()) {
        String::AsciiValue error(try_catch.Exception());
        fprintf(stderr, "compile error: %s\n", *error);
        return Undefined();
    }
    else {
        Handle<Value> result = script->Run();

        if (result.IsEmpty()) {
            String::AsciiValue error(try_catch.Exception());
            fprintf(stderr, "execute error: %s\n", *error);
            return Undefined();
        }
        else {
            return result;
        }
    }
}

/*
    このへんにさきほどの enum_properties まわりをいれる
 */

static Handle<Value>
show_props_(const Arguments &args)
{
    if (args.Length() < 1)
        return Undefined();

    Handle<Array> props = call_enum_properties(args.This(), args[0]);

    for (uint32_t i = 0; i < props->Length(); i ++) {
        fprintf(stdout, "[%u]: '%s'\n",
                    i,
                    * String::AsciiValue(props->Get(Uint32::New(i)))
        );
    }

    return Undefined();
}

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

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

    global->Set(String::New("show_props"), FunctionTemplate::New(show_props_));

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

    if (! register_enum_properties())
        return 1;

    Handle<Value> result
        = execute_source(
            context,
            "var hash = {"                      "\n"
            "    field1: false,"                "\n"
            "    field2: 1,"                    "\n"
            "    field3: 'abc'"                 "\n"
            "};"                                "\n"
                                                "\n"
            "show_props(hash);"                 "\n"
        );

    if (result.IsEmpty())
        return 1;

    return 0;
}

このサンプルでは show_props_() という C++ の関数で Object のプロパティ列挙を使っています。

実行すると,

% ./enumprops

[0]: 'field1'
[1]: 'field2'
[2]: 'field3'

無事列挙できました。


なんともまわりくどい手ですね。はやくきちんとした API が実装されるといいなぁー。

*1:V8 のコードのインタフェースは,意外にも?実直に ECMA-262 の仕様に合わせてあります。