Electron原生模块开发与调用实践指南
Electron原生模块开发与调用实践指南
在Electron应用开发过程中,开发者常常会遇到一些标准Node环境和Electron API无法直接解决的特殊场景。这些场景可能涉及系统底层交互、高性能计算、特定硬件接口访问等复杂需求。为了突破这些限制,开发者需要借助原生模块——即.dll、.dylib、.so或.node文件——来扩展应用的功能边界。
原生模块为Electron应用提供了强大的扩展能力,使开发者能够:
- 突破JavaScript的性能瓶颈,通过使用C/C++、Rust等高性能语言编写核心功能模块。
- 直接访问底层系统硬件和操作系统接口,实现JavaScript难以直接完成的底层操作。
- 集成现有的系统级库和第三方依赖,极大地扩展应用的功能可能性。
- 优化计算密集型任务的执行效率,显著提升应用的整体性能表现。
然而,原生模块的开发和集成并非易事。它要求开发者具备跨平台编译、系统底层编程、性能优化等多方面的专业技能。本文将系统性地介绍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这个库,不过现在这个库已经不怎么维护了,而且随着Electron的升级,这个库的兼容性也越来越差。还有一个点就是依赖环境可能会让你非常头疼,因为它严重依赖node-gyp环境。后来发现koffi这个库,简直就是救星,不管是从性能还是兼容性都有相当大的提升。
基础使用
这里我们演示一下基础的使用,由点到面去了解如何使用koffi。
构建dylib、dll文件
我们以构建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程序设计语言
- 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的兼容性以及安全性,做好容错相关的措施,不断的实践和调优才能创建出一个健壮的程序。