C++アドオン#

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

アドオンを実装するには、Node-API、nan、または内部V8、libuv、およびNode.jsライブラリの直接使用の3つのオプションがあります。 Node-APIによって公開されていない機能に直接アクセスする必要がある場合を除き、Node-APIを使用してください。Node-APIの詳細については、Node-APIを使用したC/C++アドオンを参照してください。

Node-APIを使用しない場合、アドオンの実装は複雑で、いくつかのコンポーネントとAPIの知識が必要です。

  • V8: Node.jsがJavaScript実装を提供するために使用するC++ライブラリ。 V8は、オブジェクトの作成、関数の呼び出しなどのメカニズムを提供します。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 itself は、アドオンが使用できるC++ APIをエクスポートします。その中で最も重要なのは、node::ObjectWrapクラスです。

  • Node.jsには、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::Object;
using v8::String;
using v8::Value;

void Method(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(String::NewFromUtf8(
      isolate, "world").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. */
} 

もう1つのオプションは、マクロ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キーワードを使用して実現できます。
  • 上記で作成したインスタンスとDeleteInstance()へのポインターを渡して、node::AddEnvironmentCleanupHook()を呼び出します。 これにより、環境が破棄されたときにインスタンスが削除されます。
  • クラスのインスタンスを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();
} 
ワーカーのサポート#

メインスレッドやワーカースレッドなど、複数の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 = 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ファイルにコンパイルする必要があります。 これを行うには、JSONのような形式を使用してモジュールのビルド構成を記述する、プロジェクトのトップレベルにbinding.gypという名前のファイルを作成します。 このファイルは、Node.jsアドオンをコンパイルするために specifically に記述されたツールである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 はアドオンを見つけて初期化します。ただし、1 つの注意点として、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#

安定性: 2 - 安定

Node-API は、ネイティブアドオンを構築するための API です。基盤となる JavaScript ランタイム(例:V8)からは独立しており、Node.js 自体の一部として維持されています。この API は、Node.js のバージョン間でアプリケーションバイナリインターフェイス(ABI)が安定します。アドオンを基盤となる JavaScript エンジンの変更から分離し、あるバージョン用にコンパイルされたモジュールを再コンパイルせずに Node.js の以降のバージョンで実行できるようにすることを目的としています。アドオンは、このドキュメント(node-gyp など)で概説されているのと同じアプローチ/ツールを使用してビルド/パッケージ化されます。唯一の違いは、ネイティブコードで使用される API のセットです。V8 または 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 の 埋め込みガイド を参照してください。

これらの各例では、次の 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' 

関数ファクトリ#

もう 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.Holder());
  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.Holder());
  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