Skip to main content

原生代码和 Electron

🌐 Native Code and Electron

Electron 最强大的功能之一是能够将网络技术与本地代码结合使用——无论是用于计算密集型逻辑,还是在需要时用于偶尔的本地用户界面。

🌐 One of Electron's most powerful features is the ability to combine web technologies with native code - both for compute-intensive logic as well as for the occasional native user interface, where desired.

Electron 通过建立在“原生 Node.js 插件”之上来实现这一点。你可能已经遇到过一些这样的插件——像著名的 sqlite 这样的包就使用原生代码来结合 JavaScript 和原生技术。你可以利用这个功能,用任何一个完全原生的应用可以做到的方式来扩展你的 Electron 应用:

🌐 Electron does so by building on top of "Native Node.js Addons". You've probably already come across a few of them - packages like the famous sqlite use native code to combine JavaScript and native technologies. You can use this feature to extend your Electron application with anything a fully native application can do:

  • 访问 JavaScript 无法使用的原生平台 API。你可以使用任意 macOS、Windows 或 Linux 操作系统的 API。
  • 创建与原生桌面框架交互的 UI 组件。
  • 与现有的原生库集成。
  • 实现比 JavaScript 运行速度更快的性能关键代码。

原生 Node.js 插件是在类 Unix 系统上的动态链接共享对象,或在 Windows 上的 DLL 文件,可以使用 require()import 函数加载到 Node.js 或 Electron 中。它们的行为就像普通的 JavaScript 模块,但提供了与用 C++、Rust 或其他可以编译为本地代码的语言编写的代码的接口。

🌐 Native Node.js addons are dynamically-linked shared objects (on Unix-like systems) or DLL files (on Windows) that can be loaded into Node.js or Electron using the require() or import functions. They behave just like regular JavaScript modules but provide an interface to code written in C++, Rust, or other languages that can compile to native code.

教程:为 Electron 创建本地 Node.js 插件

🌐 Tutorial: Creating a Native Node.js Addon for Electron

本教程将引导你构建一个基本的 Node.js 原生插件,可用于 Electron 应用。我们将重点介绍适用于所有平台的通用概念,并使用 C++ 作为实现语言。一旦你完成了这个适用于所有原生 Node.js 插件的教程,就可以继续学习我们针对特定平台的教程。

🌐 This tutorial will walk you through building a basic Node.js native addon that can be used in Electron applications. We'll focus on concepts common to all platforms, using C++ as the implementation language. Once you complete this tutorial common to all native Node.js addons, you can move on to one of our platform-specific tutorials.

要求

🌐 Requirements

本教程假设你已经安装了 Node.js 和 npm,以及在你的平台上编译代码所需的基本工具(例如 Windows 上的 Visual Studio、macOS 上的 Xcode,或 Linux 上的 GCC/Clang)。你可以在 node-gyp 说明文档 中找到详细的安装说明。

🌐 This tutorial assumes you have Node.js and npm installed, as well as the basic tools necessary for compiling code on your platform (like Visual Studio on Windows, Xcode on macOS, or GCC/Clang on Linux). You can find detailed instructions in the node-gyp readme.

要求:macOS

🌐 Requirements: macOS

要在 macOS 上构建本地 Node.js 插件,你需要 Xcode 命令行工具。这些工具提供了必要的编译器和构建工具(即 clangclang++make)。如果命令行工具尚未安装,以下命令会提示你进行安装。

🌐 To build native Node.js addons on macOS, you'll need the Xcode Command Line Tools. These provide the necessary compilers and build tools (namely, clang, clang++, and make). The following command will prompt you to install the Command Line Tools if they aren't already installed.

xcode-select --install

要求:Windows

🌐 Requirements: Windows

官方的 Node.js 安装程序提供可选的“本地模块工具”安装,它会安装基本编译 C++ 模块所需的一切——具体包括 Python 3 和“使用 C++ 的 Visual Studio 桌面开发”工作负载。或者,你也可以使用 chocolateywinget 或 Windows 应用商店。

🌐 The official Node.js installer offers the optional installation of "Tools for Native Modules", which installs everything required for the basic compilation of C++ modules - specifically, Python 3 and the "Visual Studio Desktop development with C++" workload. Alternatively, you can use chocolatey, winget, or the Windows Store.

要求:Linux

🌐 Requirements: Linux

1) 创建一个软件包

🌐 1) Creating a package

首先,创建一个包含原生插件的新 Node.js 包:

🌐 First, create a new Node.js package that will contain your native addon:

mkdir my-native-addon
cd my-native-addon
npm init -y

这将创建一个基本的 package.json 文件。接下来,我们将安装所需的依赖:

🌐 This creates a basic package.json file. Next, we'll install the necessary dependencies:

npm install node-addon-api bindings
  • node-addon-api:这是一个针对低级 Node.js API 的 C++ 封装,使构建插件更加容易。它提供了一个面向对象的 C++ API,比原始的 C 风格 API 更方便、更安全使用。
  • bindings:一个辅助模块,用于简化加载已编译本地插件的进程。它会自动处理查找已编译的 .node 文件。

现在,让我们更新我们的 package.json,以包含适当的构建脚本。我们将在下面进一步解释这些脚本具体的作用。

🌐 Now, let's update our package.json to include the appropriate build scripts. We will explain what these specifically do further below.

package.json
{
"name": "my-native-addon",
"version": "1.0.0",
"description": "A native addon for Electron",
"main": "js/index.js",
"scripts": {
"clean": "node -e \"require('fs').rmSync('build', { recursive: true, force: true })\"",
"build": "node-gyp configure && node-gyp build"
},
"dependencies": {
"bindings": "^1.5.0",
"node-addon-api": "^8.3.0"
},
"devDependencies": {
"node-gyp": "^11.1.0"
}
}

这些脚本将:

🌐 These scripts will:

  • clean:删除构建目录,允许全新构建
  • build:运行标准 node-gyp 构建进程以编译你的插件

2) 设置构建系统

🌐 2) Setting up the build system

Node.js 插件使用一种名为 node-gyp 的构建系统,这是一种用 Node.js 编写的跨平台命令行工具。它在幕后使用特定平台的构建工具来编译 Node.js 的本地插件模块:

🌐 Node.js addons use a build system called node-gyp, which is a cross-platform command-line tool written in Node.js. It compiles native addon modules for Node.js using platform-specific build tools behind the scenes:

  • 在 Windows 上:Visual Studio
  • 在 macOS 上:Xcode 或命令行工具
  • 在 Linux 上:GCC 或类似编译器

配置 node-gyp

🌐 Configuring node-gyp

binding.gyp 文件是一个类似 JSON 的配置文件,用来告诉 node-gyp 如何构建你的本地插件。它类似于 make 文件或项目文件,但采用平台无关的格式。让我们创建一个基本的 binding.gyp 文件:

🌐 The binding.gyp file is a JSON-like configuration file that tells node-gyp how to build your native addon. It's similar to a make file or a project file but in a platform-independent format. Let's create a basic binding.gyp file:

binding.gyp
{
"targets": [
{
"target_name": "my_addon",
"sources": [
"src/my_addon.cc",
"src/cpp_code.cc"
],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")",
"include"
],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
],
"defines": [
"NODE_ADDON_API_CPP_EXCEPTIONS"
],
"cflags!": ["-fno-exceptions"],
"cflags_cc!": ["-fno-exceptions"],
"xcode_settings": {
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
"CLANG_CXX_LIBRARY": "libc++",
"MACOSX_DEPLOYMENT_TARGET": "10.14"
},
"msvs_settings": {
"VCCLCompilerTool": {
"ExceptionHandling": 1
}
}
}
]
}

让我们分解此配置:

🌐 Let's break down this configuration:

  • target_name:你的插件名称。这将决定编译后的模块文件名(my_addon.node)。
  • sources:要编译的源文件列表。我们将有两个文件:主插件文件和我们实际的C++实现文件。
  • include_dirs:用于搜索头文件的目录。那行看起来晦涩的 <!@(node -p \"require('node-addon-api').include\") 命令运行了一个 Node.js 命令,以获取 node-addon-api 包含目录的路径。
  • dependenciesnode-addon-api 依赖。类似于包含目录,这会执行一个 Node.js 命令来获取正确的配置。
  • defines:预处理器定义。在这里我们为 node-addon-api 启用 C++ 异常。平台特定设置:
  • cflags! 和 cflags_cc!: 类 Unix 系统的编译器标志
  • xcode_settings:特定于 macOS/Xcode 编译器的设置
  • msvs_settings:特定于 Windows 上的 Microsoft Visual Studio 的设置

现在,为我们的项目创建目录结构:

🌐 Now, create the directory structure for our project:

mkdir src
mkdir include
mkdir js

这将创建:

🌐 This creates:

  • src/:我们的源文件将放在哪里
  • include/:对于头文件
  • js/:对于我们的 JavaScript 封装器

3) 来自 C++ 的“Hello World”

🌐 3) "Hello World" from C++

让我们先在头文件中定义我们的 C++ 接口。创建 include/cpp_code.h:

🌐 Let's start by defining our C++ interface in a header file. Create include/cpp_code.h:

#pragma once
#include <string>

namespace cpp_code {
// A simple function that takes a string input and returns a string
std::string hello_world(const std::string& input);
} // namespace cpp_code

#pragma once 指令是一个头文件保护机制,用于防止同一编译单元中多次包含该文件。实际的函数声明在命名空间内,以避免潜在的名称冲突。

🌐 The #pragma once directive is a header guard that prevents the file from being included multiple times in the same compilation unit. The actual function declaration is inside a namespace to avoid potential name conflicts.

接下来,我们在 src/cpp_code.cc 中实现该函数:

🌐 Next, let's implement the function in src/cpp_code.cc:

src/cpp_code.cc
#include <string>
#include "../include/cpp_code.h"

namespace cpp_code {
std::string hello_world(const std::string& input) {
// Simply concatenate strings and return
return "Hello from C++! You said: " + input;
}
} // namespace cpp_code

这是一个简单的实现,只需将一些文本添加到输入字符串并返回它。

🌐 This is a simple implementation that just adds some text to the input string and returns it.

现在,让我们创建将 C++ 代码与 Node.js/JavaScript 世界连接的插件代码。创建 src/my_addon.cc

🌐 Now, let's create the addon code that bridges our C++ code with the Node.js/JavaScript world. Create src/my_addon.cc:

src/my_addon.cc
#include <napi.h>
#include <string>
#include "../include/cpp_code.h"

// Create a class that will be exposed to JavaScript
class MyAddon : public Napi::ObjectWrap<MyAddon> {
public:
// This static method defines the class for JavaScript
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
// Define the JavaScript class with method(s)
Napi::Function func = DefineClass(env, "MyAddon", {
InstanceMethod("helloWorld", &MyAddon::HelloWorld)
});

// Create a persistent reference to the constructor
Napi::FunctionReference* constructor = new Napi::FunctionReference();
*constructor = Napi::Persistent(func);
env.SetInstanceData(constructor);

// Set the constructor on the exports object
exports.Set("MyAddon", func);
return exports;
}

// Constructor
MyAddon(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<MyAddon>(info) {}

private:
// Method that will be exposed to JavaScript
Napi::Value HelloWorld(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();

// Validate arguments (expecting one string)
if (info.Length() < 1 || !info[0].IsString()) {
Napi::TypeError::New(env, "Expected string argument").ThrowAsJavaScriptException();
return env.Null();
}

// Convert JavaScript string to C++ string
std::string input = info[0].As<Napi::String>();

// Call our C++ function
std::string result = cpp_code::hello_world(input);

// Convert C++ string back to JavaScript string and return
return Napi::String::New(env, result);
}
};

// Initialize the addon
Napi::Object Init(Napi::Env env, Napi::Object exports) {
return MyAddon::Init(env, exports);
}

// Register the initialization function
NODE_API_MODULE(my_addon, Init)

让我们分解此代码:

🌐 Let's break down this code:

  1. 我们定义了一个继承自 Napi::ObjectWrap<MyAddon>MyAddon 类,它负责将我们的 C++ 类封装到 JavaScript 中。
  2. Init 静态方法: 2.1 定义一个包含名为 helloWorld 方法的 JavaScript 类 2.2 创建对构造函数的持久引用(以防垃圾回收) 2.3 导出类构造函数
  3. 构造函数只是将其参数传递给父类。
  4. HelloWorld 方法: 4.1 获取 Napi 环境 4.2 验证输入参数(期望为字符串) 4.3 将 JavaScript 字符串转换为 C++ 字符串 4.4 调用我们的 C++ 函数 4.5 将结果转换回 JavaScript 字符串并返回
  5. 我们定义一个初始化函数并用 NODE_API_MODULE 宏注册它,这使得我们的模块可以被 Node.js 加载。

现在,让我们创建一个 JavaScript 封装器,以便更容易使用该插件。创建 js/index.js:

🌐 Now, let's create a JavaScript wrapper to make the addon easier to use. Create js/index.js:

js/index.js
const EventEmitter = require('node:events')

// Load the native addon using the 'bindings' module
// This will look for the compiled .node file in various places
const bindings = require('bindings')
const native = bindings('my_addon')

// Create a nice JavaScript wrapper
class MyNativeAddon extends EventEmitter {
constructor () {
super()

// Create an instance of our C++ class
this.addon = new native.MyAddon()
}

// Wrap the C++ method with a nicer JavaScript API
helloWorld (input = '') {
if (typeof input !== 'string') {
throw new TypeError('Input must be a string')
}
return this.addon.helloWorld(input)
}
}

// Export a singleton instance
if (process.platform === 'win32' || process.platform === 'darwin' || process.platform === 'linux') {
module.exports = new MyNativeAddon()
} else {
// Provide a fallback for unsupported platforms
console.warn('Native addon not supported on this platform')

module.exports = {
helloWorld: (input) => `Hello from JS! You said: ${input}`
}
}

此 JavaScript 封装器:

🌐 This JavaScript wrapper:

  1. 使用 bindings 加载我们编译的本地插件
  2. 创建一个扩展 EventEmitter 的类(对于可能触发事件的未来扩展很有用)
  3. 实例化我们的 C++ 类并提供更简单的 API
  4. 在 JavaScript 端添加一些输入验证
  5. 导出封装器的单例实例
  6. 句柄优雅地支持不受支持的平台

构建和测试插件

🌐 Building and testing the addon

现在我们可以构建原生插件了:

🌐 Now we can build our native addon:

npm run build

这将运行 node-gyp configurenode-gyp build 将我们的 C++ 代码编译成 .node 文件。 让我们创建一个简单的测试脚本来验证一切是否正常工作。在项目根目录下创建 test.js

🌐 This will run node-gyp configure and node-gyp build to compile our C++ code into a .node file. Let's create a simple test script to verify everything works. Create test.js in the project root:

test.js
// Load our addon
const myAddon = require('./js')

// Try the helloWorld function
const result = myAddon.helloWorld('This is a test')

// Should print: "Hello from C++! You said: This is a test"
console.log(result)

运行测试:

🌐 Run the test:

node test.js

如果一切正常,你应该看到:

🌐 If everything works correctly, you should see:

Hello from C++! You said: This is a test

在 Electron 中使用插件

🌐 Using the addon in Electron

要在 Electron 应用中使用此插件,你需要:

🌐 To use this addon in an Electron application, you would:

  1. 将其作为依赖包含在你的 Electron 项目中
  2. 针对你的特定 Electron 版本构建它。electron-forge 会自动为你处理此步骤—更多详情,请参见 Native Node Modules
  3. 在启用了 Node.js 的进程中,像任何其他模块一样导入和使用它。
// In your main process
const myAddon = require('my-native-addon')

console.log(myAddon.helloWorld('Electron'))

参考和进一步学习

🌐 References and further learning

本地插件开发可以用多种语言编写,而不仅仅是 C++。Rust 可以与像 napi-rsneonnode-bindgen 这样的 crate 一起使用。Objective-C/Swift 可以通过 macOS 上的 Objective-C++ 使用。

🌐 Native addon development can be written in several languages beyond C++. Rust can be used with crates like napi-rs, neon, or node-bindgen. Objective-C/Swift can be used through Objective-C++ on macOS.

具体的实现细节因平台而异,尤其是在访问特定平台的 API 或用户界面框架时,例如 Windows 的 Win32 API、COM 组件、UWP/WinRT,或者 macOS 的 Cocoa、AppKit 或 ObjectiveC 运行时。

🌐 The specific implementation details differ significantly by platform, especially when accessing platform-specific APIs or UI frameworks, like Windows' Win32 API, COM components, UWP/WinRT - or macOS's Cocoa, AppKit, or ObjectiveC runtime.

这意味着你很可能会为你的本地代码使用两组参考资料:首先,在 Node.js 端,使用 N-API 文档 来了解如何向 JavaScript 创建和暴露复杂结构——比如异步线程安全函数调用或创建 JavaScript 原生对象(errorpromise 等)。其次,在你正在使用的技术端,你可能需要查看它们的底层文档:

🌐 This means that you'll likely use two groups of references for your native code: First, on the Node.js side, use the N-API documentation to learn about creating and exposing complex structures to JavaScript - like asynchronous thread-safe function calls or creating JavaScript-native objects (error, promise, etc). Secondly, on the side of the technology you're working with, you'll likely be looking at their lower-level documentation: