为什么
如果我们的应用想要实现这样一个需求:监听电脑的usb接口,当有新的设备(移动硬盘或者U盘)接入电脑时,能够获取里面的移动设备的情况并更新到应用程序的界面上。
按照 Electron 或者 Node.js 现成的接口,我们无法直接实现。
这时候,我们就可以根据我们自己的情况,对系统的底层接口封装成独立的动态链接库(.dll),然后把动态链接库暴露给Electron 或者 Node.js 进行调用,从而实现需求。
简介
Node.js 可以通过对接 .dll,调用系统底层接口从而实现原有接口不提供的功能。同理,Electron 是基于Node.jss 进行封装的,Electron 也就拥有了对接动态链接库,能够把不可能变为可能。
方案选择
截至本文完成时间(2018-12-17)
现有的主流的方案一共有两项
-
自定义C++ Addons
-
node-ffi 对接方案
(暂不讨论 .net core 等对接方式)
原理分析
首先,明确一个观点,Electron 是基于 Node.js 封装的框架,Electron 对接 动态链接库 的底层方案,是基Node.js c++ Addons机制
从 Node.js 官网上可以了解到,Addons机制涉及多个组件和 API 的知识:
-
V8: Node.js 目前用于提供 JavaScript 实现的 C++ 库。 V8 提供了用于创建对象、调用函数等的机制。 V8 的 API 文档主要在
v8.h
头文件中(Node.js 源代码中的deps/v8/include/v8.h
),也可以在查看V8 在线文档。 -
libuv: 实现了 Node.js 的事件循环、工作线程、以及平台所有的的异步操作的 C 库。 它也是一个跨平台的抽象库,使所有主流操作系统中可以像 POSIX 一样访问常用的系统任务,比如与文件系统、socket、定时器、以及系统事件的交互。 libuv 还提供了一个类似 POSIX 多线程的线程抽象,可被用于强化更复杂的需要超越标准事件循环的异步插件。 建议插件开发者多思考如何通过在 libuv 的非阻塞系统操作、工作线程、或自定义的 libuv 线程中降低工作负载来避免在 I/O 或其他时间密集型任务中阻塞事件循环。
-
内置的 Node.js 库:Node.js 自身开放了一些插件可以使用的 C++ API。 其中最重要的是
node::ObjectWrap
类。 -
Node.js 包含一些其他的静态链接库:如 OpenSSL,这些库位于 Node.js 源代码中的
deps/
目录。 只有 V8 和 OpenSSL 符号是被 Node.js 开放的,并且通过插件被用于不同的场景。 更多信息可查看链接到 Node.js 自身的依赖。
总结来说,Node.js 可以理解为 js 和 c++ dll 相互工作的桥梁,而 Node.js 自身也提供了扩展 c++ dll 调用的插件机制。
同理,Electron 也可以从这个机制中获益。
目前,主流的 .dll 调用方案的关系图如下所示:
解释如下:
-
node-ffi 本质上是 Addons 机制下,进行过抽象封装的方案
-
addons插件需要针对对应版本的Node.js编译后,才能被对应版本的Node.js进行调用;换言之,如果addons插件在编译时的目标版本是 Node.js v8.3.1,那么它编译后的代码就不能被 Node.js v6.0.0的版本进行调用
-
node-gyp 可以帮助开发者脱离当前全局安装的Node.js版本,指定任意 Node.js 版本进行模块的编译,在编译前,需要下载对应版本的原生模块头文件,头文件的默认下载地址为 nodejs.org
-
Electron想要调用 .dll 文件,也需要进行对 addons 插件的编译,编译用的头文件也需要额外下载,和Node.js不同,Electron对应的头文件的默认下载地址为 atom.io/downloaded
-
头文件的版本必须与 调用者 ( Node.js 或者 Electron )版本一致,这样 addons插件(包括 自定义 addon 和 node-ffi)才能正确运行
执行情况:
Node.js的原生模块编译,通过 node-gyp 可以比较方便地进行编译
Electron的原生模块编译,由于头文件与 Node.js 的头文件并不一致,直接用 node-gyp 进行编译的话,还需要进行一些额外的配置(头文件下载地址、版本映射等),相对没这么方便。幸好,开源社区已经准备好了一个封装好的工具 Electron-rebuild ,它底层原理也是使用 node-gyp 进行编译,不过就不需要开发者进行额外的配置了
根据 Electron 版本的不同(主要是 v4 和 以前的版本不同),需要在应用中执行额外的代码
编译环境要提前准备,三大操作系统(Windows、MacOS、Linux)各不相同,看官需要根据 node-gyp 的文档,提前调整好自己的编译环境。参考文档(截至 2018-12-17):node-gyp.readme
案例分享
万事俱备,我们把源码准备好,按照 node-gyp的教程准备好编译环境,开始操作:
本次的案例方案为 Node-FFI,想要自定义addon的看管,可以先了解Electron addon的编写后,再进行编译 和 使用
第一步:
在项目路径下,
安装好所有依赖
npm install
复制代码
安装 node-ffi、ref、ref-array:
npm install node-ffi --save
npm install ref --save
npm install ref-array --save
复制代码
全局安装好 Electron-rebuild
npm install -g Electron-rebuild
复制代码
第二步:
假设我们的Electron版本为 v3.0.11,32位应用
在项目路径下,执行 Electron-rebuild 命令,重新编译 node-ffi、ref、ref-array 原生模块:
Electron-rebuild -v 3.0.11 -a ia32
复制代码
第三步:
如无意外,编译成功后,我们就可以通过 Electron 应用调用 ffi 和 ref 模块了
var ffi = require('ffi');
var ref = require('ref');
复制代码
第四步:
使用 ref 定义好数据类型,因为 c++ 的数据类型的内存模型不可能和 js 的是一致的,使用时,需要利用 ref 库进行转换
var intPointer = ref.refType('int');
var doublePointer = ref.refType('double');
var charPointer = ref.refType('char');
var stringPointer = ref.refType(ref.types.CString);
var boolPointer = ref.refType('bool');
复制代码
pointer,可以理解为对应 c++ 里面的指针,pointer.ref() 则是获取指针对应的数据
使用 ffi 连接 .dll 文件
var usbLib = ffi.Library(libpath, {
'InitSDK': ['int', ['pointer']],
'GetData': ['int', ['char', stringPointer]],
'ClearData': ['int', [charPointer, charPointer]],
'GetRemovableDrives': ['int', [stringPointer]]
});
复制代码
libpath 为 .dll所在的路径,相对路径与绝对路径均可,考虑到后续的安装包打包,建议为相对路径
第五步:
把连接好的dll使用起来
var data = new Buffer(1000);
var result = usbLib.GetData(driveName, data);
var resultStr = '';
if(result === 0){
resultStr = wideCharBufferToString(data);
}
return resultStr;
复制代码
如上述代码,js 调用 .dll中定义好的 GetData 方法
.dll中的C++源码如下:
int GetData(string driveName, char *data)
复制代码
函数调用结果,通过 data 参数返回,调用状态 通过 int 的数据格式返回到 js 的 result 变量中
js中的data,是一个 ref 生成的 StringPointer(实际上是通过Buffer扩展出的数据结构)
当函数调用结束,函数的结果也以指针的形式赋值给了data
接下来,把data这个指针指向的数据解析出来,即可获取函数的返回数据
问题记录
截至 2018-12-17
1、NodeJs v10.x 与 Electron v3.x 对应的原生模块头文件,都无法和 Node-FFI latest 版本完成编译(互相不兼容)
解决方法:
开源社区上已经有开发者提交了 Node-FFI 的 PR 并通过了测试
开发人员可以先安装 PR 版本的 Node-FFI ,实测可以正常编译与正常使用
npm install node-ffi/node-ffi#169773d
复制代码