当前位置: 首页 > 工具软件 > napi > 使用案例 >

使用ffi-napi和NodeJS Buffer创建NodeJs addon

凤伟泽
2023-12-01

创建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);
    }

 

 

 类似资料: