node.jsのnative addonを作るときはNANを使おう。

さて、 Node.js v0.12 で変わることの一つとして、native addonを作る時に後方互換性を壊す変更が加えられています。

これにより、v0.10でnative addonを作っているモジュール達は、ほとんど動かなくなってしまうことが考えられます。

V8側がこの後方互換性を壊す変更をしているため、V8に追従しているNode.js側としてはこのbreaking changesを受けざるを得なかったんだと思います。*1

どれくらい変更されてるのかは node.js の native addon で Hello World モジュールを作る方法が載っているのでそれをまずは参考にします。

// これまでの v0.10ではこう書いてた。
#include <node.h>
#include <v8.h>

using namespace v8;

Handle<Value> Method(const Arguments& args) {
  HandleScope scope;
  return scope.Close(String::New("world"));
}

void init(Handle<Object> exports) {
  exports->Set(String::NewSymbol("hello"),
      FunctionTemplate::New(Method)->GetFunction());
}

NODE_MODULE(hello, init)
// v0.11以降はこう書く

#include <node.h>

using namespace v8;

void Method(const FunctionCallbackInfo<Value>& args) {
  // 現在のisolate(current thread)を取得する。
  Isolate* isolate = Isolate::GetCurrent();
  HandleScope scope(isolate);
  // args.GetReturnValueで値をreturnする。
  // Stringのインスタンス方法も変更されてる。
  args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world"));
}

void init(Handle<Object> exports) {
  // NODE_SET_METHODというFunctionTemplateのヘルパー関数を利用してる。
  // これ自体は v0.8から既にあった様子。
  NODE_SET_METHOD(exports, "hello", Method);
}

NODE_MODULE(addon, init)

とまぁこうなります。

まぁお世辞にも変更が少ないとは言えないような感じです。
このHello Worldのモジュールだけでも、かなり変更されている事がわかります。

これって

V8に追従している以上しょうがないんですけど、ネイティブモジュールの作成者側からするとかなり辛いです。

しかも一度この変更に対処してしまうと、v0.10ではそのモジュールの最新に追従できないという問題も発生します。

というわけで、毎回毎回辛い思いをしているんですが、そんな状況に終止符を打つためのNANという抽象化ラッパーが登場しています。

NAN (Native Abstractions for Node.js) とは

node.js NAN で検索してもNaNの事しかヒットしないのでググラビリティとは何なのか、という名前ですが、提供してくれる機能は強力です。


先ほどの breaking changes に対応してくれるためのモジュールで、NANが提供するラップされた関数を使ってnative add-onを作っておけば、NANがnode.jsのバージョンを見て、v0.8, v0.10, v0.11のいずれかの変更に対応してくれます。先ほどの問題を解決してくれるわけです。

NANというのは、Native Abstractions for Node.jsの略称です。直訳するとNode.jsのためのネイティブ抽象化、という意味ですね。

作者のメッセージを引用します。

Thanks to the crazy changes in V8 (and some in Node core), keeping native addons compiling happily across versions, particularly 0.10 to 0.11/0.12, is a minor nightmare. The goal of this project is to store all logic necessary to develop native Node.js addons without having to inspect NODE_MODULE_VERSION and get yourself into a macro-tangle.

This project also contains some helper utilities that make addon development a bit more pleasant.

V8 (と Node coreのいくつかのモジュール) の crazy な変更のおかげで、native addonがバージョン間を跨いでコンパイルするのは困難になってしまったよ。 特に 0.10-0.11/0.12のマイナーチェンジは悪夢だね。
このプロジェクトのゴールはNode.jsのバージョン変更を調査しなくても、native addon を開発するために必要なロジック、ライブラリを提供することだ。

このプロジェクトには、バージョン間の差異を吸収するだけじゃなく、addon 開発をより楽しくするためのヘルパーユーティリティも含むよ。

ということです、NANをベースに作っていきましょう。

NAN 対応する

とりあえず、HelloWorldをNAN対応しましょう。

まずはインストールから

$ npm install nan -S

installしたらbinding.gypのtarget property以下に下記の文字列を記述します。

      "include_dirs" : [
        "<!(node -e \"require('nan')\")"
      ]


終わったら、以下のようにnativeモジュールをNAN対応していきます。

addon.cc

#include <node.h>
// nan.hを読み込む

#include <nan.h>

using namespace v8;

// v8に公開したいMETHODの定義時にNAN_METHODを使う
NAN_METHOD(Method) {
  // NanScopeで始める、これはHandleScopeをラップする奴。
  NanScope();
  // NanReturnValueで値を返す、return のラッパー
  // ちなみに文字列もNanSymbolで囲む
  NanReturnValue(NanSymbol("world"));
}

void init(Handle<Object> exports) {
  // exports->Setで定義する方向に固定。
  // その代わりNanSymbolを使う
  exports->Set(NanSymbol("hello"),
      FunctionTemplate::New(Method)->GetFunction());
}

NODE_MODULE(addon, init)


ここまででaddon.ccモジュールのNAN対応が実施されました。

NAN対応済みのサンプルプロジェクトは以下になります。

yosuke-furukawa/NANSample · GitHub

これを使って実際にv0.8 - v0.11で使えるのかtravisで確認しました。

f:id:yosuke_furukawa:20140407204934p:plain

結果はこんな感じで全部のバージョンでビルドができて、テストが通る事を確認できました。

Hello World以上の事を知りたい場合は

NANプロジェクトにあるexamplesが参考になります。円周率を求めるためのモジュールですが、syncのAPIとasyncのAPIがそれぞれ用意されてて非常に参考になります。

NAN example

後はsindresorhusさんが、WindowsOSXの時の差を吸収するためのモジュールの書き方を実践してくれているのでそれも参考になるかもしれません。

sindresorhus/fullname-native · GitHub

2014/04/08 追記 NAN対応を実践してみた。

実践してみました。ハマりどころを紹介しておきます。

NAN対応でハマった所:

通常のコードをNANでラップするのはそこまで難しくなかった。
一応、雰囲気としては、HandleScope scope;から始まるメソッドの始まりはNanScope();にして、returnで返す値はNanReturnXXXXを使う、

jsに公開するメソッド Handle Hoge(const Arguments& args) 等はNAN_METHOD(Hoge)に変更するだけ。

という感じにやっていけば機械的に変更できる。
PersistentからLocalにする時に一瞬ハマったけど、公式ドキュメントにあるNanPersistentToLocalを使えば大丈夫。

大変だったのは、、、

  • v8::StringでMayContainNonAsciiとWriteAsciiの2つのメソッドが消えていること
  • ArrayBufferクラスのインスタンスの取り扱いが変わっていること

の2点。

MayContainNonAsciiとWriteAsciiの2つのメソッドが消えている

MayContainNonAsciiとWriteAsciiはバグってた様子。共通で使っているHasOnlyAsciiCharsっていうメソッドが正しい値を返さないことがあるってことで、APIから削除されました。

んで、新しくIsOneByteっていうメソッドが追加されているのでv0.11以降はそれを使いましょう。v0.10ではまだこのメソッドが追加されていないので、そこは諦めてこうやって書きましょう。

#if (NODE_MODULE_VERSION > 0x000B)
        // node.js v0.11+
        bool hasMultiByte = !str->IsOneByte();
#else
        // node.js v0.10
        bool hasMultiByte = str->MayContainNonAscii();
#endif
        if (hasMultiByte) {

WriteAsciiはテストコード読むとWriteUtf8を使うようになったので、そちらを使いましょう。

ArrayBufferの取り扱いが変わった件

Bufferから値を読み込む時に値が入っているかどうかを確認するために使う、HasIndexedPropertiesInExternalArrayDataがtrueを返さなくなった。

詳しくは、このgist見てもらうといいんですけど、ArrayBufferを渡すのではなく、TypedArrayそのものを渡すと動いたので、そういう動作に変更されたようです。

というところでかなりハマったんですが、なんとか抜け出せました。:)

一旦ここまで

本当は、NANの紹介だけじゃなくてどんな時にどのラッパーを使うかとか、API翻訳とか、あまりにもブログのエントリが長くなりそうなので、紹介まで。

気が向いたらもう少し長く紹介します。

*1:Node.jsが安全性を担保する上で要求した変更も多いようなので、breaking changesに関してはV8だけを攻めるのは筋違いですね。 see: https://groups.google.com/forum/#!msg/v8-users/6kSAbnUb-rQ/e-GI0b1ThA4J