rust是如何在Rspack中运行的?

739 阅读9分钟

科技新品发布企业机构公众号首图.jpg

前言

作为一个前端,一直在听说前端工具链都在用rust重构,可我一直很纳闷,Rust语言跟JavaScript语言之间怎么互通呢?
比如说现在比较火的打包工具Rspack,它就是用Rust写的。
先来说说为啥用Rust

为什么是Rust

众所周知,作为一个前端的打包工具,一直是个没有解决的问题,试想一下,当你运行一个大型的React项目的时候,一个run start就花了十几分钟甚至更长的时间,你的内心直接😈了。
而作为Rust,它有以下几个非常优秀的特点:

  1. 性能:Rust 是一种系统编程语言,具有与 C/C++ 类似的低级别控制,同时提供了内存安全性和并发性。使用 Rust 开发的工具通常比用 JavaScript 或其他动态语言开发的工具更快。Rspack 通过使用 Rust 可以显著提高打包速度,特别是在处理大规模代码库时,能够减少构建时间。
  2. 内存安全性:Rust 的内存管理模型可以在编译时避免很多常见的内存管理错误(如空指针、缓冲区溢出等),从而提高工具的稳定性和安全性。这对于开发一个稳定的打包工具非常重要,因为打包工具需要处理复杂的依赖关系和文件系统操作。
  3. 并发性:Rust 提供了强大的并发编程支持,使得 Rspack 能够更好地利用多核 CPU 的性能。通过在多个线程上并行处理任务,Rspack 可以进一步加快构建过程。
  4. 生态系统和互操作性:Rust 的生态系统正在快速发展,拥有丰富的库和工具支持。Rust 还可以通过 FFI(外部函数接口)与 C 和其他语言的库进行互操作,进一步增强了工具的扩展性和灵活性。
  5. 未来发展和维护:Rust 的设计使得代码更容易维护和扩展,尤其是在大型项目中。这有助于确保 Rspack 在未来能够适应更多需求和技术演进。

由此总结来看,通过使用 Rust,Rspack 能够在性能、稳定性和并发性方面优于传统的 JavaScript 打包工具,为开发者提供更高效的开发体验,这也就是为什么它如此之快的原因。
接下来我们再了解一下RustRspack中具体做了哪些事情,毕竟不可能全部用Rust去实现,有的东西还是需要Js自己去做的。

Rust 做了啥

在 Rspack 中,Rust 主要负责核心的构建任务,包括代码解析、依赖分析、模块打包、优化和生成最终的打包文件等。Rspack 利用 Rust 的性能优势来处理前端打包过程中涉及的大量计算和复杂操作。总结有一下几点:

  1. 代码解析和依赖分析

Rspack 需要解析不同类型的文件(如 JavaScript、TypeScript、CSS 等),并提取其中的依赖关系。包括两部分:

  • 语法解析:通过解析代码文件,识别模块之间的依赖关系。Rust 的性能使得解析大量文件和复杂的代码结构变得更高效。
  • AST(抽象语法树)生成:在解析代码时,Rspack 会生成 AST,方便后续的代码分析和转换。
  • 依赖图的构建:Rust 负责将所有模块的依赖关系构建成依赖图,以便后续的模块打包。
  1. 模块打包

在前端打包过程中,模块打包是核心步骤之一。Rspack 使用 Rust 来处理模块打包的各个方面:

  • 模块合并:将多个模块合并成一个或多个打包文件,这些文件可以直接在浏览器中运行。
  • 代码拆分:根据配置,Rust 会将代码拆分成不同的块(chunk),以实现按需加载(code splitting)。
  • Tree Shaking:Rust 负责分析并移除未使用的代码,以减小打包文件的大小。
  1. 性能优化

Rspack 中的性能优化部分由 Rust 实现,以确保打包过程高效且生成的文件性能良好:

  • 代码压缩(Minification):使用 Rust 实现的压缩算法来减少最终生成文件的大小。
  • 并行化处理:Rspack 使用 Rust 的多线程能力来并行处理打包任务,这大大提高了打包速度。
  • 缓存机制:Rust 处理的缓存系统能够保存打包过程中生成的中间结果,从而加速重复打包任务。
  1. 代码生成

Rust 负责将处理后的模块和资源生成最终可以在浏览器中运行的文件,包括:

  • 生成 JavaScript 文件:将解析和优化后的 JavaScript 模块生成最终的打包文件。
  • 生成 CSS 文件:处理和生成最终的 CSS 文件,确保样式能正确应用。
  • 资源文件的打包:处理图片、字体等静态资源,并生成最终的打包文件。
  1. 插件系统

Rspack 可能会使用 Rust 来实现一些核心插件,这些插件扩展了 Rspack 的功能:

  • 插件机制:Rspack 的插件机制允许开发者通过 Rust 编写插件,从而扩展或修改打包流程。
  • 加载器机制:类似于 Webpack 中的加载器,Rspack 的加载器可以用 Rust 实现,用于处理特定类型的文件(如转换 TypeScript 到 JavaScript)。
  1. 错误处理和调试

Rust 在 Rspack 中还负责处理错误和调试信息的输出:

  • 错误捕获:Rust 通过其内置的错误处理机制,捕获并报告打包过程中出现的各种错误。
  • 调试输出:Rspack 可能使用 Rust 来生成详细的调试信息,以帮助开发者分析打包过程中的问题。
  1. 跨平台支持

由于 Rust 的跨平台能力,Rspack 能够在不同操作系统上高效运行,生成适用于各种环境的打包文件。

Rust怎么做的

那既然做了这么多事情,到底怎么做的?开始我们的正题,Rust语言到底是怎么跟JavaScript语言打交道的?
其实,在 Rspack 的实现中,Rust 与 JavaScript 的交互是通过 FFI(Foreign Function Interface,外部函数接口)实现的。FFI 允许不同编程语言之间进行函数调用和数据交换。在 Rust 和 JavaScript 之间,FFI 通常通过 Node.js 的原生模块来实现。Rust 编写的代码会被编译成共享库(如 .dll.so.dylib 文件),然后通过 Node.js 的原生模块接口调用这些库。
也就是说,我可以用Rust写几个方法,然后编译成不同操作系统(跨平台能力)的共享库,JavaScript就可以直接调用这些方法了。这不是exportimport吗?好像就是这么回事。

基本流程

  1. 编写 Rust 代码
    • 编写需要暴露给 JavaScript 的 Rust 函数,并使用 #[no_mangle] 注解和 extern "C" 关键字,使这些函数能够被其他语言调用。
    • 将 Rust 代码编译成共享库。
  2. 创建 Node.js 原生模块
    • 在 Node.js 中,使用 node-ffi-napineon 等工具加载并调用 Rust 共享库中的函数。
  3. 通过 Node.js 调用 Rust 代码
    • 在 JavaScript 中调用这些函数,并与 Rust 代码交互。

下面我们来实操一下。

基本示例

需要提前安装 Rust 运行环境,博主是 Mac 下运行的。

Step 1: 编写 Rust 代码
首先,编写一个简单的 Rust 函数,将其暴露给 JavaScript:

# Cargo.toml
[package]
name = "my_rust_lib"
version = "0.1.0"
edition = "2021"

[dependencies]
# `libc` is a commonly used dependency when interfacing with C or other languages
libc = "0.2"

[lib]
crate-type = ["cdylib"]
// src/lib.rs
use std::ffi::{CStr, CString};
use std::os::raw::c_char;

#[no_mangle]
pub extern "C" fn hello_from_rust() -> *const c_char {
    let greeting = CString::new("Hello from Rust!").unwrap();
    greeting.into_raw() // 转换为指针返回
}

#[no_mangle]
pub extern "C" fn free_string(s: *mut c_char) {
    if s.is_null() { return }
    unsafe {
        CString::from_raw(s); // 释放内存
    }
}
  • #[no_mangle]:这指示 Rust 编译器不要改变函数名称,使其保持与原始函数名一致,这样可以在其他语言中直接调用。
  • extern "C":使函数使用 C ABI(应用二进制接口),这使得它可以被其他语言(如 JavaScript/Node.js)调用。
  • hello_from_rust 函数返回一个指向 C 风格字符串的指针,字符串内容是 "Hello from Rust!"。
  • free_string 函数用来释放通过 hello_from_rust 返回的字符串指针所占用的内存。

Step 2: 编译 Rust 代码
编译这个 Rust 代码为共享库(动态链接库,类似npm link):

cargo build --release

target/release 目录下,你会得到一个编译后的共享库文件。例如,在 macOS 上它可能是 libmy_rust_lib.dylib,在 Linux 上是 libmy_rust_lib.so,在 Windows 上是 my_rust_lib.dll
具体文件截图如下:
image.png
Step 3: 创建 Node.js 原生模块
接下来,在 Node.js 中加载这个共享库并调用 Rust 函数。
安装必要的 npm 包:

npm install ffi-napi ref-napi
  • ffi-napi:用于加载和调用动态链接库中的函数。
  • ref-napi:用于处理指针等复杂数据类型。

编写 Node.js 代码 (index.js):

const ffi = require('ffi-napi');
const ref = require('ref-napi');

// 定义 C 的 `char *` 类型
const charPtr = ref.refType(ref.types.CString);

// 加载 Rust 编译后的共享库
const lib = ffi.Library('./target/release/libmy_rust_lib', {
  'hello_from_rust': [charPtr, []],
  'free_string': ['void', [charPtr]]
});

// 调用 Rust 函数
const messagePtr = lib.hello_from_rust();
const message = ref.readCString(messagePtr, 0);
console.log(message); // 输出 "Hello from Rust!"

// 释放在 Rust 中分配的内存
lib.free_string(messagePtr);

在这个代码中:

  • ffi.Library:加载 Rust 共享库,并定义可以调用的函数。
  • hello_from_rust 函数返回一个 char * 类型的字符串指针。
  • free_string 函数用来释放通过 hello_from_rust 函数返回的指针所占用的内存。

Step 4: 运行 Node.js 代码
运行 Node.js 代码:

node index.js

你将看到输出:

Hello from Rust!

总结

在实际发布的过程中,开发者会将编译好的二进制文件与其他 JavaScript 代码一起打包,并发布到 npm 这样的包管理平台。最终的 npm 包中已经包含了这些二进制文件,用户只需要安装该 npm 包即可,无需额外安装 Rust。
当用户通过 npm install rspack 安装 Rspack 时,npm 会下载已经预编译好的二进制文件(或在本地编译),并将其集成到用户的 Node.js 环境中。用户不需要关心底层的 Rust 代码,因为所有的细节都已经封装好。
用户通过 Node.js 来调用 Rspack 的功能,实际上是在调用已经编译好的二进制文件。这些二进制文件以 Node.js 原生模块的形式存在,并且可以直接在 JavaScript 中调用。

讲了这么多,你可能对Rust语言不了解,但是作为一个前端,至少大概要明白我们已经站在了巨人的肩膀上了。