Roto:一款为 Rust 设计的编译型脚本语言
Introducing Roto: A Compiled Scripting Language for Rust
Team NLnet Labs
2025年5月21日 • 阅读需时3分钟
Photo by Joseph Barrientos / Unsplash
作者:Terts Diepraam
我们正在开发一种用于 Rust 的嵌入式脚本语言。这种语言名为 Roto,旨在成为一种简单、快速且可靠的 Rust 应用程序脚本语言。
对 Roto 的需求源于 Rotonda,我们用 Rust 编写的 BGP 引擎。 成熟的 BGP 应用程序通常具有某种过滤传入路由宣告的方式。 这些过滤器的复杂性通常超出配置语言的功能。 通过 Rotonda,我们希望让我们的用户能够轻松编写更复杂的过滤器。 因此,我们决定赋予他们完整脚本语言的能力。
我们对这种语言有一些严格的要求。 首先,我们需要这些过滤器快速。 其次,Rotonda 是关键基础设施,因此运行时崩溃是不可接受的。 这排除了动态类型语言,Rust 社区中有很多这样的语言。[1] 我们需要一种静态类型语言,它可以为我们提供更高的类型安全性和速度。 最后,我们希望这种语言易于上手;它应该感觉像是你习惯使用的脚本语言的静态类型版本。
Roto 满足了我们的这一需求。 简而言之,它是一种静态类型、JIT 编译、热重载的嵌入式脚本语言。 为了获得良好的性能,Roto 脚本在运行时使用 cranelift 编译器后端编译为机器代码。
下面是一个 Roto 脚本的小例子。 在这个脚本中,我们定义了一个 filtermap
,它会产生 accept
或 reject
的结果。 在这种情况下,当 IP 地址在给定范围内时,我们 accept
。
filtermap within_range(range: AddrRange, ip: IpAddr) {
if range.contains(ip) {
accept ip
} else {
reject
}
}
我们可以编写一个更传统的 function
,它可以简单地 return
一个值,而不是 filtermap
。 filtermap
是 Roto 支持的一种构造,可以更轻松地编写过滤器。
那里的 Roto 代码可能看起来很简单,但有一个问题:AddrRange
不是内置类型。 相反,它由宿主应用程序(例如 Rotonda)添加到 Roto,使其可用于脚本中。[2] 同样,AddrRange
上的 contains
方法也由宿主应用程序提供。 运行上述脚本所需的完整代码如下所示。 此示例也可在 我们的 GitHub 存储库中找到。
use std::net::IpAddr;
use std::path::Path;
use roto::{roto_method, FileTree, Runtime, Val, Verdict};
#[derive(Clone)]
struct AddrRange {
min: IpAddr,
max: IpAddr,
}
fn run_script(path: &Path) {
// Create a runtime
let mut runtime = Runtime::new();
// Register the AddrRange type into the runtime with a docstring
runtime
.register_clone_type::<AddrRange>("A range of IP addresses")
.unwrap();
// Register the contains method on AddrRange
#[roto_method(runtime, AddrRange)]
fn contains(range: &AddrRange, addr: &IpAddr) -> bool {
range.min <= addr && addr <= range.max
}
// Compile the program
let program =
FileTree::read(path).compile(runtime).unwrap();
// Extract the Roto filtermap, which is accessed as a function
let function = program
.get_function::<(), (Val<AddrRange>, IpAddr), Verdict<IpAddr, ()>>(
"within_range"
)
.unwrap();
// Run the filtermap
let range = AddrRange {
min: "10.10.10.10".parse().unwrap(),
max: "10.10.10.12".parse().unwrap(),
};
let in_range = "10.10.10.11".parse().unwrap();
println!("{:?}", function.call(&mut (), range, in_range)));
let out_of_range = "10.10.11.10".parse().unwrap();
println!("{:?}", function.call(&mut (), range, out_of_range));
}
请注意,当脚本加载时,脚本中的任何内容都不会自动运行,就像许多其他脚本语言中发生的那样。 宿主应用程序决定从脚本中提取哪些函数和 filtermap,以及何时运行它们。
Roto 与 Rust 紧密集成。 许多 Rust 类型[3]、方法和函数可以直接注册以在 Roto 中使用。 这些类型可以以极低的成本传递给 Roto; Roto 和 Rust 之间没有序列化。 对于 Rotonda,这意味着 Roto 可以对原始 BGP 消息进行操作,而无需昂贵的转换过程。
注册机制还确保 Roto 不仅限于 Rotonda,并且可以轻松地在该上下文之外使用。 它被设计为一种通用的脚本或插件语言。
我们在 Roto 的路线图上规划了许多功能,并将继续改进这种语言。 这也意味着该语言不应被认为是稳定的,但如果您尝试使用它,我们很乐意听到反馈。 如果您有兴趣,请查看 文档、存储库 和 示例。
- 例如 Rhai, Rune, Mlua, Deno, PyO3, Dyon & Koto. ↩︎
- 请注意,类型的注册不受 Rust 的 孤儿规则 的阻碍,因为它除了
Clone
之外不需要任何特定的 traits。 这使得可以将来自外部库的类型公开给 Roto。 ↩︎ - 具体来说,是实现了
Clone
或Copy
的类型。 未实现这些 traits 的类型可以包装在Rc
或Arc
中以传递给 Roto。 ↩︎