Node.js 使用 FFI 调用 win32 API
FFI 全称 Foreign Function Interface .
主要解决在 Node.js 里用 JS 调用 C/C++ 写的动态库的问题.
https://www.npmjs.com/package/ffi-napi
- 在安装 ffi 之前,请先安装好 node-gyp 相关的依赖,具体请看官方安装说明 https://github.com/nodejs/node-gyp .
- Node.js 12 及以上的版本,请安装 ffi-napi 包,而不是 ffi 包. 原理请看 N-API 介绍: https://xcoder.in/2017/07/01/nodejs-addon-history/
- 请安装 ref-napi 包,而不是 ref 包,参数是指针类型时需要. 其它像 struct, union, array 请找对应的 napi 的包装. https://github.com/node-ffi-napi
先看 FFI官方给的示例:
var ffi = require('ffi-napi');
var libm = ffi.Library('libm', {
'ceil': [ 'double', [ 'double' ] ]
});
libm.ceil(1.5); // 2
示例是调用 C 的 math 相关的库的 ceil 函数. 在 Mac 或 Linux 下,我们可以通过 man ceil
看函数的 C 签名.
NAME
ceil -- round to smallest integral value not less than x
SYNOPSIS
#include <math.h>
double
ceil(double x);
long double
ceill(long double x);
float
ceilf(float x);
DESCRIPTION
The ceil() functions return the smallest integral value greater than or
equal to x.
可以看到 ceil 函数返回一个 double 值,且需要一个 double 值.
https://github.com/node-ffi/node-ffi/wiki/Node-FFI-Tutorial 手册描述 ffi.Library
的函数签名如下:
ffi.Library(libraryFile, { functionSymbol: [ returnType, [ arg1Type, arg2Type, ... ], ... ]);
所以示例代码调用 libm 的 ceil 意思就很明白.
之所以需要 ref 包,是因为在调用指针等 JS 里没有的类型时,需要用它来构建参数的值.
拿 FFI 手册 里的代码来举例
sqlite3_open
等函数的签名请看 https://www.sqlite.org/capi3ref.html#sqlite3_open
var ref = require('ref');
var ffi = require('ffi');
// typedef
var sqlite3 = ref.types.void; // we don't know what the layout of "sqlite3" looks like
var sqlite3Ptr = ref.refType(sqlite3);
var sqlite3PtrPtr = ref.refType(sqlite3Ptr);
var stringPtr = ref.refType(ref.types.CString);
// binding to a few "libsqlite3" functions...
var libsqlite3 = ffi.Library('libsqlite3', {
'sqlite3_open': [ 'int', [ 'string', sqlite3PtrPtr ] ],
'sqlite3_close': [ 'int', [ sqlite3Ptr ] ],
'sqlite3_exec': [ 'int', [ sqlite3Ptr, 'string', 'pointer', 'pointer', stringPtr ] ],
'sqlite3_changes': [ 'int', [ sqlite3Ptr ]]
});
// now use them:
var dbPtrPtr = ref.alloc(sqlite3PtrPtr);
libsqlite3.sqlite3_open("test.sqlite3", dbPtrPtr);
var dbHandle = dbPtrPtr.deref();
函数签名我们可以用 ref.types 来构建,也可以直接写成 string 字符,对于复杂的指针类型,我们其实可以直接用 'pointer'
就行了.但是使用 ref.alloc 创建指针等复杂参数值时,就必须得 ref.types 方式来构建了.
ref.alloc
函数返回的是指针,如果是需要传值,需要调用再调用 deref
方法. 从手册里的下文示例代码可以看出 alloc
返回的是指针类型.
var intPtr = ref.refType('int');
var libmylibrary = ffi.Library('libmylibrary', { ...,
'manipulate_number': [ 'void', [ intPtr ] ]
});
var outNumber = ref.alloc('int'); // allocate a 4-byte (32-bit) chunk for the output data
libmylibrary.manipulate_number(outNumber);
var actualNumber = outNumber.deref();
异步调用函数示例代码如下:
https://github.com/node-ffi/node-ffi/wiki/Node-FFI-Tutorial#async-library-calls
var libmylibrary = ffi.Library('libmylibrary', {
'mycall': [ 'int', [ 'int' ] ]
});
libmylibrary.mycall.async(1234, function (err, res) {});
开始主题,调用 win32 api . 以 SystemParametersInfo 函数为例.
首先找到 SystemParametersInfo 的函数原型:
https://github.com/tpn/winsdk-10/blob/master/Include/10.0.10240.0/um/WinUser.h
在上文件搜索,没有 SystemParametersInfo 函数,只有 SystemParametersInfo 常量,根据是否为 UNICODE 模式,决定是调用 SystemParametersInfoA(ANSI) 还是 SystemParametersInfoW (WideChar) .因为我们在 Node.js 里调用没有引入头文件编译,所以我们任意选择一个即可,本文选择用 SystemParametersInfoA 来做示例.
SystemParametersInfoA 函数的文档请看: https://docs.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-systemparametersinfoa
BOOL SystemParametersInfoA(
UINT uiAction,
UINT uiParam,
PVOID pvParam,
UINT fWinIni
);
1,2,4 参数为数字类型, 3 参数为指针.
示例是调用此方法检测系统是否进入屏保状态, 我们定位到 SPI_GETSCREENSAVERRUNNING 关键的那一行,告诉我们参数3需要为一个 bool 的指针,如果检测屏保已经启动,会把指针的值设为 true .
Determines whether a screen saver is currently running on the window station of the calling process. The pvParam parameter must point to a BOOL variable that receives TRUE if a screen saver is currently running, or FALSE otherwise. Note that only the interactive window station, WinSta0, can have a screen saver running.
uiParam,fWinIni 看文档的说明,在调用 SPI_GETSCREENSAVERRUNNING 时,我们传 0 即可.
所以最终的 js 调用代码如下.
const ffi = require('ffi-napi')
const ref = require('ref-napi')
const user32 = ffi.Library('user32.dll', {
SystemParametersInfoA: ['bool', ['uint', 'uint', 'pointer', 'uint']]
})
// 0x0072 SPI_GETSCREENSAVERRUNNING, 从头文件或文档里查,因不是 C++ 项目,所以只能以直接写值,否则可以写 SPI_GETSCREENSAVERRUNNING ,然后 C++ 的预处理器会帮忙替换.
let checkResult = user32.SystemParametersInfoA(0x0072, 0, isEnable, 0)
console.log(`checkResult ${checkResult} isEnable ${isEnable.deref()}`)
使用 ffi 调用 win32 api 示例即完成了,如需调用其它函数,只需要找到相关的头文件和文档说明按照上代码的方式编写即可.