背景
最近使用electron开发新的系统,需要调用底层用C++写的SDK库,网上查的资料给的示例都是比较简单的,就是简单调用一下c++写的加法,很多复杂的写法都没有涉及到,自己慢慢摸索了一天,看了官方的一些文档,才完成了功能.
这篇文章主要涉及到各种指针的调用、回调函数、复杂结构体的调用,还有就是大数据在electron主进程和渲染进程之间的传输,做个记录,也方便以后学习.
使用的库
"ref-napi": "^3.0.3" <br>
"ref-struct-napi": "^1.1.1"<br>
"ffi-napi": "^4.0.3"<br>
"ref-wchar-napi": "^1.0.3" <br>
"ref-array-napi": "^1.2.2" <br>
依赖的环境
visual studio(我用的是2022,老一点的版本应该也可以,需要安装对应的c++桌面开发包)
python (我使用的是3.6.4,python 应该也可以的)
还有就是electron版本使用的20.3.8,再高一点的版本就不行了,我是为了偷懒,就使用低版本,想支持高版本更复杂一点,详细原因可以看看这篇文章:
Electron and the V8 Memory Cage | Electron (electronjs.org)
使用过程
1. 初始化sdk
以下是对应的c++中的函数定义
using DeviceHandle = void*;
using PreviewHandle = void*;
enum class ErrorCode : uint32_t{
OK=0x00000000,
ERROR=0x02000000,
}
enum class DeviceModel : uint32_t{
UNKNOWN=0x00000000,
Series=0x02000000,
}
void(STDCALL *OnLog)(LogLevel level, const char* moduleName, const char* log, void* userData);
void(STDCALL *OnCameraEvent)(DeviceHandle handle, CameraEvent event, void* data, int32_t dataLen, void* userData);
void(STDCALL *OnCameraFrame)(void* camera, Frame* frame, bool noMoreFrame, void* userData);
ErrorCode Init(OnLog funLog);
bool SetOpenSyncMode(bool sync);
DeviceHandle OpenIPDevice(const char* ip, DeviceModel mode, OnCameraEvent onEvent, void* userData);
PreviewHandle PreviewStart(DeviceHandle handle, OnCameraFrame funOnFrame, void* userData, RenderHandle render);
对应js中的初始化
import ffi from 'ffi-napi';
import ref from 'ref-napi';
import StructType from 'ref-struct-napi';
//SDKPathName 代表引用的dll文件的路径,直接用文件名就行了,不需要写成 xxx.dll, 我是直接放在同级目录的,如果放在其他目录可以写为绝对路径或者相对路径
const SDK = new ffi.Library('SDKPathName', {
Init: ['int', ['pointer']], //pointer 对应的是回调函数
SetOpenSyncMode: ['bool', ['bool']],
OpenIPDevice: ['uint32*', ['string', 'int', 'pointer', 'void*']],
PreviewStart: ['void*', ['void*', 'pointer', 'int*', 'void*']],
});
//回调函数定义
const OnLog = ffi.Callback(
'void',
['int', 'string', 'string', 'void*'],
function (level, moduleName, log, userData) {
// do something
}
);
const OnCameraEvent = ffi.Callback(
'void',
['void*', 'int', 'void*', 'int', 'void*'],
function (handle, event, data, dataLen, userData) {
// do something
}
);
const OnCameraFrame = ffi.Callback(
'void',
['void*', FramePtr, 'bool', 'uint32*'],
function (camera, frame, noMoreFrame, userData) {
// do something
}
);
//一开始的时候没有下面的代码,运行两三秒就会崩溃掉
//调用ffi.Callback时,如果未给ffi.Callback返回值加个引用的话,那么就可能会被垃圾回收掉。做法就是每次返回时,加一个全局引用
globalThis.SDKCallbacks = [OnLog, OnCameraEvent, OnCameraFrame];
2. 常规的调用sdk
const result = SDK.SetOpenSyncMode(true);
console.log(result); //打印出true 或者 false
3. 回调函数的调用
重要的事儿:由于回调函数传递的时候都是传递的指针,js是有垃圾回收的,定义的回调函数,如果没有引用了,就会被回收掉,c++库在调用的时候就找不到对应的函数了,就会崩溃,我的解决方案是加了一个全局引用
//全局引用,避免被回收
globalThis.SDKCallbacks = [OnLog, OnCameraEvent, OnCameraFrame];
//以下是调用
const result = SDK.Init(OnLog);
console.log( result); // 打印出true 或者 false
//当然也可以传null
SDK.Init(null)
//返回的值 也可以用作其他函数的入参
const DeviceHandle = SDK.OpenIPDevice(ip, mode, null, null);
SDK.PreviewStart(DeviceHandle, OnCameraFrame, null, null);
4. 复杂结构体的调用
//OnCameraFrame回调中有一个复杂的结构体FramePtr,以下是定义
const FrameHeader = StructType({
mColor: ref.types.uint32,
mExtType: ref.types.uint16,
mWidth: ref.types.uint16,
mPitch: ref.types.uint16,
mHeight: ref.types.uint16,
mLength: ref.types.uint32,
mIndex: ref.types.uint64,
mTimestamp: ref.types.uint64,
mExtent: ref.types.uint64,
});
const Frame = StructType({
mHeader: FrameHeader, //可以嵌套定义的结构
mData: ref.refType(ref.types.uint8), //这块是一个指针,指向一块连续的uint8数组,后续往渲染进程传递会用到这一块数据
});
const FramePtr = ref.refType(Frame);//转为指针
const OnCameraFrame = ffi.Callback(
'void',
['void*', FramePtr, 'bool', 'uint32*'],
function (camera, frame, noMoreFrame, userData) {
const frameValue = frame.deref(); //调用deref获取指针的内容,frameValue里面就可以读取到上面定义的mHeader,mData
//读取mData对应的buffer,frameBuffer 就是最后需要的渲染图像buffer
const frameBuffer = ref.readPointer(
frameValue.mData.ref(),
0,
frameValue.mHeader.mLength
);
}
);
5. void*的使用
如果在回调函数中用了void*,在回调函数中直接打印出来时没有长度的,要读取对应的数据,需要指定对应的长度
const onEvent = ffi.Callback(
'void',
[ 'void*', 'int32'],
function ( data, dataLen) {
// dataLen需要c++端提供,告诉回调函数需要读取多长
const dataBuf = ref.readPointer(data.ref(), 0, dataLen); // 这时候读取出来的是一个buffer
const mID = dataBuf.readUInt8(1); //根据实际情况读取buffer,我这里是读取第二个字节为数字
}
);
6. 数组和宽字符的使用
需要用到 ref-array-napi和ref-wchar-napi,先安装一下
import refArray from 'ref-array-napi';
import wchar_t from 'ref-wchar-napi';
const ExportParam = StructType({
mSavePath: refArray(wchar_t, 256),
});
// 下面是SDK的定义
// const ExportPtr = ref.refType(ExportParam);
// ExportStart: [
// 'void*',
// [ ExportPtr'],
// ],
// 以下是调用
const savePath='D:/123/';
const pathBuffer = Buffer.alloc(512);
pathBuffer.fill(savePath, 0, savePath.length, 'utf8'); // 这个路径传中文 会有问题,还没有找到解决办法,希望大神支招
const exportParam = new ExportParam({
mSavePath:Array.from(pathBuffer)
})
const exportParamPtr = ref.alloc(ExportParam, exportParam);
SDK.ExportStart(
exportParamPtr,
);
7. 从主进程传输大数据到渲染进程
上面拿到了对应的图像数据,但是还需要把数据传到渲染进程,主要用到了electron的ipc通讯, 参考electron官网
这里选用的是主进程主动推送方式,当然其他几种也可以
//main.js
mainWindow.webContents.send(
`getFrame`,
//frameBuffer是上面获取到的数据,不能直接传递,渲染进程不接受这种数据类型,需要进行转换,我是为了方便后续渲染,直接用了Uint8Array,其他的浏览器支持的类型也是可以的
new Uint8Array(frameBuffer)
);
//preload.js
onGetFrame: (
callback: (event: IpcMainInvokeEvent, data: Uint8Array) => void
) => {
ipcRenderer.on(`getFrame`, callback);
},
//前端页面
window.electronAPI.onGetFrame( (_event, data) => {
//data 即为传递的Uint8Array
//do something
});
向渲染进程传递数据支持类型: The structured clone algorithm - Web APIs | MDN (mozilla.org)
8. 数据渲染
拿到数据就可以进行渲染了 参考另外一篇:WebGL在视频播放器上的应用 - 掘金 (juejin.cn)
总结
以上实现了从sdk拿取数据并且渲染的流程: sdk->electron主进程->渲染进程,其实 主进程中写的代码也是可以直接应用在nodejs中,此方式对内存的占用会较高,一份数据会同时存在3个地方:sdk、主进程、渲染进程,要做好内存管理,避免内存泄漏。同时因为会不停地拷贝数据,对CPU的占用也挺高的,实际运用中,需要考虑对数据进行压缩。
参考文章
Node FFI Tutorial · node-ffi/node-ffi Wiki · GitHub
"ref" documentation v0.3.3 (tootallnate.github.io)
GitHub - TooTallNate/ref-struct: Create ABI-compliant "struct" instances on top of Buffers
node-ffi/example/sqlite.js at master · node-ffi/node-ffi · GitHub
github.com