相关网址
NodeJS地址:https://nodejs.org/en/download/
Electron版本:https://electronjs.org/releases/stable
Edge.js源码: https://github.com/agracio/edge-js
electron-edge-js:https://github.com/agracio/electron-edge-js
node-ffi源码:https://github.com/node-ffi/node-ffi
node-ffi兼容node12版:https://github.com/lxe/node-ffi/tree/node-12
node-ffi-napi源码:https://github.com/node-ffi-napi/node-ffi-napi
electron-rebuild:https://github.com/electron/electron-rebuild
node-win32-api:https://github.com/waitingsong/node-win32-api
winmm.dll函数:https://baike.baidu.com/item/winmm.dll/10962979?fr=aladdin
版本对比表
查看Node的版本对应关系: https://nodejs.org/zh-cn/download/releases/
Electron版本 | NODE_MODULE_VERSION | Node版本 |
---|---|---|
v3.1.13 | 64 | v10.2.0 |
v4.2.11 | 69 | v10.11.0 |
v5.0.11 | 70 | v12.0.0 |
v6.0.12 | 73 | v12.4.0 |
- Electron中的Node的NODE_MODULE_VERSION版本和官方给出的对应关系不太一样
- Electron 4和5的语法变动较大
- Electron 2已停止维护
也就是说node-ffi官方版本支持的Electron版本已停止维护
为什么使用DLL
- 需要使用系统 API 操作或扩展应用程序;
- 需要调用第三方的接口API,特别是与硬件设备进行通信,而这些接口 API 基本上都是通过 C++ 动态链接库(DLL)实现的;
- 需要调用C++实现的一些复杂算法等。
Edge.js
开源项目 edge 可以帮助我们实现 Node 和 .NET 之间的相互调用
我们最常见就是使用它来调用C#的代码方法或者C#生成的DLL文件的方法
C/C++生成的DLL就要用node-ffi 因为我是要调用系统的DLL所以主要使用node-ffi
node-ffi简介
node-ffi是一个用于使用纯JavaScript加载和调用动态库的Node.js插件。它可以用来在不编写任何C ++代码的情况下创建与本地DLL库的绑定。同时它负责处理跨JavaScript和C的类型转换。
与Node.js Addons
相比,此方法有如下优点:
- 不需要源代码。
- 不需要每次重编译
node
,Node.js Addons
引用的.node
会有文件锁,会对`electron应用热更新造成麻烦。 - 不要求开发者编写C代码,但是仍要求开发者具有一定C的知识。
缺点是:
- 性能有折损
- 类似其他语言的FFI调试,此方法近似黑盒调用, 查错比较困难。
node-ffi安装
不要使用Electron v6.0.x
不要使用Electron v6.0.x
不要使用Electron v6.0.x
重要事情说三遍不要用Electron v6.0.x 不能成功编译node-ffi
第三方修改版也不支持
系统的Node版本最好用v10.16.3 不要用最新的Node版本 各种问题坑死人
node-ffi
通过Buffer
类,在C代码和JS代码之间实现了内存共享,
类型转换则是通过ref、ref-array、ref-struct实现。
设置镜像地址
npm镜像
更新npm的包镜像源
1 | npm config set registry https://registry.npm.taobao.org |
还原默认
1 | npm config set registry https://registry.npmjs.org |
electron镜像
查看配置文件的位置
1 | npm config list |
可以查看到本机的userconfig
在哪,即.npmrc
文件在哪
比如我的
userconfig C:\Users\Jian.npmrc
打开该文件 添加
1 | registry=https://registry.npm.taobao.org |
rebuild镜像
提前写在这里 具体看下文
1 | "scripts": { |
配置编译环境
由于node-ffi
/ref
包含C原生代码,所以安装需要配置Node原生插件编译环境。
配置Node原生插件编译环境
1 | # 管理员运行bash/cmd/powershell,否则会提示权限不足 |
上面的操作会自动把Python和C++开发工具包都集成进去
手动下载C++编译环境
Visual Studio Build Tools (using “Visual C++ build tools” workload)
或者
Visual Studio 2017 Community (using the “Desktop development with C++” workload)
如果没有Python则下载Python2.x版本,不支持Python3。传送门
设置python路径(根据自己的实际情况设置)
1 | npm config set python C:\Users\Jian\.windows-build-tools\python27\python.exe |
查看npm全局安装目录
1 | npm root -g |
安装依赖/重新编译
根据需要安装对应的库
1 | npm install ffi --save |
如果是electron
项目,则项目可以安装electron-rebuild插件,能够方便遍历node-modules
中所有需要rebuild
的库进行重编译。
1 | npm install electron-rebuild --save |
在package.json中配置快捷运行方式
推荐 下面的方法能使用淘宝的镜像,防止构建时下载依赖失败
1 | "scripts": { |
-v 为Electron的版本号
之后执行
1 | npm run rebuild |
操作即可完成electron
的重编译。
需要
electron-rebuild
重新build的模块必须在dependencies
中,不能在devDependencies
中。
因为electron-rebuild
只会rebuilddependencies
中依赖。
Node>=10编译失败
Electron内置的Node版本10或10以上编译ffi会失败
两种解决方法:
使用新的ffi-napi(api是一样的,同时支持node.js新的napi)
1
npm install ffi-napi --save
引用
1
const ffi = require('ffi-napi');
使用第三方修改过的ffi
注意这种方法只支持到Electron v5.0.11
Electron v6.x也不成功推荐用这种方法 因为上面那种用的人还不够多 遇见坑的话不好解决
在package.json中
原
1
2
3
4"ffi": "^2.3.0",
"ref": "^1.3.5",
"ref-array": "^1.2.0",
"ref-struct": "^1.1.0"
改为
1 | "ffi": "github:lxe/node-ffi#node-12", |
安装依赖
1 | npm install |
Rebuild编译失败
问题一 rebuild不生效
解决方式就是不要使用cnpm 使用npm安装依赖
cnpm安装的依赖的文件夹是软连接 在重新构建时是不会生效的
问题二
App threw an error during load
Error: Could not locate the bindings file.
运行
1 | npm rebuild |
问题三
node.lib : fatal error LNK1106
尝试删除下面文件夹(根据实际情况)下对应Electron版本的文件夹 重新rebuild
1 | C:\Users\Jian\.electron-gyp |
问题四
gyp ERR! configure error
gyp ERR! stack Error: 403 status code downloading arm64 node.lib
删除上面的文件夹下对应Electron版本的文件夹
删除项目下的node_modules
用npm安装依赖
删除npm缓存
1 | npm cache clean -f |
使用阿里源
1 | "scripts": { |
重新安装
1 | npm install |
node-ffi语法详解
变量类型
C语言中有4种基础数据类型—-整型 浮点型 指针 聚合类型
基础
整型、字符型都有分有符号和无符号两种。
类型 | 最小范围 |
---|---|
char | 0 ~ 127 |
signed char | -127 ~ 127 |
unsigned char | 0 ~ 256 |
在不声明unsigned时 默认为signed型
ref
中unsigned
会缩写成u
, 如 uchar
对应 usigned char
。
浮点型中有 float
double
long
double
。
ref
库中已经帮我们准备好了基础类型的对应关系。
C++类型 | ref对应类型 |
---|---|
void | ref.types.void |
int8 | ref.types.int8 |
uint8 | ref.types.uint8 |
int16 | ref.types.int16 |
uint16 | ref.types.uint16 |
float | ref.types.float |
double | ref.types.double |
bool | ref.types.bool |
char | ref.types.char |
uchar | ref.types.uchar |
short | ref.types.short |
ushort | ref.types.ushort |
int | ref.types.int |
uint | ref.types.uint |
long | ref.types.long |
ulong | ref.types.ulong |
DWORD | ref.types.ulong |
DWORD为
winapi
类型,下文会详细说明
更多拓展可以去ref doc
ffi.Library
中,既可以通过ref.types.xxx的方式申明类型,也可以通过文本(如uint16
)进行申明。
字符型
字符型由char
构成,在GBK
编码中一个汉字占2个字节,在UTF-8中占用3~4个字节。一个ref.types.char
默认一字节。根据所需字符长度创建足够长的内存空间。这时候需要使用ref-array
库。
1 | const ref = require('ref') |
在传递中文字符型时,必须预先得知DLL
库的编码方式。node默认使用UTF-8编码。若DLL不为UTF-8编码则需要转码,推荐使用iconv-lite
1 | npm install iconv-lite --save |
转码
1 | const iconv = require('iconv-lite') |
注意!使用encode转码后cstr
为Buffer
类,可直接作为当作uchar
类型
iconv.encode(str.’gbk’)中gbk默认使用的是
unsigned char | 0 ~ 256
储存。假如C代码需要的是signed char | -127 ~ 127
,则需要将buffer中的数据使用int8类型转换。
1 | const Cstring100 = refArray(ref.types.char, 100) |
C代码为字符数组
char[]
/char *
设置的返回值,通常返回的文本并不是定长,不会完全使用预分配的空间,末尾则会是无用的值。如果是预初始化的值,一般末尾是一大串的0x00
,需要手动做trimEnd
,如果不是预初始化的值,则末尾不定值,需要C代码明确返回字符串数组的长度returnValueLength
。
内置简写
ffi中内置了一些简写
1 | ref.types.int => 'int' |
只建议使用’string’。
字符串虽然在js中被认为是基本类型,但在C语言中是以对象的形式来表示的,所以被认为是引用类型。所以string其实是char* 而不是char
聚合类型
多维数组
遇到定义为多维数组的基本类型 则需要使用ref-array进行创建
C
1 | char cName[50][100] // 创建一个cName变量储存级50个最大长度为100的名字 |
JS
1 | const ref = require('ref') |
结构体
结构体是C中常用的类型,需要用到ref-struct
进行创建
C
1 | typedef struct { |
JS
1 | const ref = require('ref') |
指针
指针是一个变量,其值为实际变量的地址,即内存位置的直接地址,有些类似于JS中的引用对象。
C语言中使用*
来代表指针
例如 int* a 则就是 整数型a变量的指针 , &
用于表示取地址
1 | int a=10, |
node-ffi
实现指针的原理是借助ref
,使用Buffer
类在C代码和JS代码之间实现了内存共享,让Buffer
成为了C语言当中的指针。
注意,一旦引用ref
,会修改Buffer
的prototype
,替换和注入一些方法,请参考文档ref文档
1 | const buf = new Buffer(4) // 初始化一个无类型的指针 |
要明确一下两个概念 一个是结构类型,一个是指针类型,通过代码来说明。
1 | // 申明一个类的实例 |
可以通过ref.alloc(Object|String type, ? value) → Buffer
直接得到一个引用对象
1 | const iAgePointer = ref.alloc(ref.types.int, 18) // 初始化一个指向`int`类的指针,值为18 |
回调函数
C的回调函数一般是用作入参传入。
1 | const ref = require('ref') |
注意!如果你的CallBack是在setTimeout中调用,可能存在被GC的BUG
1 | process.on('exit', () => { |
代码实例
举个完整引用例子
C
1 | // 头文件 |
JS
1 |
|
常见错误
- Dynamic Linking Error: Win32 error 126
这个错误有三种原因
- 通常是传入的DLL路径错误,找不到Dll文件,推荐使用绝对路径。
- 如果是在x64的
node
/electron
下引用32位的DLL,也会报这个错,反之亦然。要确保DLL要求的CPU架构和你的运行环境相同。 - DLL还有引用其他DLL文件,但是找不到引用的DLL文件,可能是VC依赖库或者多个DLL之间存在依赖关系。
- Dynamic Linking Error: Win32 error 127:DLL中没有找到对应名称的函数,需要检查头文件定义的函数名是否与DLL调用时写的函数名是否相同。
Path设置
如果你的DLL是多个而且存在相互调用问题,会出现Dynamic Linking Error: Win32 error 126
错误3。这是由于默认的进程Path
是二进制文件所在目录,即node.exe/electron.exe
目录而不是DLL所在目录,导致找不到DLL同目录下的其他引用。可以通过如下方法解决:
1 | //方法一, 调用winapi SetDllDirectoryA设置目录 |
闪崩问题
实际node-ffi
调试的时候,很容易出现内存错误闪崩,甚至会出现断点导致崩溃的情况。这个是往往是因为非法内存访问造成,可以通过Windows
日志看到错误信息,但是相信我,那并没有什么用。C的内存差错是不是一件简单的事情。
GetLastError
简单说node-ffi
通过winapi来调用DLL,这导致GetLastError
永远返回0。最简单方法就是自己写个C++ addon
来绕开这个问题。
参考Issue GetLastError() always 0 when using Win32 API 参考PR github.com/node-ffi/no…
PVOID返回空
PVOID返回空,即内存地址FFFFFFFF
闪崩
winapi中,经常通过判断返回的pvoid
指针是否存在来判断是否成功,但是在node-ffi
中,对FFFFFFFF
的内存地址deref()
会造成程序闪崩。必须迂回采用指针的指针类型进行特判
1 | HDEVNOTIFY |
JS
1 | const apiDef = SetupDiGetClassDevsW: [ |
简单范例
DLL源码
1 | extern "C" int __declspec(dllexport)My_Test(char *a, int b, int c); |
调用DLL
1 | import ffi from 'ffi' |
实例-禁用右键菜单
现在使用 ffi 调用 user32.dll
中的 GetSystemMenu
函数来解决这个问题,首先新建一个 user32.js
文件,为了展示 ffi
,我多定义了几个API函数:
1 | //const ffi = require('ffi'); |
修改 main.js
文件,首先导入 user32.js
:
1 | const user32 = require('./app/scripts/user32').User32 |
然后修改如下内容:
1 | mainWindow.once('ready-to-show', () => { |
再运行项目,系统菜单就消失的无影无踪了。
注意窗口初始化时设置
show: false
否则ready-to-show
不会调用
示例-获取窗口
1 | const ffi = require('ffi'); |
附录
DLL分析工具
Dependency Walker
可以查看DLL链接库的所有信息、以及DLL依赖关系的工具,但是很遗憾不支持WIN10
。如果你不是WIN10
用户,那么你只需要这一个工具即可,下面工具可以跳过。
Viewdll
只能查看DLL中的函数 支持WIN10
链接:https://pan.baidu.com/s/19emkiUdbCdaPRKrY9ahyWw
提取码:i32n
Process Monitor
可以查看进程执行时候的各种操作,如IO、注册表访问等。这里用它来监听node
/electron
进程的IO操作,用于排查Dynamic Linking Error: Win32 error
错误原因3,可以查看ffi.Libary
时的所有IO请求和对应结果,查看缺少了什么DLL
。
dumpbin
dumpbin.exe为Microsoft COFF二进制文件转换器,它显示有关通用对象文件格式(COFF)二进制文件的信息。可用使用dumpbin检查COFF对象文件、标准COFF对象库、可执行文件和动态链接库等。
以管理员身份运行适用于 VS 2017 的x86_x64 兼容工具命令提示 输入下面命令
1 | # 返回DLL头部信息,会说明是32 bit word Machine/64 bit word Machine |
自动转换工具
tjfontaine大神提供了一个node-ffi-generate,可以根据头文件,自动生成node-ffi
函数申明,注意这个需要Linux
环境,简单用KOA包了一层改成了在线模式ffi-online,可以丢到VPS中运行。
WINAPI
winapi存在大量的自定义的变量类型,waitingsong大侠的轮子 node-win32-api中完整翻译了全套windef.h
中的类型,而且这个项目采用TS来规定FFI的返回Interface,很值得借鉴。
使用win32-api(放弃)
安装
1 | npm install win32-api --save |
即
1 | "win32-api": "^6.2.0", |
安装
1 | npm install |
这个库用的ffi也是不支持node v12 修改源码用支持node12的ffi也是rebuild不成功(原因是构建时依赖无法下载,哎国内这墙啊) 放弃使用该库了