【翻译】如何使用静态Hermes将JavaScript编译为C语言

6 阅读5分钟

原文链接:devongovett.me/blog/static…

作者:Devon

最近我一直在推进将更多Parcel功能移植到Rust的工作。基于Rust的工具面临的挑战之一是如何支持插件。许多常用工具已有Rust版本:JavaScript的SWCOXC、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模块,所有内容必须自成体系,也不能依赖fspath等内置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=Foundation macOS 专属,用于链接 Foundation 框架

现在你可以运行程序,见证它通过 Rust 编译 Less!🪄

./main

结论

这只是一个简单的初始示例,但它展示了原生工具与预编译的基于JS的插件集成时的潜力,无需嵌入解释器。另一个潜在用例是基于Babel的React编译器——对许多人而言,这是其构建管道中仅存的基于JS的工具之一。我曾短暂尝试过,但遇到了若干问题,这可能是当前功能尚不完善所致。