この記事について
サンプルレベルの 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>
namespacedemo{voidThrowTypeError(v8::Isolate*isolate,constchar*msg){size_tmsgSize=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 JavaScriptisolate->ThrowException(v8::Exception::TypeError(v8Msg));}voidAddMethod(constv8::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 typesif(!args[0]->IsNumber()||!args[1]->IsNumber()){ThrowTypeError(isolate,"Wrong arguments");return;}// Perform the operationdoublearg0=args[0].As<v8::Number>()->Value();doublearg1=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);}voidInitialize(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 を用意。成功する場合と失敗する場合の両方を試してる。
constmyaddon=require('./build/Release/myaddon')// 成功する場合 → 8 が返る。constans1=myaddon.add(5,3)console.log(ans1)// 失敗する場合(引数に数値ではなく文字列を渡してる) → 例外が投げられる。try{constans2=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>
namespacedemo{Napi::ValueAddMethod(constNapi::CallbackInfo&info){Napi::Envenv=info.Env();// Check the number of arguments passed.if(info.Length()<2){Napi::TypeError::New(env,"Wrong number of arguments").ThrowAsJavaScriptException();returnenv.Null();}// Check the argument typeif(!info[0].IsNumber()||!info[1].IsNumber()){Napi::TypeError::New(env,"Wrong arguments").ThrowAsJavaScriptException();returnenv.Null();}// Perform the operationdoublep1=info[0].As<Napi::Number>().DoubleValue();doublep2=info[1].As<Napi::Number>().DoubleValue();Napi::Numberanswer=Napi::Number::New(env,p1+p2);returnanswer;}Napi::ObjectInitialize(Napi::Envenv,Napi::Objectexports){exports.Set(Napi::String::New(env,"add"),Napi::Function::New(env,AddMethod));returnexports;}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." ↩