C++ 外掛#

外掛Addons)是使用 C++ 編寫的動態連結共享物件。require() 函式可以像載入普通 Node.js 模組一樣載入外掛。外掛為 JavaScript 和 C/C++ 庫之間提供了一個介面。

實現外掛有三種選擇:

除非需要直接訪問未由 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 執行緒的執行緒抽象,供需要超越標準事件迴圈的更復雜的非同步外掛使用。外掛作者應避免透過 I/O 或其他耗時任務阻塞事件迴圈,而應透過 libuv 將工作解除安裝到非阻塞系統操作、工作執行緒或自定義的 libuv 執行緒中。

  • 內部 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_GYP_MODULE_NAME 作為 NODE_MODULE() 的第一個引數,可以確保最終二進位制檔案的名稱會被傳遞給 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() 後的函式體內,可以使用以下三個變數:

  • Local<Object> exports,
  • Local<Value> module,以及
  • Local<Context> context

構建上下文感知的外掛需要仔細管理全域性靜態資料以確保穩定性和正確性。由於外掛可能被多次載入,甚至可能來自不同的執行緒,因此外掛中儲存的任何全域性靜態資料都必須得到妥善保護,並且不能包含任何對 JavaScript 物件的持久引用。原因是 JavaScript 物件只在一個上下文中有效,當從錯誤的上下文或從建立它們的執行緒以外的執行緒訪問時,很可能會導致崩潰。

可以透過執行以下步驟來構造上下文感知的外掛以避免全域性靜態資料:

  • 定義一個類,用於存放每個外掛例項的資料,並擁有一個形式如下的靜態成員:
    static void DeleteInstance(void* data) {
      // Cast `data` to an instance of the class and delete it.
    } 
  • 在外掛初始化器中,使用 new 關鍵字在堆上分配該類的一個例項。
  • 呼叫 node::AddEnvironmentCleanupHook(),將上面建立的例項和指向 DeleteInstance() 的指標傳遞給它。這將確保在環境拆卸時例項被刪除。
  • 將類的例項儲存在 v8::External 中,並且
  • 透過將 v8::External 傳遞給 v8::FunctionTemplate::New()v8::Function::New()(用於建立原生支援的 JavaScript 函式),將其傳遞給所有暴露給 JavaScript 的方法。v8::FunctionTemplate::New()v8::Function::New() 的第三個引數接受 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();
} 
Worker 支援#

為了能從多個 Node.js 環境(例如主執行緒和 Worker 執行緒)中載入,一個外掛需要:

  • 是一個 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 = 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-gyp 使用,這是一個專門用於編譯 Node.js 外掛的工具。

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

node-gyp 工具的一個版本隨 Node.js 一起捆綁和分發,作為 npm 的一部分。這個版本不直接提供給開發者使用,僅用於支援使用 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 執行時可以使用 --nodedir 標誌指向一個本地的 Node.js 原始碼映象。使用此選項,外掛將可以訪問完整的依賴項集合。

使用 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#

穩定性: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 

此示例使用了 Init() 的雙引數形式,它接收完整的 module 物件作為第二個引數。這允許外掛用單個函式完全覆蓋 exports,而不是將該函式作為 exports 的一個屬性來新增。

要測試它,請執行以下 JavaScript:

// test.js
const addon = require('./build/Release/addon');

addon((msg) => {
  console.log(msg);
// Prints: 'hello world'
}); 

在此示例中,回撥函式是同步呼叫的。

物件工廠#

外掛可以在 C++ 函式內部建立並返回新物件,如以下示例所示。一個物件被建立並返回,其帶有一個 msg 屬性,該屬性回顯傳遞給 createObject() 的字串:

// 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++ 物件#

也可以包裝 C++ 物件/類,以便可以使用 JavaScript 的 new 運算子建立新例項:

// 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(); 

首先,在 addon.cc 中實現 createObject() 方法:

// 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 來解包並傳遞包裝物件。以下示例展示了一個函式 add(),它可以接受兩個 MyObject 物件作為輸入引數:

// 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 中,添加了一個新的公共方法,以便在解包物件後訪問私有值。

// 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