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

相关推荐
zqx_718 分钟前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己35 分钟前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称1 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色1 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2342 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河2 小时前
CSS总结
前端·css
BigYe程普2 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H2 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍2 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai2 小时前
网站开发的发展(后端路由/前后端分离/前端路由)
前端