SpiderMonkeyでJavaScriptからCの関数を呼び出す
オブジェクトへ関数を定義する
SpiderMonkeyを使えば,JavaScriptからC言語で書かれた関数を呼び出すことが出来る.サンプルによると,JSFunctionSpecの配列を定義して,JS_DefineFunctions関数を呼び出すことで,C言語の関数がJavaScriptの任意にオブジェクトに定義されるようだ.実際のコードは以下の通りとなる.(このコードはC言語じゃなくて,C++言語だが基本的な違いはない)
#include <js/jsapi.h> #include <stdio.h> #include <stdlib.h> #include <time.h> #include <iostream> JSBool myjs_rand(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval); JSBool myjs_srand(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval); void reportError(JSContext *cx, const char *message, JSErrorReport *report); static JSClass global_class = { "global", JSCLASS_GLOBAL_FLAGS, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub, JSCLASS_NO_OPTIONAL_MEMBERS }; static JSFunctionSpec myjs_global_functions[] = { JS_FS("rand", myjs_rand, 0, 0, 0), JS_FS("srand", myjs_srand, 0, 0, 0), JS_FS_END }; /* * A simple JS wrapper for the rand() function, from the C standard library. * This example shows how to return a number from a JSNative. * This is nearly the simplest possible JSNative function. */ JSBool myjs_rand(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { return JS_NewNumberValue(cx, rand(), rval); } /* * A wrapper for the srand() function, from the C standard library. * This example shows how to handle optional arguments. */ JSBool myjs_srand(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { uint32 seed; if (!JS_ConvertArguments(cx, argc, argv, "/u", &seed)) return JS_FALSE; /* If called with no arguments, use the current time as the seed. */ if (argc == 0) seed = time(NULL); srand(seed); *rval = JSVAL_VOID; /* return undefined */ return JS_TRUE; } // The error reporter callback. void reportError(JSContext *cx, const char *message, JSErrorReport *report) { fprintf(stderr, "%s:%u: %s\n", report->filename ? report->filename : "<no filename>", (unsigned int) report->lineno, message); } int main(int argc, char *argv[]) { // JS variables. JSRuntime *rt; JSContext *cx; JSObject *global; /* Create a JS runtime. */ rt = JS_NewRuntime(8L * 1024L * 1024L); if (rt == NULL) { std::cerr << "failed to create a JS runtime" << std::endl; return -1; } /* Create a context. */ cx = JS_NewContext(rt, 8192); if (cx == NULL) { std::cerr << "failed to create a context" << std::endl; return -1; } JS_SetOptions(cx, JSOPTION_COMPILE_N_GO); JS_SetVersion(cx, JSVERSION_LATEST); JS_SetErrorReporter(cx, reportError); /* Create the global object. */ global = JS_NewObject(cx, &global_class, NULL, NULL); if (global == NULL) { std::cerr << "failed to create the global object" << std::endl; return -1; } /* Populate the global object with the standard globals, like Object and Array. */ if (! JS_InitStandardClasses(cx, global)) { std::cerr << "failed to populate the global object." << std::endl; return -1; } /* Define global functions */ if (!JS_DefineFunctions(cx, global, myjs_global_functions)) { std::cerr << "failed to define functions." << std::endl; return -1; } JS_DestroyContext(cx); JS_DestroyRuntime(rt); JS_ShutDown(); return 0; }
以下のようにコールバック関数を定義し,実際の動作はコールバック関数中に記述する.ここでは,C言語のrand関数とsrand関数をラップしている.
JSBool myjs_rand(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval); JSBool myjs_srand(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
次に,コールバック関数群の配列を定義する.それは以下の部分である.
static JSFunctionSpec Myjs_global_functions[] = { JS_FS("rand", myjs_rand, 0, 0, 0), JS_FS("srand", myjs_srand, 0, 0, 0), JS_FS_END };
最後に,JS_DefineFunctions関数を呼び出し,オブジェクトに関数を定義する.以下のコードでglobalオブジェクトに関数を定義している.
if (!JS_DefineFunctions(Cx, global, myjs_global_functions)) { std::cerr << "failed to define functions." << std::endl; return -1; }
グローバルオブジェクトに関数を定義しているので,JavaScript側では,
rand(); srand();
と呼び出すことが出来る.
JS_DefineFunctions関数の実装
JS_DefineFunctions関数はSpiderMonkeyのjsapi.cの4418行から定義されており,実際のコードは以下の通りである.(SpiderMonkeyのバージョンは1.8-rc1である)
/* jsapi.c */ 4418 JS_PUBLIC_API(JSBool) 4419 JS_DefineFunctions(JSContext *cx, JSObject *obj, JSFunctionSpec *fs) 4420 { 4421 uintN flags; 4422 JSObject *ctor; 4423 JSFunction *fun; 4424 4425 CHECK_REQUEST(cx); 4426 ctor = NULL; 4427 for (; fs->name; fs++) { 4428 flags = fs->flags; 4429 4430 /* 4431 * Define a generic arity N+1 static method for the arity N prototype 4432 * method if flags contains JSFUN_GENERIC_NATIVE. 4433 */ 4434 if (flags & JSFUN_GENERIC_NATIVE) { 4435 if (!ctor) { 4436 ctor = JS_GetConstructor(cx, obj); 4437 if (!ctor) 4438 return JS_FALSE; 4439 } 4440 4441 flags &= ~JSFUN_GENERIC_NATIVE; 4442 fun = JS_DefineFunction(cx, ctor, fs->name, 4443 (flags & JSFUN_FAST_NATIVE) 4444 ? (JSNative) 4445 js_generic_fast_native_method_dispatcher 4446 : js_generic_native_method_dispatcher, 4447 fs->nargs + 1, flags); 4448 if (!fun) 4449 return JS_FALSE; 4450 fun->u.n.extra = (uint16)fs->extra; 4451 fun->u.n.minargs = (uint16)(fs->extra >> 16); 4452 4453 /* 4454 * As jsapi.h notes, fs must point to storage that lives as long 4455 * as fun->object lives. 4456 */ 4457 if (!JS_SetReservedSlot(cx, FUN_OBJECT(fun), 0, PRIVATE_TO_JSVAL(fs))) 4458 return JS_FALSE; 4459 } 4460 4461 JS_ASSERT(!(flags & JSFUN_FAST_NATIVE) || 4462 (uint16)(fs->extra >> 16) <= fs->nargs); 4463 fun = JS_DefineFunction(cx, obj, fs->name, fs->call, fs->nargs, flags); 4464 if (!fun) 4465 return JS_FALSE; 4466 fun->u.n.extra = (uint16)fs->extra; 4467 fun->u.n.minargs = (uint16)(fs->extra >> 16); 4468 } 4469 return JS_TRUE; 4470 }
予想としては,JSObject *objのメンバ変数に,JSFunctionSpec *fsにある何らかの値を代入するものと考えられる.しかし,この関数ではobjへの代入は行われていない.
ここで,第三引数のJS_FunctionSpec *fsへ渡す配列を作ったことを思い出すために,その時のコードを見てみると,
static JSFunctionSpec Myjs_global_functions[] = { JS_FS("rand", myjs_rand, 0, 0, 0), JS_FS("srand", myjs_srand, 0, 0, 0), JS_FS_END };
となっている.このJS_FSはjsapi.hの1466行で定義されているマクロであり,以下のようになっている.
/* jsapi.h */ 1466 #define JS_FS(name,call,nargs,flags,extra) \ 1467 {name, call, nargs, flags, extra}
また,JSFunctionSpec構造体の定義はjsapi.hの1443行にあり,次のとおりである.
/* jsapi.h */ 1433 struct JSFunctionSpec { 1434 const char *name; 1435 JSNative call; 1436 #ifdef MOZILLA_1_8_BRANCH 1437 uint8 nargs; 1438 uint8 flags; 1439 uint16 extra; 1440 #else 1441 uint16 nargs; 1442 uint16 flags; 1443 1444 /* 1445 * extra & 0xFFFF: Number of extra argument slots for local GC roots. 1446 * If fast native, must be zero. 1447 * extra >> 16: If slow native, reserved for future use (must be 0). 1448 * If fast native, minimum required argc. 1449 */ 1450 uint32 extra; 1451 #endif 1452 };
すなわち,
JS_FS("rand", myjs_rand, 0, 0, 0),
とすると,
JSFunctionSpec.name = "rand" JSFunctionSpec.call = myjs_rand JSFunctionSpec.nargs = 0; JSFunctionSpec.flags = 0; JSFunctionSpec.extra = 0;
と構造体が初期化されることになる.
つまり,サンプルコードではJSFunctionSpec.flags = 0となるため,JS_DefineFunctions関数の4434行目のif文はfalseとなりif文の内部は実行されない.そのため,今回はこの中は気にしないことにする.従って,今回,JS_DefineFunctions関数内で注目すべきなのは,以下の部分となる.
/* jsapi.c */ 4463 fun = JS_DefineFunction(cx, obj, fs->name, fs->call, fs->nargs, flags); 4464 if (!fun) 4465 return JS_FALSE; 4466 fun->u.n.extra = (uint16)fs->extra; 4467 fun->u.n.minargs = (uint16)(fs->extra >> 16);
JS_DefineFunctions関数内では,objへの代入は行われておらず,JS_DefineFunction関数で実際の代入が行われていると考えられる.JS_DefineFunction関数は,jsapi.cの4472行目から定義されており,実際のコードは以下の通りとなる.
/* jsapi.c */ 4472 JS_PUBLIC_API(JSFunction *) 4473 JS_DefineFunction(JSContext *cx, JSObject *obj, const char *name, JSNative call, 4474 uintN nargs, uintN attrs) 4475 { 4476 JSAtom *atom; 4477 4478 CHECK_REQUEST(cx); 4479 atom = js_Atomize(cx, name, strlen(name), 0); 4480 if (!atom) 4481 return NULL; 4482 return js_DefineFunction(cx, obj, atom, call, nargs, attrs); 4483 }
ここで代入があるかと思いきや,まだである.この関数は,実質的に小文字で始まるjs_DefineFunction関数を呼び出しているだけである.ここでは"rand"や"srand"など関数の名前を表す文字列がJSAtomに変換されているが,それは後ほど見ることにする.
js_DefineFunction関数はjsfun.cの2080行から定義されており,コードは以下の通りとなる.
/* jsfun.c */ 2079 JSFunction * 2080 js_DefineFunction(JSContext *cx, JSObject *obj, JSAtom *atom, JSNative native, 2081 uintN nargs, uintN attrs) 2082 { 2083 JSFunction *fun; 2084 JSPropertyOp gsop; 2085 2086 fun = js_NewFunction(cx, NULL, native, nargs, attrs, obj, atom); 2087 if (!fun) 2088 return NULL; 2089 gsop = (attrs & JSFUN_STUB_GSOPS) ? JS_PropertyStub : NULL; 2090 if (!OBJ_DEFINE_PROPERTY(cx, obj, ATOM_TO_JSID(atom), 2091 OBJECT_TO_JSVAL(FUN_OBJECT(fun)), 2092 gsop, gsop, 2093 attrs & ~JSFUN_FLAGS_MASK, NULL)) { 2094 return NULL; 2095 } 2096 return fun; 2097 }
js_NewFunction関数か,OBJ_DEFINE_PROPERTY関数が怪しいように思われるので,それぞれ見ていく.まず,js_NewFunction関数であるが,これはjsfun.cの2020行以降で定義されている.
/* jsfun.c */ 2020 JSFunction * 2021 js_NewFunction(JSContext *cx, JSObject *funobj, JSNative native, uintN nargs, 2022 uintN flags, JSObject *parent, JSAtom *atom) 2023 { 2024 JSFunction *fun; 2025 2026 if (funobj) { 2027 JS_ASSERT(HAS_FUNCTION_CLASS(funobj)); 2028 OBJ_SET_PARENT(cx, funobj, parent); 2029 } else { /* JavaScriptのオブジェクトが生成される */ 2030 funobj = js_NewObject(cx, &js_FunctionClass, NULL, parent, 0); 2031 if (!funobj) 2032 return NULL; 2033 } 2034 JS_ASSERT(funobj->fslots[JSSLOT_PRIVATE] == JSVAL_VOID); 2035 fun = (JSFunction *) funobj; 2036 2037 /* Initialize all function members. */ 2038 fun->nargs = nargs; 2039 fun->flags = flags & (JSFUN_FLAGS_MASK | JSFUN_INTERPRETED); /* flags = 0だったので,このif文はfalseとなる */ 2040 if (flags & JSFUN_INTERPRETED) { 2041 JS_ASSERT(!native); 2042 JS_ASSERT(nargs == 0); 2043 fun->u.i.nvars = 0; 2044 fun->u.i.spare = 0; 2045 fun->u.i.script = NULL; 2046 #ifdef DEBUG 2047 fun->u.i.names.taggedAtom = 0; 2048 #endif 2049 } else { 2050 fun->u.n.native = native; 2051 fun->u.n.extra = 0; 2052 fun->u.n.minargs = 0; 2053 fun->u.n.clasp = NULL; 2054 } 2055 fun->atom = atom; 2056 2057 /* Set private to self to indicate non-cloned fully initialized function. */ 2058 FUN_OBJECT(fun)->fslots[JSSLOT_PRIVATE] = PRIVATE_TO_JSVAL(fun); 2059 return fun; 2060 }
js_DefineFunction関数では,
/* jsfun.c */ 2079 JSFunction * 2080 js_DefineFunction(JSContext *cx, JSObject *obj, JSAtom *atom, JSNative native, 2081 uintN nargs, uintN attrs) 2082 { ... 2086 fun = js_NewFunction(cx, NULL, native, nargs, attrs, obj, atom); ... 2097 }
と呼び出していたため,js_NewFunction関数の第二引数はNULLである.そのため,2027-2028行は実行されず,
/* jsfun.c */ 2030 funobj = js_NewObject(cx, &js_FunctionClass, NULL, parent, 0); 2031 if (!funobj) 2032 return NULL;
の部分が実行される.ここでは,js_NewObject関数を呼び出して,新たなJavaScriptのオブジェクトを生成していると考えられる.JavaScriptでは関数とはオブジェクトのことであることを思い出すと納得がいく.
2030行目では,js_NewObject関数の返り値をfunobj変数に格納している.返り値の型はJSObject構造体のポインタである.js_NewObject関数はjsobj.cの2459行目で定義されている.ここでは,関数の中身はみず,定義だけ見てみる.
/* jsobj.c */ 2459 JSObject * 2460 js_NewObject(JSContext *cx, JSClass *clasp, JSObject *proto, JSObject *parent, 2461 uintN objectSize)
また,JSObject構造体はjsobj.hの129行目より定義されており,
/* jsobj.h */ 129 struct JSObject { 130 JSObjectMap *map; 131 jsval fslots[JS_INITIAL_NSLOTS]; 132 jsval *dslots; /* dynamically allocated slots */ 133 };
JSFunction構造体はjsfun.hの66行目より定義されている.
/* jsfun.h */ 66 struct JSFunction { 67 JSObject object; /* GC'ed object header */ 68 uint16 nargs; /* maximum number of specified arguments, 69 reflected as f.length/f.arity */ 70 uint16 flags; /* bound method and other flags, see jsapi.h */ 71 union { 72 struct { 73 uint16 extra; /* number of arg slots for local GC roots */ 74 uint16 minargs;/* minimum number of specified arguments, used 75 only when calling fast native */ 76 JSNative native; /* native method pointer or null */ 77 JSClass *clasp; /* if non-null, constructor for this class */ 78 } n; 79 struct { 80 uint16 nvars; /* number of local variables */ 81 uint16 spare; /* reserved for future use */ 82 JSScript *script;/* interpreted bytecode descriptor or null */ 83 JSLocalNames names; /* argument and variable names */ 84 } i; 85 } u; 86 JSAtom *atom; /* name for diagnostics and decompiling */ 87 };
js_NewFunction関数では,js_NewObject関数から得られたポインタをJSFunctionポインタへキャストして,各種値を代入している.
もう一度,js_NewFunction関数を見てみると,以下のようになる.
/* jsfun.c */ 2020 JSFunction * 2021 js_NewFunction(JSContext *cx, JSObject *funobj, JSNative native, uintN nargs, 2022 uintN flags, JSObject *parent, JSAtom *atom) 2023 { 2024 JSFunction *fun; 2025 2026 if (funobj) { ... 2029 } else { 2030 funobj = js_NewObject(cx, &js_FunctionClass, NULL, parent, 0); ... 2033 } ... 2035 fun = (JSFunction *) funobj; 2036 2037 /* Initialize all function members. */ 2038 fun->nargs = nargs; 2039 fun->flags = flags & (JSFUN_FLAGS_MASK | JSFUN_INTERPRETED); 2040 if (flags & JSFUN_INTERPRETED) { ... 2049 } else { 2050 fun->u.n.native = native; /* ここで関数へのポインタが保存される */ 2051 fun->u.n.extra = 0; 2052 fun->u.n.minargs = 0; 2053 fun->u.n.clasp = NULL; 2054 } 2055 fun->atom = atom; /* ここで関数の名前が保存される */ ... 2060 }
flagsは0と初期化したことを思い出すと,js_NewFunction関数の2040行目はfalseとなる.2050行目で関数へのポンタが保存される.なお,JSNativeとはjspubtd.hの596行目でtypedefされている関数ポインタである.
596 typedef JSBool 597 (* JS_DLL_CALLBACK JSNative)(JSContext *cx, JSObject *obj, uintN argc, 598 jsval *argv, jsval *rval);
また,JSAtomは... と探したけれど定義されている箇所が見つからない...なぜだ...SpiderMonkey 1.7の場合,jsatom.hの55行目より定義されている.
/* jsatom.h of SpiderMonkey 1.7 */ 65 struct JSAtom { 66 JSHashEntry entry; /* key is jsval or unhidden atom 67 if ATOM_HIDDEN */ 68 uint32 flags; /* pinned, interned, and mark flags */ 69 jsatomid number; /* atom serial number and hash code */ 70 };
文字列からJSAtomへの変換は,前に見たJS_DefineFunction関数で行われいる.
/* jsapi.c */ 4472 JS_PUBLIC_API(JSFunction *) 4473 JS_DefineFunction(JSContext *cx, JSObject *obj, const char *name, JSNative call, 4474 uintN nargs, uintN attrs) 4475 { 4476 JSAtom *atom; ... /* atomとは原子という意味 */ 4479 atom = js_Atomize(cx, name, strlen(name), 0); ... 4483 }
js_Atomize関数はjsatom.cの713行目から定義されている.
/* jsatom.c */ 713 JSAtom * 714 js_Atomize(JSContext *cx, const char *bytes, size_t length, uintN flags) 715 { 716 jschar *chars; 717 JSString str; 718 JSAtom *atom; 719 720 /* 721 * Avoiding the malloc in js_InflateString on shorter strings saves us 722 * over 20,000 malloc calls on mozilla browser startup. This compares to 723 * only 131 calls where the string is longer than a 31 char (net) buffer. 724 * The vast majority of atomized strings are already in the hashtable. So 725 * js_AtomizeString rarely has to copy the temp string we make. 726 */ 727 #define ATOMIZE_BUF_MAX 32 728 jschar inflated[ATOMIZE_BUF_MAX]; 729 size_t inflatedLength = ATOMIZE_BUF_MAX - 1; 730 731 if (length < ATOMIZE_BUF_MAX) { /* bytesからinflatedへコピー */ 732 js_InflateStringToBuffer(cx, bytes, length, inflated, &inflatedLength); 733 inflated[inflatedLength] = 0; 734 chars = inflated; 735 } else { 736 inflatedLength = length; /* bytesがコピーされ返り値がそのコピー先 */ 737 chars = js_InflateString(cx, bytes, &inflatedLength); 738 if (!chars) 739 return NULL; 740 flags |= ATOM_NOCOPY; 741 } 742 743 JSFLATSTR_INIT(&str, (jschar *)chars, inflatedLength); 744 atom = js_AtomizeString(cx, &str, ATOM_TMPSTR | flags); 745 if (chars != inflated && str.u.chars) 746 JS_free(cx, chars); 747 return atom; 748 }
第二引数のconst char *bytesが"rand"など関数名を表す文字列へのポインタで,第三引数のsize_t lengthが文字列長である.732行目では文字列長に応じて,js_InflateStringToBuffer関数かjs_InflateString関数のどちらかが呼ばれている.
この二つの関数が行うのは,基本的にはbytesで渡した文字列の別バッファへのコピーである.したがって,元の関数名表す文字列があとで参照不可となったとしてもコピーが行われるため問題ない.
Cの関数をJavaScriptにエクスポートする際,サンプルコードではわざわざグローバルな配列を定義していたので,構造体などが永続的でないかと駄目なのかと思ったが,そうではなさそうである.文字列も途中で開放してよさそうだ.まぁ,文字列の場合はプログラム中に静的に書くのがほとんどだろうけれど.