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-napinpm install ref-napinpm install ref-array-napinpm 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_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对齐方的状况,保障后果正确,还是能够承受的。