一.概念
本質的に、Node.js 拡張は C++ 動的リンクライブラリです:
Addons are dynamically-linked shared objects written in C++.
JS から C/C++ 世界への扉に相当します:
Addons provide an interface between JavaScript and C/C++ libraries.
これらの C++ 拡張(xxx.node ファイル)も JS モジュールと同じように直接 require して使用できます。[Node モジュールロードメカニズム](/articles/node モジュールロードメカニズム/) がネイティブサポートを提供しているためです
P.S.いわゆる動的リンクライブラリとは、実行時に動的にロードできるライブラリ(.so ファイル、または Windows 下の .dll ファイル)のことです:
A shared library(.so) is a library that is linked but not embedded in the final executable, so will be loaded when the executable is launched and need to be present in the system where the executable is deployed.
これに対して静的ライブラリ(.a ファイル)は、コンパイル時に実行ファイルにリンクされ、外部からロードする必要がありません:
A static library(.a) is a library that can be linked directly into the final executable produced by the linker,it is contained in it and there is no need to have the library into the system where the executable will be deployed.
二.実装方式
Node.js において、C++ 拡張を書くには 3 つの方式があります:
-
直接手搓:Node、V8、libuv が提供する C++ API に基づいて直接書きますが、これらの API の異なる Node バージョン下での互換性問題を手動で処理する必要があります(特に V8 API は頻繁に変化します)
-
nan に基づく:Native Abstractions for Node.js の略で、異なる Node/V8 バージョン間の C++ API 差異を遮蔽するための抽象レイヤーを追加し、下層 API 互換性問題の処理をこのレイヤーに集約することを期待します
-
N-API に基づく(推奨方式):Node.js が提供するネイティブ拡張サポート API で、下層の JS ランタイム(V8)と完全に独立しており、ABI が Node バージョンを跨いで一定に保たれることを保証します。したがって異なる Node バージョン上で再コンパイルせずに実行できます
P.S.実際には、N-API という独立抽象レイヤーがあれば、C++ 拡張は JavaScript エンジン、Electron などのランタイムも跨げます。詳細は The Future of Native Modules in Node.js を参照
その中で、N-API が首选方式です。N-API で搞定できない場合のみ其它方式を検討します:
Unless there is a need for direct access to functionality which is not exposed by N-API, use N-API.
Node バージョンを跨いで(再コンパイル不要)直接実行できるのは間違いなく決定的な優位性ですが、专门提供された N-API のみが ABI 安定を保証します。つまり、N-API のみを使用し(同時に下層の Node、V8、libuv API を混用しない)ことで、C++ 拡張が異なる Node バージョン下で直接実行できることを保証できます。詳細は Implications of ABI Stability を参照
N-API を使用しない場合、手搓するのは少し複雑で、何層もの知識に関わります:
-
V8:Node.js が依存する JavaScript エンジンで、オブジェクト作成、関数呼び出しなどのメカニズムはすべて V8 が提供します。具体的な C++ API はヘッダーファイル node/deps/v8/include/v8.h を参照
-
libuv:イベントループ、Worker スレッドおよびすべてのプラットフォーム関連の非同期動作は libuv が提供し、ファイルシステム、socket、タイマー、システムイベントなどにクロスプラットフォーム抽象を提供します。C++ 拡張では libuv を通じて非ブロッキング方式で各種操作を実装でき、I/O または其它の時間がかかるタスクがイベントループをブロックするのを回避できます
-
Node 内部ライブラリ:Node.js 自身もいくつかの C++ API を公開しています。例えば
node::ObjectWrapクラス -
Node 依存ライブラリ:Node.js が依存するいくつかの静的リンクライブラリも C++ 拡張で使用できます。例えば OpenSSL(より多くの依存ライブラリは node/deps/ を参照)
P.S.Node.js ソースコード依存、実行メカニズムに関する詳細情報は、Node.js アーキテクチャ剖析 を参照
三.Hello World
明確にするため、ここでは最も原始的な方式を採用し、最もシンプルな C++ 拡張を手搓します:
// hoho.cc
// 見 https://github.com/nodejs/node/blob/master/src/node.h
#include <node.h>
// 見 https://github.com/nodejs/node/blob/master/deps/v8/include/v8.h
using namespace v8;
namespace demo {
void Method(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(
isolate, "hoho, there.", NewStringType::kNormal).ToLocalChecked());
}
void Initialize(Local<Object> exports) {
NODE_SET_METHOD(exports, "hoho", Method);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
}
その中で重要な 2 行に注目:
// 初期化メソッドを実装
void Initialize(Local<Object> exports) { /* ... */ }
// モジュール名に対応する初期化ロジックを登録
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
C++ 拡張は Node.js が提供する NODE_MODULE マクロを通じて初期化メソッド(Initialize)を公開します。その中で NODE_GYP_MODULE_NAME はマクロ(macro)で、コンパイル前の前処理段階で node-gyp コマンドが传入するモジュール名に展開されます
P.S.マクロ展開は文字列置換と理解できます。詳細は Macros を参照
コンパイル実行
C++ ソースコードをコンパイルする前に、まずコンパイル設定が必要です:
{
"targets": [
{
"target_name": "hoho",
"sources": [ "hoho.cc" ]
}
]
}
設定ファイル名は binding.gyp で、プロジェクトルートディレクトリに配置します(package.json に類似)。node-gyp コンパイル用に使用します
P.S.binding.gyp 具体的なフォーマット及各フィールドの意味は Input Format Reference を参照
まず node-gyp コマンドをインストールします:
npm install -g node-gyp
P.S.もちろん、npm install node-gyp で現在のプロジェクトにインストールし、npx node-gyp で呼び出すこともできます
次に node-gyp configure コマンドを通じて、現在プラットフォームの構築プロセスに必要な設定ファイルを生成します(Unix システム下では Makefile を生成、Windows 下は vcxproj ファイル)。例えば(Mac OSX):
$ node-gyp configure
gyp info it worked if it ends with ok
...
gyp info ok
# 生成されたファイルは build ディレクトリ下に位置
$ tree build/
build/
├── Makefile
├── binding.Makefile
├── config.gypi
├── gyp-mac-tool
└── hoho.target.mk
コンパイルして .node バイナリファイルを取得:
$ node-gyp build
gyp info it worked if it ends with ok
gyp info using node-gyp @6.1.0
gyp info using node @10.18.0 | darwin | x64
gyp info spawn make
gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ]
CXX(target) Release/obj.target/hoho/hoho.o
SOLINK_MODULE(target) Release/hoho.node
gyp info ok
コンパイル産物は Release/hoho.node に位置。試玩:
// index.js
// 後綴名を省略、自動的に hoho.node を見つけてロード、初期化
const hoho = require('./build/Release/hoho');
console.log(hoho.hoho());
実行結果:
$ node index.js
hoho, there.
上例は直接 Node、V8 が提供する C++ API を使用しました。バージョン跨ぎの互換性問題が存在する可能性があります(数バージョン後にはコンパイルエラーになる可能性があります)。また、異なるバージョンの Node 環境下ではすべて再コンパイルが必要です。否则実行時エラーが発生します:
$ node -v
v10.18.0
# 8.17.0 に切り替え
$ n 8.17.0
# 再コンパイルせずに直接実行
$ node index.js
module.js:682
return process.dlopen(module, path._makeLong(filename));
^
Error: The module '/path/to/hoho/build/Release/hoho.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 64. This version of Node.js requires
NODE_MODULE_VERSION 57. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or `npm install`).
再コンパイル、実行する必要があります:
$ node-gyp rebuild
gyp info it worked if it ends with ok
...
gyp info ok
$ node index.js
hoho, there.
では、一労永逸の方式はあるのか?
あります。N-API です
四.N-API
直接 Node、V8 などの下層 C/C++ モジュールが公開する API を使用せず、すべて N-API に置き換えます:
// hoho-anywhere.cc
#include <node_api.h>
namespace demo {
napi_value Method(napi_env env, napi_callback_info args) {
napi_value greeting;
napi_status status;
status = napi_create_string_utf8(env, "hoho, anywhere.", NAPI_AUTO_LENGTH, &greeting);
if (status != napi_ok) return nullptr;
return greeting;
}
napi_value init(napi_env env, napi_value exports) {
napi_status status;
napi_value fn;
status = napi_create_function(env, nullptr, 0, Method, nullptr, &fn);
if (status != napi_ok) return nullptr;
status = napi_set_named_property(env, exports, "hoho", fn);
if (status != napi_ok) return nullptr;
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, init)
}
ヘッダーファイル node_api.h のみをインポートし、値タイプなども直接使用 v8::String を使用しません
コンパイル設定 binding.gyp を修正:
{
"targets": [
{
"target_name": "hoho",
"sources": [ "hoho-anywhere.cc" ]
}
]
}
コンパイル実行:
$ node-gyp rebuild
$ node index.js
hoho, anywhere.
# Node バージョンを切り替え
$ n 8.17.0
# コンパイル不要、直接実行可能!
$ node index.js
hoho, anywhere.
P.S.より複雑な用法、および N-API に関する詳細情報は、N-API を参照
P.S.また、N-API が提供するものはすべて C インターフェースです。C++ 環境に対しては、node-addon-api を採用できます
五.応用シーン
いくつかのシーンでは、C++ 拡張で実装するのが特に適しています:
-
計算集約型モジュール。C++ の実行パフォーマンスは一般に JS より高い
-
既存の C++ ライブラリを低コストで Node.js 拡張にカプセル化し、Node エコシステムに提供
-
Node.js が提供するネイティブ能力がニーズを満たせない場合。例えば fsevents
-
JS 言語はいくつかの方面で先天的な不足が存在(例えば数値精度、ビット演算など)。C++ で補足可能
P.S.注意、実行時に C++ テンプレートを初期化すること自体にいくつかのオーバーヘッドが存在します。性能を苛求するシーンではこの要因を考慮に入れる必要があります。また、C++ が常に JS より速いわけではありません(例えば正規表現マッチの某些のシーン)
コメントはまだありません