本文首发同名微信公众号:前端徐徐
大家好,我是徐徐。今天我们讲讲如何在 Electron 中进行原生模块开发以及相应的调用实践指南。
前言
在 Electron 应用开发的过程中,开发者常常会遇到一些标准 Node 环境 和 Electron API 无法直接解决的特殊场景。这些场景可能涉及系统底层交互、高性能计算、特定硬件接口访问等复杂需求。为了突破这些限制,开发者需要借助原生模块——即 .dll
、.dylib
、.so
或 .node
文件——来扩展应用的功能边界。
原生模块为 Electron 应用提供了强大的扩展能力,使开发者能够:
1. 突破 JavaScript 的性能瓶颈,通过使用 C/C++、Rust 等高性能语言编写核心功能模块。
2. 直接访问底层系统硬件和操作系统接口,实现 JavaScript 难以直接完成的底层操作。
3. 集成现有的系统级库和第三方依赖,极大地扩展应用的功能可能性。
4. 优化计算密集型任务的执行效率,显著提升应用的整体性能表现。
然而,原生模块的开发和集成并非易事。它要求开发者具备跨平台编译、系统底层编程、性能优化等多方面的专业技能。本文将系统性地介绍 Electron 原生模块的开发流程、调用方法和最佳实践,旨在帮助开发者全面掌握原生模块开发的关键技能,为 Electron 应用赋能。
调用原生模块的技术选择
对于 Electron 调用原生模块,主要有以下技术选择:
- C/C++ 编写的 Node.js 扩展,node-gyp 编译构建。这种方式性能最高,但开发复杂度最大。
- FFI (Foreign Function Interface) 模式,使用
ffi-napi
,ref-napi
,Koffi
等库。这种方式最为灵活,调用系统库最方便。 - Rust 编写的 Node.js 扩展,NAPI-RS 或者 Neon 编译构建。这种方式可以得到安全性和性能的平衡。
- WebAssembly 方式。这种方式可以有跨平台,多语言支持。
- N-API 模式。这种方式官方推荐,版本兼容性好。
对于上面提到的技术,每种技术都有自己的适用场景,可以根据自身的业务场景做相应的选择。第一种方式和最后一种方式都是最复杂的,而且对技术的要求相对较高,需要懂 C++ 相关的开发,环境也是最为复杂的,其余三种在环境和实践操作上更加容易上手,适合前端工程师。当然,用 Rust 开发的话也需要一定的要求,但是我个人觉得 Rust 的环境更为简单和方便,依赖较少。这篇文章主要是讲解第二种和第三种方式的调用,我也是在这两种方式中有相应的实践,所以就选择他们来做讲解。
FFI 调用原生模块
在早期,在 Electron 中 FFI 动态调用动态链接库(.dll, .so, .dylib)大部分都是使用
node-ffi-napi(GitHub - node-ffi-napi/node-ffi-napi: A foreign function interface (FFI) for Node.js, N-API style)
这个库,不过现在这个库已经不怎么维护了,而且随着 Electorn 的升级,
这个库的兼容性也越来越差(Error with Electron and ffi-napi · Issue #238 · node-ffi-napi/node-ffi-napi · GitHub),
还有一个点就是依赖环境可能会让你非常头疼,因为它严重依赖node-gyp 环境。我前期开发 Electorn 也使用的是这个库,
随着时间的推移后面发现了 koffi (Koffi)这个库,
简直就是救星,不管是从性能还是兼容性都有相当大的提升。下图是一个比较
基础使用
这里我们演示一下基础的使用,由点到面去了解如何使用 koffi。
构建dylib、dll文件
我本地的环境是OS X
,我们为了演示方便,就以构建dylib
为例子。我们写一个简单的 C 函数就可以做验证了,我们创建一个sum.c
文件,内容如下
#include <stdint.h>
#if defined(WIN32) || defined(_WIN32)
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif
EXPORT uint64_t sum(int a,int b) {
return a + b;
}
然后你可以执行
gcc -dynamiclib -undefined suppress -flat_namespace sum.c -o sum.dylib
这样就会构建一个sum.dylib
文件,下面我们就可以愉快的调用它了。
当然如果你是 windows 的话,可以执行
cl.exe /D_USRDLL /D_WINDLL sum.c /link /DLL /OUT:sum.dll
node调用.dylib、.dll
我们先安装 koffi
yarn add koffi
然后就可以很愉快的使用了。在 src/main 下新建一个 native 的目录,添加 index.ts,koffi 使用非常简单,如下
import koffi from 'koffi'
import path from 'path'
const sumLib = koffi.load(path.resolve(
__dirname,
"../../resources/dylib/sum.dylib"
))
const dylibNativeSum = sumLib.func('__stdcall','sum','int',['int','int'])
export const dylibCallNativeSum = (a:number,b:number) => {
return dylibNativeSum(a,b)
}
这里需要注意一个点,就是我们需要改动一下 config/vite/main.js 中 rollupOptions 的 external,把koffi导入包转成外部依赖,不然在构建运行的时候会报错。
rollupOptions: {
external: [
"electron",
"sqlite3",
"koffi",
...builtinModules,
],
output: {
entryFileNames: "[name].cjs",
},
},
更多的用法可以参考下面的文档,里面有非常多的用法,包括传值,注册回调等
Rust 编写的 Node.js 扩展
至于为什么要选择 Rust 实现,其实也不是为了学习 Rust 而 Rust,是因为在开发的过程中的确遇到了瓶颈,然后用 Rust 来处理了一些问题,其实也是多了一种选择,特别是处理耗时任务的时候,Rust 表现非常优异,当然他还有其他的优点,比如:内存安全、跨平台编译、零成本抽象、并发模型支持优秀等。下面我们就来尝试在 Electron 中来调用 Rust 构建的 node 包。
Rust是什么
两个链接告诉你,环境的搭建步骤也告诉你了:
https://course.rs/about-book.html
只有把环境搭建好,才可以开始下面的步骤哦。
如何通过 Rust 构建 node 包
这里推荐两个框架:
- NAPI-RS(Home – NAPI-RS)
- NEON-RS(Neon - Electrify Node.js with the power of Rust! | Neon)
两个框架的比较可以参考:Comparison with neon – NAPI-RS
我们在这里选择 NAPI-RS。
首先全局安装一下 @napi-rs/cli
脚手架
pnpm add -g @napi-rs/cli
然后用 napi new
创建一个新的项目,当然,如果你有成熟的分包管理工具也可以在原项目下创建项目,后面在打包构建的时候整合,我们这个为了方便简单演示其原理我们就新建一个项目。
这里我们就选择所有平台,简直就是跨端大杀器。
创建项目后会如下所示。
这里我们可以在 sum 的下面加一个减法的函数 subtraction ,测试一下它的易用性
#[napi]
pub fn subtraction(a: i32, b: i32) -> i32 {
a - b
}
然后直接
pnpm run build
首次构建可能有点慢,但是后面就很快了。构建之后会出现一个你现在构建平台的一个 .node
文件。
我们将其拷贝至我们原有的 Electron 项目中的 resources/node 目录下。现在我们要来引用这个node文件,超级简单。
const rsNative = require(path.resolve(
__dirname,
"../../resources/node/rs-native.darwin-x64.node"
))
export const rsNativeSum = (a:number,b:number) => {
return rsNative.sum(a,b)
}
export const rsNativeSubtraction = (a:number,b:number) => {
return rsNative.subtraction(a,b)
}
是不是感觉上层调用非常方便,不用关心数据类型。到这里一个基础的 Rust 编写的 Node.js 扩展的例子就完成了,上手非常简单。
更加高级的应用就是编写一些 Rust 的应用程序,然后满足一些非常规的需求。
windows 相关的扩张可以参考:GitHub - microsoft/windows-rs: Rust for Windows
mac Os 相关的扩张可以参考:https://crates.io/categories/os::macos-apis
结语
我们这一节给大家展示了如何在 Electron 中开发原生模块以及一些基础调用方式,原生方法的扩展大大得扩展了 Electron 的应用场景,弥补了一些框架的局限性。当然在实际的开发中,我们需要注意一些三方 dll 或者 node 的兼容性以及安全性,做好容错相关的措施,不断的实践和调优才能创建出一个健壮的程序。