概要
Motivation
Node.jsのNative Extensionの解説記事はたくさんあるのですが、情報が古かったり、公式のドキュメントもやや使いづらかったりするので、今回の記事を執筆する運びとなりました。
この記事が目標としていること
動くサンプルを通じて、Native Addonを作るための最低限の要素に触れ、C++の知識があれば大抵のものは作れるようになります(なるはずです)
最低限の要素とは以下の4つです。
- JavaScript - C++の橋渡し
- V8のAPIの基本的な操作(配列操作、オブジェクトの操作を含む)
- 内部にC++のオブジェクトを持ったJavaScriptのオブジェクトの作成
- 非同期処理
ついでに実現できること
V8がどう動いているのかがほんの少しわかります。
バージョン情報
- Mac OS X 10.12.6
- Node.js v9.3.0
リポジトリ
1. はじめの一歩
まずは、C++の関数を呼び出してNodeの文字列を作成し、JSの世界に返すだけのサンプルから入ります。ほぼ公式のドキュメントのままです。
とりあえずビルドツールを準備する
npmとnodeはインストール済みであるとします。
$ npm install -g node-gyp
はじめに何を作るか
これと等価なNodeモジュールを作成します。
module.exports.hello = () => "world";
実装
その壱
function Hello() {
return "world";
}
まずは、上記JavaScriptの関数に対応するコードをC++で書いてみましょう。
void Hello(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world"));
}
全体が型がv8::FunctionCallbackInfo<v8::Value>
のargs
を引数にとる関数になっています。引数args
からは、JS側で指定した関数の引数を受け取ることができ、またargs.GetReturnValue().Set
を呼び出してやると、関数の返り値を設定することができます。
その弐
module.exports.hello = Hello
次に上記コードに対応するC++のコードを書いてみます。
void init(Local<Object> exports) {
NODE_SET_METHOD(exports, "hello", Hello);
}
NODE_SET_METHOD
というマクロのふりをした関数がよしなにやってくれます(昔はマクロだったらしい)。v8::Local<v8::Object>
はV8オブジェクトへのポインタを意味し、適当なタイミングでGCの対象となります。
マクロの中身をちょっと覗いてやるとこんな感じになっています。v8::HandleScope handle_scope(isolate)
を書いてやることによって、関数を抜ける時、HandleScope
のデストラクタが呼ばれたタイミングで、Local
型で宣言された変数がGCの対象となるようになります。
inline void NODE_SET_METHOD(v8::Local<v8::Object> recv,
const char* name,
v8::FunctionCallback callback) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::FunctionTemplate> t = v8::FunctionTemplate::New(isolate,
callback);
v8::Local<v8::Function> fn = t->GetFunction();
v8::Local<v8::String> fn_name = v8::String::NewFromUtf8(isolate, name);
fn->SetName(fn_name);
recv->Set(fn_name, fn);
}
上では大雑把な解説を書きましたが、実際には上のHello関数の型(void Hello(const FunctionCallbackInfo<Value>& args)
)をもつ関数は、v8::FunctionTemplate::New
の引数となることができて、v8::Local<v8::FunctionTemplate>
のGetFunction
を呼んでやると、V8上の関数になります。
その参
壱および弐がNodeモジュールとして機能するために最後の仕上げが必要です。
NODE_MODULE(NODE_GYP_MODULE_NAME, init)
マクロです
その肆
C++の部分を全部をまとめるとこのようなコードになります。
#include <node.h>
namespace demo {
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
void Hello(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world"));
}
void init(Local<Object> exports) {
NODE_SET_METHOD(exports, "hello", Hello);
}
// このマクロの後にはセミコロンは不要
NODE_MODULE(NODE_GYP_MODULE_NAME, init)
} // namespace demo
その伍
Makefileに相当するものをまだ書いてないので、node-gyp
がきちんと動くように、binding.gyp
を書いてやります。
{
"targets": [
{
"target_name": "addon",
"sources": [ "index.cc" ]
}
]
}
以上で実装は完了です。node-gyp configure
した後にnode-gyp build
を行うことで、以下のJavaScriptのコードが期待したとおりに動くようになります。
const addon = require('./build/Release/addon');
console.log(addon.hello()); // world
解説
Isolateについて
Isolateを用いることで、実行中のJavaScriptのスコープを得ることができます。後にも出てきますが、HandleScope
がよしなにメモリ管理を行ってくれます。
Isolateについてもう少し詳しく
Isolateを経由して現在のスコープを取得することができますが、Isolate自体は独立したV8のランタイムを示すものです。スコープの制御の他に、ヒープ管理機構や、ガベージコレクタも含みます。同時に一つのスレッドしかIsoateにアクセスすることはできませんが、複数のスレッドから操作することができます。
ところが、IsolateだけではV8のランタイムとしては成立せず、Contextというものも準備する必要があります。Contextはルートオブジェクトを用意してくれます。たとえば、iframeの内部などはContextを分けて制御しています。
また、iframeの例からもわかるように、Isolateの内部には複数のContextが存在することができ、かつ、Isolateが排他ロック制御を行ってくれるおかげで、別々のContextに含まれるObjectは簡単にかつ安全に共有することができます。
さらにIsolateについて
https://github.com/v8/v8/wiki/Embedder's-Guide に書いてあります。
V8をゼロからまるっと動かすサンプルもあります。
https://github.com/v8/v8/blob/master/samples/hello-world.cc
完全なサンプル
https://github.com/kentrino/native-node-addon-samples の./hello_world
以下にあります
2. つぎ
JavaScriptの世界から値を受け取って、C++側で利用してみましょう。
何を作るか
function printEverything() {
if (typeof arguments.length < 2) {
throw new TypeError("Wrong number of arguments.");
}
if (typeof arguments[0] !== "number") {
throw new TypeError("Argument #1 must be a number.");
}
if (typeof arguments[1] !== "string") {
throw new TypeError("Argument #2 must be a string.");
}
console.log(`${arguments[0]}, ${arguments[1]}`);
}
module.exports.printEverything = printEverything;
とだいたい等価なモジュールを作ってみましょう(実際は配列とオブジェクトも引数にとり、それらを表示する関数になっています)
- 引数の数、型のチェックがあること
- 例外を投げているところ
が新しい要素です。先程扱っていないところだけを紹介します。
引数の数、型チェックを行う
if (args.Length() < 2) {
//
}
if (!args[0]->IsNumber()) {
//
}
if (!args[1]->IsString()) {
//
}
このようにして、引数の数をみたり、型チェックを行ったりすることができます。
V8の例外を投げる
isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Argument #2 must be string.")));
V8の値からC++で扱いやすい形に変換する
数値の場合
double number = args[0]->NumberValue();
文字列の場合
String::Utf8Value string(args[1]);
std::string cpp_string = std::string(*string);
配列の場合
Local<Array> array = Local<Array>::Cast(args[2]);
std::vector<double> vec;
unsigned int length = array->Length();
for (unsigned int i = 0; i < length; i++) {
if (!array->Get(i)->IsNumber()) {
isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Argument #3 must consist only of number.")));
}
vec.push_back((double)Local<Number>::Cast(array->Get(i))->NumberValue());
}
オブジェクトの場合
std::map<std::string, std::string> mp;
Local<Context> context = isolate->GetCurrentContext();
Local<Array> props = object->GetOwnPropertyNames(context).ToLocalChecked();
unsigned int length = props->Length();
for(unsigned int i = 0; i < length; ++i) {
Local<Value> local_key = props->Get(i);
Local<Value> local_val = object->Get(context, local_key).ToLocalChecked();
if (!local_key->IsString() || !local_val->IsString()) {
isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Argument #4 must have type of { [key: string]: string; }.")));
}
std::string key = *String::Utf8Value(local_key);
std::string val = *String::Utf8Value(local_val);
mp[key] = val;
}
完全なサンプル
https://github.com/kentrino/native-node-addon-samples の./print_everything
以下にあります。
3. さらにつぎ
今度は、C++のオブジェクトを内部に含んだJavaScriptのオブジェクトを作ってみましょう。実用的なNative Addonを作るために必ず必要となってくるテクニックです。
何を作るか
helloプロパティを呼ぶとworldが返ってくるフレンドリーなスタッククラスを作ってみます。
const h = new HelloStack();
h.hello; // "world"
h.push(3);
h.pop(); // 3
実装
自前のV8オブジェクトを作るクラスの概要
実際にはV8オブジェクトの内部に収まるクラスを作成します。
クラス定義では、node::ObjectWrap
を継承します。
#include <node_object_wrap.h>
class HelloStack : public node::ObjectWrap {
public:
// コンストラクタをヒープにロードして、呼び出せるようにする
// プロトタイプの関数のセット、インスタンスが内部にC++のフィールドを持っていることはここで宣言する
static void LoadConstructor(v8::Isolate* isolate);
// インスタンスを作成する。下記のConstructorを直接JavaScript側から叩くことができないので、この関数で仲介する
static void CreateNewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);
// プロトタイプ関数の実装
static void Push(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Pop(const v8::FunctionCallbackInfo<v8::Value>& args);
// スタックの内部実装で使うvector
std::vector<double> stack_;
private:
explicit HelloStack();
~HelloStack();
// Constructorの実装。プロパティhelloの作成はここで行う
static void Constructor(const v8::FunctionCallbackInfo<v8::Value>& args);
// V8ヒープにのっているコンストラクタ
static v8::Persistent<v8::Function> constructor;
};
モジュール作成部分
Init時にHelloStack::LoadConstructor
を呼んでいます。
void CreateNewInstance(const FunctionCallbackInfo<Value>& args) {
HelloStack::CreateNewInstance(args);
}
void Init(Local<Object> exports, Local<Object> module) {
HelloStack::LoadConstructor(exports->GetIsolate());
NODE_SET_METHOD(module, "exports", CreateNewInstance);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Init);
} // namespace demo
個々の実装
InstanceTemplate
はインスタンス化されたときの形状を指定します。
void HelloStack::LoadConstructor(Isolate* isolate) {
HandleScope scope(isolate);
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, Constructor);
tpl->SetClassName(String::NewFromUtf8(isolate, "HelloStack"));
// V8のオブジェクトには、JavaScriptから見えない内部フィールドを持たせることができ、今回の場合、
// C++のオブジェクトをラップしたV8オブジェクトを作るので1以上である必要がある *1
tpl->InstanceTemplate()->SetInternalFieldCount(1);
// プロトタイプの関数を定義する
NODE_SET_PROTOTYPE_METHOD(tpl, "push", Push);
NODE_SET_PROTOTYPE_METHOD(tpl, "pop", Pop);
HelloStack::constructor.Reset(isolate, tpl->GetFunction());
}
*1 Assertion failed: (handle->InternalFieldCount() > 0), function Wrap, file .../include/node/node_object_wrap.h, line 77.
が起こる
void HelloStack::Constructor(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
HandleScope scope(isolate);
// new演算子 or Local<Function>#NewInstanceによる呼び出しの場合
if (args.IsConstructCall()) {
HelloStack* obj = new HelloStack();
Local<Object> that = args.This();
that->Set(String::NewFromUtf8(isolate, "hello"), String::NewFromUtf8(isolate, "world"));
obj->Wrap(that);
args.GetReturnValue().Set(that);
return;
}
}
解説
##### マクロとかの中身
NODE_SET_PROTOTYPE_METHOD
の実装です。先程のNODE_SET_METHOD
とほぼ同じことをしていますね(レシーバーが適切かどうかを判定するためのSignature
を設定していますが、どのように機能するのかは調べきれていません)。PrototypeTemplate
はSomeObject.prototype
と同義です。
inline void NODE_SET_PROTOTYPE_METHOD(v8::Local<v8::FunctionTemplate> recv, const char* name, v8::FunctionCallback callback) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Signature> s = v8::Signature::New(isolate, recv);
v8::Local<v8::FunctionTemplate> t = v8::FunctionTemplate::New(isolate, callback, v8::Local<v8::Value>(), s);
v8::Local<v8::String> fn_name = v8::String::NewFromUtf8(isolate, name);
t->SetClassName(fn_name);
recv->PrototypeTemplate()->Set(fn_name, t);
}
obj->Wrap
の中身です。きちんとMakeWeak()
されているので、V8オブジェクトの破棄時にV8がHelloStack
をGCしてくれます。
inline void Wrap(v8::Local<v8::Object> handle) {
assert(persistent().IsEmpty());
assert(handle->InternalFieldCount() > 0);
handle->SetAlignedPointerInInternalField(0, this);
persistent().Reset(v8::Isolate::GetCurrent(), handle);
MakeWeak();
}
Persistentなオブジェクトについて
Local
のオブジェクトはHandleScope
のデストラクタが呼ばれるタイミングで(= C++の関数を抜けるタイミングで)GCの対象となりますが、Persistent
のオブジェクトは明示的に.Reset()
を呼ばない限りGCされることはありません
完全なサンプル
https://github.com/kentrino/native-node-addon-samples の./hello_stack
以下にあります
4. ラスト
いよいよlibuvを用いた非同期処理を含んだ、Native Addonの作成にうつります。今回はフィボナッチ数列を計算してくれる、こんな感じに動作するモジュールを作ります。
computeFibonacci(10, console.log); // 55
実装
わかりやすいところから始めます。
フィボナッチ数列を計算するところ
std::string ComputeFibonacci(int number) {
int i;
long long a, b, c;
a = 1;
b = 1;
if (number == 1) return std::to_string(a);
if (number == 2) return std::to_string(b);
for (i = 0; i < number - 2; ++i) {
c = a + b;
a = b;
b = c;
}
return std::to_string(c);
}
まずはデータから
class AsyncWork {
public:
// フィボナッチ数列の計算が終わったあと、JavaScriptの世界へ渡す処理
// PersistentにしておかないとGCされてしまう
v8::Persistent<v8::Function> callback;
// フィボナッチ数列の何項目か
int number;
// フィボナッチ数列の計算結果を詰める
std::string result;
// あとでメモリを解放するために突っ込んである
uv_work_t uv_request;
};
データ型をみると、ある程度処理の様子が想像できると思います。
非同期処理のキューに詰めるところ
uv_work_t
型の&work->uv_request
をこのように指定してやります。uv_work_t
は内部のフィールドにdata
を保持しており、ここに処理するべき内容を突っ込んでやります。
DoWork
がキューのイベントの非同期処理を行う関数で、AfterWork
では非同期処理終了後の処理を渡しています。
uv_queue_work(uv_default_loop(), &work->uv_request, (uv_work_cb)DoWork, (uv_after_work_cb)AfterWork);
他の部分と合わせるとこんな感じになります。
void ComputeFibonacciAsync(const FunctionCallbackInfo<Value> &args) {
Isolate *isolate = Isolate::GetCurrent();
HandleScope scope(isolate);
int number = (args[0]->NumberValue());
// JavaScriptのコールバック関数
Local<Function> cb_local = Local<Function>::Cast(args[1]);
AsyncWork *work = new AsyncWork;
work->uv_request.data = work;
work->callback.Reset(isolate, cb_local);
work->number = number;
uv_queue_work(uv_default_loop(), &work->uv_request, (uv_work_cb)DoWork, (uv_after_work_cb)AfterWork);
}
DoWork
キューのイベントを処理するところです。簡単ですね。
static void DoWork(uv_work_t *req) {
AsyncWork *work = static_cast<AsyncWork *>(req->data);
std::string result = ComputeFibonacci(work->number);
work->result = result;
}
AfterWork
非同期処理終了後の処理になります。work
から、result
を取り出し、その結果をJavaScriptのコールバックに渡してやります。
static void AfterWork(uv_work_t *req, int status) {
Isolate *isolate = Isolate::GetCurrent();
HandleScope scope(isolate);
AsyncWork *work = static_cast<AsyncWork *>(req->data);
Local<String> result = String::NewFromUtf8(isolate, work->result.c_str());
const unsigned argc = 1;
Local<Value> argv[argc] = {result};
Local<Function> cb = Local<Function>::New(isolate, work->callback);
cb->Call(isolate->GetCurrentContext()->Global(), argc, argv);
work->callback.Reset();
delete work;
}
完全なサンプル
https://github.com/kentrino/native-node-addon-samples の./async_fibonacci
以下にあります
GMPを使ったバージョン
上のサンプルだとlong long
の範囲でしか計算できないので、GMPを使った任意精度版も用意しました。
https://github.com/kentrino/native-node-addon-samples の./gmp_async_fibonacci
以下にあります
GMPのインストール
$ brew install gmp
その他
NAN(https://github.com/nodejs/nan) を使ったものだが優れたサンプルが豊富にある。
参考
- https://github.com/nikhilm/uvbook
- https://github.com/v8/v8/wiki/Embedder's%20Guide
- https://nodejs.org/api/addons.html
- https://mattn.kaoriya.net/software/lang/c/20110325015245.htm
- https://stackoverflow.com/questions/19383724/what-exactly-is-the-difference-between-v8isolate-and-v8context
- https://nodeaddons.com/how-not-to-access-node-js-from-c-worker-threads/
質問・マサカリ
まで気軽にお声掛けください。