3 个版本

使用旧的Rust 2015

0.1.137 2022年7月2日
0.1.102 2019年11月3日
0.1.101 2019年10月14日

#1609过程宏

MIT/Apache

1MB
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 运行时是完全安全代码,无依赖项。在此环境中运行时,宏与世界的唯一可能的交互仅限于消耗标记并产生标记。这对于宏自身可能包含多少不安全代码是真实的!在 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中,将每个宏入口点改为no_mangle 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" }

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

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

接下来,我们需要创建一个小的proc-macro shimming crate,以便将编译好的Wasm字节传递给Watt运行时。由于另一个crate不会发布到crates.io,所以可以给它和上一个crate相同的crate名称。在一个新的Cargo.toml中,输入以下内容:

[lib]
proc-macro = true

[dependencies]
watt = "0.5"

并在其src/lib.rs中,定义与之前在另一个crate中定义的每个no_mangle 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二进制文件复制到实现crate的src目录下,然后就可以发布了!


剩余工作

  • 性能。 Watt的编译速度很快,但到目前为止,我还没有对运行时进行任何优化。这意味着宏展开可能比使用原生编译的proc宏耗时更长。

    请注意,Wasm环境的性能开销部分被我们以发布模式编译proc宏的事实所抵消,这意味着下游的cargo build将运行发布模式的宏,而如果是传统的proc宏,则会运行调试模式。

    一个不错的方法是提供一个类似cargo install watt-runtime的命令,该命令在本地安装优化的Wasm运行时,Watt crate可以检测并传递代码,如果可用。这样就可以完全避免在调试模式运行时运行代码。这个实验性的开始可以在jit/目录下找到。

  • 工具。 Watt的入门部分显示了构建宏有很多步骤,并且proc-macro2的修补相当复杂。理想情况下,这一切都应该更直接,包括为进行可重复构建Wasm工件并确认它确实是从公共源编译的提供简单工具。

  • RFC。 快速编译时间、隔离和确定性可能使得在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 license,您可根据自己的选择。`runtime`目录受 ISC license 许可。
除非您明确声明,否则您提交给此crate的任何有意包含的内容,根据Apache-2.0许可证的定义,将根据上述许可证双重许可,不附加任何额外条款或条件。

依赖项