C++ 外掛程式#

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

實作外掛程式有三個選項:Node-API、nan 或直接使用 V8、libuv 和 Node.js 函式庫的內部。除非有需要直接存取 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 的方式存取許多常見的系統工作,例如與檔案系統、socket、計時器和系統事件互動。libuv 也提供類似於 POSIX 執行緒的執行緒抽象,以利需要超出標準事件迴圈的更精密的非同步附加元件。附加元件作者應避免透過 libuv 將工作卸載至非封鎖系統作業、工作執行緒或自訂使用 libuv 執行緒,以 I/O 或其他耗時工作封鎖事件迴圈。

  • 內部 Node.js 函式庫。Node.js 本身會匯出附加元件可使用的 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_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 = 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.cc 使用 AddEnvironmentCleanupHook

// addon.cc
#include <node.h>
#include <assert.h>
#include <stdlib.h>

using node::AddEnvironmentCleanupHook;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::Object;

// Note: In a real-world application, do not rely on static/global data.
static char cookie[] = "yum yum";
static int cleanup_cb1_called = 0;
static int cleanup_cb2_called = 0;

static void cleanup_cb1(void* arg) {
  Isolate* isolate = static_cast<Isolate*>(arg);
  HandleScope scope(isolate);
  Local<Object> obj = Object::New(isolate);
  assert(!obj.IsEmpty());  // assert VM is still alive
  assert(obj->IsObject());
  cleanup_cb1_called++;
}

static void cleanup_cb2(void* arg) {
  assert(arg == static_cast<void*>(cookie));
  cleanup_cb2_called++;
}

static void sanity_check(void*) {
  assert(cleanup_cb1_called == 1);
  assert(cleanup_cb2_called == 1);
}

// Initialize this addon to be context-aware.
NODE_MODULE_INIT(/* exports, module, context */) {
  Isolate* isolate = context->GetIsolate();

  AddEnvironmentCleanupHook(isolate, sanity_check, nullptr);
  AddEnvironmentCleanupHook(isolate, cleanup_cb2, cookie);
  AddEnvironmentCleanupHook(isolate, cleanup_cb1, isolate);
} 

執行以下指令以在 JavaScript 中進行測試

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

編譯#

撰寫原始碼後,必須將其編譯成二進制檔案 addon.node。為此,請在專案頂層目錄中建立一個名為 binding.gyp 的檔案,並使用類似 JSON 的格式描述模組的編譯設定。此檔案由 node-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 捆綁版本執行這組相同的動作,依據使用者的平台產生外掛程式的已編譯版本。

建置完成後,可透過指向建置好的 addon.node 模組來從 Node.js 內部使用二進位元附加元件 require()

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

console.log(addon.hello());
// Prints: 'world' 

由於編譯附加元件二進位檔的確切路徑會因編譯方式而異(例如,有時可能在 ./build/Debug/ 中),因此附加元件可以使用 bindings 套件來載入編譯好的模組。

雖然 bindings 套件實作在尋找附加元件模組的方式上較為複雜,但它基本上使用類似於下列內容的 try…catch 模式

try {
  return require('./build/Release/addon.node');
} catch (err) {
  return require('./build/Debug/addon.node');
} 

連結至 Node.js 附帶的函式庫#

Node.js 使用靜態連結函式庫,例如 V8、libuv 和 OpenSSL。所有附加元件都必須連結至 V8,也可以連結至任何其他相依性。通常,這只要包含適當的 #include <...> 陳述式(例如 #include <v8.h>)即可,而 node-gyp 會自動找到適當的標頭。不過,有幾個需要注意的注意事項

  • node-gyp 執行時,它會偵測 Node.js 的特定發行版本,並下載完整的原始碼 tarball 或僅下載標頭。如果下載完整的原始碼,附加元件將可以完整存取 Node.js 相依性的完整組。但是,如果僅下載 Node.js 標頭,則僅會提供 Node.js 匯出的符號。

  • node-gyp 可以使用指向本機 Node.js 原始碼映像的 --nodedir 旗標執行。使用此選項,附加元件將可以存取完整的相依性組。

使用 require() 載入附加元件#

已編譯附加元件二進制檔的副檔名為 .node (與 .dll.so 相反)。require() 函式會尋找副檔名為 .node 的檔案,並將這些檔案初始化為動態連結函式庫。

呼叫 require() 時,通常可以省略 .node 副檔名,而 Node.js 仍會尋找並初始化附加元件。但有一個例外,就是 Node.js 會先嘗試尋找並載入與二進制檔具有相同基本名稱的模組或 JavaScript 檔案。例如,如果與二進制檔 addon.node 在同一個目錄中有一個檔案 addon.js,則 require('addon') 會優先載入 addon.js 檔案。

Node.js 的原生抽象#

本文件中說明的每個範例都直接使用 Node.js 和 V8 API 來實作附加元件。V8 API 可能會在 V8 的不同版本 (以及 Node.js 的不同主要版本) 之間發生重大變更。每次變更時,附加元件可能需要更新並重新編譯才能繼續運作。Node.js 的發布時程旨在將此類變更的頻率和影響降到最低,但 Node.js 幾乎無法確保 V8 API 的穩定性。

Node.js 的原生抽象(或 nan)中提供了一組工具,建議外掛程式開發人員使用這些工具以保持 V8 和 Node.js 過去和未來版本之間的相容性。請參閱 nan 範例,以了解如何使用它。

Node-API#

穩定性: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 中需要並使用範例附加元件

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

首先,在 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(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 解封裝並傳遞封裝的物件。以下範例顯示一個函式 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(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