C++ アドオン
アドオン は、C++ で記述された動的にリンクされる共有オブジェクトです。require()
関数は、アドオンを通常の Node.js モジュールとしてロードできます。アドオンは、JavaScript と C/C++ ライブラリ間のインターフェースを提供します。
アドオンを実装するためのオプションは 3 つあります。
- Node-API
nan
(Native Abstractions for Node.js)- V8、libuv、および Node.js ライブラリの直接使用
Node-API で公開されていない機能に直接アクセスする必要がない限り、Node-API を使用してください。Node-API の詳細については、Node-API を使用した C/C++ アドオン を参照してください。
Node-API を使用しない場合、アドオンの実装はより複雑になり、複数のコンポーネントと API の知識が必要です。
- V8: Node.js が JavaScript 実装を提供するために使用する C++ ライブラリです。オブジェクトの作成、関数の呼び出しなどのメカニズムを提供します。V8 の API は、主に
v8.h
ヘッダーファイル(Node.js ソースツリーのdeps/v8/include/v8.h
)に記載されており、オンラインでも利用できます。 - libuv: Node.js イベントループ、そのワーカスレッド、およびプラットフォームのすべての非同期動作を実装する C ライブラリです。また、クロスプラットフォームの抽象化ライブラリとしても機能し、ファイルシステム、ソケット、タイマー、システムイベントとの対話など、多くの一般的なシステムタスクへの簡単で POSIX 風のアクセスを主要なオペレーティングシステム全体で提供します。libuv は、標準のイベントループを超える必要があるより高度な非同期アドオンのために、POSIX スレッドと同様のスレッド抽象化も提供します。アドオンの作者は、libuv を介して非ブロッキングシステム操作、ワーカスレッド、または libuv スレッドのカスタム使用に作業をオフロードすることにより、I/O やその他の時間のかかるタスクでイベントループをブロックしないようにする必要があります。
- Node.js 内部ライブラリ: Node.js 自体は、アドオンで使用できる C++ API をエクスポートします。その中で最も重要なのは
node::ObjectWrap
クラスです。 - その他の静的にリンクされたライブラリ(OpenSSL を含む):これらの他のライブラリは、Node.js ソースツリーの
deps/
ディレクトリにあります。libuv、OpenSSL、V8、および zlib のシンボルのみが Node.js によって意図的に再エクスポートされ、アドオンによってさまざまな範囲で使用される場合があります。詳細については、Node.js に含まれるライブラリへのリンク を参照してください。
以下のすべての例はダウンロードでき、アドオンの出発点として使用できます。
Hello world
この"Hello world"の例は、C++で記述されたシンプルなアドオンであり、次の JavaScript コードと同等です。
module.exports.hello = () => 'world'
まず、hello.cc
ファイルを作成します。
// hello.cc
#include <node.h>
namespace demo {
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
using v8::Object;
using v8::String;
using v8::Value;
void Method(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(
isolate, "world", NewStringType::kNormal).ToLocalChecked());
}
void Initialize(Local<Object> exports) {
NODE_SET_METHOD(exports, "hello", Method);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace demo
全ての Node.js アドオンは、以下のパターンに従った初期化関数をエクスポートする必要があります。
void Initialize(Local<Object> exports);
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
NODE_MODULE
の後にセミコロンはありません(node.h
を参照)。
module_name
は、最終的なバイナリファイル名(.node
サフィックスを除く)と一致する必要があります。
hello.cc
の例では、初期化関数はInitialize
であり、アドオンモジュール名はaddon
です。
node-gyp
を使用してアドオンをビルドする場合、NODE_MODULE()
の最初の引数としてNODE_GYP_MODULE_NAME
マクロを使用すると、最終的なバイナリの名前がNODE_MODULE()
に渡されます。
NODE_MODULE()
で定義されたアドオンは、同時に複数のコンテキストまたは複数のスレッドでロードすることはできません。
コンテキスト認識アドオン
Node.js アドオンを複数のコンテキストで複数回ロードする必要がある環境があります。たとえば、Electronランタイムは、単一のプロセスで Node.js の複数のインスタンスを実行します。各インスタンスには独自のrequire()
キャッシュがあり、そのため、各インスタンスではネイティブアドオンがrequire()
を介してロードされたときに正しく動作する必要があります。これは、アドオンが複数の初期化をサポートする必要があることを意味します。
コンテキスト認識アドオンは、Node.js がアドオンのロード時に見つけることを期待する関数の名前を展開するNODE_MODULE_INITIALIZER
マクロを使用して構築できます。したがって、アドオンは次の例のように初期化できます。
using namespace v8;
extern "C" NODE_MODULE_EXPORT void
NODE_MODULE_INITIALIZER(Local<Object> exports,
Local<Value> module,
Local<Context> context) {
/* Perform addon initialization steps here. */
}
別のオプションとしてNODE_MODULE_INIT()
マクロを使用することもできます。これはコンテキスト認識アドオンも構築します。NODE_MODULE()
は特定のアドオン初期化関数を囲むアドオンを構築するために使用されますが、NODE_MODULE_INIT()
は、関数本体が続くそのような初期化子の宣言として機能します。
NODE_MODULE_INIT()
の呼び出し後に続く関数本体内では、次の 3 つの変数を使用できます。
Local<Object> exports
、Local<Value> module
、およびLocal<Context> context
コンテキスト認識アドオンを構築するには、安定性と正確性を確保するために、グローバル静的データの慎重な管理が必要です。アドオンは複数回ロードされる可能性があり、異なるスレッドからロードされる可能性もあるため、アドオンに格納されているグローバル静的データは適切に保護する必要があり、JavaScript オブジェクトへの永続的な参照を含んではいけません。その理由は、JavaScript オブジェクトは 1 つのコンテキストでのみ有効であり、間違ったコンテキストから、または作成されたスレッドとは異なるスレッドからアクセスすると、クラッシュを引き起こす可能性が高いからです。
グローバル静的データを回避するために、コンテキスト認識アドオンを次のように構成できます。
- アドオンごとのインスタンスデータを保持し、
static
メンバを持つクラスを定義します。 - アドオン初期化子でこのクラスのインスタンスをヒープに割り当てます。これは
new
キーワードを使用して行うことができます。 node::AddEnvironmentCleanupHook()
を呼び出し、作成したインスタンスとDeleteInstance()
へのポインタを渡します。これにより、環境が破棄されるときにインスタンスが削除されます。- クラスのインスタンスを
v8::External
に格納し、 - ネイティブバックの JavaScript 関数を生成する
v8::FunctionTemplate::New()
またはv8::Function::New()
に渡すことで、JavaScript に公開されたすべてのメソッドにv8::External
を渡します。v8::FunctionTemplate::New()
またはv8::Function::New()
の 3 番目のパラメータはv8::External
を受け入れ、v8::FunctionCallbackInfo::Data()
メソッドを使用してネイティブコールバックで使用できるようにします。
これにより、アドオンごとのインスタンスデータが JavaScript から呼び出すことができる各バインディングに到達することが保証されます。アドオンごとのインスタンスデータは、アドオンが作成する非同期コールバックにも渡す必要があります。
次の例は、コンテキスト認識アドオンの実装を示しています。
#include <node.h>
using namespace v8;
class AddonData {
public:
explicit AddonData(Isolate* isolate):
call_count(0) {
// Ensure this per-addon-instance data is deleted at environment cleanup.
node::AddEnvironmentCleanupHook(isolate, DeleteInstance, this);
}
// Per-addon data.
int call_count;
static void DeleteInstance(void* data) {
delete static_cast<AddonData*>(data);
}
};
static void Method(const v8::FunctionCallbackInfo<v8::Value>& info) {
// Retrieve the per-addon-instance data.
AddonData* data =
reinterpret_cast<AddonData*>(info.Data().As<External>()->Value());
data->call_count++;
info.GetReturnValue().Set((double)data->call_count);
}
// Initialize this addon to be context-aware.
NODE_MODULE_INIT(/* exports, module, context */) {
Isolate* isolate = context->GetIsolate();
// Create a new instance of `AddonData` for this instance of the addon and
// tie its life cycle to that of the Node.js environment.
AddonData* data = new AddonData(isolate);
// Wrap the data in a `v8::External` so we can pass it to the method we
// expose.
Local<External> external = External::New(isolate, data);
// Expose the method `Method` to JavaScript, and make sure it receives the
// per-addon-instance data we created above by passing `external` as the
// third parameter to the `FunctionTemplate` constructor.
exports->Set(context,
String::NewFromUtf8(isolate, "method").ToLocalChecked(),
FunctionTemplate::New(isolate, Method, external)
->GetFunction(context).ToLocalChecked()).FromJust();
}
Worker サポート
[履歴]
バージョン | 変更 |
---|---|
v14.8.0, v12.19.0 | クリーンアップフックが非同期になるようになりました。 |
メインスレッドとワーカースレッドなど、複数の Node.js 環境からロードするために、アドオンは次のいずれかの方法にする必要があります。
- Node-API アドオンにするか、
- 上記のように
NODE_MODULE_INIT()
を使用してコンテキスト認識として宣言する
Worker
スレッドをサポートするために、アドオンは、そのようなスレッドが終了したときに割り当てた可能性のあるすべてのリソースをクリーンアップする必要があります。これは、AddEnvironmentCleanupHook()
関数の使用によって実現できます。
void AddEnvironmentCleanupHook(v8::Isolate* isolate,
void (*fun)(void* arg),
void* arg);
この関数は、特定の Node.js インスタンスがシャットダウンする前に実行されるフックを追加します。必要に応じて、RemoveEnvironmentCleanupHook()
を使用して、実行前にそのようなフックを削除できます。これは同じシグネチャを持ちます。コールバックは、後入れ先出しの順序で実行されます。
必要に応じて、クリーンアップフックがコールバック関数を取るAddEnvironmentCleanupHook()
とRemoveEnvironmentCleanupHook()
の追加のペアがあります。これは、アドオンによって登録された libuv ハンドルなど、非同期リソースのシャットダウンに使用できます。
次のaddon.cc
はAddEnvironmentCleanupHook
を使用します。
// addon.cc
#include <node.h>
#include <assert.h>
#include <stdlib.h>
using node::AddEnvironmentCleanupHook;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::Object;
// Note: In a real-world application, do not rely on static/global data.
static char cookie[] = "yum yum";
static int cleanup_cb1_called = 0;
static int cleanup_cb2_called = 0;
static void cleanup_cb1(void* arg) {
Isolate* isolate = static_cast<Isolate*>(arg);
HandleScope scope(isolate);
Local<Object> obj = Object::New(isolate);
assert(!obj.IsEmpty()); // assert VM is still alive
assert(obj->IsObject());
cleanup_cb1_called++;
}
static void cleanup_cb2(void* arg) {
assert(arg == static_cast<void*>(cookie));
cleanup_cb2_called++;
}
static void sanity_check(void*) {
assert(cleanup_cb1_called == 1);
assert(cleanup_cb2_called == 1);
}
// Initialize this addon to be context-aware.
NODE_MODULE_INIT(/* exports, module, context */) {
Isolate* isolate = context->GetIsolate();
AddEnvironmentCleanupHook(isolate, sanity_check, nullptr);
AddEnvironmentCleanupHook(isolate, cleanup_cb2, cookie);
AddEnvironmentCleanupHook(isolate, cleanup_cb1, isolate);
}
JavaScript でテストするには、次のように実行します。
// test.js
require('./build/Release/addon')
ビルド
ソースコードの記述が完了したら、バイナリファイル addon.node
にコンパイルする必要があります。そのためには、プロジェクトの最上位レベルに binding.gyp
というファイルを作成し、JSON ライクな形式でモジュールのビルド構成を記述します。このファイルは、Node.js アドオンをコンパイルするために特別に記述されたツールである node-gyp によって使用されます。
{
"targets": [
{
"target_name": "addon",
"sources": ["hello.cc"]
}
]
}
node-gyp
ユーティリティのバージョンは、npm
の一部として Node.js にバンドルされ、配布されています。このバージョンは、開発者が直接使用できるようにはなっていないため、npm install
コマンドを使用してアドオンをコンパイルおよびインストールする機能のみをサポートすることを目的としています。node-gyp
を直接使用したい開発者は、npm install -g node-gyp
コマンドを使用してインストールできます。プラットフォーム固有の要件など、詳細については、node-gyp
の インストール手順 を参照してください。
binding.gyp
ファイルを作成したら、node-gyp configure
を使用して、現在のプラットフォームに適したプロジェクトビルドファイルを作成します。これにより、build/
ディレクトリに Makefile
(Unix 系プラットフォーム)または vcxproj
ファイル(Windows)が生成されます。
次に、node-gyp build
コマンドを実行して、コンパイル済みの addon.node
ファイルを生成します。これは build/Release/
ディレクトリに配置されます。
npm install
を使用して Node.js アドオンをインストールする場合、npm は独自のバンドルされたバージョンの node-gyp
を使用して、この一連のアクションを実行し、ユーザーのプラットフォーム向けにアドオンのコンパイル済みバージョンをオンデマンドで生成します。
ビルドが完了すると、require()
にビルドされた addon.node
モジュールを指定することで、Node.js 内からバイナリアドオンを使用できます。
// hello.js
const addon = require('./build/Release/addon')
console.log(addon.hello())
// Prints: 'world'
コンパイルされたアドオンバイナリへの正確なパスは、コンパイル方法によって異なる可能性があるため(つまり、場合によっては ./build/Debug/
にある場合もあります)、アドオンはコンパイル済みモジュールをロードするために bindings パッケージを使用できます。
bindings
パッケージの実装は、アドオンモジュールの場所を特定する方法においてより洗練されていますが、本質的には次のような try…catch
パターンを使用しています。
try {
return require('./build/Release/addon.node')
} catch (err) {
return require('./build/Debug/addon.node')
}
Node.js に含まれるライブラリへのリンク
Node.js は、V8、libuv、OpenSSL などの静的にリンクされたライブラリを使用します。すべてのアドオンは V8 にリンクする必要があり、他の依存関係にもリンクすることがあります。通常、これは適切な#include <...>
ステートメント(例:#include <v8.h>
)を含めるだけで、node-gyp
が適切なヘッダーを自動的に見つけてくれます。ただし、いくつか注意すべき点があります。
node-gyp
が実行されると、Node.js の特定のリリースバージョンを検出し、完全なソースの tarball またはヘッダーのみをダウンロードします。完全なソースがダウンロードされた場合、アドオンは Node.js 依存関係の完全なセットに完全にアクセスできます。ただし、Node.js のヘッダーのみがダウンロードされた場合、Node.js によってエクスポートされたシンボルのみが使用できます。node-gyp
は、ローカルの Node.js ソースイメージを指す--nodedir
フラグを使用して実行できます。このオプションを使用すると、アドオンは依存関係の完全なセットにアクセスできます。
require()
を使用したアドオンの読み込み
コンパイルされたアドオンバイナリのファイル拡張子は.node
です(.dll
または.so
とは異なります)。require()
関数は.node
ファイル拡張子のファイルを探し、それらを動的にリンクされたライブラリとして初期化するように記述されています。
require()
を呼び出すとき、.node
拡張子は通常省略できますが、Node.js はそれでもアドオンを見つけ、初期化します。ただし、例外として、Node.js は最初に、同じベース名を持つモジュールまたは JavaScript ファイルの検索と読み込みを試みます。たとえば、バイナリaddon.node
と同じディレクトリにaddon.js
ファイルがある場合、require('addon')
はaddon.js
ファイルを優先して読み込みます。
Node.js ネイティブ抽象化
このドキュメントで説明されている例はそれぞれ、アドオンの実装に Node.js と V8 の API を直接使用しています。V8 API は、V8 のリリース間(および主要な Node.js のリリース間)で劇的に変更される可能性があります。変更ごとに、アドオンを更新して再コンパイルして機能を継続する必要がある場合があります。Node.js のリリーススケジュールは、このような変更の頻度と影響を最小限に抑えるように設計されていますが、Node.js が V8 API の安定性を確保するためにできることはほとんどありません。
Node.js ネイティブ抽象化(またはnan
)は、アドオン開発者が V8 と Node.js の過去と将来のリリース間の互換性を維持するために使用することをお勧めするツールのセットを提供します。使用方法については、nan
の例を参照してください。
Node-API
Node-API は、ネイティブアドオンを構築するための API です。基盤となる JavaScript ランタイム(例:V8)とは独立しており、Node.js 自体の一部として維持されています。この API は、Node.js のバージョン間でアプリケーションバイナリインターフェース(ABI)が安定しています。これは、基盤となる JavaScript エンジンでの変更からアドオンを保護し、あるバージョン用にコンパイルされたモジュールを再コンパイルせずに、それ以降のバージョンの Node.js で実行できるようにすることを目的としています。アドオンは、このドキュメント(node-gyp など)で概説されているのと同じアプローチ/ツールを使用して構築/パッケージ化されます。唯一の違いは、ネイティブコードで使用される API のセットです。V8 またはNative Abstractions for Node.js API を使用する代わりに、Node-API で使用可能な関数が使用されます。
Node-API によって提供される ABI の安定性の恩恵を受けるアドオンの作成と維持には、特定の実装上の考慮事項が伴います。
上記の「Hello world」の例で Node-API を使用するには、hello.cc
の内容を次のように置き換えます。その他の指示はすべて同じです。
// hello.cc using Node-API
#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, "world", 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, "hello", fn);
if (status != napi_ok) return nullptr;
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, init)
} // namespace demo
使用可能な関数とその使用方法については、Node-API を使用した C/C++アドオンに記載されています。
Addon 例
開発者が開始するのに役立つ例として、いくつかの Addon 例を以下に示します。これらの例では、V8 API を使用しています。様々な V8 呼び出しについてはオンラインのV8 リファレンスを、ハンドル、スコープ、関数テンプレートなどのいくつかの概念の説明については V8 のエンベッダーガイドを参照してください。
これらの例はすべて、以下のbinding.gyp
ファイルを使用しています。
{
"targets": [
{
"target_name": "addon",
"sources": ["addon.cc"]
}
]
}
.cc
ファイルが複数ある場合は、sources
配列に追加のファイル名を追加するだけです。
"sources": ["addon.cc", "myexample.cc"]
binding.gyp
ファイルの準備ができたら、node-gyp
を使用して例 Addon を構成してビルドできます。
node-gyp configure build
関数引数
Addon は通常、Node.js 内で実行されている JavaScript からアクセスできるオブジェクトと関数を公開します。関数が JavaScript から呼び出されると、入力引数と戻り値は C/C++コードとの間でマッピングする必要があります。
次の例は、JavaScript から渡された関数引数をどのように読み取り、結果を返すかを示しています。
// addon.cc
#include <node.h>
namespace demo {
using v8::Exception;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;
// これは "add" メソッドの実装です
// 入力引数は const FunctionCallbackInfo<Value>& args 構造体を使用して渡されます
void Add(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
// 渡された引数の数をチェックします。
if (args.Length() < 2) {
// JavaScript に戻される Error をスローします
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate,
"Wrong number of arguments").ToLocalChecked()));
return;
}
// 引数の型をチェックします
if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate,
"Wrong arguments").ToLocalChecked()));
return;
}
// 演算を実行します
double value =
args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
Local<Number> num = Number::New(isolate, value);
// 戻り値を設定します (渡された FunctionCallbackInfo<Value>& を使用します)
args.GetReturnValue().Set(num);
}
void Init(Local<Object> exports) {
NODE_SET_METHOD(exports, "add", Add);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
} // namespace demo
コンパイル後、この例 Addon は Node.js 内から require して使用できます。
// test.js
const addon = require('./build/Release/addon')
console.log('This should be eight:', addon.add(3, 5))
コールバック
アドオン内で、JavaScript 関数を C++関数に渡し、そこから実行することが一般的です。次の例は、そのようなコールバックを呼び出す方法を示しています。
// addon.cc
#include <node.h>
namespace demo {
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Null;
using v8::Object;
using v8::String;
using v8::Value;
void RunCallback(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<Function> cb = Local<Function>::Cast(args[0]);
const unsigned argc = 1;
Local<Value> argv[argc] = {
String::NewFromUtf8(isolate,
"hello world").ToLocalChecked() };
cb->Call(context, Null(isolate), argc, argv).ToLocalChecked();
}
void Init(Local<Object> exports, Local<Object> module) {
NODE_SET_METHOD(module, "exports", RunCallback);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
} // namespace demo
この例では、完全なmodule
オブジェクトを第 2 引数として受け取るInit()
の 2 引数形式を使用しています。これにより、アドオンはexports
のプロパティとして関数を追加する代わりに、単一関数でexports
を完全に上書きできます。
テストするには、次の JavaScript を実行します。
// test.js
const addon = require('./build/Release/addon')
addon(msg => {
console.log(msg)
// Prints: 'hello world'
})
この例では、コールバック関数は同期的に呼び出されます。
オブジェクトファクトリ
アドオンは、次の例に示すように、C++関数内で新しいオブジェクトを作成して返すことができます。createObject()
に渡された文字列をエコーするプロパティmsg
を持つオブジェクトが作成され、返されます。
// addon.cc
#include <node.h>
namespace demo {
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
void CreateObject(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<Object> obj = Object::New(isolate);
obj->Set(context,
String::NewFromUtf8(isolate,
"msg").ToLocalChecked(),
args[0]->ToString(context).ToLocalChecked())
.FromJust();
args.GetReturnValue().Set(obj);
}
void Init(Local<Object> exports, Local<Object> module) {
NODE_SET_METHOD(module, "exports", CreateObject);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
} // namespace demo
JavaScript でテストするには:
// test.js
const addon = require('./build/Release/addon')
const obj1 = addon('hello')
const obj2 = addon('world')
console.log(obj1.msg, obj2.msg)
// Prints: 'hello world'
関数ファクトリ
もう 1 つの一般的なシナリオは、C++関数をラップする JavaScript 関数を生成し、それを JavaScript に戻すことです。
// addon.cc
#include <node.h>
namespace demo {
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
void MyFunction(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(
isolate, "hello world").ToLocalChecked());
}
void CreateFunction(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, MyFunction);
Local<Function> fn = tpl->GetFunction(context).ToLocalChecked();
// omit this to make it anonymous
fn->SetName(String::NewFromUtf8(
isolate, "theFunction").ToLocalChecked());
args.GetReturnValue().Set(fn);
}
void Init(Local<Object> exports, Local<Object> module) {
NODE_SET_METHOD(module, "exports", CreateFunction);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
} // namespace demo
テストするには:
// test.js
const addon = require('./build/Release/addon')
const fn = addon()
console.log(fn())
// Prints: 'hello world'
C++オブジェクトのラップ
JavaScript のnew
演算子を使用して新しいインスタンスを作成できるように、C++オブジェクト/クラスをラップすることも可能です。
// addon.cc
#include <node.h>
#include "myobject.h"
namespace demo {
using v8::Local;
using v8::Object;
void InitAll(Local<Object> exports) {
MyObject::Init(exports);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)
} // namespace demo
次に、myobject.h
で、ラッパー クラスはnode::ObjectWrap
を継承します。
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H
#include <node.h>
#include <node_object_wrap.h>
namespace demo {
class MyObject : public node::ObjectWrap {
public:
static void Init(v8::Local<v8::Object> exports);
private:
explicit MyObject(double value = 0);
~MyObject();
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);
double value_;
};
} // namespace demo
#endif
myobject.cc
で、公開するさまざまなメソッドを実装します。次のコードでは、plusOne()
メソッドはコンストラクタのプロトタイプに追加することで公開されます。
// myobject.cc
#include "myobject.h"
namespace demo {
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::ObjectTemplate;
using v8::String;
using v8::Value;
MyObject::MyObject(double value) : value_(value) {
}
MyObject::~MyObject() {
}
void MyObject::Init(Local<Object> exports) {
Isolate* isolate = exports->GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<ObjectTemplate> addon_data_tpl = ObjectTemplate::New(isolate);
addon_data_tpl->SetInternalFieldCount(1); // 1 field for the MyObject::New()
Local<Object> addon_data =
addon_data_tpl->NewInstance(context).ToLocalChecked();
// Prepare constructor template
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New, addon_data);
tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
// Prototype
NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);
Local<Function> constructor = tpl->GetFunction(context).ToLocalChecked();
addon_data->SetInternalField(0, constructor);
exports->Set(context, String::NewFromUtf8(
isolate, "MyObject").ToLocalChecked(),
constructor).FromJust();
}
void MyObject::New(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.IsConstructCall()) {
// Invoked as constructor: `new MyObject(...)`
double value = args[0]->IsUndefined() ?
0 : args[0]->NumberValue(context).FromMaybe(0);
MyObject* obj = new MyObject(value);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
} else {
// Invoked as plain function `MyObject(...)`, turn into construct call.
const int argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons =
args.Data().As<Object>()->GetInternalField(0)
.As<Value>().As<Function>();
Local<Object> result =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(result);
}
}
void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.This());
obj->value_ += 1;
args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}
} // namespace demo
この例をビルドするには、myobject.cc
ファイルをbinding.gyp
に追加する必要があります。
{
"targets": [
{
"target_name": "addon",
"sources": ["addon.cc", "myobject.cc"]
}
]
}
以下でテストします。
// test.js
const addon = require('./build/Release/addon')
const obj = new addon.MyObject(10)
console.log(obj.plusOne())
// Prints: 11
console.log(obj.plusOne())
// Prints: 12
console.log(obj.plusOne())
// Prints: 13
ラッパーオブジェクトのデストラクタは、オブジェクトがガベージコレクションされたときに実行されます。デストラクタのテストには、ガベージコレクションを強制的に行うことができるコマンドラインフラグがあります。これらのフラグは、基盤となる V8 JavaScript エンジンによって提供されます。これらはいつでも変更または削除される可能性があります。Node.js または V8 では文書化されておらず、テスト以外で使用しないでください。
プロセスのシャットダウン時またはワーカスレッドでは、デストラクタは JS エンジンによって呼び出されません。したがって、これらのオブジェクトを追跡し、リソースリークを回避するために適切な破棄を保証するのはユーザーの責任です。
ラップされたオブジェクトのファクトリ
あるいは、JavaScript のnew
演算子を使ってオブジェクトインスタンスを明示的に作成することを避けるために、ファクトリパターンを使用することも可能です。
const obj = addon.createObject()
// instead of:
// const obj = new addon.Object();
まず、createObject()
メソッドはaddon.cc
で実装されます。
// addon.cc
#include <node.h>
#include "myobject.h"
namespace demo {
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
void CreateObject(const FunctionCallbackInfo<Value>& args) {
MyObject::NewInstance(args);
}
void InitAll(Local<Object> exports, Local<Object> module) {
MyObject::Init(exports->GetIsolate());
NODE_SET_METHOD(module, "exports", CreateObject);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)
} // namespace demo
myobject.h
では、オブジェクトのインスタンス化を処理する静的メソッドNewInstance()
が追加されます。このメソッドは、JavaScript でnew
を使用する代わりに用いられます。
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H
#include <node.h>
#include <node_object_wrap.h>
namespace demo {
class MyObject : public node::ObjectWrap {
public:
static void Init(v8::Isolate* isolate);
static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);
private:
explicit MyObject(double value = 0);
~MyObject();
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::Global<v8::Function> constructor;
double value_;
};
} // namespace demo
#endif
myobject.cc
の実装は前の例と似ています。
// myobject.cc
#include <node.h>
#include "myobject.h"
namespace demo {
using node::AddEnvironmentCleanupHook;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;
// Warning! This is not thread-safe, this addon cannot be used for worker
// threads.
Global<Function> MyObject::constructor;
MyObject::MyObject(double value) : value_(value) {
}
MyObject::~MyObject() {
}
void MyObject::Init(Isolate* isolate) {
// Prepare constructor template
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
// Prototype
NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);
Local<Context> context = isolate->GetCurrentContext();
constructor.Reset(isolate, tpl->GetFunction(context).ToLocalChecked());
AddEnvironmentCleanupHook(isolate, [](void*) {
constructor.Reset();
}, nullptr);
}
void MyObject::New(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.IsConstructCall()) {
// Invoked as constructor: `new MyObject(...)`
double value = args[0]->IsUndefined() ?
0 : args[0]->NumberValue(context).FromMaybe(0);
MyObject* obj = new MyObject(value);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
} else {
// Invoked as plain function `MyObject(...)`, turn into construct call.
const int argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(instance);
}
}
void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
const unsigned argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Context> context = isolate->GetCurrentContext();
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(instance);
}
void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.This());
obj->value_ += 1;
args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}
} // namespace demo
再び、この例をビルドするには、myobject.cc
ファイルをbinding.gyp
に追加する必要があります。
{
"targets": [
{
"target_name": "addon",
"sources": ["addon.cc", "myobject.cc"]
}
]
}
以下でテストします。
// test.js
const createObject = require('./build/Release/addon')
const obj = createObject(10)
console.log(obj.plusOne())
// Prints: 11
console.log(obj.plusOne())
// Prints: 12
console.log(obj.plusOne())
// Prints: 13
const obj2 = createObject(20)
console.log(obj2.plusOne())
// Prints: 21
console.log(obj2.plusOne())
// Prints: 22
console.log(obj2.plusOne())
// Prints: 23
ラップされたオブジェクトの受け渡し
C++オブジェクトのラップと返却に加えて、Node.js ヘルパー関数node::ObjectWrap::Unwrap
を使用して、ラップされたオブジェクトをアンラップすることで受け渡すことが可能です。次の例は、2 つのMyObject
オブジェクトを入力引数として受け取る関数add()
を示しています。
// addon.cc
#include <node.h>
#include <node_object_wrap.h>
#include "myobject.h"
namespace demo {
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;
void CreateObject(const FunctionCallbackInfo<Value>& args) {
MyObject::NewInstance(args);
}
void Add(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
MyObject* obj1 = node::ObjectWrap::Unwrap<MyObject>(
args[0]->ToObject(context).ToLocalChecked());
MyObject* obj2 = node::ObjectWrap::Unwrap<MyObject>(
args[1]->ToObject(context).ToLocalChecked());
double sum = obj1->value() + obj2->value();
args.GetReturnValue().Set(Number::New(isolate, sum));
}
void InitAll(Local<Object> exports) {
MyObject::Init(exports->GetIsolate());
NODE_SET_METHOD(exports, "createObject", CreateObject);
NODE_SET_METHOD(exports, "add", Add);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)
} // namespace demo
myobject.h
では、オブジェクトをアンラップした後、プライベート値にアクセスできるように、新しいパブリックメソッドが追加されています。
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H
#include <node.h>
#include <node_object_wrap.h>
namespace demo {
class MyObject : public node::ObjectWrap {
public:
static void Init(v8::Isolate* isolate);
static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);
inline double value() const { return value_; }
private:
explicit MyObject(double value = 0);
~MyObject();
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::Global<v8::Function> constructor;
double value_;
};
} // namespace demo
#endif
myobject.cc
の実装は、以前のバージョンとほぼ同様です。
// myobject.cc
#include <node.h>
#include "myobject.h"
namespace demo {
using node::AddEnvironmentCleanupHook;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
// Warning! This is not thread-safe, this addon cannot be used for worker
// threads.
Global<Function> MyObject::constructor;
MyObject::MyObject(double value) : value_(value) {
}
MyObject::~MyObject() {
}
void MyObject::Init(Isolate* isolate) {
// Prepare constructor template
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
Local<Context> context = isolate->GetCurrentContext();
constructor.Reset(isolate, tpl->GetFunction(context).ToLocalChecked());
AddEnvironmentCleanupHook(isolate, [](void*) {
constructor.Reset();
}, nullptr);
}
void MyObject::New(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.IsConstructCall()) {
// Invoked as constructor: `new MyObject(...)`
double value = args[0]->IsUndefined() ?
0 : args[0]->NumberValue(context).FromMaybe(0);
MyObject* obj = new MyObject(value);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
} else {
// Invoked as plain function `MyObject(...)`, turn into construct call.
const int argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(instance);
}
}
void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
const unsigned argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Context> context = isolate->GetCurrentContext();
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(instance);
}
} // namespace demo
以下でテストします。
// test.js
const addon = require('./build/Release/addon')
const obj1 = addon.createObject(10)
const obj2 = addon.createObject(20)
const result = addon.add(obj1, obj2)
console.log(result)
// Prints: 30