熟悉Node.js的人都知道,Node.js是基於C++開發的一個JavaScript運行時,既然Node.js是用C++開發的,那麼我可否將C++代碼引入到Node.js中呢,這固然是能夠的,這項技術被稱爲C++模塊。官方對Node.js C++模塊解釋以下javascript
Node.JS插件是使用C++編寫的動態連接庫,能夠被Node.JS以require的形式載入,在使用時就像Node.js原生模塊同樣。主要被用於在Node.js的JavaScript和C或者C++庫之間創建起橋樑的關係。html
動態連接庫,即window平臺的.dll文件,linux下的.so文件。只不過Node.js模塊導出的是.node文件。java
動態連接庫提供了一種方法,使進程能夠調用不屬於其可執行代碼的函數,函數的可執行代碼位於一個.dll (window)或.so (linux)文件中,該文件包含一個或多個已被編譯、連接並與使用它們的進程分開存儲的函數。說到動態連接庫,不得不提一下靜態連接庫,靜態連接庫是指在編譯階段就把相關的函數庫(靜態庫)連接,合成一個可執行文件。node
那麼,爲何須要C++模塊?python
JavaScript是基於異步,單線程的語言,對於一些異步任務很是佔優點,但對於一些計算密集型的任務,也有明顯的劣勢(也許這是腳本語言的缺點)。換句話說使用JavaScript解釋器執行JavaScript代碼的效率一般是比直接執行一個C++編譯好的二進制文件效率要低。除此以外,其實不少開源庫是基於C++寫的,好比圖像處理庫(ImageMagick),像咱們團隊使用的圖像處理庫,也是基於C++編寫(用JavaScript寫,性能達不到要求),因此對於一些問題使用C++來實現,效率和性能能有顯著的提高,何樂而不爲呢。linux
所以本文從C++插件基本原理以及幾種編寫方式來向讀者介紹如何將C++代碼加載到JavaScript中(編寫Node.js C++模塊)ios
前面提到,Node.js的C++模塊是以動態連接庫存在的(.node),那麼Node.JS是如何加載C++模塊的呢。首先Node.js的一個模塊時一個遵循CommonJS規範書寫的JavaScript源文件(.js),也多是一個C++模塊二進制文件(.node),這些文件經過Node.js中的 require() 函數被引入並使用。c++
在Node.js中引入C++模塊的本質就是在Node.js運行時引入一個動態連接庫的過程。在Node.js中經過 require() 函數加載模塊,不管是Node.js模塊仍是C++模塊。那麼知道這個函數怎麼實現的就知道怎麼加載模塊的。git
爲了揭開require的正面目,咱們翻開Node.js的源碼(Node.js Github)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.js下的擴展構建工具,在安裝C++插件時,經過一個binding.gyp描述文件來生成不一樣系統所須要的C++項目文件(UNIX 的 Makefile,Windows下的Visual Studio項目),而後調用相應的構建工具(gcc)來進行構建。
安裝
mac 上保證安裝了xcode(應用商店直接下載便可),而後命令行
npm install node-gyp -g
複製代碼
node-gyp的經常使用命令
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文件中使用
下面是一個簡單舉個簡單的例子,更多示例請參考: github.com/Node.js/nod…
{
"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"
]
}
複製代碼
首先使用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:github.com/sundial-dre…
主函數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());
複製代碼
運行一下
隨着Node.js C++插件編寫方式的變化, 本文總結出瞭如下幾種編寫C++插件的方式
原生的方式是指直接使用內部的V8,libuv和Node.js庫來建立插件,這種方式編寫一個插件可能比較複雜,涉及到如下組件和API。
node::ObjectWrap
類。V8(v8文檔)引擎是一個可獨立運行的JavaScript運行時,回顧瀏覽器端和Node.js端的區別,大概就是對V8引擎的上層封裝不同,也就是說咱們能夠拿着V8引擎本身包裝一個本身的Node.js。
Node.js是V8引擎的一個宿主,其很大部分都是直接使用Chrome V8所暴露出來的API。
V8的一些基本概念
一個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" }
複製代碼
就是一個包含一段已經編譯好的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
即句柄,它是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
和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中的每一種數據類型都有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);
}
複製代碼
函數回調信息,包含一個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()
來返回一本空字符串。模版,即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()); // 實例化這個對象
}
複製代碼
內置字段,將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());
複製代碼
執行
即便用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做爲Node.js的另外一大依賴庫,爲Node.js提供了多操做系統異步操做的抽象。
因爲篇幅有限,本文不打算具體介紹libuv,感興趣的讀者能夠閱讀libuv官方文檔
在介紹NAN以前,咱們來思考使用原生的方式開發Node.js C++插件會出現什麼問題
在這個前提下,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版本判斷而後肯定展開成何種形式。
npm install --save nan
複製代碼
{
"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)
複製代碼
咱們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
其實若是讀者有了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[])
複製代碼
對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文檔)是用於構建本地插件的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
來獲取最後的出錯信息
本文依然以實現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")));
複製代碼
執行結果
基本數據類型
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;
}
複製代碼
因爲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版本運行。
npm install --save node-addon-api
複製代碼
{
"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類型的名字而已。
從上面的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
的函數
Callback
或VoidCallback
類型的C++函數來建立JavaScript函數Function fn1 = Function::New(env, OneFunc, "oneFunc");
Function fn2 = Function::New(env, TwoFunc);
複製代碼
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.com/sundial-dre…
《Node.js來一打C++擴展》
v8文檔:v8.dev/docs
Node.js文檔:nodejs.org/dist/latest…
Nan官方文檔:github.com/nodejs/nan
N-API官方文檔:nodejs.org/dist/latest…
node-addon-api官方文檔:github.com/nodejs/node…