首页 > 编程语言 >node-ffi使用指南11

node-ffi使用指南11

时间:2022-10-04 17:04:04浏览次数:73  
标签:node 11 const int char ffi 使用指南 ref types

node-ffi使用指南

node-ffi使用指南11_ico

​Githoniel​

码农

在​​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代码之间实现了内存共享,类型转换则是通过​​ref​​​、​​ref-array​​​、​​ref-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型

​ref​​​中​​unsigned​​​会缩写成​​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 | ​​​http://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转码后​​cstr​​​为​​Buffer​​​类,可直接作为当作​​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​​​,会修改​​Buffer​​​的​​prototype​​​,替换和注入一些方法,请参考文档​​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, ()

标签:node,11,const,int,char,ffi,使用指南,ref,types
From: https://blog.51cto.com/u_15689678/5731378

相关文章

  • 111
    考试管理系统项目演示管理账号:admin/admin学员账号:person/person介绍一款多角色在线培训考试系统,系统集成了用户管理、角色管理、部门管理、题库管理、试题管理、......
  • Python 教程之控制流(11)无限迭代器
    Python的Itetool是一个模块,它提供了各种函数,这些函数在迭代器上工作以产生复杂的迭代器。该模块作为一个快速,内存效率的工具,可以单独使用或组合使用以形成迭代器代数。例如......
  • Spring让人眼前一亮的11个小技巧
    前言我们一说到spring,可能第一个想到的是IOC(控制反转)和AOP(面向切面编程)。没错,它们是spring的基石,得益于它们的优秀设计,使得spring能够从众多优秀框架中脱颖而出。除......
  • 运行yarn报错:error C:\liuyan\tools\echarts-5.4.0\node_modules\cwebp-bin: Com
    完成warning和报错信息如下。通过报错信息提示,锁定cwebp-bin,在waring中发现有提示说要更新至7或更高版本。解决方案:在package.json中,将cwebp-bin设置版本为:"cwebp-bin":......
  • NameNode Metadata备份和恢复最佳实践
    温馨提示:如果使用电脑查看图片不清晰,可以使用手机打开文章单击文中的图片放大查看高清原图。Fayson的github:​​https://github.com/fayson/cdhproject​​提示:代码块部分可......
  • Oracle-ORA-03113与ORA-04031导致的PDB启动失败
    查阅了网上的大部分文章,原因有很多:归档使用率高导致的(​​ORA-03113​​​),冷备后出现日志文件比控制文件新导致的(​​ORA-03113​​​),没有一致性关库导致的(​​ORA-03113​......
  • 0526-6.1-如果你不小心删了一个NameNode1
    温馨提示:如果使用电脑查看图片不清晰,可以使用手机打开文章单击文中的图片放大查看高清原图。Fayson的github:​​https://github.com/fayson/cdhproject​​提示:代码块部分可......
  • 集群JournalNode服务重启导致NameNode挂掉分析
    温馨提示:如果使用电脑查看图片不清晰,可以使用手机打开文章单击文中的图片放大查看高清原图。Fayson的github:​​https://github.com/fayson/cdhproject​​提示:代码块部分可......
  • 0530-6.1-如何只是迁移NameNode或JournalNode
    温馨提示:如果使用电脑查看图片不清晰,可以使用手机打开文章单击文中的图片放大查看高清原图。Fayson的github:​​https://github.com/fayson/cdhproject​​提示:代码块部分可......
  • prometheus+grafana+node-exporter部署监控系统实战
    1、在grafana中添加prometheus数据源  2、添加dashboard       ......