なんで?
コンピュータのサービスって大体インターフェースとエンジンにわけられるじゃないですか。で、エンジン部分に高速性を求められるとき、C++ で書きたいなと思う場合があるじゃないですか。でも C++ で全部を書こうとすると、インターフェース部分、つまり HTTP サーバーを作ったり、JSON の対応したりするのが鬼のように大変じゃないですか。
ここで N-API の出番です。これを使うとインターフェースを node.js、エンジンを C++ みたいなサービスをちょっとの手間で作ることができます。
最初に N-API をそのままで、次にそのラッパーの node-addon-api を使ってやってみます。
補足
-
Node の C++ の Native アドオンを作る方法は、他に V8 直とか、nan とかありますが私の2019年のおすすめはこの最新の N-API です。
-
この記事は以下のアドレスのサンプルをベースにいろいろ手を加えたものです。
https://github.com/nodejs/abi-stable-node-addon-examples
準備
node-gyp を使ってビルドするので、グローバルにインストールしておきます。
$ npm i -g node-gyp
作業用のディレクトリを作ってそこで作業をします。
$ mkdir Experimental
$ cd Experimental
N-API そのまま
まずは足し算をする例
python2.7, node, npm は入れておいてください。
アドオンの作成
- ソースファイル
#include <node_api.h>
double
Double( napi_env env, napi_value p ) {
double v;
napi_get_value_double( env, p, &v );
return v;
}
napi_value
Double( napi_env env, double p ) {
napi_value v;
napi_create_double( env, p, &v );
return v;
}
bool
Is( napi_env env, napi_value p, napi_valuetype type ) {
napi_valuetype w;
napi_typeof( env, p, &w );
return w == type;
}
napi_value
Add( napi_env env, napi_callback_info info ) {
size_t argc = 2;
napi_value args[ 2 ];
napi_get_cb_info( env, info, &argc, args, 0, 0 );
if ( argc != 2 ) {
napi_throw_type_error( env, 0, "Wrong number of arguments" );
return 0;
}
if ( !Is( env, args[ 0 ], napi_number ) || !Is( env, args[ 1 ], napi_number ) ) {
napi_throw_type_error( env, 0, "Wrong arguments" );
return 0;
}
return Double( env, Double( env, args[ 0 ] ) + Double( env, args[ 1 ] ) );
}
napi_value
Init( napi_env env, napi_value exports ) {
napi_property_descriptor w = { "add", 0, Add, 0, 0, 0, napi_default, 0 };
napi_define_properties( env, exports, 1, &w );
return exports;
}
NAPI_MODULE( NODE_GYP_MODULE_NAME, Init )
- node-gyp でビルドするための設定ファイル。
{
"targets": [
{
"target_name": "addon",
"sources": [ "addon.cpp" ]
}
]
}
ビルド
$ node-gyp rebuild
build/Release/addon.node
という名前でアドオンができます。
呼び出し側の JavaScript
var addon = require( './build/Release/addon' );
console.log( addon.add( 3, 5 ) );
実行
$ node index.js
8
掛け算も入れてみる。
一つのアドオンで複数の機能を持たすのはこんな風にすればいいです。
#include <node_api.h>
double
Double( napi_env env, napi_value p ) {
double v;
napi_get_value_double( env, p, &v );
return v;
}
napi_value
Double( napi_env env, double p ) {
napi_value v;
napi_create_double( env, p, &v );
return v;
}
bool
Is( napi_env env, napi_value p, napi_valuetype type ) {
napi_valuetype w;
napi_typeof( env, p, &w );
return w == type;
}
template < typename F > napi_value
TwoDoubles( napi_env env, napi_callback_info info, F p ) {
size_t argc = 2;
napi_value args[ 2 ];
napi_get_cb_info( env, info, &argc, args, 0, 0 );
if ( argc != 2 ) {
napi_throw_type_error( env, 0, "Wrong number of arguments" );
return 0;
}
if ( !Is( env, args[ 0 ], napi_number ) || !Is( env, args[ 1 ], napi_number ) ) {
napi_throw_type_error( env, 0, "Wrong arguments" );
return 0;
}
return Double( env, p( Double( env, args[ 0 ] ), Double( env, args[ 1 ] ) ) );
}
napi_value
Add( napi_env env, napi_callback_info info ) {
return TwoDoubles( env, info, []( double l, double r ){ return l + r; } );
}
napi_value
Mul( napi_env env, napi_callback_info info ) {
return TwoDoubles( env, info, []( double l, double r ){ return l * r; } );
}
napi_value
Init( napi_env env, napi_value exports ) {
napi_property_descriptor w[] = {
{ "add", 0, Add, 0, 0, 0, napi_default, 0 }
, { "mul", 0, Mul, 0, 0, 0, napi_default, 0 }
};
napi_define_properties( env, exports, 2, w );
return exports;
}
NAPI_MODULE( NODE_GYP_MODULE_NAME, Init )
node-addon-api を使ってみる。
N-API をラップする node-addon-apiというのがあって、これを使うともうちょっとすっきり書けます。
インストール:
$ npm i node-addon-api
足し算の例をやってみます。
#include <napi.h>
using namespace Napi;
Value
Add( const CallbackInfo& info ) {
Env env = info.Env();
if ( info.Length() < 2 ) {
TypeError::New( env, "Wrong number of arguments" ).ThrowAsJavaScriptException();
return env.Null();
}
if ( !info[ 0 ].IsNumber() || !info[ 1 ].IsNumber() ) {
TypeError::New( env, "Wrong arguments" ).ThrowAsJavaScriptException();
return env.Null();
}
return Number::New(
env
, info[ 0 ].As<Number>().DoubleValue() + info[ 1 ].As<Number>().DoubleValue()
);
}
Object
Init( Env env, Object exports ) {
exports.Set( String::New( env, "add" ), Function::New( env, Add ) );
return exports;
}
NODE_API_MODULE( addon, Init )
{ "targets":
[ { "target_name" : "addon"
, "sources" : [ "addon.cpp" ]
, "defines" : [ "NAPI_DISABLE_CPP_EXCEPTIONS" ]
, "include_dirs" : [ "<!@(node -p \"require( 'node-addon-api' ).include\")" ]
}
]
}
すっきりしますね!
macOSでの注意点
例外と c++17 を使ったソースで実装しようとした場合、node-gyp まわりでいろいろひっかかってしまいましたので共有しておきます。
c++17 標準を使いたい場合
node-gyp が macOS の場合、cflags を binding.gyp の xcode_settings / OTHER_CFLAGS から持ってきているようので、binding.gyp に以下の記述をすればいいようです。
'xcode_settings': { 'OTHER_CFLAGS': [ "-std=c++17" ] }
c++ の例外を使いたい場合
node-gyp がデフォルトで -fno_exception を定義しているのでこのままでは使えません。
macOS 以外では以下のようにやればいいらしいのですが
"defines" : [ "NAPI_CPP_EXCEPTIONS" ]
"cflags_cc!" : [ "-fno-exception" ]
node-gyp は上に述べたように macOS の場合 cflags_cc を見ていません。
node-gyp のソースに手を入れて cflags_cc を見るようにする以外の手がおもいつかなかったので、やってみました。
手を入れるところは gyp/pylib/gyp/generator/make.py 1217 行あたりです。
- 修正前
if self.flavor == 'mac':
cflags = self.xcode_settings.GetCflags(configname)
cflags_c = self.xcode_settings.GetCflagsC(configname)
cflags_cc = self.xcode_settings.GetCflagsCC(configname)
cflags_objc = self.xcode_settings.GetCflagsObjC(configname)
cflags_objcc = self.xcode_settings.GetCflagsObjCC(configname)
else:
cflags = config.get('cflags')
cflags_c = config.get('cflags_c')
cflags_cc = config.get('cflags_cc')
- 修正後
cflags = config.get('cflags')
cflags_c = config.get('cflags_c')
cflags_cc = config.get('cflags_cc')
if self.flavor == 'mac':
cflags_objc = self.xcode_settings.GetCflagsObjC(configname)
cflags_objcc = self.xcode_settings.GetCflagsObjCC(configname)
実行するには修正したレポジトリの中の bin/node-gyp.js を使えばいいです。:
$ node /・・・/node-gyp-master/bin/node-gyp.js rebuild