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工具了。我简单试了下,但遇到了一些问题,可能是目前还缺少某些特性

相关推荐
利刃大大13 小时前
【Vue】自定义指令directives && 指令钩子 && IntersectionObserver
前端·javascript·vue.js
共享家952719 小时前
搭建 AI 聊天机器人:”我的人生我做主“
前端·javascript·css·python·pycharm·html·状态模式
Halo_tjn20 小时前
基于封装的专项 知识点
java·前端·python·算法
摘星编程21 小时前
OpenHarmony环境下React Native:自定义useTruncate文本截断
javascript·react native·react.js
Duang007_21 小时前
【LeetCodeHot100 超详细Agent启发版本】字母异位词分组 (Group Anagrams)
开发语言·javascript·人工智能·python
superman超哥1 天前
Serde 性能优化的终极武器
开发语言·rust·编程语言·rust serde·serde性能优化·rust开发工具
2601_949868361 天前
Flutter for OpenHarmony 电子合同签署App实战 - 主入口实现
开发语言·javascript·flutter
m0_748229991 天前
Vue2 vs Vue3:核心差异全解析
前端·javascript·vue.js
C澒1 天前
前端监控系统的最佳实践
前端·安全·运维开发
xiaoxue..1 天前
React 手写实现的 KeepAlive 组件
前端·javascript·react.js·面试