Node.js使用ffi-napi,ref-array-napi,ref-struct-napi调用动态库

0x01 概述

使用electron开进行桌面程序的开发,似乎成了WEB前端开发人员转桌面程序开发的首选。近期有一些使用在electron中使用加密锁的需求,学习了一下在Node.js中通过ffi-napi模块调用动态链接库,把几款加密锁产品的动态库使用javascript封装了一下,实现了electron中使用加密锁功能。

开发过程中遇到了一些问题,踩了一些坑,这里总结记录一下。这里使用接口函数参数类型比较复杂的ROCKEY-ARM的动态链接库来进行开发。

NOTE: javascript封装的ROCKEY-ARM接口模块源码,我已经分享出来,如果只是需要electron或者Node.js工程中使用ROCKEY-ARM的网友,可以直接使用。

# 克隆
$ git clone https://github.com/youngbug/js-rockeyarm.git

0x02 准备

1. 安装依赖模块

首先需要在node.js项目中安装调用动态链接库时需要依赖的模块ffi-napi,ref-napi,ref-array-napi,ref-struct-napi

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

下面大概介绍一下这几个模块的用途:

  • ffi-napi: 在javascript中调用动态链接库(.dll/.so),在Node.js中使用这个模块可以不写任何C/C++代码来创建一个对本地库的绑定。

  • ref-napi: 这个模块定义了很多C/C++的常见数据类型,可以在声明和调用动态库的时候直接使用。

  • ref-array-napi: 这个模块在Node.js中提供了一个数组的实现,在声明和调用函数中,所有的指针都可以声明成一个uchar数组。

  • ref-struct-napi: 这个模块在Node.js中提供了一个结构体类型的实现。ROCKEY-ARM的函数很多参数都是结构体指针,如果声明称uchar的数组,那么传出的数据都是uchar数组,解析的时候不方便,需要自己拼接,除了麻烦,还要考虑字节序的问题。如果使用结构体,并定义一个结构体数组来作为指针传入,函数返回的结构体参数,就可以直接用结构体进行解析,会比较方便。

2.准备调用的动态库

飞天诚信购买ROCKEY-ARM加密锁产品,可以获得ROCKEY-ARM的SDK,可以获得Windows和Linux的动态链接库,文件名一般为Dongle_d.和libRockeyARM.so.0.3。

0x03 声明函数接口

ffi-napi支持Windows,Linux系统,所以.dll和.so都可以支持,在不同的操作系统下去加载不同的动态库文件就可以了。加载动态库的方法如下:

import { Library as ffi_Library } from 'ffi-napi'

libRockey = new ffi_Library('d:/rockey/x64/Dongle_d.dll',rockeyInterface)

Library()第一个参数是.dll的路径,Linux系统是.so的路径。第二个参数rockeyInterface是动态库导出函数的声明,ROCKEY-ARM的导出函数比较多,我单独拿出来定义。具体下面会讲到。

1. 声明几个简单函数

首先从ROCKEY-ARM中找几个参数简单的函数来声明一下。

typedef void * DONGLE_HANDLE;

DWORD WINAPI Dongle_Open(DONGLE_HANDLE * phDongle, int nIndex);

DWORD WINAPI Dongle_ResetState(DONGLE_HANDLE hDongle);

DWORD WINAPI Dongle_Close(DONGLE_HANDLE hDongle);

DWORD WINAPI Dongle_GenRandom(DONGLE_HANDLE hDongle, int nLen, BYTE * pRandom);

首先看一下上面几个接口用到的数据类型有:DONGLE_HANDLE,DWORD,DONGLE_HANDLE*,int,BYTE*这几种。
再看下ffi-napi支持的ref-napi支持的数据类型有以下类型:

void,int64,ushort,int,uint64,float,uint,long,
double,int8,ulong,Object,uint8,longlong,CString,
int16,char,byte,int32,uchar,size_t,uint32,short

参数这里应该用长度一致的数据类型,可以有以下匹配。

C类型长度ref-napi类型说明
DONGLE_HANDLE4/8uintC的定义是void*,是一个指针长度是4/8字节,用uint
DONGLE_HANDLE*4/8ptrHandle定义一个指向DONGLE_HANDLE的指针,用uint应该也是可以,但我没测试
int4int
BYTE*4/8prtByte定义一个指向uchar的指针,用uint应该也是可以,但我没测试

声明的写法如下:

 const rockeyInterface = {
    'Dongle_Open' :             ['int', [ptrHandle, 'int']],
    'Dongle_ResetState' :       ['int', [ryHandle]],
    'Dongle_Close':             ['int', [ryHandle]],
    'Dongle_GenRandom' :        ['int', [ryHandle, 'int', ptrByte]]
 }

一个json,key是动态库导出函数名,比如’Dongle_Open’,value是个列表,第一个元素是返回值,第二个元素是参数。其中参数还是个列表。这个ref-napi中有适合类型的,直接写称具体类型即可,比如返回值DWORD和传入的长度int,我这里都用’int’。其他的参数我额外定义了句柄ryHandle、句柄的指针ptrHandle、字节的指针ptrByte。其中ryHandle,ptrryHandle,ptrByte的定义如下:

const refArray = require('ref-array-napi')

var ryHandle = refArray(ref.types.uint)
var ptrHandle = refArray(ryHandle)
var ptrByte = refArray(ref.types.uchar)

2. void*类型参数

DONGLE_HANDLE本质是void *类型, void* 类型最开始的时候妄图定义一个void的数组,然后用void数组来表示void*,然后发现报断言错误,数组不支持void类型。所以就直接用无符号数来表示void指针,在64位系统是8字节,32位系统是4字节,使用uint类型就可以了。DONGLE_HANDLE*。

3. 结构体数组类型参数

在ROCKEY-ARM的函数中也有很多带参数的接口,比如:

typedef struct {
	unsigned int  bits;                   
	unsigned int  modulus;				  
	unsigned char exponent[256];     

} RSA_PUBLIC_KEY;

typedef struct {
	unsigned int  bits;                   
	unsigned int  modulus;                
	unsigned char publicExponent[256];    
	unsigned char exponent[256];          

} RSA_PRIVATE_KEY;

typedef struct
{
	unsigned short  m_Ver;               
	unsigned short  m_Type;              
	unsigned char   m_BirthDay[8];       
	unsigned long   m_Agent;             
	unsigned long   m_PID;               
	unsigned long   m_UserID;            
	unsigned char   m_HID[8];            
	unsigned long   m_IsMother;          
  unsigned long   m_DevType;        

} DONGLE_INFO;

DWORD WINAPI Dongle_Enum(DONGLE_INFO * pDongleInfo, int * pCount);

DWORD WINAPI Dongle_RsaGenPubPriKey(DONGLE_HANDLE hDongle, WORD wPriFileID, RSA_PUBLIC_KEY * pPubBakup, RSA_PRIVATE_KEY * pPriBakup);

拿以上两个函数接口举例,Dongle_Enum中的第一个参数是一个指向DONGLE_INFO结构体的指针,运行后返回设备信息的列表,使用ROCKEY-ARM的时候需要通过枚举函数获得设备信息列表,然后比较产品ID或者硬件ID决定打开哪一个设备。为了方便从枚举函数返回的设备信息中方便的解析出产品ID或者硬件ID等信息,需要把DONGLE_INFO* pDongleInfo这个参数声明成一个结构体数组。Dongle_RsaGenPubPriKey()函数中有RSA_PUBLIC_KEY*,RSA_PRIVATE_KEIY*两个结构体指针参数,因为在这里一般用户并不需要解析RSA密钥中的n,d,e等分量,可以直接做作为一个字节数组,直接声明成上面的ptrByte类型即可。所以在声明如下:

const ref = require('ref-napi')
const refArray = require('ref-array-napi')
const StructType = require ('ref-struct-napi')

var dongleInfo = StructType({
    m_VerL:     ref.types.uchar,
    m_VerR:     ref.types.uchar,
    m_Type:     ref.types.ushort,
    m_BirthdayL:ref.types.uint32,
    m_BirthdayR:ref.types.uint32,
    m_Agent:    ref.types.uint32,
    m_PID:      ref.types.uint32,
    m_UserID:   ref.types.uint32,
    m_HIDL:     ref.types.uint32,
    m_HIDR:     ref.types.uint32,
    m_IsMother: ref.types.uint32,
    m_DevType:  ref.types.uint32
})

var ptrInt = refArray(ref.types.int)
var ryHandle = refArray(ref.types.uint)
var ptrHandle = refArray(ryHandle) 
var ptrDongleInfo = refArray(dongleInfo)
var ptrByte = refArray(ref.types.uchar)

const rockeyInterface = {
  'Dongle_Enum' :             ['int', [ptrDongleInfo, ptrInt]],
  'Dongle_RsaGenPubPriKey' :  ['int', [ryHandle, 'ushort', ptrByte, ptrByte]]
}

0x04 调用声明的函数

调用ffi-napi声明的函数,主要是给自己定义的数据类型赋初值以及获得自定义参数的返回值。下面分别说明。

1. int*

这里的int*,是让函数返回设备的数量,或者传入输入数据的长度或者传出输出数据的长度,所以只要定义一个长度为1的int数组即可,如下:

 var piCount = new ptrInt(1) //
 piCount[0] = 0

给传入的数据赋值,只要给下标为0的元素赋值即可。

2. DONGLE_INFO*

这个参数是枚举函数传出枚举到设备信息的列表,枚举到多少设备,就传出多少个DONGLE_INFO,所以需要传入足够数量的的DONGLE_INFO,如下:

libRockey.Dongle_Enum(null, piCount)//获得设备的数量
var DongleList = new ptrDongleInfo(piCount[0])
libRockey.Dongle_Enum(DongleList, piCount)
console.log(DongleList[0].m_PID) //输出枚举到的第一个设备的PID

3. BYTE*

这个参数一般是作为传入传出数据的缓冲区的,所以创建数组的时候,需要创建足够长的空间,如下:

var buffer = new ptrByte(len)

0x05 踩坑总结

开发的过程中,踩到一些坑耽误了不少时间,这里总结一下。

ROCKEY-ARM的结构体是按字节对齐的,ref-struct-napi没有找到设置字节对齐的方法。当时声明的结构体如下:

var dongleInfo = StructType({
    m_VerL:     ref.types.uchar,
    m_VerR:     ref.types.uchar,
    m_Type:     ref.types.ushort,
    m_Birthday: ref.types.uint64,
    m_Agent:    ref.types.uint32,
    m_PID:      ref.types.uint32,
    m_UserID:   ref.types.uint32,
    m_HID:      ref.types.uint64,
    m_IsMother: ref.types.uint32,
    m_DevType:  ref.types.uint32
})

测试的时候会发现定义的结构体和ROCKEY-ARM定义的结构体对齐方式不一样,于是把m_Birthday和m_HID两个成员从ref.types.uint64,拆分成左右两个uint32,这样就可以让结构体对齐方式和ROCKEY-ARM的一致。使用m_Birthday和m_HID的时候,需要讲左右两个uint32拼接一些,稍微麻烦一点,但是在没找到配置StructType对齐方的情况,保证结果正确,还是可以接受的。

<think>嗯,用户问的是Node.jsFFI调用结构体函数,我得先回忆一下相关知识。首先,FFI指的是Foreign Function Interface,用于调用其他语言写的库,比如C。Node.js中常用的库是node-ffi-napi,因为原来的node-ffi已经不维护了。结构体在C中很常见,但JavaScript本身没有结构体,所以需要转换。 接下来,用户可能需要知道如何定义结构体,并传递到C函数中。首先,应该用ref-struct-napi来定义结构体,比如用Struct类型。例如,定义一个Point结构体,包含x和y的int类型字段。然后,需要创建这个结构体的实例,并设置值。 然后,调用C函数的时候,参数类型要正确声明。比如,C函数接受结构体指针,那么在node-ffi-napi中要用ref.types.CString(这里可能需要纠正,正确的应该是使用指针类型,比如StructType.ptr)。或者可能需要使用ref.refType来获取指针类型。 可能还需要处理内存分配的问题,比如使用alloc和reinterpret函数。比如,用ref.alloc分配内存,然后把结构体实例写入内存,再传递给C函数。另外,返回的结构体指针需要转换成JavaScript对象,可能需要用到reinterpret,并指定偏移量和大小。 用户可能遇到的问题包括类型不匹配、内存管理错误或者结构体对齐问题。这时候需要检查结构体定义是否正确,是否和C中的声明一致,包括字段类型和顺序。此外,结构体内存对齐可能需要设置align选项,尤其是在跨平台的时候。 总结步骤应该是:安装依赖,定义结构体,声明C函数,创建实例,分配内存,调用函数,处理返回结果。同时,提醒用户注意内存管理和类型匹配,以及可能的异步调用问题。</think>以下分步介绍在 Node.js使用 FFI 调用 C 语言结构体函数的实现方法: 1. **安装依赖** ```bash npm install ffi-napi ref-napi ref-struct-napi ``` 2. **C语言结构体示例(libdemo.c)** ```c typedef struct { int x; int y; } Point; void printPoint(Point p) { printf("Point(%d, %d)\n", p.x, p.y); } Point* createPoint(int x, int y) { Point* p = malloc(sizeof(Point)); p->x = x; p->y = y; return p; } ``` 3. **编译动态库** ```bash gcc -shared -o libdemo.so -fPIC libdemo.c ``` 4. **Node.js 调用实现** ```javascript const ffi = require('ffi-napi'); const ref = require('ref-napi'); const Struct = require('ref-struct-napi'); // 定义结构体类型 const Point = Struct({ x: 'int', y: 'int' }); // 定义函数接口 const libdemo = ffi.Library('./libdemo', { 'printPoint': ['void', [Point]], 'createPoint': [ref.refType(Point), ['int', 'int']] }); // 创建结构体实例 const p = new Point(); p.x = 10; p.y = 20; // 调用函数 libdemo.printPoint(p); // 输出 Point(10, 20) // 处理指针返回值 const pPtr = libdemo.createPoint(30, 40); const result = pPtr.deref(); // 解引用 console.log(result.x, result.y); // 输出 30 40 ``` **关键点说明:** 1. 结构体映射 - 使用 `ref-struct-napi` 定义与C语言对应的结构体 - 字段类型必须严格匹配(int对应'int',double对应'double') 2. 内存管理 - 返回指针需使用 `deref()` 解引用 - 如C函数有 `malloc` 需自行实现 `free` 函数 3. 注意事项 - 32/64位系统需注意指针大小差异 - 结构体对齐需使用 `align` 选项处理内存对齐问题 - 复杂结构体可使用 `Union` 处理联合体 **扩展应用:** 嵌套结构体定义示例: ```javascript const Line = Struct({ start: Point, end: Point }); ``` 建议结合 `node-gyp` 编译扩展模块处理更复杂的跨语言调用场景。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值