node-ffi 食用指南(难吃) - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
aswe6587
V2EX    Node.js

node-ffi 食用指南(难吃)

  •  
  •   aswe6587 2018-07-27 11:22:18 +08:00 9571 次点击
    这是一个创建于 2632 天前的主题,其中的信息可能已经有所发展或是发生改变。

    node 调 c++撞了一头包,最后含泪写出了这指南。 ...好像不支持 markdown 的 table 语法,凑合着看吧。

    人生苦短,别调 dll。

    node-ffi 使用指南

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

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

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

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

    缺点是:

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

    安装

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

    // 管理员运行 bash/cmd/powershell,否则会提示权限不足 npm install --global --production windows-build-tools npm install -g node-gyp 

    根据需要安装对应的库

    npm install ffi npm install ref npm install ref-array npm install ref-struct 

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

    npm install electron-rebuild 

    在 package.json 中配置快捷方式

    package.json "scripts": { "rebuild": "cd ./node_modules/.bin && electron-rebuild --force --module-dir=../../" } 

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

    简单范例

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

    变量类型

    C 语言中有 4 种基础数据类型----整型 浮点型 指针 聚合类型

    基础

    整型、字符型都有分有符号和无符号两种。

    类型 | 最小范围 -------- | --- char | 0 ~ 127 signed char | -127 ~ 127 unsigned char | 0 ~ 256

    在不声明 unsigned 时 默认为 signed 型

    refunsigned会缩写成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库。

    const ref = require('ref') const refArray = require('ref-array') const CharArray100 = refArray(ref.types.char, 100) // 申明 char[100]类型 CharArray100 const bufferValue = Buffer.from('Hello World') // Hello World 转换 Buffer // 通过 Buffer 循环复制, 比较嗦 const value1 = new CharArray100() for (let i = 0, l = bufferValue.length; i < l; i++) { value1[i] = bufferValue[i] } // 使用 ref.alloc 初始化类型 const strArray = [...bufferValue] //需要将`Buffer`转换成`Array` const value2 = ref.alloc(CharArray100, strArray) 

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

    npm install iconv-lite 
    const icOnv= require('iconv-lite') const cstr = iconv.encode(str, 'gbk') 

    注意!使用 encode 转码后cstrBuffer类,可直接作为当作uchar类型

    iconv.encode(str.'gbk')中 gbk 默认使用的是unsigned char | 0 ~ 256储存。假如 C 代码需要的是signed char | -127 ~ 127,则需要将 buffer 中的数据使用 int8 类型转换。

    const Cstring100 = refArray(ref.types.char, 100) const cString = new Cstring100() const uCstr = iconv.encode('农企药丸', 'gbk') for (let i = 0; i < uCstr.length; i++) { cString[i] = uCstr.readInt8(i) } 

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

    内置简写

    ffi 中内置了一些简写

    ref.types.int => 'int' ref.refType('int') => 'int*' char* => 'string' 

    只建议使用'string'。

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

    聚合类型

    多维数组

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

     char cName[50][100] // 创建一个 cName 变量储存级 50 个最大长度为 100 的名字 
     const ref = require('ref') const refArray = require('ref-array') const CName = refArray(refArray(ref.types.char, 100), 50) const cName = new CName() 

    结构体

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

    typedef struct { char cTMycher[100]; int iAge[50]; char cName[50][100]; int iNo; } Class; typedef struct { Class class[4]; } Grade; 
    const ref = require('ref') const Struct = require('ref-struct') const refArray = require('ref-array') const Class = Struct({ // 注意返回的`Class`是一个类型 cTMycher: RefArray(ref.types.char, 100), iAge: RefArray(ref.types.int, 50), cName: RefArray(RefArray(ref.types.char, 100), 50) }) const Grade = Struct({ // 注意返回的`Grade`是一个类型 class: RefArray(Class, 4) }) const grade3 = new Grade() // 新建实例 

    指针

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

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

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

    int a=10, int *p; // 定义一个指向整数型的指针`p` p=&a // 将变量`a`的地址赋予`p`,即`p`指向`a` 

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

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

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

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

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

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

    回调函数

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

    const ref = require('ref') const ffi = require('ffi') const testDLL = ffi.Library('./testDLL', { setCallback: ['int', [ ffi.Function(ref.types.void, // ffi.Function 申明类型, 用`'pointer'`申明类型也可以 [ref.types.int, ref.types.CString])]] }) const uiInfocallback = ffi.Callback(ref.types.void, // ffi.callback 返回函数实例 [ref.types.int, ref.types.CString], (resultCount, resultText) => { console.log(resultCount) console.log(resultText) }, ) const result = testDLL.uiInfocallback(uiInfocallback) 

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

    process.on('exit', () => { /* eslint-disable-next-line */ uiInfocallback // keep reference avoid gc }) 

    代码实例

    举个完整引用例子

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

    常见错误

    • 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 同目录下的其他引用。可以通过如下方法解决:

    //方法一, 调用 winapi SetDllDirectoryA 设置目录 const ffi = require('ffi') const kernel32 = ffi.Library("kernel32", { 'SetDllDirectoryA': ["bool", ["string"]] }) kernel32.SetDllDirectoryA("pathToAdd") //方法二(推荐),设置 Path 环境环境 process.env.PATH = `${process.env.PATH}${path.delimiter}${pathToAdd}` 

    DLL 分析工具

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

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

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

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

    闪崩问题

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

    附录

    自动转换工具

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

    WINAPI

    轮子

    winapi 存在大量的自定义的变量类型,waitingsong 大侠的轮子 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 https://github.com/node-ffi/node-ffi/pull/275

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

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

    HDEVNOTIFY WINAPI RegisterDeviceNotificationA( _In_ HANDLE hRecipient, _In_ LPVOID NotificationFilter, _In_ DWORD Flags); HDEVNOTIFY hDevNotify = RegisterDeviceNotificationA(hwnd, &notifyFilter, DEVICE_NOTIFY_WINDOW_HANDLE); if (!hDevNotify) { DWORD le = GetLastError(); printf("RegisterDeviceNotificationA() failed [Error: %x]\r\n", le); return 1; } 
    const apiDef = SetupDiGetClassDevsW: [W.PVOID_REF, [W.PVOID, W.PCTSTR, W.HWND, W.DWORD]] // 注意返回类型`W.PVOID_REF`必须设置成 pointer,就是不设置 type,则 node-ffi 不会尝试`deref()` const hDEVINFOPTR = this.setupapi.SetupDiGetClassDevsW(null, typeBuffer, null, setupapiConst.DIGCF_PRESENT | setupapiConst.DIGCF_ALLCLASSES ) const hDEVINFO = winapi.utils.getPtrValue(hDEVINFOPTR, W.PVOID) // getPtrValue 特判,如果地址为全`FF`则返回空 if (!hDEVINFO) { throw new ErrorWithCode(ErrorType.DEVICE_LIST_ERROR, ErrorCode.GET_HDEVINFO_FAIL) } 
    2 条回复    2024-01-07 17:59:08 +08:00
    kimown
        1
    kimown  
       2018-07-31 07:45:38 +08:00
    不是有 n-api 吗?
    zhangyuang
        2
    zhangyuang  
       2024-01-07 17:59:08 +08:00
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1166 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 17:40 PVG 01:40 LAX 10:40 JFK 13:40
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86