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

やはりやっつけで。Joke module です。

いろいろ書きたいことがあるけど,そのうち(追記するかも)。

libv8 と XS の間をとりもつ bridge.cc

/* bridge.cc */
#include <v8.h>

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include "ppport.h"

/* harmful macro!, but, but, ... */
#undef New

#include "bridge.h"

using namespace v8;

class V8Context {
public:

    V8Context() {
        HandleScope scope;

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

        context_ = Persistent<Context>::New(context);
    };

    virtual ~V8Context() {
        context_.Dispose();
    };

    Handle<Context> context() { return context_; };

private:
    Persistent<Context> context_;
};

V8CONTEXT *
create_v8context(void)
{
    return new V8Context();
}

void
release_v8context(V8CONTEXT *ctx)
{
    V8Context *context = static_cast<V8Context *>(ctx);

    delete context;
}

static SV *
_convert_v8value_to_sv(Handle<Value> value)
{
    if (0) ;
    else if (value->IsUndefined())
        return &PL_sv_undef;
    else if (value->IsNull())
        return &PL_sv_undef;
    else if (value->IsInt32())
        return newSViv(value->Int32Value());
    else if (value->IsBoolean())
        return newSVuv(value->Uint32Value());
    else if (value->IsNumber())
        return newSVnv(value->NumberValue());
    else if (value->IsString())
        return newSVpv(*(String::AsciiValue(value)), 0);
    else {
        Perl_warn(aTHX_ "Unsupported value type");
        return &PL_sv_undef;
    }
}

static Handle<Value>
_convert_sv_to_v8value(SV *sv)
{
    HandleScope scope;

    if (0) ;
    else if (SvIOK_UV(sv))
        return Uint32::New(SvUV(sv));
    else if (SvIOK(sv))
        return Integer::New(SvIV(sv));
    else if (SvNOK(sv))
        return Number::New(SvNV(sv));
    else if (SvPOK(sv))
        return String::New(SvPV_nolen(sv));

    return Undefined();
}

static Handle<Value>
_perl_method_by_name(const Arguments &args)
{
    dSP;
    int count;
    Handle<Value> result = Undefined();
    char ** arguments;

    ENTER;
    SAVETMPS;

    String::AsciiValue method(args.Data()->ToString());
    if (0) Perl_warn(aTHX_ "method called: %s", *method);

    arguments = new char *[args.Length() + 1];
    for (int i = 0; i < args.Length(); i ++) {
        String::AsciiValue str(args[i]);
        arguments[i] = savepv(*str);
    }
    arguments[args.Length()] = NULL;

    count = call_argv(*method, G_SCALAR, arguments);

    for (int i = 0; i < args.Length(); i ++) {
        Safefree(arguments[i]);
    }
    delete arguments;

    SPAGAIN;

    if (count > 1) {
        result = _convert_sv_to_v8value(POPs);
    }

    PUTBACK;
    FREETMPS;
    LEAVE;

    return result;
}

void
v8context_register_method_by_name(V8CONTEXT *ctx, const char *method)
{
    V8Context *context = static_cast<V8Context *>(ctx);
    HandleScope scope;
    TryCatch try_catch;

    Context::Scope context_scope(context->context());

    context->context()->Global()->Set(
        String::New(method),
        FunctionTemplate::New(_perl_method_by_name,
                              String::New(method))->GetFunction()
    );
}


SV *
v8context_execute(V8CONTEXT *ctx, const char *source)
{
    V8Context *context = static_cast<V8Context *>(ctx);
    HandleScope scope;
    TryCatch try_catch;

    Context::Scope context_scope(context->context());

    Handle<Script> script
        = Script::Compile(String::New(source), Undefined());
    if (script.IsEmpty()) {
        String::AsciiValue error(try_catch.Exception());
        Perl_croak(aTHX_ "execute(): compile error: %s", *error);
        return &PL_sv_undef;
    }
    else {
        Handle<Value> result = script->Run();
        if (result.IsEmpty()) {
            String::AsciiValue error(try_catch.Exception());
            Perl_croak(aTHX_ "execute(): execute error: %s", *error);
            return &PL_sv_undef;
        }
        else {
            return _convert_v8value_to_sv(result);
        }
    }
    /* NOTREACHED */
}

ヘッダファイル。XS でも読み込んでます。

/* bridge.h */

#ifdef __cplusplus
extern "C" {
#endif

typedef void V8CONTEXT;

V8CONTEXT * create_v8context(void);
void release_v8context(V8CONTEXT *ctx);
void v8context_register_method_by_name(V8CONTEXT *ctx, const char *method);
SV * v8context_execute(V8CONTEXT *ctx, const char *source);

#ifdef __cplusplus
}
#endif

これらの bridge.o を生成するための MakefileMakefile.PL に組み込めればかっこいいんですけど。

# bridge.mk

V8=../google-v8

PERL_INC=$(shell perl -MConfig -e 'print $$Config{archlib}')/CORE
CFLAGS+=-Wall -fPIC -fno-rtti -I$(PERL_INC) -I$(V8)/include

all:    build

build:  bridge.o

clean:
        rm -f bridge.o

bridge.o:       bridge.cc bridge.h
        g++ $(CFLAGS) -c -o $@ $<

bridge.o:       bridge.mk

.PHONY:	all build clean


んで,XS のコード。の前に,typemap

TYPEMAP
V8CONTEXT *     T_V8CONTEXT

INPUT
T_V8CONTEXT
        if (SvOK($arg) && SvROK($arg))
            $var = (V8CONTEXT *) mg_find(SvRV($arg), PERL_MAGIC_ext)->mg_obj;
        else
            $var = NULL;

XS の実体,V8.xstypemap とブリッジのおかげであまりコードがないです。

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include "ppport.h"

#include "bridge.h"

MODULE = Acme::JavaScript::V8   PACKAGE = Acme::JavaScript::V8::Context

void
_bind_new_v8context(sv)
        SV *sv;
    PREINIT:
        V8CONTEXT *ctx;
    CODE:
        ctx = create_v8context();
        sv_magic(SvRV(sv), NULL, PERL_MAGIC_ext, NULL, 0);
        mg_find(SvRV(sv), PERL_MAGIC_ext)->mg_obj = (void *) ctx;

void
_destroy_v8context(ctx)
        V8CONTEXT *ctx;
    CODE:
        release_v8context(ctx);

MODULE = Acme::JavaScript::V8	PACKAGE = Acme::JavaScript::V8::Context PREFIX = v8context_

void
v8context_register_method_by_name(ctx, method)
        V8CONTEXT *ctx;
        const char *method;

SV *
v8context_execute(ctx, source)
        V8CONTEXT *ctx;
        const char *source;

Perl 側のライブラリインタフェース Acme/JavaScript/V8.pmXSLoader で読み込んで,Context の newDESTROY を実装しているだけ。

package Acme::JavaScript::V8;

use strict;
use warnings;

our $VERSION = '0.01';

require XSLoader;
XSLoader::load(__PACKAGE__, $VERSION);

package Acme::JavaScript::V8::Context;

sub new {
    my $class = shift;
    my $self = bless {}, $class;

    _bind_new_v8context($self);

    return $self;
}

sub DESTROY {
    my $self = shift;
    _destroy_v8context($self);
}

1;
__END__

念のため,Makefile.PL もおいときますね。

# Makefile.PL

use inc::Module::Install;

name 'Acme-JavaScript-V8';
all_from 'lib/Acme/JavaScript/V8.pm';

my $V8 = '../google-v8';
makemaker_args(
    LDFROM => join(q{ }, '$(OBJECT)', 'bridge.o'),
    LIBS   => join(q{ }, makemaker_args()->{LIBS}, "-L${V8}", '-lv8', '-lstdc++'),
);

build_requires 'Test::More';

auto_include;
WriteAll;

libstdc++ をリンクするとリンカとして g++ を指定しなくてもいいみたい。とりあえず。libv8 のリンクの仕方を変えました。


以下サンプルスクリプト

use strict;
use warnings;

use Acme::JavaScript::V8;

my $ctx = Acme::JavaScript::V8::Context->new();

my $r = $ctx->execute(<<'END_JS');
a = 1;
b = 2;
a + b;
END_JS

print $r, "\n";     # => 3

sub foo {
    print "foo called: ", join(q{, }, @_), "\n";
    return "bar";
}

# 'foo' という名前の Perl サブルーチンを登録する
$ctx->register_method_by_name("foo");

$r = $ctx->execute(<<'END_JS');
foo(a, b, "baz");   /* => foo called: 1, 2, baz */
END_JS

print $r, "\n";     # => bar

Perl コードサイドの関数を名前でしか呼び出せないのが,まだカコワルイです。あと Global を Perl からいじれるようにしたいですねぇ。