熟悉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)

复制代码
编写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();
复制代码
能够理解为浏览器上的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…