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にエクスポートする際,サンプルコードではわざわざグローバルな配列を定義していたので,構造体などが永続的でないかと駄目なのかと思ったが,そうではなさそうである.文字列も途中で開放してよさそうだ.まぁ,文字列の場合はプログラム中に静的に書くのがほとんどだろうけれど.