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
( LeftHandSideExpressionin
Expression ) Statement は、次のように評価される:
- Expression を評価。
- GetValue(Result(1)) を呼出す。
- ToObject(Result(2)) を呼出す。
- V = empty とする。
- 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
から呼び出す場合,Arguments
の This()
あたりを与えてやればよろしい。
返り値は 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 が実装されるといいなぁー。