16
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

N-API を使って Node.js に C++ によるアドオンを実装する最短の道。

Last updated at Posted at 2019-01-28

なんで?

コンピュータのサービスって大体インターフェースとエンジンにわけられるじゃないですか。で、エンジン部分に高速性を求められるとき、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 は入れておいてください。

アドオンの作成

  • ソースファイル
addon.cpp
#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 でビルドするための設定ファイル。
binding.gyp
{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "addon.cpp" ]
    }
  ]
}

ビルド

$ node-gyp rebuild

build/Release/addon.node

という名前でアドオンができます。

呼び出し側の JavaScript

index.js
var addon = require( './build/Release/addon' );
console.log( addon.add( 3, 5 ) );

実行

$ node index.js
8

掛け算も入れてみる。

一つのアドオンで複数の機能を持たすのはこんな風にすればいいです。

addon.cpp
#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

足し算の例をやってみます。

addon.cpp
#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 )
binding.gyp
{	"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 行あたりです。

  • 修正前
gyp/pylib/gyp/generator/make.py
      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')
  • 修正後
gyp/pylib/gyp/generator/make.py
      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
16
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?