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

2,026 阅读6分钟

背景

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