node-ffi使用指南


 

nodejs/elctron中,可以通过node-ffi,通过Foreign Function Interface调用动态链接库,俗称调DLL,实现调用C/C++代码,从而实现许多node不好实现的功能,或复用诸多已实现的函数功能。

node-ffi是一个用于使用纯JavaScript加载和调用动态库的Node.js插件。它可以用来在不编写任何C ++代码的情况下创建与本地DLL库的绑定。同时它负责处理跨JavaScript和C的类型转换。

Node.js Addons相比,此方法有如下优点:

  1.   1. 不需要源代码。
  2.   2. 不需要每次重编译`node`,`Node.js Addons`引用的`.node`会有文件锁,会对`electron应用热更新造成麻烦。
  3.   3. 不要求开发者编写C代码,但是仍要求开发者具有一定C的知识。
  4.   复制代码

缺点是:

  1.   1. 性能有折损
  2.   2. 类似其他语言的FFI调试,此方法近似黑盒调用,差错比较困难。
  3.   复制代码

安装

node-ffi通过Buffer类,在C代码和JS代码之间实现了内存共享,类型转换则是通过ref、ref-array、ref-struct实现。由于node-ffi/ref包含C原生代码,所以安装需要配置Node原生插件编译环境。

  1.   // 管理员运行bash/cmd/powershell,否则会提示权限不足
  2.   npm install --global --production windows-build-tools
  3.   npm install -g node-gyp
  4.   复制代码

根据需要安装对应的库

  1.   npm install ffi
  2.   npm install ref
  3.   npm install ref-array
  4.   npm install ref-struct
  5.   复制代码

如果是electron项目,则项目可以安装electron-rebuild插件,能够方便遍历node-modules中所有需要rebuild的库进行重编译。

  1.   npm install electron-rebuild
  2.   复制代码

在package.json中配置快捷方式

  1.   package.json
  2.   "scripts": {
  3.   "rebuild": "cd ./node_modules/.bin && electron-rebuild --force --module-dir=../../"
  4.   }
  5.   复制代码

之后执行npm run rebuild 操作即可完成electron的重编译。

简单范例

  1.   extern "C" int __declspec(dllexport)My_Test(char *a, int b, int c);
  2.   extern "C" void __declspec(dllexport)My_Hello(char *a, int b, int c);
  3.   复制代码
  1.   import ffi from 'ffi'
  2.   // `ffi.Library`用于注册函数,第一个入参为DLL路径,最好为文件绝对路径
  3.   const dll = ffi.Library( './test.dll', {
  4.   // My_Test是dll中定义的函数,两者名称需要一致
  5.   // [a, [b,c....]] a是函数出参类型,[b,c]是dll函数的入参类型
  6.   My_Test: ['int', ['string', 'int', 'int']], // 可以用文本表示类型
  7.   My_Hello: [ref.types.void, ['string', ref.types.int, ref.types.int]] // 更推荐用`ref.types.xx`表示类型,方便类型检查,`char*`的特殊缩写下文会说明
  8.   })
  9.    
  10.   //同步调用
  11.   const result = dll.My_Test('hello', 3, 2)
  12.    
  13.   //异步调用
  14.   dll.My_Test.async('hello', 3, 2, (err, result) => {
  15.   if(err) {
  16.   //todo
  17.   }
  18.   return result
  19.   })
  20.   复制代码

变量类型

C语言中有4种基础数据类型----整型 浮点型 指针 聚合类型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')
  2.   const refArray = require('ref-array')
  3.    
  4.   const CharArray100 = refArray(ref.types.char, 100) // 申明char[100]类型CharArray100
  5.   const bufferValue = Buffer.from('Hello World') // Hello World转换Buffer
  6.   // 通过Buffer循环复制, 比较啰嗦
  7.   const value1 = new CharArray100()
  8.   for (let i = 0, l = bufferValue.length; i < l; i++) {
  9.   value1[i] = bufferValue[i]
  10.   }
  11.   // 使用ref.alloc初始化类型
  12.   const strArray = [...bufferValue] //需要将`Buffer`转换成`Array`
  13.   const value2 = ref.alloc(CharArray100, strArray)
  14.   复制代码

在传递中文字符型时,必须预先得知DLL库的编码方式。node默认使用UTF-8编码。若DLL不为UTF-8编码则需要转码,推荐使用iconv-lite

  1.   npm install iconv-lite
  2.   复制代码
  1.   const iconv = require('iconv-lite')
  2.   const cstr = iconv.encode(str, 'gbk')
  3.   复制代码

注意!使用encode转码后cstrBuffer类,可直接作为当作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)
  2.   const cString = new Cstring100()
  3.   const uCstr = iconv.encode('农企药丸', 'gbk')
  4.   for (let i = 0; i < uCstr.length; i++) {
  5.   cString[i] = uCstr.readInt8(i)
  6.   }
  7.   复制代码

C代码为字符数组char[]/char *设置的返回值,通常返回的文本并不是定长,不会完全使用预分配的空间,末尾则会是无用的值。如果是预初始化的值,一般末尾是一大串的0x00,需要手动做trimEnd,如果不是预初始化的值,则末尾不定值,需要C代码明确返回字符串数组的长度returnValueLength

内置简写

ffi中内置了一些简写

  1.   ref.types.int => 'int'
  2.   ref.refType('int') => 'int*'
  3.   char* => 'string'
  4.   复制代码

只建议使用'string'。

字符串虽然在js中被认为是基本类型,但在C语言中是以对象的形式来表示的,所以被认为是引用类型。所以string其实是char* 而不是char

聚合类型

多维数组

遇到定义为多维数组的基本类型 则需要使用ref-array进行创建

  1.   char cName[50][100] // 创建一个cName变量储存级50个最大长度为100的名字
  2.   复制代码
  1.   const ref = require('ref')
  2.   const refArray = require('ref-array')
  3.    
  4.   const CName = refArray(refArray(ref.types.char, 100), 50)
  5.   const cName = new CName()
  6.   复制代码

结构体

结构体是C中常用的类型,需要用到ref-struct进行创建

  1.   typedef struct {
  2.   char cTMycher[100];
  3.   int iAge[50];
  4.   char cName[50][100];
  5.   int iNo;
  6.   } Class;
  7.    
  8.   typedef struct {
  9.   Class class[4];
  10.   } Grade;
  11.   复制代码
  1.   const ref = require('ref')
  2.   const Struct = require('ref-struct')
  3.   const refArray = require('ref-array')
  4.    
  5.   const Class = Struct({ // 注意返回的`Class`是一个类型
  6.   cTMycher: RefArray(ref.types.char, 100),
  7.   iAge: RefArray(ref.types.int, 50),
  8.   cName: RefArray(RefArray(ref.types.char, 100), 50)
  9.   })
  10.   const Grade = Struct({ // 注意返回的`Grade`是一个类型
  11.   class: RefArray(Class, 4)
  12.   })
  13.   const grade3 = new Grade() // 新建实例
  14.   复制代码

指针

指针是一个变量,其值为实际变量的地址,即内存位置的直接地址,有些类似于JS中的引用对象。

C语言中使用*来代表指针

例如 int a* 则就是 整数型a变量的指针 , &用于表示取地址

  1.   int a=10,
  2.   int *p; // 定义一个指向整数型的指针`p`
  3.   p=&a // 将变量`a`的地址赋予`p`,即`p`指向`a`
  4.   复制代码

node-ffi实现指针的原理是借助ref,使用Buffer类在C代码和JS代码之间实现了内存共享,让Buffer成为了C语言当中的指针。注意,一旦引用ref,会修改Bufferprototype,替换和注入一些方法,请参考文档ref文档

  1.   const buf = new Buffer(4) // 初始化一个无类型的指针
  2.   buf.writeInt32LE(12345, 0) // 写入值12345
  3.    
  4.   console.log(buf.hexAddress()) // 获取地址hexAddress
  5.    
  6.   buf.type = ref.types.int // 设置buf对应的C类型,可以通过修改`type`来实现C的强制类型转换
  7.   console.log(buf.deref()) // deref()获取值12345
  8.    
  9.   const pointer = buf.ref() // 获取指针的指针,类型为`int **`
  10.    
  11.   console.log(pointer.deref().deref()) // deref()两次获取值12345
  12.   复制代码

要明确一下两个概念 一个是结构类型,一个是指针类型,通过代码来说明。

  1.   // 申明一个类的实例
  2.   const grade3 = new Grade() // Grade 是结构类型
  3.   // 结构类型对应的指针类型
  4.   const GradePointer = ref.refType(Grade) // 结构类型`Grade`对应的指针的类型,即指向Grade
  5.   // 获取指向grade3的指针实例
  6.   const grade3Pointer = grade3.ref()
  7.   // deref()获取指针实例对应的值
  8.   console.log(grade3 === grade3Pointer.deref()) // 在JS层并不是同一个对象
  9.   console.log(grade3['ref.buffer'].hexAddress() === grade3Pointer.deref()['ref.buffer'].hexAddress()) //但是实际上指向的是同一个内存地址,即所引用值是相同的
  10.   复制代码

可以通过ref.alloc(Object|String type, ? value) → Buffer直接得到一个引用对象

  1.   const iAgePointer = ref.alloc(ref.types.int, 18) // 初始化一个指向`int`类的指针,值为18
  2.   const grade3Pointer = ref.alloc(Grade) // 初始化一个指向`Grade`类的指针
  3.   复制代码

回调函数

C的回调函数一般是用作入参传入。

  1.   const ref = require('ref')
  2.   const ffi = require('ffi')
  3.    
  4.   const testDLL = ffi.Library('./testDLL', {
  5.   setCallback: ['int', [
  6.   ffi.Function(ref.types.void, // ffi.Function申明类型, 用`'pointer'`申明类型也可以
  7.   [ref.types.int, ref.types.CString])]]
  8.   })
  9.    
  10.    
  11.   const uiInfocallback = ffi.Callback(ref.types.void, // ffi.callback返回函数实例
  12.   [ref.types.int, ref.types.CString],
  13.   (resultCount, resultText) => {
  14.   console.log(resultCount)
  15.   console.log(resultText)
  16.   },
  17.   )
  18.    
  19.   const result = testDLL.uiInfocallback(uiInfocallback)
  20.   复制代码

注意!如果你的CallBack是在setTimeout中调用,可能存在被GC的BUG

  1.   process.on('exit', () => {
  2.   /* eslint-disable-next-line */
  3.   uiInfocallback // keep reference avoid gc
  4.   })
  5.   复制代码

代码实例

举个完整引用例子

  1.   // 头文件
  2.   #pragma once
  3.    
  4.   //#include "../include/MacroDef.h"
  5.   #define CertMaxNumber 10
  6.   typedef struct {
  7.   int length[CertMaxNumber];
  8.   char CertGroundId[CertMaxNumber][2];
  9.   char CertDate[CertMaxNumber][2048];
  10.   } CertGroud;
  11.    
  12.   #define DLL_SAMPLE_API __declspec(dllexport)
  13.    
  14.   extern "C"{
  15.    
  16.   //读取证书
  17.   DLL_SAMPLE_API int My_ReadCert(char *pwd, CertGroud *data,int *iCertNumber);
  18.   }
  19.   复制代码
  1.   const CertGroud = Struct({
  2.   certLen: RefArray(ref.types.int, 10),
  3.   certId: RefArray(RefArray(ref.types.char, 2), 10),
  4.   certData: RefArray(RefArray(ref.types.char, 2048), 10),
  5.   curCrtID: RefArray(RefArray(ref.types.char, 12), 10),
  6.   })
  7.    
  8.   const dll = ffi.Library(path.join(staticPath, '/key.dll'), {
  9.   My_ReadCert: ['int', ['string', ref.refType(CertGroud), ref.refType(ref.types.int)]],
  10.   })
  11.    
  12.   async function readCert({ ukeyPassword, certNum }) {
  13.   return new Promise(async (resolve) => {
  14.   // ukeyPassword为string类型, c中指代 char*
  15.   ukeyPassword = ukeyPassword.toString()
  16.   // 根据结构体类型 开辟一个新的内存空间
  17.   const certInfo = new CertGroud()
  18.   // 开辟一个int 4字节内存空间
  19.   const _certNum = ref.alloc(ref.types.int)
  20.   // certInfo.ref()作为certInfo的指针传入
  21.   dll.My_ucRMydCert.async(ukeyPassword, certInfo.ref(), _certNum, () => {
  22.   // 清除无效空字段
  23.   let cert = bufferTrim.trimEnd(new Buffer(certInfo.certData[certNum]))
  24.   cert = cert.toString('binary')
  25.   resolve(cert)
  26.   })
  27.   })
  28.   }
  29.   复制代码

常见错误

  • Dynamic Linking Error: Win32 error 126

这个错误有三种原因

  1. 通常是传入的DLL路径错误,找不到Dll文件,推荐使用绝对路径。
  2. 如果是在x64的node/electron下引用32位的DLL,也会报这个错,反之亦然。要确保DLL要求的CPU架构和你的运行环境相同。
  3. 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设置目录
  2.   const ffi = require('ffi')
  3.    
  4.   const kernel32 = ffi.Library("kernel32", {
  5.   'SetDllDirectoryA': ["bool", ["string"]]
  6.   })
  7.   kernel32.SetDllDirectoryA("pathToAdd")
  8.    
  9.   //方法二(推荐),设置Path环境环境
  10.   process.env.PATH = `${process.env.PATH}${path.delimiter}${pathToAdd}`
  11.   复制代码

DLL分析工具

    1. Dependency Walker

可以查看DLL链接库的所有信息、以及DLL依赖关系的工具,但是很遗憾不支持WIN10。如果你不是WIN10用户,那么你只需要这一个工具即可,下面工具可以跳过。

    1. Process Monitor

可以查看进程执行时候的各种操作,如IO、注册表访问等。这里用它来监听node/electron进程的IO操作,用于排查Dynamic Linking Error: Win32 error错误原因3,可以查看ffi.Libary时的所有IO请求和对应结果,查看缺少了什么DLL

    1. dumpbin

dumpbin.exe为Microsoft COFF二进制文件转换器,它显示有关通用对象文件格式(COFF)二进制文件的信息。可用使用dumpbin检查COFF对象文件、标准COFF对象库、可执行文件和动态链接库等。 通过开始菜单 -> Visual Studio 20XX -> Visual Studio Tools -> VS20XX x86 Native Command Prompt启动。

  1.   dumpbin /headers [dll路径] // 返回DLL头部信息,会说明是32 bit word Machine/64 bit word Machine
  2.   dumpbin /exports [dll路径] // 返回DLL导出信息,name列表为导出的函数名
  3.   复制代码

闪崩问题

实际node-ffi调试的时候,很容易出现内存错误闪崩,甚至会出现断点导致崩溃的情况。这个是往往是因为非法内存访问造成,可以通过Windows日志看到错误信息,但是相信我,那并没有什么用。C的内存差错是不是一件简单的事情。

附录

自动转换工具

tjfontaine大神提供了一个node-ffi-generate,可以根据头文件,自动生成node-ffi函数申明,注意这个需要Linux环境,简单用KOA包了一层改成了在线模式ffi-online,可以丢到VPS中运行。

WINAPI

node-win32-api中完整翻译了全套windef.h中的类型,而且这个项目采用TS来规定FFI的返回Interface,很值得借鉴。

注意!里面的类型不一定都是对的,相信作者也没有完整的测试过所有变量,实际使用中也遇到过里面类型错误的坑。

GetLastError

简单说node-ffi通过winapi来调用DLL,这导致GetLastError永远返回0。最简单方法就是自己写个C++ addon来绕开这个问题。

参考Issue GetLastError() always 0 when using Win32 API 参考PR github.com/node-ffi/no…

PVOID返回空,即内存地址FFFFFFFF闪崩

winapi中,经常通过判断返回的pvoid指针是否存在来判断是否成功,但是在node-ffi中,对FFFFFFFF的内存地址deref()会造成程序闪崩。必须迂回采用指针的指针类型进行特判

  1.   HDEVNOTIFY
  2.   WINAPI
  3.   RegisterDeviceNotificationA(
  4.   _In_ HANDLE hRecipient,
  5.   _In_ LPVOID NotificationFilter,
  6.   _In_ DWORD Flags);
  7.    
  8.   HDEVNOTIFY hDevNotify = RegisterDeviceNotificationA(hwnd, ¬ifyFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
  9.   if (!hDevNotify) {
  10.   DWORD le = GetLastError();
  11.   printf("RegisterDeviceNotificationA() failed [Error: %x]\r\n", le);
  12.   return 1;
  13.   }
  14.   复制代码
  1.   const apiDef = SetupDiGetClassDevsW: [W.PVOID_REF, [W.PVOID, W.PCTSTR, W.HWND, W.DWORD]] // 注意返回类型`W.PVOID_REF`必须设置成pointer,就是不设置type,则node-ffi不会尝试`deref()`
  2.   const hDEVINFOPTR = this.setupapi.SetupDiGetClassDevsW(null, typeBuffer, null,
  3.   setupapiConst.DIGCF_PRESENT | setupapiConst.DIGCF_ALLCLASSES
  4.   )
  5.   const hDEVINFO = winapi.utils.getPtrValue(hDEVINFOPTR, W.PVOID) // getPtrValue特判,如果地址为全`FF`则返回空
  6.   if (!hDEVINFO) {
  7.   throw new ErrorWithCode(ErrorType.DEVICE_LIST_ERROR, ErrorCode.GET_HDEVINFO_FAIL)
  8.   }
  9.   复制代码

转载于:https://juejin.im/post/5b58038d5188251b186bc902

  相关资源:node-ffi模块下载包_nodeffi-Javascript工具类资源-CSDN文库