Node.js C++插件實踐指南

簡介

熟悉Node.js的人都知道,Node.js是基於C++開發的一個JavaScript運行時,既然Node.js是用C++開發的,那麼我能否將C++代碼引入到Node.js中呢,這當然是可以的,這項技術被稱爲C++模塊。官方對Node.js C++模塊解釋如下

Node.JS插件是使用C++編寫的動態鏈接庫,可以被Node.JS以require的形式載入,在使用時就像Node.js原生模塊一樣。主要被用於在Node.js的JavaScript和C或者C++庫之間建立起橋樑的關係。

動態鏈接庫,即window平臺的.dll文件,linux下的.so文件。只不過Node.js模塊導出的是.node文件。

動態鏈接庫提供了一種方法,使進程可以調用不屬於其可執行代碼的函數,函數的可執行代碼位於一個.dll (window)或.so (linux)文件中,該文件包含一個或多個已被編譯、鏈接並與使用它們的進程分開存儲的函數。說到動態鏈接庫,不得不提一下靜態鏈接庫,靜態鏈接庫是指在編譯階段就把相關的函數庫(靜態庫)鏈接,合成一個可執行文件。

那麼,爲什麼需要C++模塊?

JavaScript是基於異步,單線程的語言,對於一些異步任務非常佔優勢,但對於一些計算密集型的任務,也有明顯的劣勢(也許這是腳本語言的缺點)。換句話說使用JavaScript解釋器執行JavaScript代碼的效率通常是比直接執行一個C++編譯好的二進制文件效率要低。除此之外,其實很多開源庫是基於C++寫的,比如圖像處理庫(ImageMagick),像我們團隊使用的圖像處理庫,也是基於C++編寫(用JavaScript寫,性能達不到要求),所以對於一些問題使用C++來實現,效率和性能能有顯著的提升,何樂而不爲呢。

因此本文從C++插件基本原理以及幾種編寫方式來向讀者介紹如何將C++代碼加載到JavaScript中(編寫Node.js C++模塊)

原理淺析

前面提到,Node.js的C++模塊是以動態鏈接庫存在的(.node),那麼Node.JS是如何加載C++模塊的呢。首先Node.js的一個模塊時一個遵循CommonJS規範書寫的JavaScript源文件(.js),也可能是一個C++模塊二進制文件(.node),這些文件通過Node.js中的 require() 函數被引入並使用。

在Node.js中引入C++模塊的本質就是在Node.js運行時引入一個動態鏈接庫的過程。在Node.js中通過 require() 函數加載模塊,無論是Node.js模塊還是C++模塊。那麼知道這個函數怎麼實現的就知道怎麼加載模塊的。

爲了揭開require的正面目,我們翻開Node.js的源碼(Node.js Github)

lib/internal/modules/cjs/loader.js,我們可以找到Module實現的JavaScript代碼

function Module(id = '', parent) { // Class Module
  this.id = id; // 模塊id
  this.path = path.dirname(id);
  this.exports = {}; //
  this.parent = parent;
  updateChildren(parent, this, false);
  this.filename = null;
  this.loaded = false;
  this.children = [];
}

Module._cache = ObjectCreate(null); // Object.create()
Module._pathCache = ObjectCreate(null); // 模塊緩存
Module._extensions = ObjectCreate(null); // 對於文件名的處理

let wrap = function(script) {
  // 用下面的wrapper包裹相應的js腳本
  return Module.wrapper[0] + script + Module.wrapper[1];
};

const wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ', 
  '\n});'
];

繼續往下翻,找到 require() 的實現

Module.prototype.require = function(id) {
 // ...
 return Module._load(id, this, /* isMain */ false);
 // ... 
};

Module._load = function(request, parent, isMain) {
  // 省略了大部分代碼...
  const filename = Module._resolveFilename(request, parent, isMain);
  // 模塊在緩存中,則從緩存中加載
  const cachedModule = Module._cache[filename]; 
  if (cachedModule !== undefined) {
    return cachedModule.exports;
  }
  // 內建模塊
  const mod = loadNativeModule(filename, request);
  if (mod && mod.canBeRequiredByUsers) return mod.exports;

  // 其他模塊的處理
  const module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }
  
  Module._cache[filename] = module;
  // ...
  module.load(filename); // 委託到load這個函數
  return module.exports;
};

從上面的代碼中可以看到模塊的加載規則

  1. 如果模塊在緩存裏,則直接讀緩存裏的
  2. 如果的內建模塊,則使用loadNativeModule加載模塊
  3. 其他情況使用Module.proptype.load函數來加載模塊
Module.prototype.load = function(filename) {
  // 省略。。。
  const extension = findLongestRegisteredExtension(filename);
  // 終於到重點了,對每一種擴展,使用不同的函數來處理
  Module._extensions[extension](this, filename);
  this.loaded = true;
  // 省略。。。
};

看到Module._extensions[extension](this, filename);這一行,對.js/.node/.json文件分別處理,讓我們將目光放到Module._extensions的實現上

Module.prototype._compile = function(content, filename) {
  // 省略。。。
  const dirname = path.dirname(filename);
  const require = makeRequireFunction(this, redirects);
  let result;
  /* 
  就是用上面的wrapper對content進行包裹,並將對應的參數傳進去,所以這就是我們能在Node.js中直接使用require(), __filename, __dirname的原因
  const wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
  ];
  */
  const compiledWrapper = wrapSafe(filename, content, this);
  const exports = this.exports;
  const thisValue = exports;
  const module = this;

  result = compiledWrapper.call(thisValue, exports, require, module,
                                  filename, dirname);
  return result;
};
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  // 省略。。。
  const content = fs.readFileSync(filename, 'utf8');
  module._compile(content, filename); // 將wrapper的內容扔到vm模塊裏去執行
};


// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
  const content = fs.readFileSync(filename, 'utf8');
  // 省略。。。
  module.exports = JSONParse(stripBOM(content));
};


// Native extension for .node
Module._extensions['.node'] = function(module, filename) {
  // 省略。。。
  return process.dlopen(module, path.toNamespacedPath(filename));
};

可以看到,對.node文件的處理是使用process.dlopen函數,但這個函數使用C++實現的(類似於C++插件的編寫形式),在src/node_process_methods.cc下能找到這個函數的定義。

env->SetMethodNoSideEffect(target, "cwd", Cwd); //process.cwd()
env->SetMethod(target, "dlopen", binding::DLOpen); // process.dlopen()
env->SetMethod(target, "reallyExit", ReallyExit);
env->SetMethodNoSideEffect(target, "uptime", Uptime);
env->SetMethod(target, "patchProcessObject", PatchProcessObject);

是不是覺得很熟悉,這些的都是process上的方法,我們重點關注binding::DLOpen函數的實現,在src/node_binding.cc下

void DLOpen(const FunctionCallbackInfo<Value>& args) { // 裏面涉及的V8數據類型,後面會介紹,其實這也算是一個C++插件
  Environment* env = Environment::GetCurrent(args);
  auto context = env->context();

  CHECK_NULL(thread_local_modpending);
  // 對照着上面的process.dlopen(module, filename)
  if (args.Length() < 2) {
    env->ThrowError("process.dlopen needs at least 2 arguments.");
    return;
  }

  int32_t flags = DLib::kDefaultFlags;
  if (args.Length() > 2 && !args[2]->Int32Value(context).To(&flags)) {
    return env->ThrowTypeError("flag argument must be an integer.");
  }

  Local<Object> module;
  Local<Object> exports;
  Local<Value> exports_v;
  if (!args[0]->ToObject(context).ToLocal(&module) ||
      !module->Get(context, env->exports_string()).ToLocal(&exports_v) ||
      !exports_v->ToObject(context).ToLocal(&exports)) {
    return;  // Exception pending.
  }
  // 拿到文件名
  node::Utf8Value filename(env->isolate(), args[1]); // *filename 得到char* 類型 
  // 使用TryLoadAddon加載插件
  env->TryLoadAddon(*filename, flags, [&](DLib* dlib) { // C++ lambda 表達式,引用捕獲上層作用域的全部變量
    static Mutex dlib_load_mutex; // 多線程環境,上鎖
    Mutex::ScopedLock lock(dlib_load_mutex);

    const bool is_opened = dlib->Open(); // open 動態鏈接庫

    node_module* mp = thread_local_modpending;
    thread_local_modpending = nullptr;

    if (!is_opened) {
      dlib->Close();
      // ...
      return false;
    }

    if (mp != nullptr) {
      if (mp->nm_context_register_func == nullptr) { // 獲取C++插件的註冊函數
        if (env->options()->force_context_aware) {
          dlib->Close();
          return false;
        }
      }
      mp->nm_dso_handle = dlib->handle_; // 將動態鏈接庫句柄保存
      dlib->SaveInGlobalHandleMap(mp);
    } else {
      if (auto callback = GetInitializerCallback(dlib)) { // 普通插件
        callback(exports, module, context);
        return true;
      } else if (auto napi_callback = GetNapiInitializerCallback(dlib)) { // 使用napi寫的插件
        napi_module_register_by_symbol(exports, module, context, napi_callback);
        return true;
      } else {
        mp = dlib->GetSavedModuleFromGlobalHandleMap();
        if (mp == nullptr || mp->nm_context_register_func == nullptr) {
          dlib->Close();
          // ...
          return false;
        }
      }
    }

    // -1 is used for N-API modules
    if ((mp->nm_version != -1) && (mp->nm_version != NODE_MODULE_VERSION)) {
      if (auto callback = GetInitializerCallback(dlib)) {
        callback(exports, module, context);
        return true;
      }
      // 。。。
      return false;
    }
    CHECK_EQ(mp->nm_flags & NM_F_BUILTIN, 0);

    // Do not keep the lock while running userland addon loading code.
    Mutex::ScopedUnlock unlock(lock); // 釋放鎖
    if (mp->nm_context_register_func != nullptr) {
      mp->nm_context_register_func(exports, module, context, mp->nm_priv);
    } else if (mp->nm_register_func != nullptr) {
      mp->nm_register_func(exports, module, mp->nm_priv);
    } else {
      dlib->Close();
      return false;
    }

    return true;
  });

}

Node.js中將動態鏈接庫的操作封裝成一個DLib類,dlib->Open()其實是調用到uv_dlopen()函數來加載鏈接庫。

 int ret = uv_dlopen(filename_.c_str(), &lib_); // [out] _lib

uv_dlopen()是libuv中提供的一個加載動態鏈接庫的函數,其返回一個uv_lib_t句柄類型

typeof strcut uv_lib_s uv_lib_t;
struct uv_lib_s {
  char* errmsg;
  void* handle;
};

handle保存鏈接庫句柄。callback(exports, module, context);來調用一個編寫的C++插件(對於Node.js v8纔出現的N-API有另一種處理,但對於一般的C++插件其實就是類似於這種void Init(Local<Object> exports, Local<Object> module, Local<Object> context) {}形式的函數,然後在上面調用),下面用一張流程圖來描述整個加載過程

準備工作

終於到了實踐環節,但別急,工欲善其事,必先利其器,先準備好開發環境。

編輯器

通過對比Vim/Vs Code/Qt Creator/CLoin幾款編輯器後,得到一個結論: Vim沒有代碼提示(太菜了,不會配),Vs Code寫C++代碼異常的卡,動不動代碼提示、高亮就全沒了。Qt Creator寫C++很不錯,但是轉手寫JavaScript時很頭疼,最後還是選擇CLoin,無論是C++還是JavaScript都支持的非常好(jetbrian大法好),最重要的是提示不會寫着寫着就沒了,只不過要稍微寫寫CmakeList.txt文件。

node-gyp

node-gyp是Node.js下的擴展構建工具,在安裝C++插件時,通過一個binding.gyp描述文件來生成不同系統所需要的C++項目文件(UNIX 的 Makefile,Windows下的Visual Studio項目),然後調用相應的構建工具(gcc)來進行構建。

安裝

mac 上保證安裝了xcode(應用商店直接下載即可),然後命令行

npm install node-gyp -g

node-gyp的常用命令

  • help
  • configure 根據平臺和node版本,生成相應的構建文件(生成一個build目錄)
  • build 構建node插件(根據build文件夾的內容,生成.node文件)
  • clean 清除build目錄
  • rebuild 依次執行 clean、configure、build,可以方便的重新構建插件
  • install 安裝對應版本的node 頭文件,代碼提示必備
  • list 列出當前安裝的node頭文件的版本
  • remove 刪除安裝的node頭文件

binding.gyp文件初窺

上面提到了binding.gyp文件,其實它是一個類似於python的dict的一個文件,基於python的dict語法,註釋風格也和python一致。比如一個簡單的binding.gyp如下

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [
        "addon.cpp" # 編譯用的c++源文件
      ],
    }
  ]
}

targets字段是一個數組,數組中每一個元素都是將要被node-gyp構建的C++模塊,target_name是必須的,表示模塊名,編譯時會通過該名字來命名.node文件,sources字段也是必須的,用於將哪些文件當作源碼進行編譯。

  1. 基本類型

類似於python的數據類型,gyp裏面的基本類型只有 String, Integer, Lists, Dictionaries

  1. 關鍵字段

下面列舉一些比較常見的字段(鍵)

targets, target_name,sources上面解釋過了,這裏就不解釋了。

include_dirs: 頭文件搜索路徑,-I標識,比如gcc -I some.c -o some.o

defines: 爲目標添加預編譯宏,-D標識,比如gcc -D N=1000 some.c -o some.i,直接在源文件中添加#define N 1000

libraries: 爲編譯添加鏈接庫,-L編譯標識

cflags: 自定義編譯標識

dependencies: 如果代碼中用了第三方的C++代碼,就需要在binding.gyp中將這個庫編譯爲靜態鏈接庫,然後在主target使用dependencies將第三方庫依賴進來。

conditions: 分支條件處理字段

type: 編譯類型,有三種值:shared_library(動態鏈接庫),static_library(靜態鏈接庫),loadable_module(Node.js可直接載入的C++擴展動態鏈接庫, binding.gyp的默認類型)

variables: 變量字段,可以寫一些變量在gyp文件中使用

下面是一個簡單舉個簡單的例子,更多示例請參考: https://github.com/Node.js/node-gyp/wiki/%22binding.gyp%22-files-out-in-the-wild

{
  "targets": [
    {
    "target_name": "some_library",
    "sources": [
      "some.cc"
      ]
    },
    {
      "target_name": "main_addon",
      "variables": { # 定義變量
        "main": "main.cc",
        "include": ["./lib", "../src"]
      },
      "cflags": ["-Werror"] # g++編譯標識
      "sources": [
        "<(main)" # 使用 < 這種方式引用變量
      ],
      "defines": [ # 定義宏
        "MY_NODE_ADDON=1"
      ],
      "include_dirs": [
        "/usr/local/node/include",
        "<@(include)" # 使用 <@ 引用數組變量
      ],
      "dependencies": [ # 定義依賴
        "some_library" # 依賴上面的some_library
      ],
      "libraries": [
        "some.a", # mac
        "xxx.lib" # win
      ],
      "conditions": [ # 條件,其格式如下
        [
          ["OS=='mac'", {"sources": ["mac_main.cc"]}], 
          ["OS=='win'", {"sources": ["win_main.cc"]}],
        ]
      ]
    }
  ]
}
  1. 變量

在gyp中主要有三類變量:預定義變量用戶定義變量自動變量

預定義變量:比如OS變量,表示當前的操作系統(linux, mac, win)

用戶定義變量:在variables字段下定義的變量。

自動變量:所有的字符串鍵名都會被當作自動變量處理,變量名是鍵名加上_前綴。

變量的引用:以<開頭或>開頭,用@來區分不同類型的變量。<(VAR)>(VAR),如果VAR是一個字符串,則當作一個正常的字符串處理,如果VAR是一個數組,則按空格拼接數組每一項的字符串。<@(VAR)>@(VAR),該指令只能用在數組中,如果VAR是一個數組,數組的內容會一一插入到當前所在的數組中,如果是字符串則會按指定分隔符轉成數組再一一插入到當前所在數組裏。

  1. 指令

指令與變量類似,不過比變量高級一點,GYP讀到指令時會啓動一個進程去執行這條展開的指令,其語法格式是: 以<!開頭或者<!@開頭的,與變量相同的一點是<!@也是用於數組的。

{
  # ...
  "include_dirs": [
    "<!(node -e \"require('nan')\")" # 相當於在cmd下執行 node -e "require('nan')",並將結果放在include_dirs裏
  ]
  # ...
}
  1. 條件分支

conditions字段,其值是一個數組,那麼第一個元素是一個字符串,表示條件,條件格式跟python的條件分支一樣,例如"OS=='mac' or OS=='win'"或者"VAR>=1 and VAR <= 2"。第二個元素則是一個對象,用於根據條件合併到最近的一個上下文中的內容。

  1. 列表過濾器

用於值是數組的鍵,鍵名以!或者/結尾,其中鍵名以!結尾是一個排除過濾器,表示這裏的鍵值將被從無!的同名鍵中排除。鍵名以/結尾是一個匹配過濾器,表示通過正則匹配出相應結果,然後以指定方式(include或者exclude)進行處理。

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [
        "a.cc", "b.cc", "c.cc", "d.cc"
      ],
      "conditions": [
        ["OS=='mac'", {"sources!": ["a.cc"]}], # 排除過濾器,條件成立則從sources中排除掉a.cc
        ["OS=='win'", {"sources/": [ # 匹配過濾器
          ["include", "b|c\\.cc"], # 包含b.cc和c.cc
          ["exclude", "a\\.cc"] # 排除 a.cc
        ]}]
      ]
    }
  ]
}
  1. 合併

從上面可以看到GYP的許多操作都是通過字典和列表項合併在一起實現(條件分支),在合併操作時,最重要的是識別源和目標值之間的區別。

在合併一個字典時,遵循以下規則

  • 如果鍵在目標字典中不存在,則將其插入
  • 如果鍵已經存在
    • 如果值是字典,則源和目標值字典執行字典合併過程
    • 如果值是列表,則源和目標值列表執行列表合併過程
    • 如果值是字符串或整數,則直接將源值替換

在合併列表時,可根據附加到鍵名的後綴進行合併

  • 鍵以=結尾,源列表完全替換目標列表
  • 鍵以?結尾,則只有當鍵不在目標時,纔會將源列表設置爲目標列表
  • 鍵以+結尾,則源列表會被追加到目標列表
  • 鍵沒有修飾符,則源列表內容附加到目標列表

例如

# 源
{
  "include_dirs+": [
    "/public"
  ]
}
# 目標
{
  "include_dirs": [
    "/header"
  ],
  "sources": [
    "aa.cc"
  ]
}
# 合併後
{
  "include_dirs": [
    "/public",
    "/header"
  ],
  "sources": [
    "aa.cc"
  ]
}

第一個C++插件:Hello World

首先使用node-gyp install安裝對應版本的Node.js頭文件,安裝完後頭文件目錄位於~/.node-gyp/node-version/include/node目錄下, 或者在你的Node.js安裝目錄找到include目錄,裏面就是Node.js的頭文件。

目錄結構以及C++代碼如下,首先使用NODE_MODULE宏去註冊一個C++模塊,對應的Init函數接受Local<Object> exports參數,這裏的exports類似與Node.js中的module.exports,所以往exports掛載函數即可。

編寫CMakeLists.txt,使用include_directories將node的頭文件鏈接過來,編輯器代碼提示時非常有用

cmake_minimum_required(VERSION 3.15)
project(cpp_addon_test)

set(CMAKE_CXX_STANDARD 14)
# 鏈接node 頭文件,代碼提示時有用
include_directories(/Users/dengpengfei/.node-gyp/12.6.0/include/node) 

add_executable(cpp_addon_test main.cpp)
![](https://user-gold-cdn.xitu.io/2020/1/21/16fc641a9cc6f4fa?w=856&h=238&f=png&s=41134)

編寫binding.gyp,將sources指定爲main.cpp

{
  "targets": [
    {
      "target_name": "cpp_addon",
      "sources": [
        "main.cpp"
      ]
    }
  ]
}

使用node-gyp對C++文件進行編譯,使用node-gyp configure生成配置文件,node-gyp build構建C++插件(生成.node文件)。或者使用node-gyp rebuild直接構建C++插件。

index.js引入cpp_addon.node文件

const cpp = require("./build/Release/cpp_addon");
console.log(cpp.hello());

運行結果如下

Hello world!不過癮?那來看看幾個簡單的C++函數以及BigNumber類的封裝吧。來看幾個工具方法,lib/utils.h

int findSubStr(const char* str, const char* subStr); // 查找子串位置,kmp算法
int subStrCount(const char* str, const char* subStr); // 字串在源字符串中出現次數,kmp算法

以及BigNumber包裝類,lib/bigNumber.h

class BigNumber: node::ObjectWrap {
  public:
    static void Init(Local<Object>); // Init函數
  private:
    explicit BigNumber(const char* value): value(value) {} // 構造函數
    ~BigNumber() override = default;
    static void New(const FunctionCallbackInfo<Value>&); // New
    static void Val(const FunctionCallbackInfo<Value>&); // 返回值
    static void Add(const FunctionCallbackInfo<Value>&); // 相加
    static void Multiply(const FunctionCallbackInfo<Value>&); // 相乘
    std::string value; // 用一個std::string 來存
};

這裏使用了node::ObjectWrap封裝類,將C++ Class與JavaScript Class相連接的工具類(位於 node_object_wrap.h頭文件中,下文會具體介紹這個工具類)。由於篇幅有限,這裏只展示函數以及類的定義,相關實現以及示例可以參考GitHub:https://github.com/sundial-dreams/node_cpp_addon

主函數main.cpp

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <node.h>
#include <node_object_wrap.h>
#include "lib/utils.h"
#include "lib/bigNumber.h"
const int N = 10000;
using namespace v8;
// 對findSubStr(const char*, const char*)的包裝
void FindSubStr(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  if (!args[0]->IsString() || !args[1]->IsString()) {
    isolate->ThrowException(Exception::TypeError(ToLocalString("type error")));
  }
  // 將Local<String> 轉化到 char*類型,下文會介紹
  String::Utf8Value str(isolate, args[0].As<String>());
  String::Utf8Value subStr(isolate, args[1].As<String>());
  int i = findSubStr(*str, *subStr);
  args.GetReturnValue().Set(Number::New(isolate, i));
}

// 對 subStrCount(const char*, const char*)的包裝
void SubStrCount(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  if (!args[0]->IsString() || !args[1]->IsString()) {
    isolate->ThrowException(Exception::TypeError(ToLocalString("type error")));
  }
  // 將Local<String> 轉化到 char*類型,下文會介紹
  String::Utf8Value str(isolate, args[0].As<String>());
  String::Utf8Value subStr(isolate, args[1].As<String>());
  int i = subStrCount(*str, *subStr); // 調用c++側的方法
  args.GetReturnValue().Set(Number::New(isolate, i));
}

void Init(Local<Object> exports) {
  // 暴露出兩個函數
  NODE_SET_METHOD(exports, "findSubStr", FindSubStr);
  NODE_SET_METHOD(exports, "subStrCount", SubStrCount);
  // 利用BigNumber的Init靜態方法來暴露BigNumber類
  BigNumber::Init(exports);
}

NODE_MODULE(addon, Init)

binding.gyp文件如下

{
  "targets": [
      {
          "target_name": "addon",
          "sources": [
              "lib/utils.cpp",
              "lib/bigNumber.cpp",
              "main.cpp"
          ]
      } 
  ]
}

就是將lib/utils.cpp,和lib/bigNumber.cpp都加入到sources裏,使用node-gyp rebuild構建插件。

然後JavaScript側

const { findSubStr, subStrCount, BigNumber } = require("./build/Release/addon");
console.log("subStr index is: ", findSubStr("abcabdacac", "cab"));
console.log("subStr count is: ", subStrCount("abababcda", "ab"));

let n = new BigNumber("9999");
n.add(n);
console.log("add: ", n.val());
n.multiply("12222");
console.log("multiply: ", n.val());

運行一下

編寫C++插件的幾種方式

隨着Node.js C++插件編寫方式的變化, 本文總結出了以下幾種編寫C++插件的方式

  1. 原生拓展
  2. 使用NAN
  3. 使用N-API
  4. 使用node-addon-api

原生擴展

原生的方式是指直接使用內部的V8,libuv和Node.js庫來創建插件,這種方式編寫一個插件可能比較複雜,涉及到以下組件和API。

  • V8: JavaScript運行時,用於解釋執行JavaScript。V8提供了創建對象,調用函數等機制。
  • libuv: 實現Node.js事件循環。
  • 內部Node.js庫: Node.js本身會導出插件可以使用的C++API,比較重要的是node::ObjectWrap類。
  • Node.js其他靜態鏈接庫: 包括OpenSSL,zlib等,可以使用zlib.h,openssl等來在自己的插件中引用。

V8

V8(v8文檔)引擎是一個可獨立運行的JavaScript運行時,回顧瀏覽器端和Node.js端的區別,大概就是對V8引擎的上層封裝不一樣,也就是說我們可以拿着V8引擎自己包裝一個自己的Node.js。

Node.js是V8引擎的一個宿主,其很大部分都是直接使用Chrome V8所暴露出來的API。

V8的一些基本概念

  1. Isolate
  • 一個Isolate就是一個V8引擎實例,也稱隔離實例(Isolated instance),實例內部擁有完全獨立的各種狀態,包括堆管理,垃圾回收等。

  • Isolate通常傳遞給其他V8 API函數,並提供一些API來管理JavaScript引擎的行爲或者查詢一些相關信息,比如內存使用情況。

  • 一個Isolate生成的任何對象都不能在另一個Isolate中使用。

  • 在Node.js插件中Isolate可通過以下方式獲取

// 直接獲取
Isolate* isolate = Isolate::GetCurrent();
// 如果有Context
Isolate* isolate = context->GetIsolate(); 
// 在binding函數中有const FunctionCallback<Value>& args
Isolate* isolate = args.GetIsolate();
// 如果有Environment
Isolate* isolate = env->isolate();


2. Context

可以理解爲瀏覽器上的window,其實Node也有自己的context,即global,甚至我們也可以對context進行包裝,比如

Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
Local<String> key = String::NewFromUtf8(isolate, "CONST", NewStringType::kNormal).ToLocalChecked();
Local<String> value = String::NewFromUtf8(isolate, "I am global value", NewStringType::kNormal).ToLocalChecked();
global->Set(key, value);
Local<Context> myContext = Context::New(isolate, nullptr, global); // 使用這種方式創建Context,然後現在的Context就是 { CONST: "I am global value" }
  1. Script

就是一個包含一段已經編譯好的JavaScript腳本對象,數據類型是Script,並且在編譯時與一個Context進行綁定。我們可以實現一個eval函數,將一段JavaScript代碼進行編譯,並且封裝一個自己的Context,來看C++代碼

#include <node.h>
#include <iostream>
using namespace v8;
// 這塊的代碼並不複雜
void Eval(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate(); // 拿到 isolate
  HandleScope handleScope(isolate); // 定義句柄作用域
  Local<Context> context = isolate->GetCurrentContext(); // 拿到Context

  // 定義一個global對象併爲他設置相應的鍵和值
  Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
  Local<String> key = String::NewFromUtf8(isolate, "CONST", NewStringType::kNormal).ToLocalChecked();
  Local<String> value = String::NewFromUtf8(isolate, "I am global value", NewStringType::kNormal).ToLocalChecked();
  global->Set(key, value);
  Local<String> printStr = String::NewFromUtf8(isolate, "print", NewStringType::kNormal).ToLocalChecked(); // let printStr = "print";

  global->Set(printStr, FunctionTemplate::New(isolate, [](const FunctionCallbackInfo<Value>& args) -> void {
    Isolate* isolate = args.GetIsolate();
    for (size_t i = 0; i < args.Length(); i++) {
      Local<String> str = args[i].As<String>();
      String::Utf8Value s(isolate, str); // 數據轉換,將Local轉到char*,以便用cout輸出 
      std::cout<<*s<<" ";
    }
    std::cout<<std::endl;
  }));
  /*
   global = {
     CONST: "I am global value"
     print: function(...args) {
       for(let i = 0; i < args.length; i++)
           console.log(args[i])
     }
   }
   */
  // 將global綁到自己的context上
  Local<Context> myContext = Context::New(isolate, nullptr, global);
  Local<String> code = args[0].As<String>();
  // 編譯JavaScript代碼
  Local<Script> script = Script::Compile(myContext, code).ToLocalChecked(); // 與myContext上下文進行綁定
  // 運行並將結果返回
  args.GetReturnValue().Set(script->Run(context).ToLocalChecked());
}

void Init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "eval", Eval);
}

NODE_MODULE(addon, Init)

C++的邏輯其實非常簡單,用Local<ObjectTemplate> global = ObjectTemplate::New(isolate);來產生一個空對象,並且在其上掛載了CONST常量和print函數,然後使用Local<Context> myContext = Context::New(isolate, nullptr, global);將global綁在一個Context上。Script::Compile(myContext, code)編譯JavaScript代碼並將運行結果返回回去args.GetReturnValue().Set(script->Run(context).ToLocalChecked());。這裏涉及到的LocalFunctionCallbackInfoHandleScope以及一些數據類型下面將做解釋

node-gyp rebuild或者node-gyp clean && node-gyp configure && node-gyp build進行編譯生成.node文件

再來看JavaScript側,加載.node文件

const cpp = require("./build/Release/addon");
const code = `
  let arr = ["hello", "world"];
  print(CONST); // 這個print函數以及CONST就是我們綁到global上的函數和常量 
  for(let v of arr) {
    print(v);
  }
`;
console.log(cpp.eval(code));

運行JavaScript

  1. Handle

即句柄,它是V8中的一個重要概念,提供對堆內存中JavaScript數據對象的一個引用。當一個對象不再被句柄所引用時,那麼它將被認爲是垃圾,V8的垃圾收集機制會不時的對其進行回收。

在window中,句柄是用來標識別應用程序所建立或使用的對象的唯一整數(編號),可以理解爲標識符,用來標識對象或者項目。

在V8中,句柄有以下幾種(句柄的存在形式是一個C++模板類,根據不同的V8數據類型進行不同的聲明)

  • Local: 本地句柄,在編寫C++擴展是最常用的句柄,它存在於棧內存中,並在對應的析構函數被調用時刪除,它們的生命週期由其所在的句柄作用域(HandleScope)決定。大多數時候可以通過JavaScript數據類的一些靜態方法來獲取一個Local句柄
HandleScope handleScope(isolate);
Local<Number> n = Number::New(isolate, 22);
Local<String> str = String::NewFromUtf8(isolate, "fff");
Local<Function> func = Function::New(context, Eval).ToLocalChecked();
Local<Array> arr = Array::New(isolate, 20);

使用handle.Clear()清除一個句柄(類似於指針指向“空”),使用handle.IsEmpty()判斷當前句柄是否爲空,使用As()/Cast()函數進行句柄類型轉換

Local<String> str = Local<String>::Cast(handle); // 使用Cast
Local<String> str1 = handle.As<String>(); // 使用As
  • MaybeLocal: 有時候需要在句柄使用的地方用handle.IsEmpty()來判斷是否爲空,類似於判斷是否爲空指針,但在每一個句柄使用的地方都加這種判斷的話就有點增加代碼量以及系統複雜度了,因此MaybeLocal就誕生了,那些有可能返回空Local句柄的接口都使用MaybeLocal去替代,而如果想獲得正真的Local句柄的話,就得用ToLocalChecked()
MaybeLocal<String> s = String::NewFromUtf8(isolate, "sss", NewStringType::kNormal);
Local<String> str = s.ToLocalChecked();
double a = args[0]->ToNumber(context).ToLocalChecked();

// 或者使用ToLocal方法,如果句柄不爲空,則返回true,並把值給out
Local<String> out;
if (s.ToLocal(&out)) {
  // 
} else {
  // 
}
  • Persistent/Global: 持久句柄,提供在堆內存聲明JavaScript對象的引用(比如瀏覽器的DOM),因此持久句柄和Local本地句柄在生命週期的管理上是兩種不同的方式。持久句柄可以使用SetWeak來變爲弱持久句柄,當堆中的JavaScript對象的引用只剩一個弱持久句柄時,V8的垃圾回收器就會觸發一個回調。
Local<String> str = String::NewFromUtf8(isolate, "fff", NewStringType::kNormal).ToLocalChecked();
Global<String> gStr(isolate, str); // 根據Local句柄構造一個持久句柄

與其他句柄一樣,持久句柄依然可以用Clear()清除,IsEmpty()判斷是否爲空。

Reset重新設置句柄引用,Get獲得當前句柄引用

Local<String> str1 = String::NewFromUtf8(isolate, "yyy", NewStringType::kNormal).ToLocalChecked();
gStr.Reset(isolate, str1); //重設句柄
  
Local<String> r = gStr.Get(isolate);

SetWeak設爲弱持久句柄,其函數原型如下

void PersistentBase<T>::SetWeak(
  P* parameter, typename WeakCallbackInfo<P>::Callback callback,
  WeakCallbackType type) 

其中parameter時任意數據類型,callback是當一個JavaScript對象的引用只剩一個弱持久句柄時觸發的一個回調,type是一個枚舉值,取值有kParameterkInternalFieldskFinalizer

int* p = new int;
// gStr快被回收時,則將p對象傳到回調函數中去,並且在回調中通過data.GetParameter()來獲取p
gStr.SetWeak(p, [](const WeakCallbackInfo<int> &data) -> void {
    int* p = data.GetParameter();
    delete p;
}, WeakCallbackType::kParameter);

ClearWeak是用於取消弱持久句柄,將其變爲持久句柄,IsWeak用於判斷是否爲弱持久句柄。

  • Eternal: 永生句柄,這種句柄在程序的整個生命週期中都不會被刪除,也由於這個特性,它比持久句柄開銷更小。
  1. HandleScope

句柄作用域,一個維護句柄的容器,當一個句柄作用域對象的析構函數被調用時(對象被銷燬時),在這個作用域中創建的句柄都會被從棧中抹去,失去所有引用,然後被垃圾回收器處理。句柄作用域有兩種HandleScopeEscapableHandleScope

  • HandleScope: 一般句柄作用域,使用HandleScope handleScope(isolate)來在當前作用域下聲明句柄作用域,根據C++的語法,當handleScope所在的作用域結束時(比如函數執行完)就會調用它的析構函數來做一些收尾操作。
void Add(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate); // 定義一個句柄作用域
  // 一堆Local句柄
  Local<Number> a = args[0].As<Number>();
  Local<Number> b = args[1].As<Number>();
  double r = a->Value() + b->Value();
  Local<Number> result = Number::New(isolate, r);
  args.GetReturnValue().Set(result);

  return; // 當前作用域終結,則調用handleScope的析構函數,從棧中刪除句柄a, b, result
}
  • EscapableHandleScope: 可逃句柄作用域,顧名思義,讓一個句柄逃離當前作用域,舉個例子
Local<Number> getValue() {
  Isolate* isolate = Isolate::GetCurrent();
  HandleScope handleScope(isolate);
  Local<Number> result = Number::New(isolate, 12);
  return result;
}

上面的函數感覺沒什麼問題,但實際上,這存在一個巨坑,按照上面講的,handleScope在當前作用域結束時調用析構函數將在當前作用域的句柄result全給刪除掉,而其引用的數值實體失去引用則被標記爲垃圾,然後又在外面使用,則會出問題,因此使用EscapableHandleScope改造上面的函數

Local<Number> getValue() {
  Isolate* isolate = Isolate::GetCurrent();
//  HandleScope handleScope(isolate);
  EscapableHandleScope scope(isolate);
  Local<Number> result = Number::New(isolate, 12);
  return scope.Escape(result); // 將result逃離當前作用域
}
  1. V8 JavaScript Value

V8對JavaScript中的每一種數據類型都有C++層面的封裝,比如Number, String, Object, Function, Boolean, Date, Promise等等,這些數據類型都由Value派生而來

下面列舉幾個類型的例子

  • Value

所有數據類型的父類,或者說是所有數據類型的抽象。它有兩個比較重要的API,Is..., To...Is...判斷是哪種類型,例如args[0].IsNumber()args[0].IsFunction()等,To...轉化爲某種類型args.ToNumber(context),返回一個MayBeLocal句柄。

  • Number

數值類型,該類型比較簡單,通過Number::New(isolate, 222)可以創建一個數值對象句柄,number->Value()獲得具體的值,返回是一個double

  • String

String是比較常用的數據類型,使用String::NewFromUtf8(isolate, "fff", NewStringType::kNormal)可以構造一個字符串句柄,返回是一個MaybeLocal句柄。

每次創建一個String句柄都需要寫一長串代碼,所以封裝了一個ToLocalString函數

Local<String> ToLocalString(const char* str) {
  Isolate* isolate = Isolate::GetCurrent();
  EscapableHandleScope scope(isolate);
  Local<String> result = String::NewFromUtf8(isolate, str, NewStringType::kNormal).ToLocalChecked();
  return scope.Escape(result);
}

在許多情況下,我們需要將String轉化爲C++的char*類型,這個時候可以藉助String::Utf8Value

Local<String> str = String::NewFromUtf8(isolate, "hello world", NewStringType::kNormal).ToLocalChecked();
String::Utf8Value value(isolate, str);
std::cout<<value.length()<<std::endl;
const char* cppStr = *value; // *value可以轉化爲char* 或const char* 類型
std::cout<<cppStr<<std::endl;

和上面一樣,將轉化爲原生字符串封裝成一個函數ToCString

char* ToCString(Local<String> from) {
  Isolate* isolate = Isolate::GetCurrent();
  String::Utf8Value v(isolate, from);
  return *v;
}
  • Function

函數也是對象的一種,所以它繼承自Object類,對於一個函數類型,可以用Call()來調用函數,NewInstance()new的方式調用函數,以及setName()/getName()來設置函數名。

Call: 該函數原型如下

MaybeLocal<Value> Call(Local<Context> context,Local<Value> recv, int argc, Local<Value> argv[]);

context爲上下文句柄對象,recv可以理解爲綁定this的指向,可以傳一個Null(isolate)進去,類似於JavaScript中的call的第一個參數,argc爲函數參數個數,argv是一個數組,表示傳入到函數裏的參數

例子,用C++實現filter函數

#include <iostream>
#include <node.h>

using namespace v8;

// to Local<String>
Local<String> ToLocalString(const char* str) {
  Isolate* isolate = Isolate::GetCurrent();
  EscapableHandleScope scope(isolate);
  Local<String> result = String::NewFromUtf8(isolate, str, NewStringType::kNormal).ToLocalChecked();
  return scope.Escape(result);
}

void Filter(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope scope(isolate);
  Local<Context> context = isolate->GetCurrentContext();
  if (!args[0]->IsArray() && !args[1]->IsArray()) {
    isolate->ThrowException(Exception::TypeError(ToLocalString("Type error")));
    return;
  }
  Local<Array> array = args[0].As<Array>();
  Local<Function> fn = args[1].As<Function>();
  Local<Array> result = Array::New(isolate);
  Local<Value> fnArgs[3] = { Local<Value>(), Number::New(isolate, 0), array };
  for (uint32_t i = 0, j = 0; i < array->Length(); i++) {
    fnArgs[0] = array->Get(context, i).ToLocalChecked(); // v
    fnArgs[1] = Number::New(isolate, i); // i
    Local<Value> v = fn->Call(context, Null(isolate), 3, fnArgs).ToLocalChecked();
    if (v->IsTrue()) { // get return
      result->Set(context, j++, fnArgs[0]).FromJust();
    }
  }
  args.GetReturnValue().Set(result);
}

void Init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "filter", Filter);
}

NODE_MODULE(addon, Init)

  • Array

數組對象比較簡單,比較常用的就是Array::New(isolate)來創建數組,Set()/Get()來對數組進行操作,Length()獲取數組長度。參考上面的filter函數

  • Object

對象類型,很多類型,比如FunctionArray都是繼承自Object,使用Object::New(isolate)可以創建一個對象句柄,通過Set()Get()Delete()來對鍵進行操作

void CreateObject(const FunctionCallbackInfo<Value>& args) { // return { name, age }
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);
  Local<Context> context = isolate->GetCurrentContext();

  Local<Object> object = Object::New(isolate);
  Local<String> nameKey = String::NewFromUtf8(isolate, "name", NewStringType::kNormal).ToLocalChecked();
  Local<String> ageKey = String::NewFromUtf8(isolate, "age", NewStringType::kNormal).ToLocalChecked();
  Local<String> nameValue = args[0].As<String>();
  Local<Number> ageValue = args[1].As<Number>();

  object->Set(context, nameKey, nameValue).Check(); // 設置鍵
  object->Set(context, ageKey, ageValue).Check(); // 設置鍵
  args.GetReturnValue().Set(object);
}
  1. FunctionCallbackInfo

函數回調信息,包含一個JavaScript函數調用所需的各種信息(參數,this等),例如

void Add(const FunctionCallbackInfo<Value>& args) {
  
}

在使用args

  • 通過Length()來獲取傳入的參數個數
  • 通過args[i]來獲取第i個參數
  • 通過args.This()來得到函數的this
  • 通過args.Holder()得到函數調用時的this,使用call, bind,apply可以改變this指向
  • 通過args.IsConstructCall()來判斷是否爲構造函數調用,即是否用new調用
  • 通過args.GetIsolate()獲得當前的Isolate
  • 通過args.GetReturnValue()來獲取存儲返回值的對象,並且設置返回值,即使用Set()方法,可以用SetNull()來返回一個nullSetUndefined()返回一個undefinedSetEmptyString()來返回一本空字符串。
  1. Template

模版,即JavaScript對象和函數的一個模具,用來把C++函數或者數據結構包裹進JavaScript對象中。這裏介紹兩種模板:函數模板FunctionTemplate和對象模板ObjectTemplate

  • FunctionTemplate

顧名思義,用於包裹C++函數的模具,當生成一個函數模板後可以調用GetFunction來獲取其實體句柄,並且JavaScript側能直接調用這個函數。回顧NODE_SET_METHOD宏,NODE_SET_METHOD(exports, "eval", Eval);將Eval函數包裹,並且JavaScript側能夠調用,可以翻開它的實現

inline void NODE_SET_METHOD(v8::Local<v8::Object> recv,
                            const char* name,   
                            v8::FunctionCallback callback) { // 內聯函數
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Context> context = isolate->GetCurrentContext();
  // 生成一個函數模板
  v8::Local<v8::FunctionTemplate> t = v8::FunctionTemplate::New(isolate,
                                                                callback);
  // GetFunction獲得句柄實例,返回是一個MayBeLocal
  v8::Local<v8::Function> fn = t->GetFunction(context).ToLocalChecked();
  v8::Local<v8::String> fn_name = v8::String::NewFromUtf8(isolate, name,
      v8::NewStringType::kInternalized).ToLocalChecked();
  // 設置函數名
  fn->SetName(fn_name);
  recv->Set(context, fn_name, fn).Check();
}
#define NODE_SET_METHOD node::NODE_SET_METHOD

callback是一個typedef void (*FunctionCallback)(const FunctionCallbackInfo<Value>& info);類型的函數指針,例如void Method(const FunctionCallbackInfo<Value>& args) {}

  • ObjectTemplate: 對象模板用於在運行時創建對象。例如下面這個
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);

對象模板有兩個常見用途,當一個函數模板被用作一個構造函數時,該對象模板就用來配置創建出來的對象(說白了就是JavaScript中的構造函數實現類),例如下面的JavaScript類

function Person(name, age) {
    this._name = name;
    this._age = age;
}
Person.prototype.getName = function () {
    return this._name;
};
Person.prototype.getAge = function () {
    return this._age;
};

在C++中的實現就是

#include <iostream>
#include <node.h>
using namespace v8;
// return Local<String>
Local<String> ToLocalString(const char* str) { 
  Isolate* isolate = Isolate::GetCurrent();
  EscapableHandleScope scope(isolate); // EscapableHandleScope派上用場了
  Local<String> key = String::NewFromUtf8(isolate, str, NewStringType::kNormal).ToLocalChecked();
  return scope.Escape(key);
}

// Person構造函數
void Person(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);
  Local<Context> context = isolate->GetCurrentContext();
  Local<Object> self = args.This(); // 相當與js函數裏面的this

  Local<String> nameKey = ToLocalString("name");
  Local<String> nameValue = args[0].As<String>();

  Local<String> ageKey = ToLocalString("age");
  Local<Number> ageValue = args[1].As<Number>();
  self->Set(context, nameKey, nameValue).Check();
  self->Set(context, ageKey, ageValue).Check();
  args.GetReturnValue().Set(self);
}
void GetName(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);
  Local<Context> context = isolate->GetCurrentContext();

  args.GetReturnValue().Set(args.This()->Get(context, ToLocalString("name")).ToLocalChecked());
}
void GetAge(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);
  Local<Context> context = isolate->GetCurrentContext();
  args.GetReturnValue().Set(args.This()->Get(context ,ToLocalString("age")).ToLocalChecked());
}

void Init(Local<Object> exports) {
  Isolate* isolate = Isolate::GetCurrent();
  Local<Context> context = isolate->GetCurrentContext();
  HandleScope handleScope(isolate);
  // 定義一個函數模板
  Local<FunctionTemplate> person = FunctionTemplate::New(isolate, Person);
  // 設置類名
  person->SetClassName(ToLocalString("Person"));
  // 拿到函數模板的原型對象
  Local<ObjectTemplate> prototype = person->PrototypeTemplate();
  // 爲這個對象設置值
  prototype->Set(ToLocalString("getName"), FunctionTemplate::New(isolate, GetName));
  prototype->Set(ToLocalString("getAge"), FunctionTemplate::New(isolate, GetAge));

  exports->Set(context, ToLocalString("Person"), person->GetFunction(context).ToLocalChecked()).Check();
}

NODE_MODULE(addon, Init)

node-gyp rebuild編譯插件,然後JavaScript側調用

const cpp = require("./build/Release/addon");
const person = new cpp.Person("sundial-dreams", 21);
console.log(person.getName());
console.log(person.getAge());

運行效果

第二個用法就是,用對象模板創建對象,例如實現下面創建對象的JavaScript函數

function createObject(name, age) {
    return { name, age }
}

在C++側的實現就是(使用對象模板)

void CreateObject(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);
  Local<Context> context = isolate->GetCurrentContext();
  Local<ObjectTemplate> obj = ObjectTemplate::New(isolate); // 利用對象模板創建空對象
  obj->Set(ToLocalString("name"), args[0].As<String>());
  obj->Set(ToLocalString("age"), args[1].As<Number>());
  args.GetReturnValue().Set(obj->NewInstance(context).ToLocalChecked()); // 實例化這個對象
}
  1. Internal fields

內置字段,將C++層面的數據結構與V8的數據類型建立一個聯繫,該字段對於JavaScript代碼來說是不可見的,只能通過Object的特定方法獲取(可以理解爲私有屬性)。在ObjectTemplate

  • 通過objectTemplate->SetInternalFieldCount(1)來設置內置字段的個數
  • 通過objectTemplate->InternalFieldCount()來獲取內置字段的個數
  • 通過ObjectTemplate的實例
    • object->SetInternalField(0, vaule);來設置內置字段值,其中value是一個External類型的句柄,包裹任意類型指針
    • 對應的object->GetInternalField(0);來獲取對應內置字段的值
    • object->SetAlignedPointerInInternalField(0, p);設置內置字段值,只不過第二個參數p直接就是任意類型指針
    • 對應的object->GetAlignedPointerFromInternalField(0);來獲取設置的字段值,返回是一個void*指針

我們可以上面的Person進行一個改造,將nameage藏在InternalFileds

#include <iostream>
#include <node.h>
using namespace v8;
// to Local<String>
Local<String> ToLocalString(const char* str) {
  Isolate* isolate = Isolate::GetCurrent();
  EscapableHandleScope scope(isolate);
  Local<String> result = String::NewFromUtf8(isolate, str, NewStringType::kNormal).ToLocalChecked();
  return scope.Escape(result);
}

// 內置字段
struct person {
  person(const char* name, int age): name(name), age(age) {}
  std::string name;
  int age;
};

void GetAll(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);
  Local<Object> self = args.Holder(); // 獲取運行時的this
  Local<External> wrapper = Local<External>::Cast(self->GetInternalField(0)); // 或者self->GetInternalField(0).As<External>();
  auto p = static_cast<person*>(wrapper->Value()); // Value() 返回的是void* 類型

  char result[1024];
  sprintf(result, "{ name: %s, age: %d }", p->name.c_str(), p->age);
  args.GetReturnValue().Set(ToLocalString(result));
}
// { getAll() { return `{ name: ${name}, age: ${age} }` } }
void CreateObject(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);
  Local<Context> context = isolate->GetCurrentContext();

  Local<ObjectTemplate> objectTemplate = ObjectTemplate::New(isolate);
  objectTemplate->SetInternalFieldCount(1); // 設置內置字段數量
  Local<Object> object = objectTemplate->NewInstance(context).ToLocalChecked();

  Local<String> name = args[0].As<String>();
  Local<Number> age = args[1].As<Number>();
  String::Utf8Value nameValue(isolate, name); // 可以使用 *nameValue 轉化爲 char* 類型
  auto p = new person(*nameValue, int(age->Value())); // 塞到內置字段裏的person指針
  
  object->SetInternalField(0, External::New(isolate, p)); // 使用External數據類型包裝person指針,其中New的第二個參數是void* 指針
  Local<Function> getAll = FunctionTemplate::New(isolate, GetAll)->GetFunction(context).ToLocalChecked(); // GetAll函數來獲取內置字段的值
  object->Set(context, ToLocalString("getAll"), getAll).Check();
  args.GetReturnValue().Set(object);
}

void Init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "createObject", CreateObject);
}

NODE_MODULE(addon, Init)

node-gyp rebuild構建插件

const cpp = require("./build/Release/addon");
const person = cpp.createObject("sundial-dreams", 21);
console.log(person);
console.log(person.getAll());

執行

node::ObjectWrap

即使用C++封裝一個JavaScript類,雖然上面介紹了JavaScript類的封裝,但其實都是基於JavaScript的語言特性(構造函數、原型鏈等)。而C++是一門面向對象的語言,如果不能使用C++ class 那有什麼用呢。因此Node.js提供了ObjectWrap類(位於node_object_wrap.h頭文件下)來幫助我們使用C++ class來創建JavaScript class。例子中的BigNumber也是用這種方式編寫的。

  • vodi Wrap(Local<Object> handle): 將傳入的Object本地句柄弄成一個與當前ObjectWrap對象關聯的對象,即設置內置字段
  • static T* Unwrap(Local<Object> handle): 從Object本地句柄中獲取與之關聯的ObjectWrap對象

基於node::ObjectWrap,我們重新實現Person

#include <iostream>
#include <string>
#include <node.h>
#include <node_object_wrap.h>

using namespace v8;
// to Local<String>
Local<String> ToLocalString(const char* str) {
  Isolate* isolate = Isolate::GetCurrent();
  EscapableHandleScope scope(isolate);
  Local<String> result = String::NewFromUtf8(isolate, str, NewStringType::kNormal).ToLocalChecked();
  return scope.Escape(result);
}

// Person Class
class Person : public node::ObjectWrap {
  public:
    static void Init(Local<Object>);

  private:
    explicit Person(const char* name, int age) : name(name), age(age) { };

    ~Person() override = default; // 利用父類的析構函數來做收尾操作

    static void New(const FunctionCallbackInfo<Value> &);

    static void GetName(const FunctionCallbackInfo<Value> &);

    static void GetAge(const FunctionCallbackInfo<Value> &);

    std::string name;

    int age;
};

// 依然是藉助ObjectTemplate和FunctionTemplate來創建Class
void Person::Init(Local<class v8::Object> exports) {
  Isolate* isolate = exports->GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();
  Local<ObjectTemplate> dataTemplate = ObjectTemplate::New(isolate);
  dataTemplate->SetInternalFieldCount(1); // 預留1個內置字段,用來保存Person指針
  Local<Object> data = dataTemplate->NewInstance(context).ToLocalChecked();
  // 第三個參數是傳入到New函數的參數args的Data()信息,可以通過args.Data()拿到data的值
  Local<FunctionTemplate> fnTemplate = FunctionTemplate::New(isolate, New, data); // Person::New方法
  fnTemplate->SetClassName(ToLocalString("Person"));
  fnTemplate->InstanceTemplate()->SetInternalFieldCount(1);
  // 使用這個宏,設置原型函數
  NODE_SET_PROTOTYPE_METHOD(fnTemplate, "getName", GetName);
  NODE_SET_PROTOTYPE_METHOD(fnTemplate, "getAge", GetAge);

  Local<Function> constructor = fnTemplate->GetFunction(context).ToLocalChecked();
  // 將 constructor ,Person類的構造函數設置到data的內置字段裏
  data->SetInternalField(0, constructor);
  exports->Set(context, ToLocalString("Person"), constructor).FromJust();
}
// 正真的構造函數
void Person::New(const FunctionCallbackInfo<Value> &args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();
  // 通過 new Person("aa", 21)調用
  if (args.IsConstructCall()) {
    Local<String> t = args[0].As<String>();
    String::Utf8Value name(isolate, t);
    int age = int(args[1].As<Number>()->Value());
    auto person = new Person(*name, age);
    // 這裏是關鍵,將一個args.This()與person指針關聯,person存儲在args.This()的內置字段裏,這也是上面要設置內置字段的原因
    person->Wrap(args.This());
    args.GetReturnValue().Set(args.This());
  } else { // 通過 Person("aa", 21)調用
    const int argc = 2;
    Local<Value> argv[argc] = {args[0], args[1]};
    // args.Data()是Init函數的data,從內置字段裏拿到Person的構造函數(這個在Init函數裏設置了)
    Local<Function> constructor = args.Data().As<Object>()->GetInternalField(0).As<Function>();
    Local<Object> result = constructor->NewInstance(context, argc, argv).ToLocalChecked();
    args.GetReturnValue().Set(result);
  }
}

void Person::GetName(const FunctionCallbackInfo<Value> &args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);
  // 這裏是重點,利用Unwrap函數獲取綁在args.This() / args.Holder()的person指針
  auto person = node::ObjectWrap::Unwrap<Person>(args.Holder());

  args.GetReturnValue().Set(ToLocalString(person->name.c_str()));
}

void Person::GetAge(const FunctionCallbackInfo<Value> &args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);

  auto person = node::ObjectWrap::Unwrap<Person>(args.Holder());

  args.GetReturnValue().Set(Number::New(isolate, person->age));
}


void Init(Local<Object> exports) {
  Person::Init(exports);
}

NODE_MODULE(addon, Init)

總結一下,其實使用node::ObjectWrap的方式就是將函數原型鏈模板與對象內置字段相結合了起來,將C++側的數據結構封裝成私有,暴露出一些通用方法給JavaScript側,在給函數模版設置原型對象時直接用了NODE_SET_PROTOTYPE_METHOD宏,這個宏其實對之前提到的設置函數原型的一個封裝。整個對象的封裝過程其實就是對args.This()與當前的類的指針(Person)建立一個聯繫,有了這個聯繫,我們可以在args.This()裏拿到當前類的指針(Person)做一些我們想做的事(返回person->name等)。其實node::ObjetWrap還幫我們做了很多收尾工作,感興趣的讀者可以嘗試去閱讀node::ObjectWrap的源碼。

JavaScript側

const { Person } = require("./build/Release/addon");
const person = new Person("dengpengfei", 21); // new 調用
console.log(person.getName());
console.log(person.getAge());

const person1 = Person("sundial-dreams", 21); // 直接用
console.log(person1.getName());
console.log(person1.getAge());

執行結果

libuv

libuv作爲Node.js的另一大依賴庫,爲Node.js提供了多操作系統異步操作的抽象。

由於篇幅有限,本文不打算具體介紹libuv,感興趣的讀者可以閱讀libuv官方文檔

基於NAN

在介紹NAN之前,我們來思考使用原生的方式開發Node.js C++插件會出現什麼問題

  • 首先就是版本不兼容問題,由於V8在快速迭代,Node.js也跟着V8走,因此V8 API的變化將直接導致現在的Node.js C++插件運行報錯,例如上面寫的代碼在Node.js v12上可以穩定運行,但是Node.js v10不一定能運行,在Node.js v8一定不能運行(大部分V8 API已經發生變化)。想想自己花大力氣寫的C++插件不能在其他版本的Node.js上跑,是不是想想就來氣💢。
  • 第二點就是V8的API比較複雜,要想寫個Node.js C++插件還需要懂V8的一些基本概念(不過看到這了,讀者應該理解了一些V8的概念)

在這個前提下,NAN(Nan官方文檔)誕生了。NAN全稱是Native Abstraction for Node.js,Node.js原生模塊抽象接口集。NAN爲跨版本的Node.js提供了穩定的API並且提供一組實用的API來簡化一些繁瑣的開發流程。

官方解釋:A header file filled with macro and utility goodness for making add-on development for Node.js easier across versions 0.8, 0.10, 0.12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 and 13.

也就是說Nan就是封裝一堆宏和一些工具函數的頭文件,這裏的宏就是通過Node.js版本判斷然後確定展開成何種形式。

NAN初體驗

  • 安裝nan
npm install --save nan
  • 配置binding.gyp
{
  "targets": [
    {
      "target_name": "addon",
      "sources": [
        "nan.cpp"
      ],
      "include_dirs": [
        "<!(node -e \"require('nan')\")"
      ]
    }
  ]
}

其實就是多了"<!(node -e \"require('nan')\")"這一行,按照上面介紹的binding.gyp語法,這其實是一個指令展開,相當於在命令行下執行node -e "require('nan')",如下

其實就是nan的頭文件路徑,也就將nan的頭文件include進來。修改項目的CmakeLists.txt,在原有的基礎上添加如下指令

include_directories(./node_modules/nan)
  • 使用nan編寫插件

我們filter函數爲例,使用Nan去實現

#include <node.h>
#include <nan.h>
using namespace v8;

// 使用 NAN_METHOD宏聲明函數,其展開後類似於 void Filter(const FunctionCallbackInfo<Value>& info) {}
NAN_METHOD(Filter) {
  Local<Array> array = info[0].As<Array>();
  Local<Function> fn = info[1].As<Function>();
  Local<Context> context = Nan::GetCurrentContext(); // 每一種v8數據類型在Nan中都有相應的封裝
  Local<Array> result = Nan::New<Array>();

  Local<Value> argv[3] = { Nan::New<Object>(), Nan::Null(), array };
  for (uint32_t i = 0, j = 0; i < array->Length(); i++) {
    argv[0] = array->Get(context, i).ToLocalChecked();
    argv[1] = Nan::New<Number>(i);
    Local<Value> v = Nan::Call(fn, Nan::New<Object>(), 3, argv).ToLocalChecked();
    if (v->IsTrue()) {
      result->Set(context, j++, argv[0]).FromJust();
    }
  }

  info.GetReturnValue().Set(result);
}
// 使用NAN_MODULE_INIT去聲明Init函數
NAN_MODULE_INIT(Init) {
  Nan::Export(target, "filter", Filter);
}

NODE_MODULE(addon, Init)

首先使用NAN_METHOD宏來聲明一個插件函數,其展開後的形式類似於我們常寫的void Filter(const FunctionCallbackInfo<Value>& info) {},其中的info就是傳進來的參數對象。除此之外Nan還對數據類型的構造進行了相應的封裝,通過Nan::New<Type>()來獲取相應的句柄。用NAN_MODULE_INIT宏來創建Init函數,target其實是Init傳的參數,類似於void Init(Local<Object> exports)exports,然後通過Nan::Export來設置模塊的導出。

node-gyp rebuild構建插件

JavaScript側

const { filter } = require("./build/Release/addon");
console.log(filter([1, 2, 2, 3, 4], (i, v) => v >= 2));

執行JavaScript

Nan基本類型介紹

其實如果讀者有了V8的基礎的,對於Nan的一些API的理解和會很容易,在Nan中很多東西和V8的API很相近

  • 函數參數類型

在Nan中對V8的FunctionCallbackInfoReturnValue進行了一個封裝,通過Nan::FunctionCallbackInfoNan::ReturnValue來訪問。例如

void Hello(const Nan::FunctionCallbackInfo<Value>& args) {
  args.GetReturnValue().Set(Nan::New("Hello World").ToLocalChecked());
}
  • 數據類型

在Nan中使用Nan::New<Type>()來創建對應類型的句柄,比如Nan::New<Number>(12),除此之外還有Nan::Undefined()Nan::Null()Nan::True()Nan::False()Nan::EmptyString()。並且使用Nan::To<Type>()來做數據類型轉換。

  • 句柄作用域

Nan::HandleScopeNan::EscapableHandleScope,Nan對V8中的HandleScopeEscapableHandleScope做了一個封裝

  • 持久句柄

    Nan::PersistentNan::Global,由於V8API一直在變化,因此Nan也對V8的Persistent/Global句柄進行了封裝

  • 腳本

對V8的Script的封裝,包括Nan::CompileScript()Nan::RunScript(),讀者可以嘗試使用Nan的方式實現上文的Eval

  • 助手函數

還記得模版不,即FunctionTemplateObjectTemplate,每次對模版進行操作時都比較繁瑣,因此Nan對這些操作進行了一個封裝來簡化模版操作流程,

Nan::SetMethod(): 爲對象句柄設置函數

Nan::Set()/Nan::Get()/Nan::Has()/Nan::Delete(): 對象句柄設置/獲取/鍵是否存在/刪除鍵

createObject函數舉個例子

void CreateObject(const Nan::FunctionCallbackInfo<Value>& info) {
  Local<Object> object = Nan::New<Object>();
  // 給對象設置屬性
  Nan::Set(object, Nan::New("name").ToLocalChecked(), info[0].As<String>());
  Nan::Set(object, Nan::New("age").ToLocalChecked(), info[1].As<Number>());
  // 給對象設置函數
  Nan::SetMethod(object, "getAll", [](const Nan::FunctionCallbackInfo<Value>& args) -> void {
    Local<Object> self = args.Holder();
    Local<String> name = Nan::Get(self, Nan::New("name").ToLocalChecked()).ToLocalChecked().As<String>();
    Local<Number> age = Nan::Get(self, Nan::New("age").ToLocalChecked()).ToLocalChecked().As<Number>();
    Nan::Utf8String n(name); // 使用Nan::Utf8String
    int a = int(age->Value());
    
    char result[1024];
    sprintf(result, "{ name: %s, age: %d }", *n, a);
    args.GetReturnValue().Set(Nan::New(result).ToLocalChecked());
  });

  info.GetReturnValue().Set(object);
}

Nan::SetPrototypeMethod(): 爲函數模版設置原型方法

Nan::SetPrototype(): 爲函數模板設置原型屬性

例如

Local<FunctionTemplate> fnTemplate = Nan::New<FunctionTemplate>();
Nan::SetPrototype(fnTemplatem, "name", Nan::New("fff").ToLocalChecked());
Nan::SetPrototype(fnTemplatem, "age", Nan::New(2));
Nan::SetPrototypeMethod(fnTemplate, "getAll", GetAll);

Nan::Call: 一個以同步的方式進行函數調用的工具方法,函數原型如下

inline MaybeLocal<v8::Value> Call(v8::Local<v8::Function> fun, v8::Local<v8::Object> recv, int argc, v8::Local<v8::Value> argv[])

Nan::ObjectWrap

node::ObjectWrap的封裝,並且添加了一些API去適配低版本的Node.js,回到使用node::ObjectWrap來包裝的Person類,使用Nan去實現就是

#include <iostream>
#include <string>
#include <node.h>
#include <nan.h>

using namespace v8;
// Person Class, extends Nan::ObjectWrap
class Person : public Nan::ObjectWrap {
  public:
    static NAN_MODULE_INIT(Init) { // void Init(Local<Object> exports)
      Local<FunctionTemplate> fnTemplate = Nan::New<FunctionTemplate>(New);
      fnTemplate->SetClassName(Nan::New("Person").ToLocalChecked());
      fnTemplate->InstanceTemplate()->SetInternalFieldCount(1);
      Nan::SetPrototypeMethod(fnTemplate, "getName", GetName);
      Nan::SetPrototypeMethod(fnTemplate, "getAge", GetAge);
      constructor().Reset(Nan::GetFunction(fnTemplate).ToLocalChecked()); // save a constructor function in global
      Nan::Set(target, Nan::New("Person").ToLocalChecked(), Nan::GetFunction(fnTemplate).ToLocalChecked());
    }

  private:
    explicit Person(const char* name, int age) : name(name), age(age) {}

    static NAN_METHOD(New) { // void New(const FunctionCallbackInfo<Value>& args)
      if (info.IsConstructCall()) {
        Local<String> name = info[0].As<String>();
        Local<Number> age = info[1].As<Number>();
        Nan::Utf8String n(name);
        int a = int(age->Value());
        auto person = new Person(*n, a);
        person->Wrap(info.This()); // like node::ObjectWrap
        info.GetReturnValue().Set(info.This());
        return;
      }
      const int argc = 2;
      Local<Value> argv[argc] = {info[0], info[1]};
      Local<Function> c = Nan::New(constructor());
      info.GetReturnValue().Set(Nan::NewInstance(c, argc, argv).ToLocalChecked());
    }

    static NAN_METHOD(GetName) {
      auto person = static_cast<Person*>(Nan::ObjectWrap::Unwrap<Person>(info.Holder()));
      info.GetReturnValue().Set(Nan::New(person->name.c_str()).ToLocalChecked());
    }

    static NAN_METHOD(GetAge) {
      auto person = static_cast<Person*>(Nan::ObjectWrap::Unwrap<Person>(info.Holder()));
      info.GetReturnValue().Set(Nan::New(person->age));
    }
    // define a global handle of constructor
    static inline Nan::Global<Function>& constructor() {
      static Nan::Global<Function> _constructor; // global value
      return _constructor;
    }

    std::string name;
    int age;
};

NODE_MODULE(nan_addon02, Person::Init)

其大部分寫法都與node::ObjectWrap一樣,只不過用宏去替代,在node::ObjectWrap實現的Person類中,使用對象的內置字段來保存constructor的,就是data->SetInternalField(0, constructor);。而在Nan中直接就用一個Global句柄來保存constructor。除了這個不一樣外,其他寫法與node::ObjectWrap基本一樣。

基於N-API

N-API(N-API文檔)是用於構建本地插件的API,基於C語言,獨立於JavaScript運行時(V8),並且作爲Node.js的一部分進行維護。該APi在所有版本的Node.js都是穩定的應用程序二進制接口(ABI)。旨在使插件於基礎的JavaScript引擎(V8)的更改保持隔離,並且在不重新編譯的前提下,在Node.js更高的版本下運行。

N-API提供的API通常用於創建和操作JavaScript值,其API有一下特點

  • N-API是C風格的API,很多函數也是C風格的💢

  • 所有N-API調用均返回類型爲napi_status的狀態代碼。表示API是否調用成功

  • API的返回值通過out參數傳遞(函數的返回值已經是napi_status了,所以只能通過指針來接正常的返回值)

  • 所有JavaScript值都被名爲napi_value類型封裝,可以理解爲JavaScript裏的var/let

  • 如果API的調用得到得到錯誤的狀態代碼。則可以使用napi_get_last_error_info來獲取最後的出錯信息

N-API嚐鮮

本文依然以實現filter函數爲例,來嘗試一把N-API

首先是修改binding.gyp(N-API也是可以直接使用node-gyp構建的)

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

使用N-API實現filter函數

#include <node.h>
#include <node_api.h>
#include <cstdio>
#include <cstdlib>
// 爲每一次的napi調用的status判斷寫成一個宏,call爲調用結果
#define NAPI_CALL(env, call)                                   \
     do {                                                      \
       napi_status status = (call);                            \
       if (status != napi_ok) {                                \
         const napi_extended_error_info* error_info = nullptr; \
         napi_get_last_error_info((env), &error_info);         \
         bool is_pending;                                      \
         napi_is_exception_pending((env), &is_pending);        \
         if (!is_pending) {                                    \
           const char* message = error_info->error_message;    \
           napi_throw_error((env), "ERROR", message);          \
           return nullptr;                                     \
         }                                                     \
       }                                                       \
    } while(false)


napi_value filter(napi_env env, napi_callback_info info) {
  size_t argc = 2;
  napi_value argv[2];
  // 拿到參數args
  NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
  napi_value arr = argv[0], fn = argv[1];
  // 獲取傳入的數組長度
  uint32_t length = 0;
  NAPI_CALL(env, napi_get_array_length(env, arr, &length));
  // 創建存結果的數組
  napi_value result;
  NAPI_CALL(env, napi_create_array(env, &result));
  napi_value fn_argv[3] = { nullptr, nullptr, arr };

  for (uint32_t i = 0, j = 0; i < length; i++) {
    napi_value fn_ret;
    uint32_t fn_argc = 3;
    napi_value index, arr_val;
    // 拿到數組第i項
    NAPI_CALL(env, napi_get_element(env, argv[0], i, &arr_val));
    // 將i封裝成napi_value類型
    NAPI_CALL(env, napi_create_int32(env, (int32_t) i , &index));
    fn_argv[0] = arr_val;
    fn_argv[1] = index;
    // 回調函數調用
    NAPI_CALL(env, napi_call_function(env, arr, fn, fn_argc, fn_argv, &fn_ret));
    // 拿到調用結果
    bool ret;
    NAPI_CALL(env, napi_get_value_bool(env, fn_ret, &ret));
    if (ret) {
      // 爲結果數組設置值
      NAPI_CALL(env, napi_set_element(env, result, j++, arr_val));
    }
  }
  return result;
}

napi_value init(napi_env env, napi_value exports) {
  // 將上面的filter函數封裝成napi_value類型
  napi_value filter_fn;
  napi_create_function(env, "filter", NAPI_AUTO_LENGTH, filter, nullptr, &filter_fn);
  napi_set_named_property(env, exports, "filter", filter_fn);
  return exports;
}
// 使用NAPI_MODULE來初始化宏(不是NODE_MODULE)
NAPI_MODULE(addon, init)

對於每一次的N-API調用都要拿返回的status進行判斷,類似於下面的代碼

napi_status status1 = napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
if (status1 != napi_ok) {
  // error handle
  return nullptr;
}

每一個調用都要寫這種代碼顯然是不現實的,所以將這種N-API的調用以及錯誤處理封裝成一個宏,也就是上面的NAPI_CALL宏,外層的do { } while(0)其實是給NAPI_CALL後面加分號用的😊。filter函數的實現思路和之前將的實現思路是一樣的,只不過用的API不一樣而已,比如獲取參數不再是args[0]/args[1],而是napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);argv[0]/argv[1],以及函數調用和相應類型的創建都換成了N-API。

node-gyp rebuild構建插件

JavaScript側

const { filter } = require("./build/Release/addon");
console.log(filter(["abc", 1, 3, "hello", "b", true], (v, i) => (typeof v === "string")));

執行結果

N-API基本介紹

  • 基本數據類型

    1. napi_status: 表示一個N-API調用成功或者失敗的狀態代碼,它是個枚舉類型,取值比較多,這裏就不一一列舉了,比較常用的就是napi_ok用來檢查是否調用成功

    2. napi_env: 表示底層N-API的特定狀態上下文。

    3. napi_value: 一個抽象數據類型,表示JavaScript值。並且可以使用napi_get_...等API來獲取實際的值,比如

    bool ret;
    NAPI_CALL(env, napi_get_value_bool(env, fn_ret, &ret)); // 上面的NAPI_CALL宏
    
    1. napi_handle_scope: 一般句柄作用域類型,類似於v8::HandleScope,不過得使用napi_open_handle_scope()/napi_close_handle_scope()一開一合來使用句柄作用域。
    {
        napi_handle_scope scope;
    	  NAPI_CALL(env, napi_open_handle_scope(env, &scope));
        // do something
        NAPI_CALL(env, napi_close_handle_scope(env, scope));
    }
    
    1. napi_escapable_handle_scope: 可逃句柄作用域類型,也是類似於v8::EscapableHandleScope,依然使用napi_open_escapable_handle_scope()/napi_close_escapable_handle_scope()來打開或者關閉句柄作用域,並且使用napi_escape_handle()來讓一些句柄逃離當前作用域,類比於scope.Escape(result)
    napi_value make_string(napi_env env, const char* name) {
      napi_escapable_handle_scope handle_scope;
      NAPI_CALL(env, napi_open_escapable_handle_scope(env, &handle_scope));
      napi_value val = nullptr;
      // create一個string
      NAPI_CALL(env, napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &val));
      napi_value ret = nullptr;  // 保存escape後的句柄
      NAPI_CALL(env, napi_escape_handle(env, handle_scope, val, &ret));
      NAPI_CALL(env, napi_close_escapable_handle_scope(env, handle_scope));
      return ret;
    }
    
    1. napi_callback/napi_callback_info: 類比於v8::FunctionCallback/v8::FunctionCallbackInfonapi_callback函數定義如下
    typedef napi_value (*napi_callback)(napi_env env, napi_callback_info info);
    

    感覺是不是很熟悉,和v8::FunctionCallback似乎是一個意思,裏面的napi_callback_info也是參數信息

    可以用napi_get_cb_info()函數來獲取相應的參數信息,該函數原型如下

    napi_status napi_get_cb_info(
        napi_env env,               // [in] NAPI environment 句柄
        napi_callback_info cbinfo,  // [in] callback-info 句柄
        size_t* argc,      // [in-out] 參數個數
        napi_value* argv,  // [out] 參數數組
        napi_value* this_arg,  // [out] 當前函數的this
        void** data);          // [out] data指針類似於v8的args.Data();
    

    舉個例子

    size_t argc = 2;
    napi_value argv[2], self;
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, &self, nullptr));
    
    1. napi_extended_error_info: 錯誤類型,用於從napi_get_last_error_info()裏獲取錯誤信息,該函數的第二個參數就是這個napi_extended_error_info類型的。
  • 創建N-API(JavaScript)類型

    類比於v8::Number::New()來創建數值類型,N-API也提供了一些函數來創建一些常用的JavaScript類型

    1. napi_create_array/napi_create_array_with_length: 創建數組
    napi_value array = nullptr;
    NAPI_CALL(env, napi_create_array(env, &array)); // 不定長數組
    napi_value array_1024 = nullptr;
    NAPI_CALL(env, napi_create_array_with_length(env, 1024, &array_1024)); // 定長數組
    
    1. napi_create_object: 創建對象
    napi_value object = nullptr;
    NAPI_CALL(env, napi_create_object(env, &object));
    
    1. napi_create_int32/napi_create_uint32/napi_create_int64/napi_create_double: 創建數值類型
    int val = 32;
    napi_value number = nullptr;
    NAPI_CALL(env, napi_create_int32(env, (int_32)val, &number)); //
    
    1. napi_create_string_utf8: 創建字符串
    napi_value str = nullptr;
    // 其中第三個參數的字符串長度,NAPI_AUTO_LENGTH表示遍歷字符串,但遇到null時終止
    NAPI_CALL(env, napi_create_string_utf8(env, "hello world", NAPI_AUTO_LENGTH, &str));
    
    1. napi_create_function: 利用napi_callback類型創建一個函數,函數原型定義如下
    napi_status napi_create_function(napi_env env,
                                     const char* utf8name, // 字符串函數名
                                     size_t length, // utf8name的長度,NAPI_AUTO_LENGTH
                                     napi_callback cb, // 相應的函數
                                     void* data, // 傳到napi_callback_info類型裏data字段
                                     napi_value* result); // [out] 結果
    

    舉個例子

    napi_status filter_fn = nullptr;  
    napi_create_function(env, "filter", NAPI_AUTO_LENGTH, filter, nullptr, &filter_fn);
    
    1. napi_get_boolean/napi_get_global/napi_get_null/napi_get_undefined: 這些函數寫的那麼直白,就不需要解釋了
    napi_value bool_val = nullptr;
    NAPI_CALL(env, napi_get_boolean(env, false, &bool_val)); // get false;
    
    napi_value global = nullptr;
    NAPI_CALL(env, napi_get_global(env, &global)); // get global對象
    
    napi_value undefined = nullptr;
    NAPI_CALL(env, napi_get_undefined(env, &undefined)); // get undefined
    
    napi_value null_val = nullptr;
    NAPI_CALL(env, napi_get_null(env, &null_val)); // get null
    
  • N-API類型到C類型的轉換

    napi_value轉化爲C類型以方便操作

    1. napi_get_value_bool: 獲取bool類型,其函數原型如下
    napi_status napi_get_value_bool(napi_env env,
                                    napi_value value, // 對於的句柄
                                    bool* result); // [out] 輸出對應的bool值
    
    1. napi_get_value_double/napi_get_value_int32/napi_get_value_int64: 獲取double/int32/int64類型
    double val;
    napi_value double_val = nullptr;
    NAPI_CALL(env, napi_create_double(env, 233.2, &double_val));
    NAPI_CALL(env, napi_get_value_double(env, double_val, &val)); // get double
    printf("%f\n", val);
    
    1. napi_get_value_string_utf8: 獲取字符串,其函數原型如下
    napi_status napi_get_value_string_utf8(napi_env env,
                                           napi_value value, // JavaScript側字符串
                                           char* buf, // 緩衝區
                                           size_t bufsize, // 緩衝區長度 
                                           size_t* result); // [out] 結果字符串長度
    

    例如

    napi_value str_val = make_string(env, "hello world!");
    char str[1024];
    size_t str_size;
    NAPI_CALL(env, napi_get_value_string_utf8(env, str_val, str, 1024, &str_size));
    for (size_t i = 0; i < str_size; i++) {
      printf("%c", str[i]);
    }
    printf("\n");
    
  • JavaScript類型操作

    即對JavaScript值執行一些抽象的操作,包括: 將JavaScript值強制轉換爲特定類型的JavaScript值檢查JavaScript值的類型檢查兩個JavaScript值是否相等。部分函數比較簡單,就直接寫函數原型了

    1. napi_coerce_to_bool/napi_coerce_to_number/napi_coerce_to_object/napi_coerce_to_string: 強制轉換爲bool/number/object/string,類比於v8::Local<Boolean>::Cast()等,例如下面的JavaScript語句
    var a = Boolean(b);
    

    使用N-API實現就是

    napi_value a;
    NAPI_CALL(env, napi_coerce_to_bool(env, b, &a));
    
    1. napi_typeof: 類似於JavaScript側的typeof運算,其函數原型如下
    napi_status napi_typeof(napi_env env, 
                            napi_value value,
                            napi_valuetype* result); // napi_valuetype是JavaScript數據類型的枚舉,取值爲: napi_string | napi_null | napi_object等
    
    1. napi_instanceof: 類似於JavaScript側的instanceof運算,其函數原型如下
    napi_status napi_instanceof(napi_env env,
                                napi_value object, // 對象
                                napi_value constructor, // 構造函數
                                bool* result); //[out] 結果
    
    1. napi_is_array: 類似於JavaScript裏的Array.isArray(),函數原型如下
    napi_status napi_is_array(napi_env env,
                              napi_value value,
                              bool* result); // [out] 結果
    
    1. napi_strict_equals: 判斷兩JavaScript值是否嚴格相等===,其函數原型如下
    napi_status napi_strict_equals(napi_env env,
                                   napi_value lhs, // 左值
                                   napi_value rhs, // 右值
                                   bool* result); // [out] 結果
    
  • 句柄作用域

    napi_handle_scopenapi_escapable_handle_scope,上文對這兩個做了解釋和相應的用法,這裏就不在贅述了。

  • 對象操作

    對於對象,N-API也封裝了一些方法去操作,首先來看兩個有用的類型,napi_property_attributesnapi_property_descriptor

    1. napi_property_attributes: 用於控制在對象設置的屬性的行爲的標誌,可枚舉/可配置/可寫等,所以它是個枚舉類型
    typedef enum {
      napi_default = 0, // 默認值,屬性只讀,不可枚舉,不可配置
      napi_writable = 1 << 0, // 屬性可寫
      napi_enumerable = 1 << 1, // 屬性可枚舉
      napi_configurable = 1 << 2, // 屬性可配置
      napi_static = 1 << 10, // 類的靜態屬性,napi_define_class時會用到這個
    } napi_property_attributes;
    
    1. napi_proptery_descriptor: 屬性結構體,包含屬性名,具體的值,getter/setter方法等
    typedef struct {
      const char* utf8name; // 屬性名 utf8name和name必須提供一個
      napi_value name; // 屬性名 utf8name和name必須提供一個
      napi_callback method; // 屬性值函數,如果提供了這個,則value和getter和setter必須是null
      napi_callback getter; // getter函數,如果有這個,則value和method必須是null
      napi_callback setter; // setter函數,如果有這個,則value和method必須是null
      napi_value value; // 屬性值 如果有這個,則getter和setter和method和data必須是null
      napi_property_attributes attributes; // 屬性的行爲標誌
      void* data; // 這個數據會傳到method,getter,setter
    } napi_property_descriptor;
    
    1. napi_define_properties: 爲對象定義屬性,類似於JavaScript中的Object.defineProperties(),其函數原型如下
    napi_status napi_define_properties(napi_env env,
                                       napi_value object, // 對象
                                       size_t property_count, // 屬性個數
                                       const napi_property_descriptor* properties); // 屬性數組
    

    現在又來實現一遍createObject函數,不過這次是基於Object.defineProperties()來實現的,類似於下面的JavaScript代碼

    function createObject(name, age) {
        let object = {};
        Object.defineProperties(object, {
            name: { enumerable: true, value: name },
            age: { enumerable: true, value: age },
        });
        return object;
    }
    

    現在使用N-API實現這個函數就是

    napi_value create_object(napi_env env, napi_callback_info info) {
      size_t argc;
      napi_value args[2], object;
      NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, nullptr, nullptr));
      NAPI_CALL(env, napi_create_object(env, &object));
      napi_property_descriptor descriptors[] = {
              {"name", 0, 0, 0, 0, args[0], napi_enumerable, 0},
              {"age", 0, 0, 0, 0, args[1], napi_enumerable, 0}
      };
      NAPI_CALL(env, napi_define_properties(env, object, sizeof(descriptors) / sizeof(descriptors[0]), descriptors));
      return object;
    }
    
    1. napi_get_property_names: 獲取一個對象的所有屬性名,其函數原型如下
    napi_status napi_get_property_names(napi_env env,
                                        napi_value object, // 對象
                                        napi_value* result); // 返回的結果,將會是一個數組
    

    例子

    napi_value prop_list = nullptr; 
    NAPI_CALL(env, napi_get_property_names(env, object, &prop_list)); // 返回prop_list將是一個array
    
    1. napi_set_property/napi_get_property/napi_has_property/napi_delete_property/napi_has_own_property: 分別是設置對象屬性/獲取對象屬性/判斷對象是否存在某個屬性/刪除對象屬性/判斷對象的屬性是否是自己的屬性(非原型屬性)。這個比較簡單,就直接貼上這些函數的原型了
    napi_status napi_set_property(napi_env env,
                                  napi_value object, // 對象
                                  napi_value key, // 鍵
                                  napi_value value); // 值
    
    napi_status napi_get_property(napi_env env, 
                                  napi_value object, // 對象
                                  napi_value key, // 鍵
                                  napi_value* result); // [out] 屬性值
    
    napi_status napi_has_property(napi_env env,
                                  napi_value object, // 對象
                                  napi_value key, // 鍵
                                  bool* result); // [out] 判斷結果
    
    napi_status napi_delete_property(napi_env env,
                                     napi_value object, // 對象
                                     napi_value key, // 鍵
                                     bool* result); // [out] 刪除是否成功
    
    napi_status napi_has_own_property(napi_env env,
                                      napi_value object, // 對象
                                      napi_value key, // 鍵
                                      bool* result); // [out] 判斷結果
    
    1. napi_set_named_property/napi_get_named_property/napi_has_named_property: 也是設置對象屬性/獲取對象屬性/判斷對象屬性是否存在,不過與上面的不同的是,key是一個const char*類型的,在某些場景下也是也比較有用,其函數原型如下
    napi_status napi_set_named_property(napi_env env,
                                        napi_value object, // 對象
                                        const char* utf8Name, // 鍵
                                        napi_value value); // 屬性值
    napi_status napi_get_named_property(napi_env env, 
                                        napi_value object, // 對象
                                        const char* utf8Name, // 鍵
                                        napi_value* result); // [out] 返回的屬性值
    napi_status napi_has_named_property(napi_env env,
                                        napi_value object, // 對象
                                        const char* utf8Name, // 鍵
                                        bool* result); // [out] 判斷結果
    
    1. napi_set_element/napi_get_element/napi_has_element/napi_delete_element: 也是設置對象的屬性值/獲取對象的屬性值/判斷對象屬性值是否存在/刪除對象的屬性值,這裏的函數key是一個uint32_t類型的,比較適用於數組,其函數原型如下
    napi_status napi_set_element(napi_env env,
                                 napi_value object, // 對象
                                 uint32_t index, // 鍵/索引
                                 napi_value value); // 值
    
    napi_status napi_get_element(napi_env env,
                                 napi_value object, // 對象
                                 uint32_t index, // 鍵/索引
                                 napi_value* result); // [out] 屬性值
    
    napi_status napi_has_element(napi_env env,
                                 napi_value object, // 對象
                                 uint32_t index, // 鍵/索引
                                 bool* result); //[out] 判斷結果
    
    napi_status napi_delete_element(napi_env env,
                                    napi_value object, // 對象
                                    uint32_t index, // 鍵/索引
                                    bool* result); // [out] 刪除是否成功
    
  • 函數操作

    主要是函數的創建和調用以及當作構造函數來使用

    1. napi_create_function: 該方法在上文已經提到過了,這裏就不在贅述。
    2. napi_function_call: 調用函數,其函數原型如下
    napi_status napi_call_function(napi_env env,
                                   napi_value recv, //JavaScript側的this對象
                                   napi_value func, // 函數
                                   size_t argc, // 傳入函數的參數個數
                                   const napi_value* argv, // 傳入函數的參數
                                   napi_value* result); // [out] 函數調用結果
    

    例子的話,可以參考上面的filter函數的實現

    1. napi_new_target: 相當於JavaScript側的new.target判斷當前是否以構造函數的方式調用,還記得Person類嘛,現在使用N-API實現一下
    napi_value person(napi_env env, napi_callback_info info) {
      size_t argc;
      napi_value args[2], self, target;
      NAPI_CALL(env, napi_get_new_target(env, info, &target));
      if (target == nullptr) { // 非new調用
        napi_throw_error(env, "ERROR", "need new");
        return nullptr;
      }
      NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, &self, nullptr));
      NAPI_CALL(env, napi_set_named_property(env, self, "name", args[0]));
      NAPI_CALL(env, napi_set_named_property(env, self, "age", args[1]));
      return self;
    }
    

    使用napi_new_target判斷是否用new進行調用函數,如果以new調用的話,即new Person("aaa", 21)則我們能得到正確的結果

否則Person("aaa", 21)直接調用就拋異常

  1. napi_new_instance: 即以構造函數的方式來調用函數併產生實例,其函數原型如下
napi_status napi_new_instance(napi_env env,
                              napi_value constructor, // 構造函數
                              size_t argc, // 參數個數
                              const napi_value* argv, // 參數
                              napi_value* result); // 以及實例(new)出來的對象

根據上面實現的Person類,我們可以實現一個createPerson的函數,類似於JavaScript側的

function createPerson(name, age) { return new Person(name, age) }

C++代碼如下

napi_value create_person(napi_env env, napi_callback_info info) {
  size_t argc = 2;
  napi_value args[2], instance = nullptr, constructor = nullptr;
  NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, nullptr, nullptr));
  NAPI_CALL(env, napi_create_function(env, "Person", NAPI_AUTO_LENGTH, person, nullptr, &constructor));
  NAPI_CALL(env, napi_new_instance(env, constructor, argc, args, &instance));
  return instance;
}

基於node-addon-api

由於N-API是Node.js v8版本纔出的特性,那麼在Node.js v8以下的版本都無法運行,這個問題和原生擴展一樣,因此就出現了node-addon-api包。node-addon-api(node-addon-api官方文檔)是對N-API的C++包裝,基於N-API包裝了一些低開銷包裝類使得能使用C++類等特性來簡化N-API的使用,並且打包的插件能跨多個Node.js版本運行。

node-addon-api上手

  • node-addon-api安裝: 和Nan一樣,安裝後是一堆頭文件和C++源文件
npm install --save node-addon-api
  • 修改binding.gyp
{
  "targets": [
    {
      "target_name": "addon",
      "include_dirs": [
        "<!@(node -p \"require('node-addon-api').include\")"
      ],
      # 添加下面的依賴庫,根據當前Node.js版本判斷
      "dependencies": [
        "<!(node -p \"require('node-addon-api').gyp\")"
      ],
      "cflags!": ["-fno-exceptions"],
      "cflags_cc!": ["-fno-exceptions"],
      "defines": [
        "NAPI_DISABLE_CPP_EXCEPTIONS" # 記得加這個宏
      ],
      "sources": [
        "node_addon_api.cpp"
      ]
    }
  ]
}

和Nan一樣也是將node-addon-api的頭文件加到include_dirs下,其實require('node-addon-api')就是把node-addon-api/index.jsrequire過來

var path = require('path');

var versionArray = process.version
  .substr(1)
  .replace(/-.*$/, '')
  .split('.')
  .map(function(item) {
    return +item;
  });
// node版本檢查,判斷是否有N-API
var isNodeApiBuiltin = (
  versionArray[0] > 8 ||
  (versionArray[0] == 8 && versionArray[1] >= 6) ||
  (versionArray[0] == 6 && versionArray[1] >= 15) ||
  (versionArray[0] == 6 && versionArray[1] >= 14 && versionArray[2] >= 2));

var needsFlag = (!isNodeApiBuiltin && versionArray[0] == 8);
// 當前目錄
var include = [__dirname];
// 對於低版本的node,dependencies需要的.gyp文件
var gyp = path.join(__dirname, 'src', 'node_api.gyp');
// 判斷是否有N-API
if (isNodeApiBuiltin) {
  gyp += ':nothing';
} else {
  gyp += ':node-api';
  include.unshift(path.join(__dirname, 'external-napi'));
}

module.exports = {
  include: include.map(function(item) {
    return '"' + item + '"';
  }).join(' '),
  gyp: gyp,
  isNodeApiBuiltin: isNodeApiBuiltin,
  needsFlag: needsFlag
};

然後對於低版本的Node.js,需要加上dependencies字段,其實也就是將node-addon-api/src/node_api.ccnode-addon-api/src/node_internals.cc給引入進來,對高版本的Node.js就什麼不做,可以參考node-addon-api/src/node_api.gyp。接着修改CMakeLists.txt,將node-addon-api的頭文件目錄給引進來,也就是添加下面的指令。

include_directories(./node_modules/node-addon-api)
  • 又是filter函數: (至少比"hello world!"有水平😭)
#include <napi.h>
using namespace Napi;
// 類似於N-API的寫法
Array Filter(const CallbackInfo &info) {
  Env env = info.Env(); // 類似於N-API裏面的env
  Array result = Array::New(env);
  Array arr = info[0].As<Array>(); // 類似於v8
  Function fn = info[1].As<Function>();
  for (size_t i = 0, j = 0; i < arr.Length(); i++) {
    // 函數調用可以用 initializer_list 這點很好
    Boolean ret = fn.Call(arr, {arr.Get(i), Number::New(env, i), arr}).As<Boolean>();
    if (ret.Value()) {
      result.Set(j++, arr.Get(i));
    }
  }
  return result;
}

// 類似於N-API的init函數寫法
Object Init(Env env, Object exports) {
  exports.Set("filter", Function::New(env, Filter));
  return exports;
}
// 使用的是NODE_API_MODULE宏
NODE_API_MODULE(addon, Init)

假設讀者認真的閱讀了上面的v8N-API的內容,對v8類型和N-API有了個初步的認識,那麼我覺得很容易就能看懂上面的代碼,就是N-API換成了類似於v8類型的名字而已。

node-addon-api介紹

從上面的filter函數可以看到,node-addon-api的數據類型,以及一些用法和v8的非常相像

  • 基本數據類型

    1. Value: 所有類型的抽象(父類),是對napi_value的一個封裝。常用API有

      value.As<Type>(): 數據類型轉換,類似於v8的As方法。

      value.Is...(): 數據類型判斷,比如value.IsFunction(), value.IsNull()

      value.To...(): 數據類型轉換,比如value.ToBoolean(),value.ToNumber()

      napi_value(value): 將Value轉化回napi_value類型,重載函數聲明如下

      operator napi_value() const;
      

      在下面的類型中也有類似的重載代碼,比如operator std::string() const;這樣的函數來將Napi裏的數據類型轉化到C++的數據類型。

    2. Object: 對象類型,繼承自Value

      Object::New(): 創建一個對象,是靜態函數,Object::New(env)

      obj.Set(): 給對象設置屬性,重載的比較多,比如

      Object obj = Object::New(env);
      obj.Set("name", "sundial-dreams");
      obj.Set(21, "age");
      obj.Set(String::New(env, "age"), Number::New(env, 21));
      

      鍵可以是napi_value | Napi::Value | const char* | const std::string& | uint32_t類型,值可以是napi_value | Napi::Value | const char* | std::string& | bool | double類型

      obj.Delete(): 刪除對象的屬性,obj.Delete("name"),鍵的類型跟上面的一樣

      obj.Get(): 獲取對象的屬性,obj.Get(21),鍵的類型跟上面的一樣

      obj.Has()/obj.HasOwnProperty(): 這個跟上面是一樣的

      []運算符重載: 也就是說我們可以通過obj["name"] = "dpf"的方式去設置或獲取屬性的值,其中鍵的類型是uint32_t | const char* | const std::string&

      obj.GetPropertyNames(): 返回對象的所有可枚舉屬性,返回值是一個Array類型

      Object object = Object::New(env);
      object["name"] = info[0].As<String>();
      object["age"] = info[1].As<Number>();
      Array attrs = object.GetPropertyNames();
      // "name", "age"
      for (size_t i = 0; i < attrs.Length(); i++) {
        std::cout << std::string(attrs.Get(i).As<String>()) << std::endl;
      }
      

      obj.DefineProperty()/obj.DefineProperties(): 定義屬性,obj.DefineProperty()的參數是一個Napi::PropertyDescriptor&類型,而obj.DefineProperties()的參數是std::initializer_list<Napi::PropertyDescriptor>std::vector<Napi::PropertyDescriptor>類型,這裏提到了Napi::PropertyDescriptor類型,可以理解爲N-API中的napi_property_descriptor的封裝。

      Object CreateObject(const CallbackInfo &info) {
        Env env = info.Env();
        Object object = Object::New(env);
        // 定義一個屬性,鍵、值、可枚舉
        PropertyDescriptor nameProp = PropertyDescriptor::Value("name", info[0], napi_enumerable);
        PropertyDescriptor ageProp = PropertyDescriptor::Value("age", info[1], napi_enumerable);
        // 定義一個函數屬性,鍵、函數、可枚舉
        PropertyDescriptor getAllFn = PropertyDescriptor::Function("getAll", [](const CallbackInfo &args) -> void {
          Object self = args.This().ToObject();
          std::string name = self.Get("name").As<String>(); //隱式類型轉換
          int age = self.Get("age").As<Number>();
          std::cout<<name<<" "<<age<<std::endl;
        }, napi_enumerable);
        // 傳個initializer_list進去
        object.DefineProperties({ nameProp, ageProp, getAllFn });
        return object;
      }
      
    3. String/Number/Boolean/Array: 這幾個類型也很簡單,都是使用New函數來構造,String類型,可以用str.Utf8Value()/str.Utf16Value()來獲取字符串值(返回std::string/std::u16string類型),或者直接用顯示類型轉換string(str)來轉換爲std::string類型。

      String str = String::New(env, "hello world!");
      std::string str1 = str.Utf8Value();
      std::u16string u16Str = str.Utf16Value();
      std::string cpp_str = str; // 隱式類型轉換,String 重載了string()類型轉換運算符
      std::string cpp_str1 = std::string(str); // 顯式類型轉換
      std::cout<<str1<<" "<<" "<<cpp_str<<" "<<cpp_str1<<std::endl;
      

      NumberBoolean也是一樣的,所以這裏就不在贅述了,最後一個Arrayfilter函數裏也演示過了。

    4. Env: 對N-API napi_env類型的封裝,也是可以通過napi_env(env)Napi::Env類型轉換爲napi_env

      ,Env類還提供了一些方法,比如env.Global()獲取global對象、env.Undefined()獲取undefinedenv.Null()獲取null

    5. CallbackInfo: 跟v8::FunctionCallbackInfo<Value>類似,不過它通過以下方法用來拿到一個JavaScript函數的信息

      info.Env(): 拿到env對象

      info.NewTarget(): 相當於JavaScript中的new.target運算

      info.isConstructCall(): 是否以構造(new)函數的方式進行調用

      info.Length(): 傳入的參數長度

      info[i]: CallbackInfo重載了[]運算符,所以可以通過下標的方式獲取第幾個參數

      info.This(): 當前函數的this

  • 句柄作用域

    1. HandleScope: 類似於v8::HandleScope,聲明一個HandleScope只需要HandleScope scope(env)即可

    2. EscapableHandleScope: 類似於v8::EscapableHandleScope

    Value ReturnValue(Env env) {
      EscapableHandleScope scope(env);
      Number number = Number::New(env, 222);
      return scope.Escape(number); // 類似於v8::EscapableHandleScope的用法
    }
    
  • 函數

Napi::Function類,用於創建JavaScript函數對象,它繼承自Napi::ObjectNapi::Function可以用兩類C++函數來創建,即typedef void (*VoidCallback)(const Napi::CallbackInfo& info);typedef Value (*Callback)(const Napi::CallbackInfo& info);分別是返回void和返回Value的函數

  1. New: 從CallbackVoidCallback類型的C++函數來創建JavaScript函數
Function fn1 = Function::New(env, OneFunc, "oneFunc");
Function fn2 = Function::New(env, TwoFunc);
  1. Call:調用函數,這個函數重載比較多
Value Call(const std::initializer_list<napi_value>& args) const; // 使用初始化列表傳參數
Value Call(const std::vector<napi_value>& args) const; // 使用vector來傳參數
Value Call(size_t argc, const napi_value* args) const; // 直接用數組傳參數
Value Call(napi_value recv, const std::initializer_list<napi_value>& args) const; // 綁定this,初始化列表
Value Call(napi_value recv, const std::vector<napi_value>& args) const; // 綁定this,vector
Value Call(napi_value recv, size_t argc, const napi_value* args) const; // 綁定this, 數組

例如

// 使用初始化列表
fn1.Call({Number::New(env, 1), String::New(env, "sss")});
fn1.Call(self, {Number::New(env, 2)});
// 使用vector
std::vector<napi_value> args = {Number::New(env, 222)};
fn1.Call(args);
fn1.Call(self, args);
// 直接用數組
napi_value args2[] = {String::New(env, "func"), Number::New(env, 231)};
fn1.Call(2, args2);
fn1.Call(self, 2, args2);
  1. **()**重載: 調用函數,函數原型如下
Value operator ()(const std::initializer_list<napi_value>& args) const;

調用如下

fn1({Number::New(env, 22)});

總結

本文很多部分其實借鑑了死月大佬的《Node.js來一打C++擴展》,但由於這本書的內容是基於Node.js v6寫的,現在Node.js 已經更新到v12了,原生擴展中很多部分都已經發生了變化(本文中的所有示例都是基於Node.js v12)。本文也是大概介紹了Node.js C++插件的一些用途以及Node.js C++插件的基本原理,然後總結出Node.js C++插件開發的四種方式:原生開發、基於Nan、N-API、基於node-addon-api的方式來編寫我們的Node.js插件,從中也可以看到Node.js C++插件開發的一些演進。對於每一種開發方式文章中其實也只是簡單的介紹了一些API和用法,希望這些內容能讓讀者對C++插件有個基本的入門,更多的內容還請以官方文檔爲準。我覺得阻礙學習C++插件開發的不是那些API有多難用,而是可能不會C++😂😂(建議看三遍《C++ Primer Plus》)。

GitHub地址

本文所有示例地址:https://github.com/sundial-dreams/node_cpp_addon

參考

  • 《Node.js來一打C++擴展》

  • v8文檔:https://v8.dev/docs

  • Node.js文檔:https://nodejs.org/dist/latest-v12.x/docs/api/addons.html

  • Nan官方文檔:https://github.com/nodejs/nan

  • N-API官方文檔:https://nodejs.org/dist/latest-v12.x/docs/api/n-api.html

  • node-addon-api官方文檔:https://github.com/nodejs/node-addon-api

發佈了37 篇原創文章 · 獲贊 115 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章