C++ アドオン#

アドオンは、C++ で書かれた動的リンク共有オブジェクトです。require() 関数は、アドオンを通常の Node.js モジュールとして読み込むことができます。アドオンは、JavaScript と C/C++ ライブラリ間のインターフェースを提供します。

アドオンを実装するには3つの選択肢があります

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_MODULE_INITIALIZER を使用して構築できます。これは、Node.js がアドオンをロードするときに見つけることを期待する関数の名前に展開されます。したがって、アドオンは次の例のように初期化できます

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 void DeleteInstance(void* data) {
      // Cast `data` to an instance of the class and delete it.
    } 
  • アドオン初期化子でこのクラスのインスタンスをヒープに割り当てます。これは new キーワードを使用して実現できます。
  • node::AddEnvironmentCleanupHook() を呼び出し、上記で作成したインスタンスと DeleteInstance() へのポインタを渡します。これにより、環境が破棄されるときにインスタンスが削除されることが保証されます。
  • クラスのインスタンスを v8::External に格納し、
  • ネイティブバックの JavaScript 関数を作成する v8::FunctionTemplate::New() または v8::Function::New()v8::External を渡すことで、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 = Isolate::GetCurrent();

  // 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();
} 
ワーカースレッドのサポート#

メインスレッドやワーカースレッドなど、複数の 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.ccAddEnvironmentCleanupHook を使用しています

// 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 = Isolate::GetCurrent();

  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 を使用してこれと同じ一連のアクションを実行し、ユーザーのプラットフォーム用にコンパイルされたバージョンのアドオンをオンデマンドで生成します。

ビルドされると、バイナリアドオンは Node.js 内から require() にビルドされた addon.node モジュールを指し示すことで使用できます

// 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 の安定性を保証するためにできることはほとんどありません。

Native Abstractions for Node.js (または nan) は、アドオン開発者が V8 と Node.js の過去および将来のリリース間の互換性を維持するために使用することが推奨されるツールセットを提供します。それがどのように使用できるかの図解については、nanexamples を参照してください。

Node-API#

安定性: 2 - Stable

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++ アドオンに記載されています。

アドオンの例#

以下は、開発者が始めるのに役立つことを目的としたアドオンの例です。これらの例では V8 API を使用しています。様々な V8 の呼び出しについてはオンラインの V8 リファレンスを、ハンドル、スコープ、関数テンプレートなどのいくつかの概念の説明については V8 の Embedder's Guide を参照してください。

これらの例はそれぞれ、次の binding.gyp ファイルを使用しています

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "addon.cc" ]
    }
  ]
} 

複数の .cc ファイルがある場合は、単に追加のファイル名を sources 配列に追加します

"sources": ["addon.cc", "myexample.cc"] 

binding.gyp ファイルの準備ができたら、node-gyp を使用してアドオンの例を設定およびビルドできます

node-gyp configure build 

関数の引数#

アドオンは通常、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;

// This is the implementation of the "add" method
// Input arguments are passed using the
// const FunctionCallbackInfo<Value>& args struct
void Add(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  // Check the number of arguments passed.
  if (args.Length() < 2) {
    // Throw an Error that is passed back to JavaScript
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Wrong number of arguments").ToLocalChecked()));
    return;
  }

  // Check the argument types
  if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Wrong arguments").ToLocalChecked()));
    return;
  }

  // Perform the operation
  double value =
      args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
  Local<Number> num = Number::New(isolate, value);

  // Set the return value (using the passed in
  // 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 

コンパイルされると、アドオンの例は 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 

この例では、2番目の引数として完全な module オブジェクトを受け取る 2 引数形式の Init() を使用しています。これにより、アドオンは 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' 

関数ファクトリ#

もう一つの一般的なシナリオは、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 = Isolate::GetCurrent();
  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();

  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();
  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 = Isolate::GetCurrent();
  // 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();

  NODE_SET_METHOD(exports, "createObject", CreateObject);
  NODE_SET_METHOD(exports, "add", Add);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)

}  // namespace demo 

myobject.h では、オブジェクトをアンラップした後にプライベートな値にアクセスできるように、新しい public メソッドが追加されます。

// 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();
  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 = Isolate::GetCurrent();
  // 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