Electron-利用DLL实现不可能

8,307 阅读6分钟

为什么

如果我们的应用想要实现这样一个需求:监听电脑的usb接口,当有新的设备(移动硬盘或者U盘)接入电脑时,能够获取里面的移动设备的情况并更新到应用程序的界面上。

按照 Electron 或者 Node.js 现成的接口,我们无法直接实现。

这时候,我们就可以根据我们自己的情况,对系统的底层接口封装成独立的动态链接库(.dll),然后把动态链接库暴露给Electron 或者 Node.js 进行调用,从而实现需求。

简介

Node.js 可以通过对接 .dll,调用系统底层接口从而实现原有接口不提供的功能。同理,Electron 是基于Node.jss 进行封装的,Electron 也就拥有了对接动态链接库,能够把不可能变为可能。

方案选择

截至本文完成时间(2018-12-17)

现有的主流的方案一共有两项

  1. 自定义C++ Addons

  2. 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 调用方案的关系图如下所示:

5c1758d230410

解释如下:

  • 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