原文链接:devongovett.me/blog/static…
作者:Devon
最近我一直在推进将更多Parcel功能移植到Rust的工作。基于Rust的工具面临的挑战之一是如何支持插件。许多常用工具已有Rust版本:JavaScript的SWC和OXC、CSS的Lightning CSS、SVG的oxvg等。但像 React Compiler、Less 和 Sass 这样的流行工具仍基于 JavaScript 编写,因此我们需要在 Rust 工具内部运行这些插件的解决方案。
一种实现方式是通过 napi 将 Rust 核心嵌入 Node。在此模型中,程序通过 JavaScript 入口点调用 Rust 代码。当 Rust 代码需要调用基于 JS 的插件时,会回调至 JavaScript 引擎。Lightning CSS的JS插件正是如此实现。这种方案存在性能开销:我构建Lightning CSS时测得,启用JS插件时运行速度约下降7倍。
另一种类似方案是采用跨进程通信。该模型中存在Rust入口点,当需要运行插件时会启动子进程来运行Node。此方案同样存在显著性能开销。
静态Hermes
Hermes是Facebook为React Native定制的JavaScript引擎。最新版本静态Hermes采用了全新方案:不再运行时调用即时编译器(JIT),而是预先将JavaScript编译为字节码或原生二进制代码。这大大缩短了运行时编译优化代码所需的启动时间,在手机设备上尤为显著。
静态Hermes的工作原理是将JavaScript编译为C代码,再通过LLVM编译器生成机器代码。最终产出完全独立的二进制文件,无需JavaScript虚拟机即可运行。编译后的C代码调用Hermes提供的辅助函数,这些函数通过静态链接嵌入二进制文件(类似Rust等语言的标准库机制)。此方案不仅借助LLVM的高级优化提升性能,更极大简化了将JavaScript嵌入Rust等支持C接口语言程序的流程。
将Less.js编译为C语言
我着手尝试为Parcel构建一个Less插件,以便从Rust中调用。借助Static Hermes,我成功将其编译为C库,从而能够在Rust中调用。
第一步是将less npm模块打包成单个JavaScript文件,且不依赖任何外部资源。由于Hermes不支持Node模块,所有内容必须自成体系,也不能依赖fs或path等内置Node模块。为此我自然选择了Parcel进行封装。
// Use the environment-agnostic build of Less, and shim the PluginLoader.
const less = require('less/lib/less').default({}, {});
less.PluginLoader = function() {}
// Expose a global function to compile a string of Less code to CSS.
function compile(input) {
let result;
less.render(input, (err, res) => {
result = res.css;
});
return result;
}
globalThis.compile = compile;
使用Parcel进行编译:
parcel build less.js --no-optimize
这将生成 dist/less.js,一个完全自包含的文件,其中暴露了一个全局compile函数。
接下来,我们将此文件编译为 C 库。首先,你需要构建 Static Hermes 本身。请遵循其官方指南进行操作。
./build_release/bin/shermes -O -c -exported-unit=less dist/less.js
-O创建优化构建-c编译为本机对象文件,可后续链接至更大程序-exported-unit=less指示 Hermes 跳过创建主函数,并导出名为less的编译单元
这将生成 less.o 对象文件。(若需查看编译的 C 源代码,请将 -c 编译器标志替换为 -emit-c。)
接下来,我们需要一个小型C封装函数来调用JavaScript函数。
// compile.c
#include <stdlib.h>
#include <hermes/VM/static_h.h>
#include <hermes/hermes.h>
// Declaration for the `less` unit created by Static Hermes.
// This will come from `less.o`
extern "C" SHUnit sh_export_less;
extern "C" char* compile_less(char *in) {
// Initialize the hermes runtime.
static SHRuntime *s_shRuntime = nullptr;
static facebook::hermes::HermesRuntime *s_hermes = nullptr;
if (s_shRuntime == nullptr) {
s_shRuntime = _sh_init(0, nullptr);
s_hermes = _sh_get_hermes_runtime(s_shRuntime);
if (!_sh_initialize_units(s_shRuntime, 1, &sh_export_less)) {
abort();
}
}
// Get the global `compile` function and call it.
std::string res = s_hermes->global()
.getPropertyAsFunction(*s_hermes, "compile")
.call(*s_hermes, std::string(in))
.getString(*s_hermes)
.utf8(*s_hermes);
// Convert the C++ string into a C string we can return.
char* result = new char[res.size() + 1];
strcpy(result, res.c_str());
return result;
}
使用 clang++ 将此代码编译为另一个目标文件:
clang++ -c -O3 -std=c++17 -IAPI -IAPI/jsi -Iinclude -Ipublic -Ibuild_release/lib/config compile.c
-c生成对象文件-O3生成优化构建-std=c++17启用 C++17 语言特性-I包含 Hermes 的头文件
这将生成 compile.o 对象文件。
最后,我们需要从 Rust 调用 compile_less C 函数。
// main.rs
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
// Declare the C function we will call.
extern "C" {
fn compile_less(input: *const c_char) -> *const c_char;
}
fn main() {
// Create a C string.
let input = CString::new(
r#"// Variables
@link-color: #428bca;
@link-color-hover: darken(@link-color, 10%);
a,
.link {
color: @link-color;
}
a:hover {
color: @link-color-hover;
}
.widget {
color: #fff;
background: @link-color;
}
"#,
)
.unwrap();
// Call the C function and convert it to a Rust String.
let res = unsafe {
let ptr = compile_less(input.as_ptr());
CStr::from_ptr(ptr).to_string_lossy().into_owned()
};
// Print it.
println!("OUTPUT: {}", res);
}
使用rustc编译此代码并链接所有内容:
rustc main.rs -O -C link-arg=less.o -C link-arg=compile.o -Lbuild_release/lib -Lbuild_release/jsi -Lbuild_release/tools/shermes -lshermes_console_a -lhermesvm_a -ljsi -lc++ -Lbuild_release/external/boost/boost_1_86_0/libs/context/ -lboost_context -l framework=Foundation
-O生成优化构建-C link-arg=less.o -C link-arg=compile.o链接我们之前创建的 C 库-L... 添加库搜索路径-l... 链接 Hermes 库及其依赖项-l framework=FoundationmacOS 专属,用于链接 Foundation 框架
现在你可以运行程序,见证它通过 Rust 编译 Less!🪄
./main
结论
这只是一个简单的初始示例,但它展示了原生工具与预编译的基于JS的插件集成时的潜力,无需嵌入解释器。另一个潜在用例是基于Babel的React编译器——对许多人而言,这是其构建管道中仅存的基于JS的工具之一。我曾短暂尝试过,但遇到了若干问题,这可能是当前功能尚不完善所致。