electron 调用C++ 库获取大数据,并进行渲染

背景

最近使用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)

使用过程

  1. 初始化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];
  1. 常规的调用sdk
js 复制代码
const result = SDK.SetOpenSyncMode(true);
console.log(result); //打印出true 或者 false
  1. 回调函数的调用

重要的事儿:由于回调函数传递的时候都是传递的指针,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);
  1. 复杂结构体的调用
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
    );
     
  }
);
  1. 从主进程传输大数据到渲染进程

上面拿到了对应的图像数据,但是还需要把数据传到渲染进程,主要用到了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)

  1. 数据渲染 拿到数据就可以进行渲染了 参考另外一篇: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

相关推荐
崔庆才丨静觅几秒前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60611 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了1 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅1 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅2 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊2 小时前
jwt介绍
前端
爱敲代码的小鱼2 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax