nodejs 本地化模块

nodejs 本地化模块

由于nodejs脱离了浏览器的限制、更接近系统,所以难免会遇到调用本地模块(即js调用C/C++)的情况。使用nodejs调用本地模块有多种方案可以实现,例如:

  1. IPC

    zeromq、web服务器、WebSocket 等。这种方案的好处是本地化模块和node运行时彼此分离,相互之间影响较小。

  2. node-ffi

    node-ffi是一个用于使用纯JavaScript加载和调用动态库的Node.js插件。它可以用来在不编写任何C ++代码的情况下创建与本地DLL库的绑定。同时它负责处理跨JavaScript和C的类型转换。

  3. node-gyp

本文内容即为记录node-gyp入门使用

推荐阅读此博客


node-gyp Readme

1. 环境搭建

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章节

2. 使用

  1. 构建项目目录

    mkdir my_node_addon
    cd my_node_addon
    
  2. 建立编译配置模板文件(binding.gyp)

  3. 生成编译配置文件

    node-gyp configure
    
  4. 编译

    node-gyp build
    

具体使用过程可查看官方的Readme#How to Use章节

3. 开发

node-gyp涉及到nodejs的底层,包括node运行时、V8引擎、libuv,使用C++开发本地模块有点类似于绕过V8引擎编译Javascript环节,直接实现V8虚拟机执行时的机器码。关于node/V8/libuv,可以阅读一下此系列文章

1. HelloWorld

使用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 exports) {
	NODE_SET_METHOD(exports, "hi", hi);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
 
  

而当我们使用这个模块时,使用方法是一样的:

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()方法设置函数返回值。

2. Js/C++ 变量类型对应

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 obj = v8::Object::New(isolate);
// 对象的赋值
obj->Set(v8::String::NewFromUtf8(isolate, "arg1"), str);
obj->Set(v8::String::NewFromUtf8(isolate, "arg2"), retval);
// Function 类型的声明并赋值
Local tpl = v8::FunctionTemplate::New(isolate, MyFunction);
Local fn = tpl->GetFunction();
// 函数名字
fn->SetName(String::NewFromUtf8(isolate, "theFunction"));
obj->Set(v8::String::NewFromUtf8(isolate, "arg3"), fn);
// Boolean 类型的声明
Local flag = Boolean::New(isolate, true);
obj->Set(String::NewFromUtf8(isolate, "arg4"), flag);
// Array 类型的声明
Local arr = Array::New(isolate);
// Array 赋值
arr->Set(0, Number::New(isolate, 1));
arr->Set(1, Number::New(isolate, 10));
arr->Set(2, Number::New(isolate, 100));
arr->Set(3, Number::New(isolate, 1000));
obj->Set(String::NewFromUtf8(isolate, "arg5"), arr);
// Undefined 类型的声明
Local und = Undefined(isolate);
obj->Set(String::NewFromUtf8(isolate, "arg6"), und);
// null 类型的声明
Local null = Null(isolate);
obj->Set(String::NewFromUtf8(isolate, "arg7"), null);

 
  

3. 堆/栈

在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技术博客

4. 异步

参考博客

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;
}

5. Electron

electron中支持调用C++开发的本地模块,但是使用时会遇到几个问题。

  1. 由于electron和nodejs使用了不同的V8引擎,因此在编译时应当指定electron提供的头文件,使用以下命令:
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指定下载头。

引用自简书

  1. loader

这个问题并不见的一定在Electron里出现,但是如果在项目中使用了webpack(一般都用吧?),那么使用webpack打包时就会遇到node文件的依赖路径的问题。这里有两个问题:

  1. webpack应当能识别.node文件并进行处理
  2. webpack应当能将.node文件复制到输出后的目录,并设置源码中的require/import路径

这里使用native-ext-loader

好吧,这个loader怎么用我给忘了日后再查咯 ㄟ( ▔, ▔ )ㄏ

  1. asar

将可执行二进制添加到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-loaderelectron-builder来打包Electron, 思路是一样的, 只是配置上稍有不同:

你可能感兴趣的:(学习笔记,经验分享)