Parcel 作者:如何用静态Hermes把JavaScript编译成C语言

原文:devongovett.me/blog/static...,parcel 作者 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(Static Hermes)采用了全新思路:它不再在运行时使用JIT(即时)编译器,而是提前把JavaScript编译成字节码或原生二进制文件。这样就省去了运行时编译和优化代码的启动时间,这在手机上可是个不小的提升。

静态Hermes的工作原理是把JavaScript编译成C代码,再通过LLVM编译成机器码。最终生成的是完全独立的二进制文件,不需要JavaScript虚拟机就能运行。编译出的C代码会用到Hermes提供的一些辅助函数,这些函数会像Rust等语言的标准库一样被静态链接到二进制文件中。这不仅能利用LLVM的高级优化提升性能,还能让JavaScript轻松嵌入到Rust这类能和C交互的语言编写的程序中。

把Less.js编译成C

我打算给Parcel做个Less插件,能从Rust调用的那种。借助静态Hermes,我成功把它编译成了C库,然后就能从Rust调用了。

第一步是把less这个npm模块打包成一个没有外部依赖的单文件JavaScript。Hermes不支持Node模块,所以一切都得自给自足,也不能依赖fspath这些Node内置模块。当然,我用Parcel来做这件事。

javascript 复制代码
// 使用Less的环境无关版本,并模拟PluginLoader
const less = require('less/lib/less').default({}, {})
less.PluginLoader = function() {}

// 暴露一个全局函数,把Less代码字符串编译成CSS
function compile(input) {
  let result
  less.render(input, (err, res) => {
    result = res.css
  })
  return result
}

globalThis.compile = compile

用Parcel编译:

bash 复制代码
parcel build less.js --no-optimize

这样就生成了dist/less.js,一个完全独立的文件,暴露了全局的compile函数。

接下来把它编译成C库。首先得自己编译静态Hermes,按照官方文档操作就行。

bash 复制代码
./build_release/bin/shermes -O -c -exported-unit=less dist/less.js
  • -O 生成优化过的构建
  • -c 编译成原生目标文件,之后可以链接到更大的程序中
  • -exported-unit=less 告诉Hermes不要生成main函数,而是导出一个叫less的编译单元

这会生成less.o目标文件。(如果想看看编译出的C源代码,可以把-c换成-emit-c。)

然后需要一个小的C包装器来调用这个JavaScript函数。

c 复制代码
// compile.c
#include <stdlib.h>
#include <hermes/VM/static_h.h>
#include <hermes/hermes.h>

// 声明静态Hermes生成的`less`单元
// 这个会来自`less.o`
extern \"C\" SHUnit sh_export_less;

extern \"C\" char* compile_less(char *in) {
  // 初始化Hermes运行时
  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();
    }
  }

  // 获取全局的`compile`函数并调用
  std::string res = s_hermes->global()
    .getPropertyAsFunction(*s_hermes, \"compile\")
    .call(*s_hermes, std::string(in))
    .getString(*s_hermes)
    .utf8(*s_hermes);

  // 把C++字符串转成能返回的C字符串
  char* result = new char[res.size() + 1];
  strcpy(result, res.c_str());
  return result;
}

clang++把这个编译成另一个目标文件:

bash 复制代码
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函数了。

rust 复制代码
// main.rs
use std::ffi::{CStr, CString};
use std::os::raw::c_char;

// 声明要调用的C函数
extern \"C\" {
  fn compile_less(input: *const c_char) -> *const c_char;
}

fn main() {
  // 创建C字符串
  let input = CString::new(
    r#\"// 变量
      @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();

  // 调用C函数并转成Rust字符串
  let res = unsafe {
    let ptr = compile_less(input.as_ptr());
    CStr::from_ptr(ptr).to_string_lossy().into_owned()
  };

  // 打印结果
  println!(\"OUTPUT: {}\", res);
}

rustc编译并链接所有东西:

bash 复制代码
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=Foundation 是macOS特有的,用来链接Foundation框架

现在运行程序,就能看到它通过Rust编译Less了!🪄

bash 复制代码
./main

总结

这只是个简单的初步示例,但展示了原生工具整合预编译JS插件的潜力,而且不需要嵌入解释器。另一个潜在用例是基于Babel的React Compiler------对很多人来说,这可能是他们构建流程中仅存的JS工具了。我简单试了下,但遇到了一些问题,可能是目前还缺少某些特性

相关推荐
hellokatewj14 小时前
前端 Promise 全解:从原理到面试
前端
superman超哥14 小时前
Rust Vec的内存布局与扩容策略:动态数组的高效实现
开发语言·后端·rust·动态数组·内存布局·rust vec·扩容策略
天意pt14 小时前
Blog-SSR 系统操作手册(v1.0.0)
前端·vue.js·redis·mysql·docker·node.js·express
遗憾随她而去.14 小时前
Webpack5 高级篇(一)
前端
遇见~未来15 小时前
JavaScript构造函数与Class终极指南
开发语言·javascript·原型模式
疯狂踩坑人15 小时前
【React 19 尝鲜】第一篇:use和useActionState
前端·react.js
毕设源码-邱学长15 小时前
【开题答辩全过程】以 基于VUE的打车系统的设计与实现为例,包含答辩的问题和答案
前端·javascript·vue.js
用户390513321928815 小时前
JS判断空值只知道“||”?不如来试试这个操作符
前端·javascript
海云前端115 小时前
前端面试必问 asyncawait 到底要不要加 trycatch 90% 人踩坑 求职加分技巧揭秘
前端