由于nodejs脱离了浏览器的限制、更接近系统,所以难免会遇到调用本地模块(即js调用C/C++)的情况。使用nodejs调用本地模块有多种方案可以实现,例如:
IPC
zeromq
、web服务器、WebSocket 等。这种方案的好处是本地化模块和node运行时彼此分离,相互之间影响较小。
node-ffi
node-ffi是一个用于使用纯JavaScript加载和调用动态库的Node.js插件。它可以用来在不编写任何C ++代码的情况下创建与本地DLL库的绑定。同时它负责处理跨JavaScript和C的类型转换。
node-gyp
本文内容即为记录node-gyp入门使用
推荐阅读此博客
node-gyp Readme
node-gyp模块使用npm
即可简单安装,此外其需要依赖Python 2.7 和 编译工具
其中Python版本必须为 2.7,其作用是生成编译工具需要的配置文件(例如Makefile)。而编译工具视平台不同而有所区别: Make/gcc on Linux | windows-build-tools/VS on Windows | Xcode on OSX
具体安装过程可查看官方的Readme#Installation章节
构建项目目录
mkdir my_node_addon
cd my_node_addon
建立编译配置模板文件(binding.gyp)
生成编译配置文件
node-gyp configure
编译
node-gyp build
具体使用过程可查看官方的Readme#How to Use章节
node-gyp涉及到nodejs的底层,包括node运行时、V8引擎、libuv,使用C++开发本地模块有点类似于绕过V8引擎编译Javascript环节,直接实现V8虚拟机执行时的机器码。关于node/V8/libuv,可以阅读一下此系列文章
使用node-gyp开发的组件在js层几乎等同于js module,因此在C++的代码中也可以与js module的结构对应。
一个简单JS模块(CommonJS模块规范)的结构可以如下所示:
var str = "Hello World";
// var hi = () => str
function hi() {
return str;
}
module.exports = {
"hi": hi
}
对应于C++,其结构是这样子的(因nodejs的api版本不同而不同, 下述代码对应nodejs 8.12.0):
#include
#include
using namespace v8;
char *str = "Hello World";
void hi(const FunctionCallbackInfo & args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(isolate, str));
}
void Initialize(Local
而当我们使用这个模块时,使用方法是一样的:
var m = require('name');
console.log(m.hi());
涉及到V8引擎的一些基本概念,可以阅读这篇文章
来分析一下上述C++代码:
对于变量/函数定义,按照C++的规范进行即可。从外向内看,可以看到在C++中,使用了一个宏函数NODE_MODULE
将我们设定好的接口导出,在这里传入了Intialize
这个函数用来配置要导出的接口;在函数Initialize
中,我们获取到V8引擎传入的exports对象,并使用宏函数NODE_SET_METHOD
将接口名“hi“和对应的函数进行绑定。这样便将这个接口导出。
在函数hi
中,我们获取到V8传入的参数,通过这个参数获取到此时的V8引擎实例,这个实例用来实例化一个V8里定义的对象。最后使用GetReturnValue()
获取到函数返回值对象,使用其Set()
方法设置函数返回值。
V8引擎将Js中的变量类型全部封装为C++中的类,常见的几种Js变量类型如下:
Isolate* isolate = args.GetIsolate();
// Number 类型的声明
Local retval = v8::Number::New(isolate, 1000);
// String 类型的声明
Local str = v8::String::NewFromUtf8(isolate, "Hello World!");
// Object 类型的声明
Local
在C++中具有栈内存和堆内存的概念,然而在V8中,js的一切变量都存储于堆内存中(毕竟每一个变量都是一个V8里定义的类),因此所有的js变量都应该被显式释放掉。Js是自带GC的语言,所以释放堆内存的重任就交由V8了。
任何使用V8代码的C++程序(包括V8本身),都必须使用Handle模板来保存js对象的引用,其中T可以是任何js对象在V8中的实现类。Handle又可进一步细分为Local和Persistent两种。顾名思义,前者是一种局部Handle,它随着相关的HandleScope的析构而消失;后者则是与具体HandleScope无关的持久的Handle,它只有在执行Dispose()函数之后才会消失。
内容引用自V8世界探险 (1) - v8 API概览
在V8中,内存分配都是在V8的Heap中进行分配的,JavaScript的值和对象也都存放在V8的Heap中。这个Heap由V8独立的去维护,失去引用的对象将会被V8的GC掉并可以重新分配给其他对象。而Handle即是对Heap中对象的引用。V8为了对内存分配进行管理,GC需要对V8中的所有对象进行跟踪,而对象都是用Handle方式引用的,所以GC需要对Handle进行管理,这样GC就能知道Heap中一个对象的引用情况,当一个对象的Handle引用为发生改变的时候,GC即可对该对象进行回收(gc)或者移动。因此,V8编程中必须使用Handle去引用一个对象,而不是直接通过C++的方式去获取对象的引用,直接通过C++的方式去直接去引用一个对象,会使得该对象无法被V8管理。
Handle分为Local和Persistent两种。从字面上就能知道,Local是局部的,它同时被HandleScope进行管理。persistent,类似与全局的,不受HandleScope的管理,其作用域可以延伸到不同的函数,而Local是局部的,作用域比较小。Persistent Handle对象需要Persistent::New, Persistent::Dispose配对使用,类似于C++中new和delete.Persistent::MakeWeak可以用来弱化一个Persistent Handle,如果一个对象的唯一引用Handle是一个Persistent,则可以使用MakeWeak方法来如果该引用,该方法可以出发GC对被引用对象的回收。
内容引用自Mingz技术博客
参考博客
node/V8的异步实现依赖于libuv,借助于libuv可以实现多线程编程。(我在初次使用node时直接使用了Windows的create_thread函数,当程序执行到这个函数时node运行时会发生崩溃,不过考虑到当时我是初次使用V8,所以问题应该是因为我太辣鸡)
实现异步功能主要是使用libuv中的函数uv_queue_work
(Doc ) ,这个函数有四个参数:
/**
*
*
* @param loop 事件循环队列的引用:使用`uv_default_loop()`获取默认队列
* @param req 将传入到Worker线程中的数据(结构体)
* @param work_cb Worker线程函数的指针
* @param after_work_cb 回调函数的指针
* @return
*/
UV_EXTERN int uv_queue_work(uv_loop_t* loop,
uv_work_t* req,
uv_work_cb work_cb,
uv_after_work_cb after_work_cb);
// 1. 事件循环队列的引用:使用`uv_default_loop()`获取默认队列
// 2. 将传入到Worker线程中的数据(结构体)
// 3. Worker线程函数的指针
// 4. 回调函数的指针
实现异步组件,首先需要获取从Js中传过来的函数对象(除非不需要用户自行实现回调函数逻辑):
void async(const FunctionCallbackInfo& args) {
Isolate* isolate = args.GetIsolate();
// 判断传入参数是否为函数
// args[0]->IsFunction();
// 将参数转换为v8::Function对象
// Local::Cast(args[0])
}
获取到这个函数对象后,将其存储到uv_work_t
对象中,这样这个回调函数将会被libuv传输到after_work_cb
中以供调用。同时应注意,由于V8的gc,Local类型的变量可能在after_work_cb
中无法引用(超出Scope而被回收) ,因此在初始uv_work_t
对象中应当使用Persistent
模板初始化回调函数指针,使用Persistent类的Reset
函数绑定回调函数的引用:
typedef struct C {
uv_work_t request;
int result;
Persistent callback;
} CommonConfig;
void async(const FunctionCallbackInfo& args) {
Isolate* isolate = args.GetIsolate();
// 判断传入参数是否为函数
// args[0]->IsFunction();
// 将结构体指针保存到uv_work_t中,它将会被传入到worker/callback中
commonConfig->request.data = commonConfig;
commonConfig.callback.Reset(isolate, Local::Cast(args[0]));
// 将参数转换为v8::Function对象
// Local::Cast(args[0])
}
从worker函数中获取commonConfig,执行计算过程并将运算结果保存:
void ThreadFun(uv_work_t * req) {
CommonConfig * param = (CommonConfig *)(req->data);
int r=0;
// Do something
param->result = r;
}
从callback中处理结果并执行Js传入的回调函数,注意应将运算结果转换为js的适当类型:
void callBack(uv_work_t * req, int status) {
CommonConfig * params = static_cast(req->data);
int argc=1;
Isolate* isolate = Isolate::GetCurrent();
HandleScope scope(isolate);
Local argv[argc] = { Number::New(isolate, params->result) };
Local::New(isolate, params->callback)->Call(isolate->GetCurrentContext()->Global(), argc, argv);
params->callback.Reset();
delete params;
}
electron中支持调用C++开发的本地模块,但是使用时会遇到几个问题。
node-pre-gyp rebuild --target=1.6.2 --arch=x64 --target_arch=x64 --dist-url=https://atom.io/download/electron
其中 --target=1.6.2用来指定electron的版本,–arch用两种选择ia32或者x64,–dist-url指定下载头。
引用自简书
这个问题并不见的一定在Electron里出现,但是如果在项目中使用了webpack(一般都用吧?),那么使用webpack打包时就会遇到node文件的依赖路径的问题。这里有两个问题:
1. webpack应当能识别.node文件并进行处理
2. webpack应当能将.node文件复制到输出后的目录,并设置源码中的require/import路径
这里使用native-ext-loader
好吧,这个loader怎么用我给忘了日后再查咯 ㄟ( ▔, ▔ )ㄏ
将可执行二进制添加到asar包中可能会[出错], (https://github.com/electron/electron/blob/master/docs/tutorial/application-packaging.md#executing-binaries-inside-asar-archive), 参考这篇文章的" 项目构建/问题(2)" 部分. 因此需要将二进制的.node文件从asar中提取出来. 然而因此又引入了另一个问题: 解包后的app.asar
变成了目录app.asar.unpack
, 因此还需要配置loader来从新的目录里加载.node文件.
参考博文中使用了node-loader
+ electron-packager
来打包Electron, 我这里选用了native-ext-loader
和electron-builder
来打包Electron, 思路是一样的, 只是配置上稍有不同: