この記事について
サンプルレベルの Node.js アドオンを C++ で書いてみた際の作業メモです。
内容:
- ソースは node-addon-examples をベースにしてます
- 足し算をする add という関数を JavaScript 側に見せる
2_function_arguments
にちょっと手を入れた程度です
Node.js アドオンとは
C++ addons の説明が分かりやすいので引用。
Addons are dynamically-linked shared objects written in C++. The require() function can load addons as ordinary Node.js modules. Addons provide an interface between JavaScript and C/C++ libraries.
Node.js アドオンを作る方法
Node.js のアドオンを作るには以下の方法がある。
- 内部の V8/libuv/Node.js ライブラリを直接使う
- nan(Native Abstractions for Node.js) を使う
- N-API を使う
- node-addon-api を使う
それぞれの特徴をざっくりまとめると以下の通り。
- v8/libuv/Node.js ライブラリを直接使う
- 複雑、かつ、各ライブラリのバージョンアップと変更の影響をモロに受けるので大変 (特に V8 はリリースごとに大きく変わるらしい1)
- 各ライブラリを直接触りたい場合以外は使うべきではない
-
nan
- 各ライブラリのバージョン間の差異を吸収するツール(主にマクロ)を提供
- N-API
- アドオン開発向けの C言語 API で、ABI(Application Binary Interface)を保証する
- Node.js 本体と同じリポジトリでメンテされてる
-
node-addon-api
- N-API を C++ でラップして使いやすくしたもの
- Node.js プロジェクトでメンテされてる
Node.js のドキュメントでは N-API と node-addon-api を推奨してる。
今回は、「v8/libuv/Node.jsライブラリを直接使う」 と 「node-addon-api」 を使ってみる。
環境
今回試した環境は以下の通り。
- Ubuntu 18.04
- Node.js v12.18.3
- npm 6.14.6
- Python 3.6.9
v8/libuv/Node.js ライブラリを直接使う場合
開発環境の準備
ビルドに必要なツール類をインストールする。
sudo apt install build-essential
sudo npm install -g node-gyp
開発
以下、適当な作業用ディレクトリで開発を行う。
まず main.cc を記述。
#include <node.h>
#include <cstring>
namespace demo {
void ThrowTypeError(v8::Isolate *isolate, const char* msg)
{
size_t msgSize = std::strlen(msg);
v8::Local<v8::String> v8Msg =
v8::String::NewFromUtf8(isolate, msg, v8::NewStringType::kNormal, static_cast<int>(msgSize)).ToLocalChecked();
// Throw an Error that is passed back to JavaScript
isolate->ThrowException(v8::Exception::TypeError(v8Msg));
}
void AddMethod(const v8::FunctionCallbackInfo<v8::Value>& args)
{
v8::Isolate* isolate = args.GetIsolate();
// Check the number of arguments passed.
if ( args.Length() < 2 ) {
ThrowTypeError(isolate, "Wrong number of arguments");
return;
}
// Check the argument types
if ( ! args[0]->IsNumber() || ! args[1]->IsNumber() ) {
ThrowTypeError(isolate, "Wrong arguments");
return;
}
// Perform the operation
double arg0 = args[0].As<v8::Number>()->Value();
double arg1 = args[1].As<v8::Number>()->Value();
v8::Local<v8::Number> answer = v8::Number::New(isolate, arg0 + arg1);
// Set the return value (using the passed in FunctionCallbackInfo<Value>&)
args.GetReturnValue().Set(answer);
}
void Initialize(v8::Local<v8::Object> exports)
{
NODE_SET_METHOD(exports, "add", AddMethod);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace demo
ビルド
同じディレクトリにビルド用の binding.gyp を用意。
{
"targets": [
{
"target_name": "myaddon",
"sources": [ "main.cc" ]
}
]
}
上記ディレクトリからビルドを実行する。
# Makefile 等の作成。build ディレクトリ配下に出力される。
node-gyp configure
# ビルドの実行。build/Release/ に myaddon.node というファイルが作成される。
node-gyp build
実行
今回作ったアドオン myaddon
を使う JavaScript を用意。成功する場合と失敗する場合の両方を試してる。
const myaddon = require('./build/Release/myaddon')
// 成功する場合 → 8 が返る。
const ans1 = myaddon.add(5, 3)
console.log(ans1)
// 失敗する場合(引数に数値ではなく文字列を渡してる) → 例外が投げられる。
try {
const ans2 = myaddon.add(5, "abc")
console.log(ans2)
} catch (e) {
console.log(e.message)
}
実行結果。意図した通りに動いてる。
$ node sample.js
8
Wrong arguments
node-addon-api を使う場合
開発環境の準備
ビルドに必要なツール類をインストールする。node-gyp の代わりに CMake も使えるが2、今回はそのまま node-gyp を使った。
sudo apt install build-essential
sudo npm install -g node-gyp
開発
以下、適当な作業用ディレクトリで開発を行う。
package.json を用意。
npm init
# dependencies に node-addon-api を追加
npm install node-addon-api
# package.json に `"gypfile": true` を追加する
vi package.json
以下のようになる。
{
"name": "myaddon",
"version": "1.0.0",
"description": "",
"main": "sample.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"node-addon-api": "^3.0.0"
},
"gypfile": true
}
次に main.cc を用意。内容は同じく足し算の関数 add のエクスポートだが、node-addon-api を使うと V8の複雑な型が消えてだいぶすっきりする。
なお、node-addon-api を使うには napi.h
をインクルードする。v8.h
, uv.h
, node.h
などのライブラリのヘッダーを直接インクルードしてはいけない。
#include <napi.h>
namespace demo {
Napi::Value AddMethod(const Napi::CallbackInfo& info)
{
Napi::Env env = info.Env();
// Check the number of arguments passed.
if ( info.Length() < 2 ) {
Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException();
return env.Null();
}
// Check the argument type
if ( ! info[0].IsNumber() || ! info[1].IsNumber() ) {
Napi::TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
return env.Null();
}
// Perform the operation
double p1 = info[0].As<Napi::Number>().DoubleValue();
double p2 = info[1].As<Napi::Number>().DoubleValue();
Napi::Number answer = Napi::Number::New(env, p1 + p2);
return answer;
}
Napi::Object Initialize(Napi::Env env, Napi::Object exports)
{
exports.Set(Napi::String::New(env, "add"),
Napi::Function::New(env, AddMethod));
return exports;
}
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace demo
ビルド
ビルド用の binding.gyp を用意。少し複雑になるが、C++ から JavaScript への例外を無効にする設定等をしてる。詳細はここを参照。
{
"targets": [
{
"target_name": "myaddon",
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"sources": [ "main.cc" ],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
}
]
}
ビルド手順は同じ。
node-gyp configure
node-gyp build
実行
実行手順と結果もまったく同じのため、簡易的に記載する。
# sample.js は同じものを用意しておく。
$ node sample.js
8
Wrong arguments
参考サイト
以上
-
Native abstractions for Node.js には次の記述がある: " The V8 API can, and has, changed dramatically from one V8 release to the next (and one major Node.js release to the next)." ↩
-
CMake.js より引用 : "CMake.js is an alternative build system based on CMake. CMake.js is a good choice for projects that already use CMake or for developers affected by limitations in node-gyp." ↩