Electron 中的原生代码(Native Code):从零编写一个可在 Electron 中运行的 Node.js 原生插件
Electron 最具威力的特性之一,是能在同一应用里把 Web 技术与原生代码结合起来——既可以用 JavaScript/HTML/CSS 构建界面,也可以把计算密集的逻辑、乃至“偶尔需要的原生用户界面”下沉到 C++、Rust 等原生语言实现。本指南以仓库 docs/tutorial/native-code-and-electron.md 为主线,带你从环境准备开始,完整搭建一个基于 C++ 的 Native Node.js Addon,并在 Electron 主进程中调用它;同时结合仓库内的原生插件测试工程(spec/fixtures/native-addon)与构建脚本,解释这些插件为何能被 Electron 正确加载。
为什么 Electron 需要原生代码
Electron 之所以能调用原生代码,是因为它构建在 “Native Node.js Addons”(原生 Node.js 插件) 机制之上。你在生态中遇到的许多包——例如著名的 sqlite——就是用原生代码把 JavaScript 与原生技术(如 SQLite 的 C 库)缝合在一起的。借助这一特性,你的 Electron 应用可以做到任何“纯原生应用”能做到的事情:
- 访问 JavaScript 中没有的原生平台 API:macOS、Windows、Linux 上几乎任何操作系统级 API 都可调用;
- 创建与原生桌面框架交互的 UI 组件;
- 集成既有原生库(第三方的 C/C++ 库等);
- 实现比 JavaScript 更快的性能关键型代码。
原生 Node.js 插件本质上是一类动态链接共享对象(Unix 系为 .so/.dylib,Windows 上为 DLL),可用 require() 或 import 加载进 Node.js 或 Electron。它们对外行为与普通 JavaScript 模块一致,只是内部暴露的是 C++、Rust 或其他可编译为原生代码的语言所写的接口。
ABI 差异:为什么需要为 Electron 重新编译
理解 Electron 原生开发,必须先理解“重编译”的必要性。Electron 与某个给定版本的 Node.js 应用二进制接口(ABI)并不一致——例如 Electron 使用 Chromium 的 BoringSSL 而非 OpenSSL——因此原生模块必须针对 Electron 重新编译,否则运行时会得到典型的版本错误:
Error: The module '/path/to/native/module.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION $XYZ. This version of Node.js requires
NODE_MODULE_VERSION $ABC. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or `npm install`).
这一现象的完整解释见仓库文档 docs/tutorial/using-native-node-modules.md:Node 的 C++ 模块 API 会在模块内嵌 NODE_MODULE_VERSION,编译目标与加载目标不一致就会拒绝加载。而 Electron 在 Windows 等平台上不再提供 node.dll,符号由 electron.exe 导出,这也是“用 @electron/rebuild 或 electron-rebuild 重新构建、而非直接复用 Node 产物”的根本原因。在 script/spec-runner.js 的注释中可以看到 Electron 自己对此类机制的处理:
“N-API addons are ABI-stable, so that is fine for them, but addons using Node's C++ module API embed
NODE_MODULE_VERSIONand fail to load in Electron unless they are compiled against the headers configured above.”
即:基于 N-API(C ABI 稳定)的插件可直接复用,而基于 Node C++ 模块 API 的插件则必须以 electron:requiresElectronHeaders 标记并在 fixture 中被 node-gyp 针对 Electron 头文件重建。
对应能力扩展参考
在动手之前值得先了解,本仓库还有两篇平台级续篇,分别讲解 Windows/macOS/Linux 上的具体场景(GTK/Objective-C/Swift/Win32 集成):
- 原生代码与 Electron:C++(Linux/GTK3)
- 原生代码与 Electron:C++(Windows)
- 原生代码与 Electron:Objective-C(macOS)
- 原生代码与 Electron:Swift(macOS)
环境要求(Requirements)
本教程假定你已经安装了 Node.js 与 npm,且具备所在平台编译 C/C++ 的基础工具链:Windows 上的 Visual Studio、macOS 上的 Xcode,或 Linux 上的 GCC/Clang。更详细的安装指引可参阅 node-gyp 官方 README。
macOS
需要 Xcode Command Line Tools,它们提供编译与构建工具(主要是 clang、clang++ 与 make)。尚未安装时,以下命令会引导安装:
xcode-select --install
Windows
官方 Node.js 安装器提供可选的 “Tools for Native Modules” 组件,一键安装编译 C++ 模块所需的基础工具——具体包括 Python 3 与 “Visual Studio Desktop development with C++” 工作负载。此外也可以选用 chocolatey、winget 或 Windows Store 完成同等配置。
Linux
- 受支持的 Python 版本
make- 一套可用的 C/C++ 编译器工具链,例如 GCC
教程:为 Electron 创建原生 Node.js 插件
下面按“通用、跨平台”的主线,用 C++ 从零构建一个可在 Electron 里运行的基础插件。完成本教程后,你可以再进入上述任一平台专项教程。
第一步:创建并配置包(package)
首先创建承载插件的 Node.js 包:
mkdir my-native-addon
cd my-native-addon
npm init -y
然后安装两个关键依赖:
npm install node-addon-api bindings
它们的职责分别是:
node-addon-api:Node.js 底层 API 之上的 C++ 封装。它提供更顺手、更安全的 C++ 面向对象接口,替代原始的 C 风格 API(后者正是仓库 fixture spec/fixtures/native-addon/echo/binding.cc 中直接操作napi_env/napi_callback_info的写法,node-addon-api就是这类底层调用的高层封装)。bindings:加载辅助模块,负责自动找到编译好的.node文件,简化加载过程。
接着更新 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"
}
}
这两个脚本的作用:
clean:删除build目录,保证全新构建;build:执行标准 node-gyp 构建流程,把 C++ 源码编译为插件。
第二步:搭建构建系统(node-gyp 与 binding.gyp)
Node.js 插件统一使用 node-gyp 构建。这是一个用 Node.js 编写的跨平台命令行工具,它在幕后调用各平台的原生构建工具:
- Windows:Visual Studio
- macOS:Xcode 或命令行工具
- Linux:GCC 等编译器
配置 binding.gyp
binding.gyp 是类 JSON 的配置文件,告诉 node-gyp 如何构建插件,作用相当于平台无关的 makefile/project 文件。创建一个基础的 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
}
}
}
]
}
逐项拆解这份配置:
target_name:插件名,决定编译产物的文件名(即my_addon.node)。sources:需要编译的源文件列表。本教程使用两个源文件:插件主体文件 + 实际的 C++ 实现。include_dirs:头文件搜索目录。其中看起来比较晦涩的一行<!@(node -p \"require('node-addon-api').include\")会执行一次 Node.js 命令,把node-addon-api的 include 目录路径动态取回。dependencies:node-addon-api依赖项;与 include 目录类似,通过执行 Node.js 命令拿到 node-gyp 所需的正确配置。defines:预处理器宏定义。此处为node-addon-api开启 C++ 异常支持。
平台相关设置:
cflags!与cflags_cc!:Unix 系系统的编译选项(此处用于去除默认的-fno-exceptions,配合上面的异常宏)。xcode_settings:macOS/Xcode 编译器的专有设置(开启 C++ 异常、使用 libc++、设置最低部署版本)。msvs_settings:Windows Visual Studio 的专有设置(ExceptionHandling: 1即开启异常处理)。
扩展阅读:仓库中真实的 GTK3 平台示例 docs/tutorial/native-code-and-electron-cpp-linux.md 展示了更复杂的
binding.gyp,其中利用conditions把 Linux 配置包裹在['OS=="linux"', {...}]条件里,并使用pkg-config的<!@命令展开语法自动定位 GTK3 头文件与链接库。Linux 下最终链接命令需要带上 GTK3 库,这正是“动态链接共享对象”的典型形态。
接着创建项目目录结构:
mkdir src
mkdir include
mkdir js
目录规划:
src/:存放源文件;include/:存放头文件;js/:存放 JavaScript 包装层。
第三步:C++ 版 “Hello World”
先在头文件中声明 C++ 接口。创建 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 是头文件保护指令,避免同一编译单元内被多次包含;函数声明放在命名空间内以避免命名冲突。
然后在 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
实现很直白:把一段前缀文本拼接到输入字符串后返回。
编写桥接层 src/my_addon.cc
接下来创建把 C++ 代码桥接到 Node.js/JavaScript 世界的插件代码:
#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)
逐段拆解这段代码:
- 定义继承自
Napi::ObjectWrap<MyAddon>的MyAddon类。ObjectWrap负责把 C++ 类“包装”给 JavaScript 使用。 - 静态方法
Init:- 用
DefineClass定义名为MyAddon的 JavaScript 类,并注册实例方法helloWorld; - 用
Napi::Persistent创建指向构造函数的持久引用(防止被垃圾回收,确保类定义生命周期安全),并存入env的实例数据; - 把构造函数导出到
exports对象上。
- 用
- 构造函数把参数原样转交父类。
- 实例方法
HelloWorld的流程:- 取 Napi 环境(
info.Env()); - 校验入参:少于 1 个参数或首参非字符串时,抛出 JS 侧
TypeError并返回null; - 把 JavaScript 字符串转成 C++ 字符串(
info[0].As<Napi::String>()); - 调用真正的 C++ 函数
cpp_code::hello_world; - 把结果字符串转回 JavaScript 字符串并返回(
Napi::String::New)。
- 取 Napi 环境(
- 定义初始化函数
Init并通过NODE_API_MODULE(my_addon, Init)宏注册,使模块可被 Node.js/Electron 加载。
编写 JavaScript 包装层 js/index.js
为了更友好的使用体验,创建一个 JavaScript 包装类:
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}`
}
}
包装层的设计要点:
- 通过
bindings('my_addon')加载编译好的原生插件(自动搜索.node文件); - 类继承
EventEmitter,为将来需要派发事件的能力预留接口; - 实例化 C++ 类,并提供更简洁的 API;
- 在 JS 侧补充输入校验(非字符串直接抛
TypeError); - 导出单例实例;
- 对不受支持的平台提供优雅降级(打印警告并回退为纯 JS 实现)。
仓库印证:fixture spec/fixtures/native-addon/echo/lib/echo.js 展示了另一种常见加载姿势——直接用
require('../build/Release/echo.node')加载产物并二次导出同步/异步/线程安全三个函数。这也说明“如何加载”并不唯一,bindings只是最省心的辅助。
构建并测试插件
现在执行构建:
npm run build
该命令会依次运行 node-gyp configure 与 node-gyp build,把 C++ 代码编译成 .node 文件。接着在项目根目录创建简单的验证脚本 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)
运行测试:
node test.js
一切正常时输出:
Hello from C++! You said: This is a test
在 Electron 中调用该插件
要把插件用于 Electron 应用,需要三步:
- 把它作为依赖加入你的 Electron 项目;
- 针对你所使用的具体 Electron 版本重新构建(因为 Electron 与 Node 的 ABI 不同,详见 使用原生 Node 模块)。如果使用
electron-forge,这一步(通过@electron/rebuild)在开发态与打包发布态都会被自动处理; - 在任何启用 Node.js 的进程(例如主进程)中像普通模块一样
require并调用。
// In your main process
const myAddon = require('my-native-addon')
console.log(myAddon.helloWorld('Electron'))
需要注意:插件只能运行在启用了 Node.js 集成(或 preload 环境中按需引入)的进程里;默认开启沙箱(sandbox)与 contextIsolation 的渲染进程并不具备直接加载原生模块的条件。
仓库侧的原生插件是如何被验证的
本仓库自身就对“Electron 加载原生插件”做了大量端到端验证,是理解上述原理的最佳现场。测试夹具集中在 spec/fixtures/native-addon 目录,每个子工程都是独立的原生插件项目:
| fixture | 关注点 |
|---|---|
echo |
纯 N-API 实现:同步调用、基于 napi_async_work 的异步回调(经 libuv 线程池往返)、以及基于 napi_threadsafe_function + std::thread 的线程安全回调 |
object-wrap |
对照教程中 Napi::ObjectWrap 模式的实践样本 |
external-ab |
跨边界传递 ArrayBuffer 的测试 |
is-valid-window |
各平台窗口句柄有效性判断(macOS/Windows/Posix 三套实现) |
dialog-helper |
供 spec/api-dialog-spec.ts 调用的原生对话框辅助插件 |
uv-dlopen |
验证 Electron 中 libuv 的动态加载能力(见 spec/modules-spec.ts) |
virtual-display |
供 spec/api-browser-window-spec.ts 使用的虚拟显示器桥接插件 |
osr-gpu |
Offscreen 渲染场景下的原生 GPU 插件 |
其中 echo 的 binding.cc 恰好演示了教程正文所说的“底层 N-API 长什么样”:直接用 napi_create_async_work/napi_queue_async_work 排队异步任务、用 napi_create_threadsafe_function/napi_call_threadsafe_function 跨线程安全回调。教程使用的 node-addon-api 正是把这些繁琐 C API 收敛为 Napi::ObjectWrap、Napi::FunctionReference 等 C++ 抽象。
这些 fixture 的构建逻辑见 script/spec-runner.js:spec runner 会用 node-gyp 针对 Electron 的 Node 头文件重建标记了 electron:requiresElectronHeaders 的插件(凡是内嵌 NODE_MODULE_VERSION 的 C++ 模块 API 插件都必须如此);而 N-API 插件因 ABI 稳定可直接复用,无需重编译。这一点从代码层面印证了前文“为何需要/何时不需要重编译”的判断标准。另外,script/generate-config-gypi.py 也注明“node-gyp 构建的原生插件必须与内嵌 V8 的编译方式匹配”,这正是 Electron 与系统 Node 往往无法共享同一份原生产物的根本原因。
原生模块的重编译:四种常用姿势(摘要)
正式把插件部署进 Electron 应用时,重编译有几种常见途径,细节都以仓库文档 docs/tutorial/using-native-node-modules.md 为准,这里先给结论:
- 安装后统一重编译(推荐):安装
@electron/rebuild,每次npm install后执行./node_modules/.bin/electron-rebuild;Electron Forge/Packager 体系会自动集成。 - 利用 npm 环境变量:设置
npm_config_runtime=electron、npm_config_disturl=https://electronjs.org/headers、npm_config_target=<Electron 版本>等,使安装过程直接产出面向 Electron 的产物。 - node-gyp 手动重建(适合模块作者在开发期验证):
HOME=~/.electron-gyp node-gyp rebuild --target=<版本> --arch=x64 --dist-url=...。 - 针对自定义 Electron 构建:使用
npm rebuild --nodedir=.../gen/node_headers指向你自建版本的 Node 头文件。
排障要点(同样来自上述文档):拿不准就先跑 @electron/rebuild;确认模块与目标平台/架构兼容;Windows 上切勿把 win_delay_load_hook 置为 false;升级 Electron 后通常必须重建模块。Windows 下若遇到 Module did not self-register 或 The specified procedure could not be found,多半是延迟加载钩子(delay-load hook)未正确链接——需要链接 Electron 的 node.lib 并保证 /DELAYLOAD:node.exe 生效。
用非 C++ 语言编写原生代码
原生插件开发并不局限于 C++。可选语言与工具包括:
- Rust:可使用
napi-rs、neon或node-bindgen等 crate; - Objective-C / Swift:在 macOS 上通过 Objective-C++(或 Swift 桥接)直接对接 Cocoa/AppKit。
各平台的实现细节差异很大,尤其是访问平台专有 API 或 UI 框架时——例如 Windows 的 Win32 API、COM 组件、UWP/WinRT,以及 macOS 的 Cocoa、AppKit 或 Objective-C runtime。因此你通常会需要两类参考资料:一类在 Node.js 侧,查阅 N-API 官方文档,学习如何向 JavaScript 暴露更复杂的结构(异步线程安全函数、创建 error/promise 等 JS 原生对象);另一类在你对接的技术栈侧,查阅其底层文档,例如微软的 C++/C/汇编与 C++/WinRT 文档、Apple Developer 文档,以及 Objective-C 编程指南。
结论
通过本教程,你已经掌握了 Electron 原生开发的核心链路:用 node-addon-api 编写面向对象的 C++ 插件 → 用 binding.gyp 声明平台无关的构建规则 → 用 bindings 便捷加载 → 针对 Electron 版本重编译后在主进程中调用。接下来两条进阶路径值得深入:一是阅读仓库的 GTK3/Linux 平台教程(它演示了在独立 GTK 线程中运行原生 GUI,并通过 g_main_context_invoke 把事件安全回传 JavaScript 的完整架构,还解释了为何必须使用 GTK3 而非 GTK4——因为 Chromium 内部即加载 GTK3);二是通读 使用原生 Node 模块,把 ABI 重编译的细节与排障方法内化为日常开发习惯。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00