node.jsのモジュール作り方メモ

とりあえず雛形

http://nodejs.jp/nodejs.org_ja/api/addons.html
に書いてあるとおり。

wscriptを作る

srcdir = '.'
blddir = 'build'
VERSION = '0.0.1'

def set_options(opt): 
    opt.tool_options('compiler_cxx')

def configure(conf):
    conf.check_tool('compiler_cxx')
    conf.check_tool('node_addon')

def build(bld):
    obj = bld.new_task_gen('cxx', 'shlib', 'node_addon')
    obj.target = 'cstr' # とりあえずcstrという名のモジュールを作ることにした。
    obj.source = 'cstr.cpp' # ソース。cppは習慣

wafについては
http://d.hatena.ne.jp/tanakh/20100212
が詳しい。


moduleの登録

#include <v8.h>

extern "C" void init (v8::Handle<v8::Object> target) 
{
// targetがjavascriptの{}に相当する
}


buildと実行

$ node-waf 
Project not configured (run 'waf configure' first)
# 最初にビルドディレクトリで一回だけconfigureする
$ node-waf configure
Checking for program g++ or c++          : /usr/bin/g++ 
Checking for program cpp                 : /usr/bin/cpp 
Checking for program ar                  : /usr/bin/ar 
Checking for program ranlib              : /usr/bin/ranlib 
Checking for g++                         : ok  
Checking for node path                   : not found 
Checking for node prefix                 : ok /usr/local 
'configure' finished successfully (0.631s)
# configure済んだので改めてnode-waf
$ node-waf 
Waf: Entering directory '/home/focke/work/node.js/cstr/build'
[1/2] cxx: cstr.cpp -> build/default/cstr_1.o
[2/2] cxx_link: build/default/cstr_1.o -> build/default/cstr.node
Waf: Leaving directory '/home/focke/work/node.js/cstr/build'
'build' finished successfully (1.368s)
$ node
# ビルドしたモジュールを相対パスで読み込む
> require('./build/default/cstr');
{} // 空のv8::Handle<v8::Object> target
> process.exit();
$

スタート地点。

C++クラスをwrapする

wrapされるクラス。cstr.h

#include <vector>
#include <algorithm>

class CStr
{
    std::vector<unsigned char> str_;

    public:
        CStr() { }

        CStr(const char *begin, const char *end)
        {
            std::copy(begin, end, std::back_inserter(str_));
        }

        size_t length(){ str_.size(); }
        // 追記
        char* begin(){ str_.begin(); }
};

wrapするべく追記したcstr.cpp

#include <v8.h>

#include <node.h>
#include "cstr.h"
#include <string.h>

// CStrをnode.js向けにwrapする
class v8_CStr : node::ObjectWrap
{
    CStr str_;

    public:
        v8_CStr()
        {
        }

        v8_CStr(const char *begin, const char *end)
            : str_(begin, end)
        {
        }

// 以降は、javascriptへの登録用のstatic method
        // Javascriptでのコンストラクタ
        static v8::Handle<v8::Value> New(const v8::Arguments& args)
        {
            if(args.Length()==0){
                // instanceを作って, Thisをwrapする
                (new v8_CStr())->Wrap(args.This());
            }
            else{
                // 第1引数からchar*を取り出す
                char buf[1024];
                // v8::ValueからV8::Stringを取り出す
                v8::Local<v8::String> str=
                    v8::Local<v8::String>::Cast(args[0]);
                //int length=str->WriteAscii(buf, 0, 1024);
                int length=str->WriteUtf8(buf, 1024);
                const char *end=buf+length;
                while(*(end-1)=='\0'){
                    --end;
                }
                // instanceを作って, Thisをwrapする
                (new v8_CStr(buf, end))->Wrap(args.This());
            }
            return args.This();
        }

        // prototypeに登録する関数
        static v8::Handle<v8::Value> ByteLength(const v8::Arguments& args)
        {
            // instanceからc++ポインタを取り出す
            v8_CStr *str=static_cast<v8_CStr*>(
                    args.This()->GetPointerFromInternalField(0));
            return v8::Integer::New(str->str_.length());
        }
};

extern "C" void init (v8::Handle<v8::Object> target) 
{
    // NODE_SET_PROTOTYPE_METHODで必要
    using namespace v8;

    // JavascriptのObjectとしてのコンストラクタ
    v8::Local<v8::FunctionTemplate> t = 
        v8::FunctionTemplate::New(v8_CStr::New);
    // t関数をnewして作るインスタンスは中にpointerを1つ持ってますよ?
    t->InstanceTemplate()->SetInternalFieldCount(1);
    // CStr.prototype.byte_length=CStr::byte_length
    NODE_SET_PROTOTYPE_METHOD(t, "byte_length", v8_CStr::ByteLength);
    // target["CStr"]=t;
    target->Set(v8::String::NewSymbol("CStr"), t->GetFunction());
}

なるべくusing namespace無しで書いてみた。
実戦では、適当にファイルやスコープを分けて多少はusing namespaceすることになりそうだ。
Javascript objectの中にC++クラスへのポインタを埋める際に以下の3つがセットになるようだ。

// 定義
t->InstanceTemplate()->SetInternalFieldCount(1);
// インスタンスに埋める(コンストラクタ)
(new v8_CStr())->Wrap(args.This());
// インスタンスから掘り出す(メソッド実行時など)
v8_CStr *str=static_cast<v8_CStr*>(
   args.This()->GetPointerFromInternalField(0));

あとは、Javascriptの仕様がわかっていればだいたい類推できる。たぶん。


test.js

var CSTR=require('./build/default/cstr');
var s=new CSTR.CStr('あいうえお');
process.stdout.write(s.byte_length()+'\n'); // -> 15

var s=new CSTR.CStr('abc');
process.stdout.write(s.byte_length()+'\n'); // -> 3

EventEmitterの継承

最後、EventEmitterの継承について。


cstr.cppで変えるところ

#include <node_events.h>

// 新しいEvent
static v8::Persistent<v8::String> delete_symbol;

// 親クラスを変える
class v8_CStr : node::EventEmitter
{
        // デストラクタでイベント発動
        ~v8_CStr()
        {
            // イベントの引数
            v8::Local<v8::Value> values[] = { 
                v8::String::New(str_.begin(), str_.length()) 
            };
            // node::EventEmitter::Emit
            // on(addEventListener)の第1引数: delete_symbol
            // Listerの引数の数: 1
            // Listerの引数: values
            Emit(delete_symbol, 1, values);
        }
};

extern "C" void init (v8::Handle<v8::Object> target) 
{
    // 省略

    // event用のsymbol
    delete_symbol = NODE_PSYMBOL("delete");
    // EventEmitterの継承
    t->Inherit(node::EventEmitter::constructor_template);

    // 継承した後で、Setする
    // target["CStr"]=t;
    target->Set(v8::String::NewSymbol("CStr"), t->GetFunction());
}

test.js

var CSTR=require('./build/default/cstr');

var s1=new CSTR.CStr('あいうえお');
process.stdout.write(s1.byte_length()+'\n');

var s2=new CSTR.CStr('abc');
process.stdout.write(s2.byte_length()+'\n');

s1.on('delete', function(){
    process.stdout.write("s1 deleted\n");
});
s2.on('delete', function(){
    process.stdout.write("s2 deleted\n");
});

// GCが発生するまで待つ
setTimeout(function(){}, 10000);

node.js向けに作る場合は、最初からEventEmitterを継承して作っておけばよいのではないか。
EventEmitterまで作ってみたのだが、node.jsの構成はGlibに似ている。
GObjectがEventEmitterに、MainLoopがlibevに相当するわけですな。