AJAJA: Systemモジュールを拡張してsleepできるようにしてみよう

AJAJAのSystemモジュールがロードされるまでの制御フローは大よそ理解できたので、今度はSystemモジュールを拡張してsleepできるようにしてみました。なぜsleepを取り上げたかと言うと、

  • 入出力を伴わないので簡単に実装できそう
  • JavaScriptから下層レイヤーの機能を制御して、SpiderMonkey自身を停止させてみたかった

という2点が大きな理由です。
案外簡単に機能拡張ができたので、その方法を紹介したいと思います。なお、今回はrev #30のソースコードを用いました。

規則性から拡張に必要なコードを推測する

System/System.cを眺めていると、追加したい関数のパターンが見えてきます。JavaScriptから利用されるどの関数(例えば、system_exit関数)も、以下のようなパターンを持っていることがわかります。関数の戻り値の型はJSBoolで、関数名には「system_」とうprefixを付ける必要がありそうです。また関数の引数は、「JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval」になりそうです。(argvには、引数に渡されたデータが順番に入っています。例えば、system_puts関数の場合、System.puts("hello")が呼び出されたとしたら、argv[0]には、"hello"を表現するJavaScriptの値(jsval)のポインタが入っています。)

static JSBool
system_funcname(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
    (処理)
    return JS_TRUE;
}

以下の構造体も重要そうです。この構造体は、JavaScriptの世界とC言語の世界を結びつける重要な働きをしています。ここで注目したいのは「/* {name, call, nargs}, */」という親切なコメントです。このコメントから各要素が何を意味しているのか、大よそ推測できます。例えば、sysytem_exit関数は、JavaScirptの世界からSystem.exitで呼び出され、その引数は1つである、と読めます。

struct JSFunctionSpec funcs[] = {
    /* {name, call, nargs}, */
    {"exit",        system_exit,       1},
    {"gets",        system_gets,       0},
    {"puts",        system_puts,       1},
    {"getenv",      system_getenv,     1},
    {"setenv",      system_setenv,     2},
    {"readFile",    system_readFile,     2},
    {0}
};

JavaScriptの世界からC言語の世界へデータ変換

manによるとsleep関数は、unsigned intの引数をとるようです。

% man 3 sleep
SLEEP(3)                   Linux Programmer's Manual                  SLEEP(3)

NAME
       sleep - Sleep for the specified number of seconds

SYNOPSIS
       #include <unistd.h>

       unsigned int sleep(unsigned int seconds);

DESCRIPTION
       sleep()  makes  the  current  process  sleep until seconds seconds have
       elapsed or a signal arrives which is not ignored.

RETURN VALUE
       Zero if the requested time has elapsed, or the number of  seconds  left
       to sleep.

CONFORMING TO
       POSIX.1

BUGS
       sleep()  may  be implemented using SIGALRM; mixing calls to alarm() and
       sleep() is a bad idea.

       Using longjmp() from a signal handler  or  modifying  the  handling  of
       SIGALRM while sleeping will cause undefined results.

SEE ALSO
       signal(2), alarm(2)

GNU                               1993-04-07                          SLEEP(3)


ここで、System/System.cにある他の関数に目を向けると、JS_ValueToString関数によって、JavaScriptの世界でのstringをC言語の世界での文字列表現に変換しているコードが多数見つかります。文字列の変換があるのならば、整数の変換もありそうです。

そこで、整数の変換だから、恐らく「ToInt」という文字列が含まれているだろうと予想し、以下のようにgrepしてみました。(マニュアルを読んだ方が確実です。でもgrepの方が早いですw) grepしてみると、全く関係のないSQLiteのコードがマッチしたので、grepのvオプションでそれらをフィルタリングしました。include/jsapi.hの384行目に「JS_ValueToInt32(JSContext *cx, jsval v, int32 *ip)」という、お手ごろそうな関数が見つかりました。

% cd ajaja-build/bin/ajaja
% grep -rn "ToInt" . | grep -v sqlite
Binary file ./SQLite/SQLite.so matches
./include/jsapi.h:366: * for ToInt32.
./include/jsapi.h:384:JS_ValueToInt32(JSContext *cx, jsval v, int32 *ip);
./include/jsnum.h:201: * for ToInt32.
./include/jsnum.h:225:js_ValueToInt32(JSContext *cx, jsval v, int32 *ip);
./include/jsnum.h:239:js_DoubleToInteger(jsdouble d);
Binary file ./ajaja-0.2.i386/bin/ssjs matches
Binary file ./ajaja-0.2.i386/bin/asp_js matches
Binary file ./ajaja-0.2.i386/lib/js/0.2/SQLite.so matches

include/jsapi.hを見てみると、先ほどgrepで見つけたJS_ValueToInt32関数のすぐ上に、もっとふさわしい関数が見つかりました。sleep関数は、unsigned intの引数をとることから、JavaScriptの整数を32ビット型のunsigned intに変換する「JS_ValueToECMAUint32(JSContext *cx, jsval v, uint32 *ip)」を使った方が良さそうです。

% vim include/jsapi.h
[...]
/*
 * Convert a value to a number, then to an int32, according to the ECMA rules
 * for ToInt32.
 */
extern JS_PUBLIC_API(JSBool)
JS_ValueToECMAInt32(JSContext *cx, jsval v, int32 *ip);

/*
 * Convert a value to a number, then to a uint32, according to the ECMA rules
 * for ToUint32.
 */
extern JS_PUBLIC_API(JSBool)
JS_ValueToECMAUint32(JSContext *cx, jsval v, uint32 *ip);

/*
 * Convert a value to a number, then to an int32 if it fits by rounding to
 * nearest; but failing with an error report if the double is out of range
 * or unordered.
 */
extern JS_PUBLIC_API(JSBool)
JS_ValueToInt32(JSContext *cx, jsval v, int32 *ip);
[...]

完成したコード

最終的に、以下のようなコードを実装しました。ここまで説明すれば、もう何も説明しなくても理解できると思います。予想していた通り、少しの変更で、追加したい機能を実装できました。

% svn diff System/System.c
Index: System/System.c
===================================================================
--- System/System.c     (revision 30)
+++ System/System.c     (working copy)
@@ -182,6 +182,21 @@
     return JS_FALSE;
 }

+static JSBool
+system_sleep(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
+{
+    JSBool ret;
+    uint32 seconds;
+
+    ret = JS_ValueToECMAUint32(cx, argv[0], &seconds);
+    if (!ret)
+        return JS_FALSE;
+
+    sleep(seconds);
+
+    return JS_TRUE;
+}
+

 static JSClass system_class = {
     "System",
@@ -210,6 +225,7 @@
     {"getenv",      system_getenv,     1},
     {"setenv",      system_setenv,     2},
     {"readFile",    system_readFile,     2},
+    {"sleep",       system_sleep,      1},
     {0}
 };

ビルド

以下のコマンドでビルドできます。ビルドした共有ライブラリ(System.so)は、ssjsから見える所にコピーしておきます。

% cd System
% make && sudo cp System.so /usr/lib/js/0.2/

実行結果

以下のようなテストコードを実行してみました。

% vim ~/sleep.js
use('System');

System.puts("Hello\n");
System.sleep(5);
System.puts(", world\n");

一応、約5秒間sleepすることを確認できました。

% time ssjs ~/sleep.js
Hello
, world

real    0m5.008s
user    0m0.000s
sys     0m0.010s