创建NodeJS AddOn的方式很多,在NodeJS开发出c/c++ addon with NAPI之后,大家基本上都用NAPI写addon了,最基本的原因就是NAPI可以脱离v8修改的依赖,NAPI对v8中的api进行了封装,不同版本的Node会自动适用v8底层的API变化。
node-ffi-napi是一个开源的module,该模块内部通过nodejs napi加载我们自己写的dll中输出的函数(输出函数必须是extern 'c'的),由于该模块在内部使用了napi,所以我们在下载该模块的时候,会自动对模块进行编译,需要提前安装很多的模块,具体可以参考在不同的系统上怎样编译一个addon。
在使用node-ffi-napi的时候,一般也会使用ref-napi, ref-array-napi, ref-struct-napi,具体这些模块的使用可以参考npm上的readme.
Buffer是NodeJs为管理内存而设计的,Buffer是固定内存长度的byte array,是继承与Javascript 的Uint8Array, 你可以跟JS中的DataView一样去操作Buffer,具体使用可以参考NodeJS网站的说明。
下面介绍一下我在适用ffi-napi和Buffer的几个特殊的例子:
1.dll中的调用约定:
在定义dll输出函数的时候使用extern 'c', 也就是编译用的c规则,如果是extern 'c++'的话,就是用的c++规则,就会有函数重命名的问题;默认使用的是__cdecl,不要使用__stdcall,因为__stdcall会带出函数参数信息,一般自定义的都不会使用__stdcall,只有windows API定义这样,这样不利于动态输入参数。
具体可以参考以及微软的说明
1 2 3 4 5 | // api.h #define DllAPI __declspec(dllexport) extern "C" { DllAPI void GetTmpStruct(void* buffer); } |
2.在dll中设置回调函数,在c++代码中调用 typescript中的函数,有些特殊情况是一个node程序调用Addon,在addon里面需要调用回调函数,也就是typescript 的code, 需要怎么处理呢?
下面是addon中C++的code:
1 2 3 4 5 6 | typedef void (* JsCallBack)(int status, int extendstatus); //callback void SetTestCallBack(void* wrapper, JsCallBack pfunc) { wrapper* p = static_cast<wrapper*> (wrapper); p->m_pFuncCallBack = pfunc; } |
代码段中SetTestCallBack是输出函数,在typescript中通过ffi加载调用。JSCallBack是函数定义类型,也就是在typescript中的定义的函数格式要满足上面的条件.
那么在typescript中要怎么定义呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import * as ffi from 'ffi-napi' import * as ref from 'ref-napi' class Testify{ //this callback must be saved in object, so that it will not be removed by nodejs private callback: any; constructor() { if (!Testify.libObj) { Testify.InitDll(); } //set callback function for addon SetTestCallBack(null, (status, extendstatus) => { console.log(`status=${status}, extendstatus= ${extendstatus}`); }) } public static InitDll(): void { Testify.libObj = ffi.Library(this.dllPath, { 'SetTestCallBack': [ref.types.void, [lpVoid, ffi.Function(ref.types.void, [ref.types.int, ref.types.int])]] } } public SetTestCallBack(wrapper: any, pCallBack: (status: number, extendstatus: number) => void) { this.callback = ffi.Callback(ref.types.void, [ref.types.int, ref.types.int], pCallBack); Testify.libObj.SetTestCallBack(wrapper, this.callback); } } |
在上面的例子中可以看到,在Testify构造函数中加载了dll并且设置了回调函数,这个回调函数(callback变量)需要存储下来,不要做临时变量,防止被自动回收,addon调用的时候,就会出exception.
3. 设置的回调函数中参数里面含有c++ 的函数,这种状况一般是我们在为老的c++程序写addon的时候会用到,当然也可以对c++函数进行分解,防止出现这种状况,但是如果我们不想动的话,也可以保持原样,例如 c++中定义的回调函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | typedef int (__stdcall* LPMessageBox)(long p, char* text); typedef void (* JsCallBack)(LPMessageBox pFunc, long p); //callback type void SetTestCallBack(void* wrapper, JsCallBack pfunc) { ActiveXWrapper* p = static_cast<ActiveXWrapper*> (wrapper); p->m_pFuncCallBack = pfunc; } int cbMessageBox(long param, byte* value) { ::MessageBox(nullptr, (LPCTSTR)value, _T(""), MB_OK); return 100; } void DeleteScriptObj(void* wrapper, void* script) { wrapper* p = static_cast<wrapper*> (wrapper); LPMessageBox msgbox = reinterpret_cast<LPMessageBox>(cbMessageBox); p->m_pFuncCallBack(msgbox, 123); p->DeleteScriptObject(); } |
上面中LPMessageBox是c++这边的定义, JSCallBack是typescript的回调函数,从上面的代码段可以看出,调用DeleteScriptObj的时候,会调用typescript的回调函数m_pFuncCallBack, 在调用回调的时候有参数msgbox,改参数是一个函数,那么在typescript中怎么去定义和设置呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | //定义msgbox类型,跟c++对上 let messageBoxFunctionType = ffi.Function(ref.types.int, [ref.types.long, ref.refType(ref.types.char)]); //Loadlibrary的时候设置callback类型 'SetTestCallBack': [ref.types.void, [lpVoid, ffi.Function(ref.types.void, [messageBoxFunctionType, ref.types.long])]], //在构造函数中设置addon的回调函数 this.initcb = this.InitCallBack.bind(this, this.wrapper); this.SetTestCallBack(this.wrapper, this.initcb); //回调函数 public InitCallBack(wrapper: any, messageBox: any, addonVal: number) { //var buf = Buffer.alloc(9, 'abcedfghi', 'ucs-2'); var str = 'abcdefghi'; let len = Buffer.byteLength(str, 'ucs-2'); let buf2 = Buffer.alloc(len + 2, 0); ref.writeCString(buf2, 0, str, 'ucs-2'); let rs = messageBox(150, buf2); //调用c++ dll中的函数 //return 20; } |
4.有的时候我们在c++的DLL里面的内存是外面(调用dll的客户端)开辟的,然后在dll和客户端都可以使用指针对象。比如:
1 2 3 4 5 6 7 8 9 10 11 12 | struct pointerStruct { int instance; char name[50]; }; typedef void (*JsCreateObject)(pointerStruct** p); void SetCallBackToCreateObject(JsCreateObject pfunc) { pointerStruct* p = nullptr; pfunc(&p); ::MessageBoxA(nullptr, p->name, "", MB_OK); } |
在上面的c++的code代码段里面,可以发现临时变量p是个指针,需要在回调函数pfunc里面去生成。那么在typescript又怎么处理呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | //定义该类型,用来对指针变量赋值 export class pointerType implements ref.Type { size: number; indirection: number; tp: ref.Type; name?: string | undefined; alignment?: number | undefined; /** * type: base type, such as the pointer to a char array, or the pointer to a int, ushort and so on */ constructor(type: ref.Type, byteSize?: number) { this.tp = type; this.size = byteSize ? byteSize : this.tp.size; this.indirection = 1; } get(buffer: Buffer, offset: number) { switch (this.tp) { case ref.types.char: case ref.types.uchar: return ref.readCString(buffer, offset); case ref.types.int: { return ref.endianness === 'LE' ? buffer.readIntLE(offset, this.size) : buffer.readIntBE(offset, this.size); } case ref.types.ushort: { return ref.endianness === 'LE' ? buffer.readUInt16LE(offset) : buffer.readUInt16BE(offset); } case ref.types.byte: { return buffer.readUInt8(offset); } default: return null; } } set(buffer: Buffer, offset: number, value: number | Buffer): void { if (typeof value === 'number') { switch (this.tp) { case ref.types.ushort: ref.endianness === 'LE' ? buffer.writeUInt16LE(value, offset) : buffer.writeUInt16BE(value, offset); break; case ref.types.int: ref.endianness === 'LE' ? buffer.writeInt32LE(value, offset) : buffer.writeInt32BE(value, offset); break; case ref.types.byte: buffer.writeInt8(value, offset); break; default: break; } } else { value.copy(buffer, offset, 0, value.byteLength); if (buffer.byteLength > offset + value.byteLength) { let index = offset + value.byteLength; for (; index < buffer.byteLength; index++) { buffer[index] = 0; } } } } } |
在上面的typescript里面自定义了一个类型,该类型继承了ref.type,该类型是为了对指针变量赋值。在上面的c++ 代码中,我们看到指针变量‘p’所指向的对象需要在typescript中产生,那么实际上就是对指针变量p写入一个地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import * as ref from 'ref-napi' import StructType from 'ref-struct-napi'; import ArrayType from 'ref-array-napi' //pointerStruct跟c++里面的对象要对应 export let pointerStruct = StructType( { instance: ref.types.int, name: ArrayType(ref.types.char, 50) //长度50的char数组,跟c++对应 }); public CreateObject(obj: any) { console.log(pointerStruct.size); let objTmp = new pointerStruct(); objTmp.instance = 110; var buf = Buffer.from('Spring Dou'); buf.forEach((v, index) => { objTmp.name[index] = v; }) objTmp.name[buf.byteLength] = 0; this.objects = ref.alloc(pointerStruct, objTmp); obj.type.set(obj, 0, ref.address(this.objects)); } private CreateObjectCallBack: any; public SetCallBackToCreateObject() { this.CreateObjectCallBack = ffi.Callback(ref.types.void, [ref.refType(new pointerType(ref.types.int))], this.CreateObject.bind(this)); Testify.libObj.SetCallBackToCreateObject(this.CreateObjectCallBack); } |
上面的typescript可以看到我们定义回调函数的时候用了new pointerType(ref.types.int),也就是表明CreateObject(obj: any)中的参数obj是一个4字节的指针对象。然后在CreateObject里面,调用obj.type.set(...)去将产生的变量的地址写入obj中。
5.为C++指针变量的赋值,跟4中的情况不同,该指针变量实在c++端产生的,需要在typescript这边对这个变量赋值。
1 2 3 4 5 6 | void GetValue() { char a[50]{0}; pfunc(a); ::MessageBoxA(nullptr, a, "", MB_OK); } |
看上面c++函数的目的就是在pfunc回调函数中给a数组赋值。
那么pfunc中怎么给它赋值呢我?
1 2 3 4 5 | //定义回调函数 pfunc(val: any): void { var buffer = Buffer.from('Spring Dou'); val.type.set(val, 0, buffer); } |
不过这个在loadlibrary的时候我们要做该回调函数声明的时候要把val的类型声明为ref.reftype(new pointerType(ref.types.char, 50)), 其中pointerType就是上面我们自定义的类型,50就是指针所指向的内存的字节长度。
1 2 3 4 5 | public SetCallBackSetValue() { this.SetValueCallBack = ffi.Callback(ref.types.void, [ref.refType(new pointerType(ref.types.char, 50))], this.pfunc.bind(this)); Testify.libObj.SetCallBackSetValue(this.SetValueCallBack); } |