背景
最近使用electron开发新的系统,需要调用底层用C++写的SDK库,网上查的资料给的示例都是比较简单的,就是简单调用一下c++写的加法,很多复杂的写法都没有涉及到,自己慢慢摸索了一天,看了官方的一些文档,才完成了功能.
这篇文章主要涉及到各种指针的调用、回调函数、复杂结构体的调用,还有就是大数据在electron主进程和渲染进程之间的传输,做个记录,也方便以后学习.
使用的库
"ref-napi": "^3.0.3"
"ref-struct-napi": "^1.1.1"
"ffi-napi": "^4.0.3"
依赖的环境
visual studio(我用的是2022,老一点的版本应该也可以,需要安装对应的c++桌面开发包)
python (我使用的是3.6.4,python 应该也可以的)
还有就是electron版本使用的20.3.8,再高一点的版本就不行了,我是为了偷懒,就使用低版本,想支持高版本更复杂一点,详细原因可以看看这篇文章: Electron and the V8 Memory Cage | Electron (electronjs.org)
使用过程
- 初始化sdk 以下是对应的c++中的函数定义
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中的初始化
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];
- 常规的调用sdk
js
const result = SDK.SetOpenSyncMode(true);
console.log(result); //打印出true 或者 false
- 回调函数的调用
重要的事儿:由于回调函数传递的时候都是传递的指针,js是有垃圾回收的,定义的回调函数,如果没有引用了,就会被回收掉,c++库在调用的时候就找不到对应的函数了,就会崩溃,我的解决方案是加了一个全局引用
js
//全局引用,避免被回收
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);
- 复杂结构体的调用
js
//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
);
}
);
- 从主进程传输大数据到渲染进程
上面拿到了对应的图像数据,但是还需要把数据传到渲染进程,主要用到了electron的ipc通讯, 参考electron官网
这里选用的是主进程主动推送方式,当然其他几种也可以
js
//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)
- 数据渲染 拿到数据就可以进行渲染了 参考另外一篇: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