C++ 插件#

插件(Addons)是動態連結的共用物件,可以像普通的 Node.js 模組一樣透過 require() 函式載入。插件提供了 JavaScript 與原生程式碼之間的外部函數介面(FFI)。

實作插件有三種選擇:

本文件的其餘部分將重點放在後者,這需要了解多個組件和 API:

  • V8:Node.js 用於提供 JavaScript 實作的 C++ 函式庫。它提供了建立物件、呼叫函式等機制。V8 的 API 主要記錄在 v8.h 標頭檔(Node.js 原始碼樹中的 deps/v8/include/v8.h)中,也可以在 線上 查閱。

  • libuv:實作 Node.js 事件迴圈、工作執行緒以及平台所有非同步行為的 C 函式庫。它也是一個跨平台的抽象函式庫,讓開發者能以類似 POSIX 的方式輕鬆存取所有主要作業系統中的常見系統任務,例如與檔案系統、通訊端(sockets)、計時器和系統事件進行互動。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) // N.B.: no semi-colon, this is not a function

}  // namespace demo

在大多數平台上,可以使用以下 Makefile 開始:

NODEJS_DEV_ROOT ?= $(shell dirname "$(command -v node)")/..
CXXFLAGS = -std=c++23 -I$(NODEJS_DEV_ROOT)/include/node -fPIC -shared -Wl,-undefined,dynamic_lookup

hello.node: hello.cc
	$(CXX) $(CXXFLAGS) -o $@ $<

接著執行以下指令即可編譯並執行程式碼:

$ make
$ node -p 'require("./hello.node").hello()'
world

若要與 npm 生態系統整合,請參閱 建置 章節。

內容感知(Context-aware)插件#

使用 NODE_MODULE() 定義的插件無法同時在多個上下文(Context)或多個執行緒中載入。

在某些環境中,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 物件的任何持久引用(persistent references)。這是因為 JavaScript 物件僅在單一上下文中有效,當從錯誤的上下文或從與建立物件時不同的執行緒存取時,很可能會導致當機。

可以透過以下步驟建構內容感知插件以避免使用全域靜態資料:

  • 定義一個類別來保存每個插件執行個體的資料,並包含一個如下形式的靜態成員:
    static void DeleteInstance(void* data) {
      // Cast `data` to an instance of the class and delete it.
    }
    
  • 在插件初始化程式中,於堆積(heap)分配此類別的執行個體。這可以使用 new 關鍵字來達成。
  • 呼叫 node::AddEnvironmentCleanupHook(),傳入上述建立的執行個體以及指向 DeleteInstance() 的指標。這將確保在環境銷毀時刪除該執行個體。
  • 將類別的執行個體儲存在 v8::External 中,以及
  • v8::External 傳遞給所有暴露給 JavaScript 的方法,方法是將其傳遞給建立原生支援 JavaScript 函式的 v8::FunctionTemplate::New()v8::Function::New()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()(具有相同簽署)將其移除。回呼函式以「後進先出」(LIFO)的順序執行。

如有必要,還有另一對 AddEnvironmentCleanupHook()RemoveEnvironmentCleanupHook() 的多載(overloads),其中清理掛鉤接受一個回呼函式。這可用於關閉非同步資源,例如插件註冊的任何 libuv 句柄(handles)。

以下的 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 工具的一個版本作為 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 可以使用 --nodedir 旗標執行,指向本地的 Node.js 原始碼映射。使用此選項,插件將可以存取完整的依賴項集。

使用 require() 載入插件#

編譯後插件二進位檔的副檔名是 .node(而不是 .dll.so)。require() 函式被設計為尋找具有 .node 副檔名的檔案,並將其作為動態連結函式庫進行初始化。

呼叫 require() 時,通常可以省略 .node 副檔名,Node.js 仍會找到並初始化該插件。但有一點要注意,Node.js 會首先嘗試定位並載入剛好具有相同基本名稱的模組或 JavaScript 檔案。例如,如果在二進位檔 addon.node 所在的目錄中有一個 addon.js 檔案,那麼 require('addon') 將優先處理 addon.js 檔案並載入它。

使用 import 載入插件#

穩定性:1.0 - 早期開發

您可以使用 --experimental-addon-modules 旗標來啟用對靜態 import 和動態 import() 載入二進位插件的支援。

如果我們重複使用之前的 Hello World 範例,您可以這樣做:

// hello.mjs
import myAddon from './hello.node';
// N.B.: import {hello} from './hello.node' would not work

console.log(myAddon.hello());
$ node --experimental-addon-modules hello.mjs
world

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 的 C/C++ 插件

插件範例#

以下是一些插件範例,旨在幫助開發者入門。這些範例使用了 V8 API。有關各種 V8 呼叫的說明,請參考線上 V8 參考文件,有關 handles、scopes、function templates 等概念的解釋,請參考 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++ 程式碼之間進行轉換(mapping)。

以下範例說明了如何讀取從 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));

回呼(Callbacks)#

在插件中,將 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

包裝物件的解構式(destructor)將在物件被垃圾回收(garbage-collected)時執行。為了測試解構式,可以使用一些命令列旗標來強制執行垃圾回收。這些旗標由底層的 V8 JavaScript 引擎提供。它們隨時可能更改或移除。Node.js 或 V8 並未對其進行說明,且絕不應在測試以外的地方使用。

在程序或 Worker 執行緒關閉期間,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