node-ffi使用指南

栏目: C · 发布时间: 6年前

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

nodejs / elctron 中,可以通过 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代码明确返回字符串数组的长度 returnValueLength

内置简写

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 github.com/node-ffi/no…

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)
}
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Head First HTML and CSS

Head First HTML and CSS

Elisabeth Robson、Eric Freeman / O'Reilly Media / 2012-9-8 / USD 39.99

Tired of reading HTML books that only make sense after you're an expert? Then it's about time you picked up Head First HTML and really learned HTML. You want to learn HTML so you can finally create th......一起来看看 《Head First HTML and CSS》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具