首页
/ Electron 中的原生代码(Native Code):从零编写一个可在 Electron 中运行的 Node.js 原生插件

Electron 中的原生代码(Native Code):从零编写一个可在 Electron 中运行的 Node.js 原生插件

2026-09-07 13:58:10作者:廉皓灿Ida

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/rebuildelectron-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_VERSION and 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 集成):

环境要求(Requirements)

本教程假定你已经安装了 Node.js 与 npm,且具备所在平台编译 C/C++ 的基础工具链:Windows 上的 Visual Studio、macOS 上的 Xcode,或 Linux 上的 GCC/Clang。更详细的安装指引可参阅 node-gyp 官方 README。

macOS

需要 Xcode Command Line Tools,它们提供编译与构建工具(主要是 clangclang++make)。尚未安装时,以下命令会引导安装:

xcode-select --install

Windows

官方 Node.js 安装器提供可选的 “Tools for Native Modules” 组件,一键安装编译 C++ 模块所需的基础工具——具体包括 Python 3 与 “Visual Studio Desktop development with C++” 工作负载。此外也可以选用 chocolateywinget 或 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 目录路径动态取回。
  • dependenciesnode-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)

逐段拆解这段代码:

  1. 定义继承自 Napi::ObjectWrap<MyAddon>MyAddon 类。ObjectWrap 负责把 C++ 类“包装”给 JavaScript 使用。
  2. 静态方法 Init
    • DefineClass 定义名为 MyAddon 的 JavaScript 类,并注册实例方法 helloWorld
    • Napi::Persistent 创建指向构造函数的持久引用(防止被垃圾回收,确保类定义生命周期安全),并存入 env 的实例数据;
    • 把构造函数导出到 exports 对象上。
  3. 构造函数把参数原样转交父类。
  4. 实例方法 HelloWorld 的流程:
    • 取 Napi 环境(info.Env());
    • 校验入参:少于 1 个参数或首参非字符串时,抛出 JS 侧 TypeError 并返回 null
    • 把 JavaScript 字符串转成 C++ 字符串(info[0].As<Napi::String>());
    • 调用真正的 C++ 函数 cpp_code::hello_world
    • 把结果字符串转回 JavaScript 字符串并返回(Napi::String::New)。
  5. 定义初始化函数 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}`
  }
}

包装层的设计要点:

  1. 通过 bindings('my_addon') 加载编译好的原生插件(自动搜索 .node 文件);
  2. 类继承 EventEmitter,为将来需要派发事件的能力预留接口;
  3. 实例化 C++ 类,并提供更简洁的 API;
  4. 在 JS 侧补充输入校验(非字符串直接抛 TypeError);
  5. 导出单例实例;
  6. 对不受支持的平台提供优雅降级(打印警告并回退为纯 JS 实现)。

仓库印证:fixture spec/fixtures/native-addon/echo/lib/echo.js 展示了另一种常见加载姿势——直接用 require('../build/Release/echo.node') 加载产物并二次导出同步/异步/线程安全三个函数。这也说明“如何加载”并不唯一,bindings 只是最省心的辅助。

构建并测试插件

现在执行构建:

npm run build

该命令会依次运行 node-gyp configurenode-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 应用,需要三步:

  1. 把它作为依赖加入你的 Electron 项目;
  2. 针对你所使用的具体 Electron 版本重新构建(因为 Electron 与 Node 的 ABI 不同,详见 使用原生 Node 模块)。如果使用 electron-forge,这一步(通过 @electron/rebuild)在开发态与打包发布态都会被自动处理;
  3. 在任何启用 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 插件

其中 echobinding.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::ObjectWrapNapi::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 为准,这里先给结论:

  1. 安装后统一重编译(推荐):安装 @electron/rebuild,每次 npm install 后执行 ./node_modules/.bin/electron-rebuild;Electron Forge/Packager 体系会自动集成。
  2. 利用 npm 环境变量:设置 npm_config_runtime=electronnpm_config_disturl=https://electronjs.org/headersnpm_config_target=<Electron 版本> 等,使安装过程直接产出面向 Electron 的产物。
  3. node-gyp 手动重建(适合模块作者在开发期验证):HOME=~/.electron-gyp node-gyp rebuild --target=<版本> --arch=x64 --dist-url=...
  4. 针对自定义 Electron 构建:使用 npm rebuild --nodedir=.../gen/node_headers 指向你自建版本的 Node 头文件。

排障要点(同样来自上述文档):拿不准就先跑 @electron/rebuild;确认模块与目标平台/架构兼容;Windows 上切勿把 win_delay_load_hook 置为 false;升级 Electron 后通常必须重建模块。Windows 下若遇到 Module did not self-registerThe specified procedure could not be found,多半是延迟加载钩子(delay-load hook)未正确链接——需要链接 Electron 的 node.lib 并保证 /DELAYLOAD:node.exe 生效。

用非 C++ 语言编写原生代码

原生插件开发并不局限于 C++。可选语言与工具包括:

  • Rust:可使用 napi-rsneonnode-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 重编译的细节与排障方法内化为日常开发习惯。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388