foreword
Early cross-platform desktop application development mostly used Qt and C++. Affected by language learning cost and development efficiency, more and more people turned their attention to Electron. Electron is a cross-platform development framework with Nodejs and Chromium as the core.
Electron develops desktop applications based on Web technology. Web technology is widely used in the field of software development. The ecology is relatively mature, the learning cost is low, and the development efficiency is high. However, the Web is stretched when dealing with multi-threaded and concurrent scenarios. The bottom layer of Electron is supported by Nodejs, and the plug-in module of Nodejs has the ability to call C++. C++ is very suitable for processing complex services such as high concurrency, audio and video, and makes up for the performance problems of the Web. This article introduces the application of js and C++ mixed programming in Electron desktop programs.
There are several ways to use C++ in Nodejs:
- Use C++ programs as independent subprocesses.
- Called by node-ffi.
- Nodejs extension, compiling C++ code into Nodejs module, this article mainly introduces this method.
C++ extension
Introduction to C++ Extensions
Nodejs itself is written in C++, so we can use our own Nodejs modules written in C++, which can be used like Nodejs native modules. The C++ extension format is .node, which is essentially a dynamic link library, equivalent to .dll under Windows. The C++ extension is loaded in Nodejs via dlopen as a dynamic link library.
C++ extension architecture diagram:
Several ways of C++ extension implementation
There are three ways to implement C++ extensions: native mode, nan, and Node-API.
* native mode
Develop directly using Nodejs API and Chrome V8 API, which has long been abandoned.
Features: Once the interfaces of Nodejs API and Chrome V8 API are changed, C++ extensions that rely on these APIs cannot be used, and C++ extensions of a specific version can only be used in the environment of the corresponding version of Nodejs.
*nan (Native Abstractions for Nodejs)
nan is the abstract interface set of Nodejs. According to the current Nodejs version, nan uses macros to judge and execute the API of the corresponding version.
Features : C++ extensions run in different versions of Nodejs and need to be recompiled. After Nodejs is upgraded to a higher version, there is an interface incompatibility problem.
- Node-API
Node-API uses the Nodejs binary interface, which is more stable than the nan method.
Features: As long as the abi version numbers of different versions of Nodejs are the same, C++ extensions can be used directly without recompilation, eliminating the difference between Nodejs versions.
build tools
- node-gyp
node-gyp encapsulates gyp (a build tool written by Chromium), and binding.gyp is its configuration file.
node-gyp works in two processes:
a. Combine with binding.gyp to generate the project configuration under the corresponding platform, for example: generate a .sln project file under Windwos.
b. The project file is compiled to generate the C++ extension.
binding.gyp configuration file, take Windows as an example:
{
"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'
]
}
},
}
]
],
},
]
}
Field Description:
target_name: The name of the target, which will be used as the project name in the generated Visual Studio solution.
type: optional: static_library static library, executable executable file, shared_library shared library.
defines: C preprocessor definitions that will be passed on the compile command line (using the -D or /D option).
include_dirs: The directory where the C++ header files are located.
sources: C++ source files.
conditions: Adapt to different environment configuration condition blocks.
copies: Copy the dll dynamic library to the build directory.
library_dirs: Configure the lib library directory to the vs project.
libraries: Libraries that the project depends on.
msvs_settings: Property settings in Visual Studio.
node-gyp compilation command:
node-gyp clean //清空上一次构建目录
node-gyp configure //配置项目
node-gyp build //项目编译,生成C++扩展
node-gyp rebuild //重新生成C++扩展,相当于clean configure build的结合
* cmake-js
cmake-js works similarly to node-gyp. cmake-js is a build system based on CMake, and node-gyp is a gyp tool based on Goole, which will not be introduced in detail here.
callback event handling
Nodejs runs in a single thread, but it can support high concurrency by relying on event loop implementation. Simply put, the Nodejs main thread maintains an event queue, receives a time-consuming task, puts the task into the queue, and continues to execute other tasks. When the main thread is idle, it traverses the event queue, handles non-I/O tasks in person, and returns to the upper-level call through the callback function. The I/O task is put into the thread pool for execution, and the callback function is specified, and then other tasks continue to be executed.
When the C++ extension calls the js callback function, it will hang in a libuv thread pool in Nodejs to process the callback function. When the main thread of Nodejs is idle, it will traverse the thread pool and process tasks. For details of libuv, refer to nertc-electron-sdk:
https://github.com/netease-im/node-nertc-sdk/blob/main/nertc_sdk_node/nertc_node_engine_event_handler.cpp
Hybrid programming practice
Example 1
Combined with node-addon-api for demonstration, node-addon-api encapsulates the Node-API interface for easy development. This example completes the js call C++ function to realize the addition of two numbers.
- Project structure
- package.json configuration file
//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 configuration file
//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++ extension
//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 call C++ extension
var addon = require('bindings')('addon');//调用C++扩展
var engine = new addon.Engine();
console.log( `num1 + num2 = ${engine.add(1,2)}`);//输出3
In the package.json directory, execute npm install and npm run test, you can see that js successfully calls the C++ interface, and outputs the result of adding two numbers.
Example 2
Netease cloud audio and video call nertc-electron-sdk is developed using Node-API, and the C++ native sdk is encapsulated into a Nodejs module (nertc-electron-sdk.node). Combined with Electron, audio and video calls can be quickly realized.
github demo experience address: https://github.com/netease-im/Basic-Video-Call/tree/master/Group-Video/NERtcSample-GroupVideoCall-Electron
common problem
- When js calls C++ extension in Electron application, it prompts Error: The specified module could not be found.
Answer: This error means that the C++ extension module (.node) can be found but the loading fails, because .node will depend on other .dll and C++ runtime libraries. If these libraries are missing, the above error will be reported. Use depends to check which library is missing. Configure it.
- When running the Electron application with C++ extension, it prompts The specifield module could not be found.
Answer: The error indicates that the C++ extension module could not be found. Configure the extraFiles field in the project package.json file and copy the extension to the Electron loadable directory.
- When Electron loads a C++ extension, it prompts: Module parse failed: Unexpected character '�'.
Answer: webpack can only recognize js and json files but cannot recognize C++ extension mode. When Electron is packaged, you need to configure the C++ extension loader in vue.config.js.
More FAQs:
https://doc.yunxin.163.com/docs/jcyOTA0ODM/jU4NTEwNzg?platformId=50456#9
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。