FFI 全称 Foreign Function Interface .
主要解决在 Node.js 里用 JS 调用 C/C++ 写的动态库的问题.
https://www.npmjs.com/package/ffi-napi

  1. 在安装 ffi 之前,请先安装好 node-gyp 相关的依赖,具体请看官方安装说明 https://github.com/nodejs/node-gyp .
  2. Node.js 12 及以上的版本,请安装 ffi-napi 包,而不是 ffi 包. 原理请看 N-API 介绍: https://xcoder.in/2017/07/01/nodejs-addon-history/
  3. 请安装 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 示例即完成了,如需调用其它函数,只需要找到相关的头文件和文档说明按照上代码的方式编写即可.