使用 node-ffi 构建 Electron 和 C++ Library 混合桌面应用

#electron #Node.js

使用 node-ffi 可以让 Node.js 调用 C++ 的 Library 。在 Windows 下是 dll ,在 Mac OS 下是 dylib ,Linux 则是 so 。node-ffi 加载 Library 是有限制的,只能处理 C 风格的 Library 。也就是函数要被放在 extern "C" 里。

安装 node-ffi 对于不同操作系统,会有不同的环境要求。具体可以参看:https://github.com/nodejs/node-gyp#installation

for electron 编译

而对于 electron ,需要对 node-ffi 重新编译。我们安装 electron-rebuild 和 electron-prebuilt 进行编译。

npm i --save-dev electron-rebuild
npm i -g electron-prebuilt

设定环境变量:

# Electron 的版本
export npm_config_target=1.8.4
# 要构建的 electron 类型.
export npm_config_arch=x64
export npm_config_target_arch=x64
# Electron 下载地址头,可以用镜像.
export npm_config_disturl=https://atom.io/download/electron
# 告诉 node-pre-gyp 是为 Electron 编译.
export npm_config_runtime=electron
# 告诉 node-pre-gyp 从源码编译.
export npm_config_build_from_source=true
# 下载的缓存路径.
HOME=~/.electron-gyp npm install

然后 rebuild:

./node_modules/.bin/electron-rebuild -e /usr/local/lib/node_modules/electron-prebuilt

-e 是本地 electron-prebuilt 的绝对路径。

载入 Library

假设有 Library 函数如下:

int add(int n1, int n2);
int div(int d1, int d2, int* r);

那么 node-ffi 应该这样调用:

var ref = require("ref");
var ffi = require("ffi");

var intPtr = ref.refType(ref.types.int); // 创建一个 int 指针类型

var lib = ffi.Library('mylib', {
  "add": [ 'int', [ 'int', 'int' ] ],
  "div": [ 'int', [ 'int', 'int', intPtr ] ]
});

let sum = lib.add(1, 2);
console.log(`1 + 2 = ${sum}`);
let remainder = ref.alloc(ref.types.int, 0);
let quotient = lib.div(10, 3, remainder);
console.log(`10 ÷ 3 = ${quotient} ...... ${remainder.deref()}`);

关于 ffi 的其他类型,可以参考:https://github.com/ffi/ffi/wiki/Types

数组参数的调用

对于参数有包含数组的函数,如:

int analysis(int number, int factor[]);

我们则需要使用到 ref-array 来创建一个数组类型加载函数:

var ffi = require("ffi");
var ArrayType = require('ref-array');

var IntArray = ArrayType(ref.types.int);
var lib = ffi.Library('mylib', {
  "analysis": [ 'int', [ 'int', IntArray ] ]
});

let data = [];
data.length = 100;

var factors = new IntArray(data);
lib.analysis(32, factors);

console.log(`The factors of 32 are ${factors.join(',')}`);

回调函数的使用

有些 C++ Library 会包含有回调函数作为参数的调用。比如:

typedef void (*ioCallback) (int uVID, int uPID);
int device_listen(ioCallback MatchingCallback, ioCallback RemovalCallback);

node-ffi 对此有专门的用于生成回调函数参数的方法 Callback,示例:

var ffi = require("ffi");
var lib = ffi.Library('mylib', {
  'device_listen': ['int', ['pointer', 'pointer']],
});

let matchCallback = ffi.Callback('void', ['int', 'int'], (vid, pid) => {
    console.log('match device, VID: ', vid, ', PID: ', pid)
});

let removeCallback = ffi.Callback('void', ['int', 'int'], (vid, pid) => {
    console.log('remove device, VID: ', vid, ', PID: ', pid)
});

lib.device_listen(matchCallback, removeCallback);

这个地方有个坑,如果你回调函数是用于持续监听,在程序运行过程中随时可能被调用的话(比如监听设备插入拔出),可能会在程序启动一段时间后,执行回调时引起程序崩溃退出。

这是因为一段时间后,回调函数被垃圾回收了。这里可以在程序最后添加:

process.on('exit', function() {
    matchCallback;
    removeCallback;
});

这样在程序退出前都会保持引用,就不会被垃圾回收了。

参考资料:

https://github.com/electron/electron/blob/master/docs/tutorial/using-native-node-modules.md
https://github.com/node-ffi/node-ffi/wiki/Node-FFI-Tutorial
https://gist.github.com/ryosuzuki/186958bf1abb0492f626
https://github.com/node-ffi/node-ffi/issues/84