技术干货 | Electron 插件开发实践

Electron 是以 Nodejs 和 Chromiu m 为内核的跨平台开发框架。 本文结合网易
首页 新闻资讯 行业资讯 技术干货 | Electron 插件开发实践

01 前言

早期跨平台桌面应用开发大多采用 Qt 和 C++,受语言学习成本开发效率影响,越来越多的人将目光转向了 Electron。Electron 是以 Nodejs 和 Chromium 为内核的跨平台开发框架。

Electron 基于 Web 技术开发桌面应用,Web 技术在软件开发领域应用非常广泛,生态较为成熟,学习成本较低、开发效率高。但是 Web 在处理多线程、并发场景时显得捉襟见肘,Electron 底层有 Nodejs 支持,Nodejs 的插件模块具有调用 C++ 的能力,C++ 非常适合处理高并发、音视频等复杂业务,弥补了 Web 的性能问题。本文就 js 和 C++ 混合编程在 Electron 桌面程序中的应用进行介绍。

Nodejs 中使用 C++,有以下几种方式:

  • 将 C++ 程序作为独立子进程使用。

  • 通过 node-ffi 方式调用。

  • Nodejs 扩展,将 C++ 代码编译为 Nodejs 模块,本文主要针对这种方式进行介绍。

02 C++ 扩展

C++ 扩展简介 

Nodejs 本身采用 C++ 编写,所以我们可以使用 C++ 编写的自己的 Nodejs 模块,可以像 Nodejs 原生模块一样使用。C++ 扩展格式为 .node,其本质为动态链接库,相当于 Windows 下 .dll。C++ 扩展作为动态链接库,通过 dlopen 在 Nodejs 中加载。

C++ 扩展架构图:

680816c415e9cca550d120672711f0b5bf30a2.png

C++ 扩展实现的几种方式 

实现 C++  扩展有3种方式:原生模式、nan、Node-API。

  • 原生模式直接使用 Nodejs API 及 Chrome V8 API 进行开发,这种方式早已被遗弃。特点: Nodejs API 和 Chrome V8 API 接口一旦变化,依赖这些 API 的 C++ 扩展便无法使用,特定版本的 C++ 扩展只能在对应版本 Nodejs 环境中使用。

  • nan(N ative Abstractions for Nodejs)nan 是 N odejs 抽象接口集,nan 根据当前 Nodejs 版本,使用宏判断执行对应版本的 AP I。特点: C++ 扩展在不同版 本 Nodejs 中运行,需重新编译,Nodejs 升级到较高版本后出现接口不兼容问题。

  • Node-APINode-API 使用 Nodejs 二进制接口,相比 nan 方式这些二进制接口更为稳定。特点: 不同版本 Nodejs 只要 abi 版本号一致,C++ 扩展可以直接使用无需重新编译,消除了 Nodejs 版本差异。

构建工具 

  • node-gypnode-gyp  对 gyp(Chromium 编写的构建工具)进行了封装,binding.gyp 为其配置文件。node-gyp 工作分为两个过程:a.  结合 binding.gyp 生成对应平台下的工程配置,比如:Windwos 下生成 .sln 项目文件。b.  项目文件编译,生成 C++ 扩展。binding.gyp 配置文件,以 Windows 为例:

复制

{"targets": [
    {        "target_name": "addon_name", "type": "static_library"'defines': [          'DEFINE_FOO',          'DEFINE_A_VALUE=value',
        ],'include_dirs': [          './src/include',          '<!(node -e "require(\'nan\')")' // include NAN in your project],'sources': [          'file1.cc',          'file2.cc',
        ],'conditions': [
          [              'OS=="win"', 
              {'copies': [{                  'destination': '<(PRODUCT_DIR)',                  'files': ['./dll/*'  ]
                }],'defines': [                  'WINDOWS_SPECIFIC_DEFINE',
                 ],'library_dirs': [                  './lib/'],'link_settings': {                  'libraries': ['-lyou_sdk.lib'  ]
                },'msvs_settings': {                  'VCCLCompilerTool': {'AdditionalOptions': [                      '/utf-8']
                  }
               },
             }
           ]
        ],
      },
    ]
}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

  • 30.

  • 31.

  • 32.

  • 33.

  • 34.

  • 35.

  • 36.

  • 37.

  • 38.

  • 39.

  • 40.

  • 41.

  • 42.

  • 43.

  • 44.

  • 45.

  • 46.

  • 47.

  • 48.

  • 49.

  • 50.

  • 51.

字段说明:

  • target_name: 目标的名称,此名称将用作生成的 Visual Studio 解决方案中的项目名称。

  • type: 可选项:static_library 静态库、executable 可执行文件、shared_library 共享库。

  • defines: 将在编译命令行中传入的 C 预处理器定义(使用 -D 或 /D 选项)。

  • include_dirs: C++ 头文件所在的目录。

  • sources: C++ 源文件。

  • conditions: 适配不同环境配置条件块。

  • copies: 拷贝 dll 动态库到生成目录。

  • library_dirs: 配置 lib 库目录到 vs 项目中。

  • libraries: 项目依赖的库。

  • msvs_settings: Visual Studio 中属性设置。

node-gyp 编译指令:

复制

node-gyp clean //清空上一次构建目录node-gyp configure //配置项目node-gyp build //项目编译,生成C++扩展node-gyp rebuild //重新生成C++扩展,相当于clean configure build的结合
  • 1.

  • 2.

  • 3.

  • 4.

  • cmake-jscmake-js 与 node-gyp 工作原理类似。 cmake-js 是基于 CMake 的构建系统,而 node-gyp 是基于  Goole 的 gyp 工具,这里不在进行详细介绍。

回调事件处理 

Nodejs 运行在单线程中,但它能够支持高并发,就是依赖事件循环实现。简单来说 Nodejs 主线程维护一个事件队列,收到一个耗时任务将任务放入队列,继续向下执行其他任务。主线程空闲时,遍历事件队列,非 I/O 任务亲自处理,通过回调函数返回给上层调用。I/O 任务放入线程池执行,并指定回调函数,然后继续执行其他任务。

C++ 扩展调用 js 回调函数时,会在 Nodejs 挂在一个 libuv 线程池,用于处理回调函数,当 Nodejs 主线程空闲时,去遍历线程池,处理任务。libuv 具体细节参考 nertc-electron-sdk:

https://github.com/netease-im/node-nertc-sdk/blob/main/nertc_sdk_node/nertc_node_engine_event_handler.cpp

03 混合编程实践

示例1 

结合 node-addon-api 进行演示,node-addon-api 对 Node-API 接口进行了封装开发简单。该实例完成 js 调用 C++ 函数实现两个数字相加。

  • 项目结构

374668717269b27c879900a576e7d62874d2a5.png

  • package.json 配置文件

复制

//package.json{  "name": "addon-sdk",  "version": "0.1.0",  "description": "test nodejs addon sample",  "main": "./api/index.js",  "private": true,  "gypfile": true,  "dependencies": {"bindings": "~1.2.1","node-addon-api": "^3.0.0"
  },  "devDependencies": {"node-gyp": "^8.2.0"
  },  "scripts": {"test": "node ./api/index.js"
  },  "license": "ISC",  "author": "liyongqiang"}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • binding.gyp 配置文件

复制

//binding.gyp{  "targets": [
    {      "target_name": "addon",      "sources": [ "./src/addon.cc", "./src/engine.h" , "./src/engine.cpp" 
      ],      "include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"  ],      'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' 
      ]
    }
  ]
}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • C++ 扩展

复制

//addon.cc#include <napi.h>#include "engine.h"Napi::Object InitAll(Napi::Env env, Napi::Object exports) {  return nertc::Engine::Init(env, exports);
}NODE_API_MODULE(addon, InitAll)//engine.h#pragma once#include <napi.h>namespace nertc {class Engine : public Napi::ObjectWrap<Engine> { public:  static Napi::Object Init(Napi::Env env, Napi::Object exports);  Engine(const Napi::CallbackInfo& info); private:  Napi::Value add(const Napi::CallbackInfo& info);
};
}//engine.cpp#include "engine.h"namespace nertc {  
Napi::Object Engine::Init(Napi::Env env, Napi::Object exports) 
{Napi::Function func =DefineClass(env, "Engine",
                   {InstanceMethod("add", &Engine::add)});Napi::FunctionReference* constructor = new Napi::FunctionReference();*constructor = Napi::Persistent(func);env.SetInstanceData(constructor);exports.Set("Engine", func);return exports;
}Engine::Engine(const Napi::CallbackInfo& info): Napi::ObjectWrap<Engine>(info) {}Napi::Value Engine::add(const Napi::CallbackInfo& info) {  Napi::Env env = info.Env();//获取环境变量
  int ret = 0;  int length = info.Length();//获取参数个数
  if (length != 2 || !info[0].IsNumber() || !info[1].IsNumber()) 
  {      Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException();      ret = -1;      return Napi::Number::New(env, ret);
  }  int num1 = info[0].As<Napi::Number>().Int32Value();//获取第一个参数
  int num2 = info[1].As<Napi::Number>().Int32Value();////获取第二个参数
  int sum = num1 + num2;  return Napi::Number::New(env, sum);//返回结果到js层}
}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

  • 30.

  • 31.

  • 32.

  • 33.

  • 34.

  • 35.

  • 36.

  • 37.

  • 38.

  • 39.

  • 40.

  • 41.

  • 42.

  • 43.

  • 44.

  • 45.

  • 46.

  • 47.

  • 48.

  • 49.

  • 50.

  • 51.

  • 52.

  • 53.

  • 54.

  • 55.

  • 56.

  • 57.

  • 58.

  • 59.

  • 60.

  • 61.

  • 62.

  • js 调用 C++ 扩展

复制

var addon = require('bindings')('addon');//调用C++扩展var engine = new addon.Engine();console.log( `num1 + num2  = ${engine.add(1,2)}`);//输出3
  • 1.

  • 2.

  • 3.

在 package.json 目录下,执行 npm install、npm run test,可以看到 js 调用 C++ 接口成功,输出两个数字相加结果。

9816f5a921ff9c67483834fbb49033642a4436.png

示例2 

网易云信音视频通话 nertc-electron-sdk,采 Node-API 方式进行开发,将 C++ 原生 sdk 封装成 Nodejs 模块(nertc-electron-sdk.node),结合 Electron 可以快速实现音视频通话。github demo 体验地址:

https://github.com/netease-im/Basic-Video-Call/tree/master/Group-Video/NERtcSample-GroupVideoCall-Electron

04  常见问题

  • Electron 应用中 js 调用 C++ 扩展时,提示 Error: The specified module could not be found。
    答:该错误表示能找到 C++ 扩展模块(.node)但是加载失败,因为 .node 会依赖其他 .dll 和 C++ 运行库,缺少这些库时就会报上面的错误,使用 depends 查看缺少哪种库,配置即可。

  • 运行使用 C++ 扩展的 Electron 应用,提示 The specifield module could not be found。
    答:该错误表示找不到 C++ 扩展模块。在项目 package.json 文件中配置 extraFiles 字段,将扩展拷贝到 Electron 可加载目录即可。

  • Electron 加载 C++ 扩展时提示:Module parse failed: Unexpected character '�'。
    答:webpack 只能识别 js 和 json 文件无法识别 C++ 扩展模式,在 Electron 打包时需要在 vue.config.js 中配置 C++ 扩展的 loader。

  • 更多常见问题汇总:
    https://doc.yunxin.163.com/docs/jcyOTA0ODM/jU4NTEwNzg?platformId=50456#9


19    2022-05-12 11:41:16    开发 框架 程序