共计 5590 个字符,预计需要花费 14 分钟才能阅读完成。
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 筹备
首先须要在 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 数组,解析的时候不不便,须要本人拼接,除了麻烦,还要思考字节序的问题。如果应用构造体,并定义一个构造体数组来作为指针传入,函数返回的构造体参数,就能够间接用构造体进行解析,会比拟不便。
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-npai 类型 | 阐明 |
---|---|---|---|
DONGLE_HANDLE | 4,8 | uint | C 的定义是 void*,是一个指针长度是 4 / 8 字节,用 uint |
DONGLE_HANDLE* | 4,8 | ptrHandle | 定义一个指向 DONGLE_HANDLE 的指针,用 uint 应该也是能够,但我没测试 |
int | 4 | int | |
BYTE* | 4,8 | prtByte | 定义一个指向 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 对齐方的状况,保障后果正确,还是能够承受的。