v8 (Google JavaScript Engine) を Apache のモジュールにしてみた

ちまたでは Google Chrome より v8 がアツいらしいので,やっつけでつくりました。Joke module です。

ふつーに CGI モジュール的につくってもよかったんですが,なんとなくテンプレート的にしました。ほんとうは mod_perl みたいにサーバのあらゆるところに hook をかけれるようにしたほうがおもしろいんでしょうけど。

<html>
    <head>
        <title><?js print('Hello, world!'); ?></title>
    </head>
    <body>
        <ul>
<?js
    for (var i = 0; i < 10; i ++) {
?>
            <li><?js print(i + 1); ?></li>
<?js
    }
?>
        </ul>
    </body>
</html>

UA のリクエストとか全然処理してない(エンジンにわたしていない)です。だから CGI 的には使えません(フォームの内容取得したりできないってこと)。

ややはまりしたところ

  • Apache は C で書いてあるのにたいして,v8 は C++ で書かれている。
    • なので,モジュールのビルド時には g++ を使う必要がある。リンク時も。
    • 外部インタフェースとか extern "C" する必要がある。
      • ほぼ全体を囲ってしまった。
  • 最初 -lv8 ってやったら Apache の再起動時にシンボルが見つからないといわれてエラー。
    • リンク時に -lv8 ではなく libv8.a でライブラリをまるかかえすると大丈夫になった。
      • んーとなんででしたっけ?PLT 経由で呼び出すようになっちゃうからかな。
  • リンカの引数の指定順に関係があったんですね。恥ずかしい……

c++ でダイナミックモジュールを書いて C (Apache) から読み込み,な形になってますけど,c++ としてのイニシャライザ,ファイナライザまわりはうまくいってるのかしら。ともかく実験としてはおもしろいけど実用としては不安が残ります*1

まーもっと真面目に作るなら,リクエストオブジェクトの引き渡しはもちろんのことながら,外部モジュールの include をできるようにしたり(ネームスペースはどうすればいいんだろ),ダイナミックオブジェクトをロードして JavaScript から呼び出せるようにしたりするべきでしょう。


ソースはこちら。エラー処理とかはしょりまくってます。
あうあう。クォーテーションのとこ \ エスケープ文字考慮してない……

#include "apr.h"
#include "apr_strings.h"
#include "apr_buckets.h"
#include "apr_lib.h"

#include "ap_config.h"
#include "httpd.h"
#include "http_config.h"
#include "http_request.h"
#include "http_core.h"
#include "http_protocol.h"
#include "http_main.h"
#include "http_log.h"

#include <v8.h>

#define V8PP_MAGIC_TYPE "application/x-httpd-v8pp"

extern "C" {

//module AP_MODULE_DECLARE_DATA v8pp_module;

static apr_status_t
put_precode(apr_bucket_brigade *bb, apr_brigade_flush flush, void *ctx)
{
    return
        apr_brigade_puts(bb, flush, ctx,
"var server = { response: { buffer: '' } };\n"
"server.response.print   = function (msg) { server.response.buffer += msg; };\n"
"server.response.println = function (msg) { server.response.print(msg + \"\\n\"); };\n"
"\n"
"var print   = function () { server.response.print.apply(this, arguments); };\n"
"var println = function () { server.response.println.apply(this, arguments); };\n"
"\n"
        );
}

static apr_status_t
put_postcode(apr_bucket_brigade *bb, apr_brigade_flush flush, void *ctx)
{
    return
        apr_brigade_puts(bb, flush, ctx,
"\n"
"/*return*/; server.response.buffer;\n"
        );
}

static apr_status_t
qstr_to_brigade(apr_bucket_brigade *bb, apr_brigade_flush flush, void *ctx,
                const char *str, apr_size_t nbyte)
{
    apr_status_t s;
    const char *eol;
    const char *p, *q;

    if (nbyte == 0)
        return APR_SUCCESS;

    for (eol = str + nbyte; eol > str; -- eol)
        if (! ap_strchr_c("\r\n", *(eol - 1)))
            break;

    s = apr_brigade_puts(bb, flush, ctx, "server.response.");
    if (s != APR_SUCCESS)
        return s;

    if (eol < str + nbyte)
        s = apr_brigade_puts(bb, flush, ctx, "println('");
    else
        s = apr_brigade_puts(bb, flush, ctx, "print('");
    if (s != APR_SUCCESS)
        return s;

    for (p = str; p < eol; ) {
        for (q = p; q < eol; ++ q)
            if (*q == '\'')
                break;

        if (q > p) {
            s = apr_brigade_write(bb, flush, ctx, p, q - p);
            if (s != APR_SUCCESS)
                return s;
        }
        if (q >= eol)
            break;

        s = apr_brigade_puts(bb, flush, ctx, "\\'");
        if (s != APR_SUCCESS)
            return s;

        p = q + 1;
    }

    s = apr_brigade_puts(bb, flush, ctx, "');\n");

    return s;
}

static int
v8pp_handler(request_rec *r)
{
    apr_pool_t *pool;
    apr_file_t *f = NULL;
    static char buffer[4096];
    ap_regex_t *re_o, *re_c;
    int in_tag = 0;
    const char *p;
    apr_bucket_brigade *bb;
    char *source;
    apr_size_t len;

    if (strcmp(r->handler, V8PP_MAGIC_TYPE) &&
        strcmp(r->handler, "v8pp-script"))
        return DECLINED;

    pool = r->main ? r->main->pool : r->pool;

    if (r->finfo.filetype != APR_REG && r->finfo.filetype != APR_LNK)
        return HTTP_INTERNAL_SERVER_ERROR;
    
    if (apr_file_open(&f, r->filename, APR_READ, APR_OS_DEFAULT,
                      pool) != APR_SUCCESS)
        return HTTP_INTERNAL_SERVER_ERROR;

    re_o = ap_pregcomp(pool, "<\\?js\\s+", AP_REG_EXTENDED);
    re_c = ap_pregcomp(pool, "\\?>",       AP_REG_EXTENDED);
    /* @todo: check errors */

    bb = apr_brigade_create(pool, r->connection->bucket_alloc);
    /* @todo: check errors */

    if (put_precode(bb, NULL, NULL) != APR_SUCCESS) {
        /* @todo: handle errors */
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    while (apr_file_eof(f) == APR_SUCCESS) {
        if (apr_file_gets(buffer, 4096, f) != APR_SUCCESS)
            break;

        for (p = buffer; p && *p; ) {
            ap_regmatch_t match;

            if (in_tag) {
                if (ap_regexec(re_c, p, 1, &match, 0)) {
                    /* no match */

                    if (apr_brigade_puts(bb, NULL, NULL, p) != APR_SUCCESS) {
                        /* @todo: handle errors */
                        return HTTP_INTERNAL_SERVER_ERROR;
                    }
                    break;
                }

                if (apr_brigade_write(bb, NULL, NULL,
                                      p, match.rm_so) != APR_SUCCESS) {
                    /* @todo: handle errors */
                    return HTTP_INTERNAL_SERVER_ERROR;
                }

                p += match.rm_eo;
                in_tag = 0;
            }
            else {
                if (ap_regexec(re_o, p, 1, &match, 0)) {
                    /* no match */

                    if (qstr_to_brigade(bb, NULL, NULL,
                                        p, strlen(p)) != APR_SUCCESS) {
                        /* @todo: handle errors */
                        return HTTP_INTERNAL_SERVER_ERROR;
                    }
                    break;
                }

                if (qstr_to_brigade(bb, NULL, NULL,
                                    p, match.rm_so) != APR_SUCCESS) {
                    /* @todo: handle errors */
                    return HTTP_INTERNAL_SERVER_ERROR;
                }

                p += match.rm_eo;
                in_tag = 1;
            }
        }
    }

    if (put_postcode(bb, NULL, NULL) != APR_SUCCESS) {
        /* @todo: handle errors */
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    if (apr_brigade_pflatten(bb, &source, &len, pool) != APR_SUCCESS) {
        /* @todo: handle errors */
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    apr_brigade_destroy(bb);

    ap_pregfree(pool, re_c);
    ap_pregfree(pool, re_o);

    apr_file_close(f);

    {
        v8::HandleScope handle_scope;
        v8::TryCatch try_catch;

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

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

        if (compiled.IsEmpty()) {
            v8::String::AsciiValue error(try_catch.Exception());

            ap_set_content_type(r, "text/plain");
            ap_rprintf(r, "[v8] compile error: %s\n\n", *error);
            ap_rwrite(source, len, r);
        }
        else {
            v8::Handle<v8::Value> result = compiled->Run();

            if (result.IsEmpty()) {
                v8::String::AsciiValue error(try_catch.Exception());

                ap_set_content_type(r, "text/plain");
                ap_rprintf(r, "[v8] execute error: %s\n\n", *error);
                ap_rwrite(source, len, r);
            }
            else {
                v8::String::AsciiValue output(result);
                ap_set_content_type(r, "text/html");
                ap_rputs(*output, r);
            }
        }
    }

    return OK;
}

static void
register_hooks(apr_pool_t *p)
{
    ap_hook_handler(v8pp_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

module AP_MODULE_DECLARE_DATA v8pp_module =
{
    STANDARD20_MODULE_STUFF,
    NULL,                        /* dir config creater */
    NULL,                        /* dir merger --- default is to override */
    NULL,                        /* server config */
    NULL,                        /* merge server config */
    NULL,                        /* command apr_table_t */
    register_hooks               /* register hooks */
};

}   /* end of extern "C" */

ソースみるとわかりますが v8pp-scriptAddHandler するなり SetHandler するなりしてください。

ごっそり読み込んでコンパイルして,ってやってるのでメモリ効率とかよくないです。filter module として書くほうがカコイイんですけど,めんどうだったので generator として書きました。そのくせ bucket brigades を使ってます。string stream として意外に便利。

コンパイル後の実体をシリアライズできればコードキャッシュとかできそうなんだけどな。


いちおうわたしが使った Makefile もあげておきます。

V8=../google-v8     # v8 のソースツリー(適当に書き換えてね)

APXS=/usr/sbin/apxs

AP_INCLUDES=-I$(shell $(APXS) -q INCLUDEDIR)

APR_CONFIG=$(shell $(APXS) -q APR_CONFIG)
APU_CONFIG=$(shell $(APXS) -q APU_CONFIG)
APR_INCLUDES=$(shell $(APR_CONFIG) --includes)
APU_INCLUDES=$(shell $(APU_CONFIG) --includes)
APR_LIBS=$(shell $(APR_CONFIG) --cflags --ldflags)
APU_LIBS=$(shell $(APU_CONFIG) --ldflags)
APR_LDFLAGS=$(shell $(APR_CONFIG) --ldflags)

CC=$(shell $(APXS) -q CC)
CPLUSPLUS=g++

INCLUDES=$(AP_INCLUDES) $(APR_INCLUDES) $(APU_INCLUDES) -I$(V8)/include
CFLAGS=-fPIC -DPIC $(shell $(APXS) -q CFLAGS)
CFLAGS+=$(shell $(APXS) -q EXTRA_CPPFLAGS) $(shell $(APXS) -q EXTRA_CFLAGS)
CFLAGS+=$(INCLUDES)

LIBS=$(shell $(APXS) -q LIBS) $(APR_LIBS) $(APU_LIBS) -L$(V8) -lv8
LDFLAGS=-shared -fPIC $(shell $(APXS) -q LDFLAGS)
LDFLAGS+=$(shell $(APXS) -q EXTRA_LDFLAGS) $(APR_LDFLAGS)

LIBEXECDIR=$(shell $(APXS) -q LIBEXECDIR)

all:	build

build:	mod_v8pp.so

install:	mod_v8pp.so
	install -c -s -m 755 $^ $(LIBEXECDIR)/

clean:
	rm -f mod_v8pp.so
	rm -f *.o

mod_v8pp.so:	mod_v8pp.o
	$(CPLUSPLUS) $(LDFLAGS) -o $@ $^ $(LIBS)

mod_v8pp.o:	mod_v8pp.cc
	$(CPLUSPLUS) $(CFLAGS) -c -o $@ $^

.PHONY: all build install clean

c++ なので apxs -c とか素直に使えなくてめんどうでした。

*1:あくまで Apache のモジュールとして,です。