#proc-macro #macro #run-time #executing #compiler #compilation

watt

执行作为 WebAssembly 编译的 Rust 过程宏的运行时

14 个版本

0.5.0 2023 年 9 月 13 日
0.4.4 2023 年 7 月 15 日
0.4.3 2023 年 3 月 5 日
0.4.2 2022 年 8 月 3 日
0.3.0 2019 年 11 月 3 日

#47 in WebAssembly

Download history 55/week @ 2024-04-01 62/week @ 2024-04-22

247 每月下载量
用于 5 crates

MIT/Apache

235KB
5.5K SLoC

Watt

github crates.io docs.rs

Watt 是一个执行作为 WebAssembly 编译的 Rust 过程宏的运行时。

[dependencies]
watt = "0.5"

编译器支持:需要 rustc 1.42+


理由

  • 更快的编译。 通过将宏提前编译为 Wasm,我们为所有宏的下游用户节省了编译宏逻辑或其依赖项的时间。

    相反,他们编译的是一个小的自包含 Wasm 运行时(约 3 秒,由所有宏共享)以及每个宏 crate 的微小的过程宏适配器,以便将 Wasm 字节码传递给 Watt 运行时(每个依赖的过程宏 crate 大约 0.3 秒)。这比编译复杂的进程宏及其依赖项的 20 多秒要少得多。

  • 隔离。 Watt 运行时是 100% 安全的代码,没有任何依赖。在此环境中运行时,宏与世界的唯一可能交互是消耗标记和产生标记。这与宏本身可能包含多少不安全代码无关!除非 Rust 编译器或标准库存在错误,否则宏不可能做任何其他的事情,而只是重新排列标记。

  • 确定性。 从构建系统的角度来看,由 Wasm 支持的宏的优点是它可以被视为一个从输入到输出的纯粹确定性函数。没有隐式依赖的可能性,例如通过文件系统,这不会对构建系统可见或被考虑在内。


入门

首先,像平时一样实现和测试您的过程宏,使用您想要的任何依赖项(syn,quote 等)。您最终会得到一些看起来像

use proc_macro::TokenStream;

#[proc_macro]
pub fn the_macro(input: TokenStream) -> TokenStream {
    /* ... */
}

#[proc_macro_derive]#[proc_macro_attribute] 同样受到支持;一切都与此处展示的 #[proc_macro] 类似。

当你的宏准备好后,我们只需要对签名和 Cargo.toml 进行少量修改。在你的 lib.rs 中,将每个宏入口点修改为无 mangling 的 extern "C" 函数,并将签名中的 TokenStream 从 proc_macro 更改为 proc_macro2。

它看起来会是这样

use proc_macro2::TokenStream;

#[no_mangle]
pub extern "C" fn the_macro(input: TokenStream) -> TokenStream {
    /* same as before */
}

现在在你的宏的 Cargo.toml 中,原来包含以下内容

# my_macros/Cargo.toml

[lib]
proc-macro = true

改为以下内容

[lib]
crate-type = ["cdylib"]

[patch.crates-io]
proc-macro2 = { git = "https://github.com/dtolnay/watt" }

这个包将被打包成编译为 Wasm 的二进制文件。通过运行以下命令来编译它

$ cargo build --release --target wasm32-unknown-unknown

接下来,我们需要创建一个小的 proc-macro 嵌套包,以便将编译好的 Wasm 字节传递给 Watt 运行时。由于另一个包不会发布到 crates.io,因此可以给这个包与上一个包相同的包名。在新的 Cargo.toml 中放入以下内容

[lib]
proc-macro = true

[dependencies]
watt = "0.5"

然后在它的 src/lib.rs 中,定义与之前在另一个包中定义的无 mangling extern "C" 函数对应的真实 proc 宏

use proc_macro::TokenStream;
use watt::WasmMacro;

static MACRO: WasmMacro = WasmMacro::new(WASM);
static WASM: &[u8] = include_bytes!("my_macros.wasm");

#[proc_macro]
pub fn the_macro(input: TokenStream) -> TokenStream {
    MACRO.proc_macro("the_macro", input)
}

最后,将 target/wasm32-unknown-unknown/release/my_macros.wasm 下的编译好的 Wasm 二进制文件复制到你的实现包的 src 目录下,然后就可以发布了!


剩余工作

  • 性能。 Watt 编译速度相当快,但到目前为止,我还没有对运行时进行优化。这意味着宏展开可能比本机编译的 proc 宏花费更长的时间。

    请注意,Wasm 环境的性能开销部分可以通过我们的 proc 宏以发布模式编译来抵消,这意味着下游的 cargo build 将运行发布模式的宏,而原本将运行调试模式的传统 proc 宏。

    一个很好的方法是为 cargo install watt-runtime 提供某种支持,它将安装一个本地优化的 Wasm 运行时,Watt 包可以检测并传递代码,如果可用的话。这样就可以避免在调试模式运行时运行任何东西。这个实验性的开始可以在 jit/ 目录下找到。

  • 工具。 入门部分显示,构建 Watt 的宏有很多步骤,proc-macro2 的集成相当复杂。理想情况下,这一切都将更加直接,包括用于对 Wasm 艺术品进行可重复构建的简单工具,以确认它确实是从公开可用的源编译的。

  • RFCs。 快速编译时间、隔离和确定性的优势可能使得在 rustc 和 Cargo 中构建 Wasm proc 宏的第一级支持变得有意义。工具链可以提供自己的高性能 Wasm 运行时,这将比 Watt 更好,因为该运行时可以高度优化,并且宏的消费者不需要编译它。


这不可能真实

为了帮助您相信这是真实的,这里是 serde_derive 编译为 Wasm。它是从 serde-rs/serde@1afae183 提交编译的。您可以自由尝试它作为

// [dependencies]
// serde = "1.0"
// serde_json = "1.0"
// wa-serde-derive = "0.1"

use wa_serde_derive::Serialize;

#[derive(Serialize)]
struct Watt {
    msg: &'static str,
}

fn main() {
    let w = Watt { msg: "hello from wasm!" };
    println!("{}", serde_json::to_string(&w).unwrap());
}

致谢

当前底层的Wasm运行时是基于Yoann Blein和Hugo Guiroux的Rust-WASM项目的分支,是一个简单且符合规范的WebAssembly解释器。


许可证

除了`runtime`目录之外的所有内容均根据您的选择,受Apache License, Version 2.0MIT许可证的许可。`runtime`目录受ISC许可证的许可。
除非您明确声明,否则根据Apache-2.0许可证定义,您有意提交以包含在此crate中的任何贡献,将根据上述方式双许可,没有任何附加条款或条件。

无运行时依赖