创建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定义这样,这样不利于动态输入参数。
// 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:
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中要怎么定义呢?
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++中定义的回调函数如下:
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中怎么去定义和设置呢?
//定义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和客户端都可以使用指针对象。比如:
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又怎么处理呢?
//定义该类型,用来对指针变量赋值
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写入一个地址。
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这边对这个变量赋值。
void GetValue()
{
char a[50]{0};
pfunc(a);
::MessageBoxA(nullptr, a, "", MB_OK);
}
看上面c++函数的目的就是在pfunc回调函数中给a数组赋值。
那么pfunc中怎么给它赋值呢我?
//定义回调函数
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就是指针所指向的内存的字节长度。
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);
}