前言
晚期跨平台桌面利用开发大多采纳 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 模块,本文次要针对这种形式进行介绍。
C++ 扩大
C++ 扩大简介
Nodejs 自身采纳 C++ 编写,所以咱们能够应用 C++ 编写的本人的 Nodejs 模块,能够像 Nodejs 原生模块一样应用。C++ 扩大格局为 .node,其本质为动态链接库,相当于 Windows 下 .dll。C++ 扩大作为动态链接库,通过 dlopen 在 Nodejs 中加载。
C++ 扩大架构图:
C++ 扩大实现的几种形式
实现 C++ 扩大有3种形式:原生模式、nan、Node-API。
* 原生模式
间接应用 Nodejs API 及 Chrome V8 API 进行开发,这种形式早已被遗弃。
特点:Nodejs API 和 Chrome V8 API 接口一旦变动,依赖这些 API 的 C++ 扩大便无奈应用,特定版本的 C++ 扩大只能在对应版本 Nodejs 环境中应用。
* nan(Native Abstractions for Nodejs)
nan 是 Nodejs 形象接口集,nan 依据以后 Nodejs 版本,应用宏判断执行对应版本的 API。
特点:C++ 扩大在不同版本 Nodejs 中运行,需从新编译,Nodejs 降级到较高版本后呈现接口不兼容问题。
- Node-API
Node-API 应用 Nodejs 二进制接口,相比 nan 形式这些二进制接口更为稳固。
特点:不同版本 Nodejs 只有 abi 版本号统一,C++ 扩大能够间接应用无需从新编译,打消了 Nodejs 版本差别。
构建工具
- node-gyp
node-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' ] } }, } ] ], }, ]}
字段阐明:
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的联合
* cmake-js
cmake-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...
混合编程实际
示例1
联合 node-addon-api 进行演示,node-addon-api 对 Node-API 接口进行了封装开发简略。该实例实现 js 调用 C++ 函数实现两个数字相加。
- 我的项目构造
- 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"}
- 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' ] } ]}
- 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层}}
- js 调用 C++ 扩大
var addon = require('bindings')('addon');//调用C++扩大var engine = new addon.Engine();console.log( `num1 + num2 = ${engine.add(1,2)}`);//输入3
在 package.json 目录下,执行 npm install、npm run test,能够看到 js 调用 C++ 接口胜利,输入两个数字相加后果。
示例2
网易云信音视频通话 nertc-electron-sdk,采 Node-API 形式进行开发,将 C++ 原生 sdk 封装成 Nodejs 模块(nertc-electron-sdk.node),联合 Electron 能够疾速实现音视频通话。
github demo 体验地址:https://github.com/netease-im...
常见问题
- 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/do...