簡介
熟悉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;
};
從上面的代碼中可以看到模塊的加載規則
- 如果模塊在緩存裏,則直接讀緩存裏的
- 如果的內建模塊,則使用
loadNativeModule
加載模塊 - 其他情況使用
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
字段也是必須的,用於將哪些文件當作源碼進行編譯。
- 基本類型
類似於python的數據類型,gyp裏面的基本類型只有 String, Integer, Lists, Dictionaries
- 關鍵字段
下面列舉一些比較常見的字段(鍵)
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"]}],
]
]
}
]
}
- 變量
在gyp中主要有三類變量:預定義變量、用戶定義變量,自動變量。
預定義變量:比如OS
變量,表示當前的操作系統(linux, mac, win)
用戶定義變量:在variables
字段下定義的變量。
自動變量:所有的字符串鍵名都會被當作自動變量處理,變量名是鍵名加上_前綴。
變量的引用:以<
開頭或>
開頭,用@
來區分不同類型的變量。<(VAR)
或>(VAR)
,如果VAR
是一個字符串,則當作一個正常的字符串處理,如果VAR
是一個數組,則按空格拼接數組每一項的字符串。<@(VAR)
或>@(VAR)
,該指令只能用在數組中,如果VAR
是一個數組,數組的內容會一一插入到當前所在的數組中,如果是字符串則會按指定分隔符轉成數組再一一插入到當前所在數組裏。
- 指令
指令與變量類似,不過比變量高級一點,GYP讀到指令時會啓動一個進程去執行這條展開的指令,其語法格式是: 以<!
開頭或者<!@
開頭的,與變量相同的一點是<!@
也是用於數組的。
{
# ...
"include_dirs": [
"<!(node -e \"require('nan')\")" # 相當於在cmd下執行 node -e "require('nan')",並將結果放在include_dirs裏
]
# ...
}
- 條件分支
conditions
字段,其值是一個數組,那麼第一個元素是一個字符串,表示條件,條件格式跟python的條件分支一樣,例如"OS=='mac' or OS=='win'"
或者"VAR>=1 and VAR <= 2"
。第二個元素則是一個對象,用於根據條件合併到最近的一個上下文中的內容。
- 列表過濾器
用於值是數組的鍵,鍵名以!
或者/
結尾,其中鍵名以!
結尾是一個排除過濾器,表示這裏的鍵值將被從無!
的同名鍵中排除。鍵名以/
結尾是一個匹配過濾器,表示通過正則匹配出相應結果,然後以指定方式(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
]}]
]
}
]
}
- 合併
從上面可以看到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++插件的方式
- 原生拓展
- 使用NAN
- 使用N-API
- 使用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的一些基本概念
- 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" }
- 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());
。這裏涉及到的Local
,FunctionCallbackInfo
,HandleScope
以及一些數據類型下面將做解釋
用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
- 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
是一個枚舉值,取值有kParameter
和 kInternalFields
和kFinalizer
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
: 永生句柄,這種句柄在程序的整個生命週期中都不會被刪除,也由於這個特性,它比持久句柄開銷更小。
- HandleScope
句柄作用域,一個維護句柄的容器,當一個句柄作用域對象的析構函數被調用時(對象被銷燬時),在這個作用域中創建的句柄都會被從棧中抹去,失去所有引用,然後被垃圾回收器處理。句柄作用域有兩種HandleScope
和EscapableHandleScope
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逃離當前作用域
}
- 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
對象類型,很多類型,比如Function
,Array
都是繼承自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);
}
- 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()
來返回一個null
,SetUndefined()
返回一個undefined
,SetEmptyString()
來返回一本空字符串。
- 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()); // 實例化這個對象
}
- 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
進行一個改造,將name
和age
藏在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的FunctionCallbackInfo
和ReturnValue
進行了一個封裝,通過Nan::FunctionCallbackInfo
和Nan::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::HandleScope
和Nan::EscapableHandleScope
,Nan對V8中的HandleScope
和EscapableHandleScope
做了一個封裝
-
持久句柄
即
Nan::Persistent
和Nan::Global
,由於V8API一直在變化,因此Nan也對V8的Persistent/Global
句柄進行了封裝 -
腳本
對V8的Script
的封裝,包括Nan::CompileScript()
和Nan::RunScript()
,讀者可以嘗試使用Nan的方式實現上文的Eval
- 助手函數
還記得模版不,即FunctionTemplate
和ObjectTemplate
,每次對模版進行操作時都比較繁瑣,因此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基本介紹
-
基本數據類型
-
napi_status
: 表示一個N-API調用成功或者失敗的狀態代碼,它是個枚舉類型,取值比較多,這裏就不一一列舉了,比較常用的就是napi_ok
用來檢查是否調用成功 -
napi_env
: 表示底層N-API的特定狀態上下文。 -
napi_value
: 一個抽象數據類型,表示JavaScript值。並且可以使用napi_get_...
等API來獲取實際的值,比如
bool ret; NAPI_CALL(env, napi_get_value_bool(env, fn_ret, &ret)); // 上面的NAPI_CALL宏
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)); }
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; }
napi_callback/napi_callback_info
: 類比於v8::FunctionCallback/v8::FunctionCallbackInfo
,napi_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));
napi_extended_error_info
: 錯誤類型,用於從napi_get_last_error_info()
裏獲取錯誤信息,該函數的第二個參數就是這個napi_extended_error_info
類型的。
-
-
創建N-API(JavaScript)類型
類比於
v8::Number::New()
來創建數值類型,N-API也提供了一些函數來創建一些常用的JavaScript類型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)); // 定長數組
napi_create_object
: 創建對象
napi_value object = nullptr; NAPI_CALL(env, napi_create_object(env, &object));
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)); //
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));
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);
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類型以方便操作napi_get_value_bool
: 獲取bool
類型,其函數原型如下
napi_status napi_get_value_bool(napi_env env, napi_value value, // 對於的句柄 bool* result); // [out] 輸出對應的bool值
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);
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值是否相等。部分函數比較簡單,就直接寫函數原型了
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));
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等
napi_instanceof
: 類似於JavaScript側的instanceof
運算,其函數原型如下
napi_status napi_instanceof(napi_env env, napi_value object, // 對象 napi_value constructor, // 構造函數 bool* result); //[out] 結果
napi_is_array
: 類似於JavaScript裏的Array.isArray()
,函數原型如下
napi_status napi_is_array(napi_env env, napi_value value, bool* result); // [out] 結果
napi_strict_equals
: 判斷兩JavaScript值是否嚴格相等===
,其函數原型如下
napi_status napi_strict_equals(napi_env env, napi_value lhs, // 左值 napi_value rhs, // 右值 bool* result); // [out] 結果
-
句柄作用域
即
napi_handle_scope
和napi_escapable_handle_scope
,上文對這兩個做了解釋和相應的用法,這裏就不在贅述了。 -
對象操作
對於對象,N-API也封裝了一些方法去操作,首先來看兩個有用的類型,
napi_property_attributes
和napi_property_descriptor
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;
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;
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; }
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
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] 判斷結果
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] 判斷結果
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] 刪除是否成功
-
函數操作
主要是函數的創建和調用以及當作構造函數來使用
napi_create_function
: 該方法在上文已經提到過了,這裏就不在贅述。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
函數的實現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)
直接調用就拋異常
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.js
給require
過來
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.cc
和node-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)
假設讀者認真的閱讀了上面的v8和N-API的內容,對v8類型和N-API有了個初步的認識,那麼我覺得很容易就能看懂上面的代碼,就是N-API換成了類似於v8類型的名字而已。
node-addon-api介紹
從上面的filter
函數可以看到,node-addon-api
的數據類型,以及一些用法和v8的非常相像
-
基本數據類型
-
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++的數據類型。 -
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; }
-
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;
Number
和Boolean
也是一樣的,所以這裏就不在贅述了,最後一個Array
在filter
函數裏也演示過了。 -
Env: 對N-API
napi_env
類型的封裝,也是可以通過napi_env(env)
將Napi::Env
類型轉換爲napi_env
,Env類還提供了一些方法,比如
env.Global()
獲取global
對象、env.Undefined()
獲取undefined
、env.Null()
獲取null
。 -
CallbackInfo: 跟
v8::FunctionCallbackInfo<Value>
類似,不過它通過以下方法用來拿到一個JavaScript函數的信息info.Env()
: 拿到env
對象info.NewTarget()
: 相當於JavaScript中的new.target
運算info.isConstructCall()
: 是否以構造(new
)函數的方式進行調用info.Length()
: 傳入的參數長度info[i]
:CallbackInfo
重載了[]
運算符,所以可以通過下標的方式獲取第幾個參數info.This()
: 當前函數的this
-
-
句柄作用域
-
HandleScope: 類似於
v8::HandleScope
,聲明一個HandleScope
只需要HandleScope scope(env)
即可 -
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::Object
,Napi::Function
可以用兩類C++函數來創建,即typedef void (*VoidCallback)(const Napi::CallbackInfo& info);
和typedef Value (*Callback)(const Napi::CallbackInfo& info);
分別是返回void
和返回Value
的函數
- New: 從
Callback
或VoidCallback
類型的C++函數來創建JavaScript函數
Function fn1 = Function::New(env, OneFunc, "oneFunc");
Function fn2 = Function::New(env, TwoFunc);
- 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);
- **()**重載: 調用函數,函數原型如下
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