Windmill 中的 Rust 脚本开发完全指南:main 函数约定、依赖声明与异步执行
【免费下载链接】windmillOpen-source developer platform to power your entire infra and turn scripts into webhooks, workflows and UIs. Fastest workflow engine (13x vs Airflow). Open-source alternative to Retool and Temporal.项目地址: https://gitcode.com/GitHub_Trending/wi/windmill
Windmill 是一个将脚本转化为 Webhook、工作流与 UI 的开发者平台,Rust 是它原生支持的高级脚本语言之一(worker 标签rust,见 迁移脚本)。本文以官方语言规范文档 system_prompts/languages/rust.md 为主体,结合 worker 执行器与解析器的源码实现,系统讲解在 Windmill 中编写、运行与调试 Rust 脚本的完整方法。读完本文,你将掌握 Rust 脚本的标准结构、内嵌 Cargo 依赖声明语法、异步任务的正确写法,以及脚本从编写、预览到部署的完整命令行工作流。
一、Rust 脚本在 Windmill 中的定位
在 Windmill 中,Rust 与 Python、TypeScript、Go、Bash 等语言并列,是一种"脚本即代码"的执行形态:你写的不是完整可执行二进制,而是一个必须导出main函数的源码单元。平台侧的 worker 会在每次运行前自动完成依赖解析、编译、缓存与沙箱执行,开发者只需要专注业务逻辑本身。
该语言能力由以下组件共同支撑:
- rust_executor.rs:worker 端的 Rust 执行器,负责生成 Cargo 工程、编译、运行并收集结果;
- windmill-parser-rust:Rust 脚本解析器,负责提取
main函数签名(用于生成参数 UI)与内嵌依赖清单(用于生成Cargo.toml); - Cargo.toml.default:默认清单模板,内置了
serde与serde_json; - add_rust_lang 迁移:向
SCRIPT_LANG枚举注册rust并把它加入默认 worker 标签。
二、脚本结构:main 函数是唯一入口
Rust 脚本必须包含一个名为main的函数,并带有正确的返回类型。官方规范文档给出的标准骨架如下:
use anyhow::anyhow; use serde::Serialize; #[derive(Serialize, Debug)] struct ReturnType { result: String, count: i32, } fn main(param1: String, param2: i32) -> anyhow::Result<ReturnType> { Ok(ReturnType { result: param1, count: param2, }) }三条硬性约定
规范文档明确强调,main函数必须遵守:
- 参数使用拥有所有权的类型(owned types):如
String、i32,而不是&str这类借用引用。这是因为 worker 会将 JSON 参数反序列化后按值传入。 - 返回类型必须可序列化:即实现
#[derive(Serialize)]。脚本返回值最终会被序列化为 JSON,供后续流程步骤通过results.step_id引用。 - 返回类型包装在
anyhow::Result<T>中:Err分支用于表达脚本失败,错误信息会出现在执行日志里。
源码层面的印证
解析器 parse_rust_signature 用syn解析源码 AST,找到名为main的函数后遍历其参数,为每个参数生成Arg { name, otyp, typ, ... }。其中otyp保留原始 Rust 类型文本(如Vec < u8 >),而typ则通过 parse_pat_type 归一化为一套 Windmill 内部类型系统:
| Rust 参数类型 | Windmill 类型(Typ) | 说明 |
|---|---|---|
i8~i128、u8~u128、isize/usize | Int | 整数 |
String、str、&str、&mut String | Str(None) | 字符串 |
bool | Bool | 布尔 |
f32/f64 | Float | 浮点 |
Vec<T>、[T; N]、&[T] | List(Box<Typ>) | 数组/切片 |
| 其他具名类型(如自定义 struct) | Resource(snake_case 名) | 按资源(resource)处理 |
这一点在解析器的测试用例中有直接验证(见 lib.rs 测试模块):my_vec: Vec<u8>被识别为Typ::List(Box::new(Typ::Int)),自定义类型CRes被识别为Typ::Resource("c_res")。也就是说,Rust 脚本的参数可以直接引用 Windmill 资源类型,平台会自动把资源对象反序列化后传入。
另外,如果脚本中没有main函数,解析器会返回空参数签名并标记auto_kind: Some("lib"),即视为纯库模块(可被其他脚本 import),这解释了为什么"每个脚本必须导出 main"。
三、依赖声明:脚本开头的内嵌 Cargo 清单
Rust 脚本不像 Python 那样有requirements.txt单独文件,而是在脚本文件开头的注释块内嵌入一份 Cargo 清单(partial cargo.toml):
//! ```cargo //! [dependencies] //! anyhow = "1.0.86" //! reqwest = { version = "0.11", features = ["json"] } //! tokio = { version = "1", features = ["full"] } //! ``` use anyhow::anyhow; // ... rest of the code格式要点:
- 代码块必须使用doc comment(
//!)包裹,内部是 Markdown 围栏代码块,语言标识为cargo; - 内容以
[dependencies]表开始,逐行声明依赖,与标准Cargo.toml语法完全一致,支持版本号、features等完整特性; - serde 已被平台预置,无需重复声明。
解析器如何读取这段清单
在 parse_rust_deps_into_manifest 中,解析器依次尝试两种提取方式(与rust-script、cargo-eval项目的逻辑一致,见 find_embedded_manifest):
- 简写形式:首行非空注释为
// cargo-deps: dep1, dep2时,按逗号拆分生成[dependencies]表;未写版本号的依赖自动补"*"; - 代码块形式:从文档注释中用 Markdown 解析器(
pulldown-cmark)抓取第一个cargo语言围栏代码块。
随后解析器把用户声明的[dependencies]与默认清单合并。默认清单(Cargo.toml.default)如下:
[[bin]] name = "main" path = "./main.rs" [package] authors = ["Anonymous"] edition = "2021" name = "main" version = "0.1.0" [profile.release] strip = true [dependencies] serde = { version = "1.0.207", features = ["derive"] } serde_json = "1.0.124"可见平台已为你处理好了edition = "2021"、[profile.release] strip = true(发布构建自动剥离符号,减小产物体积)以及serde/serde_json预置依赖——这正对应规范中"Serde 无需再添加"的说明。
四、异步任务:在同步 main 内创建 Runtime
Windmill 的 Rust 执行器要求main是同步函数。如果需要异步能力(例如用reqwest发起 HTTP 请求),规范做法是在main内部手动创建 tokio Runtime 并用block_on驱动:
//! ```cargo //! [dependencies] //! anyhow = "1.0.86" //! tokio = { version = "1", features = ["full"] } //! reqwest = { version = "0.11", features = ["json"] } //! ``` use anyhow::anyhow; use serde::Serialize; #[derive(Serialize, Debug)] struct Response { data: String, } fn main(url: String) -> anyhow::Result<Response> { let rt = tokio::runtime::Runtime::new()?; rt.block_on(async { let resp = reqwest::get(&url).await?.text().await?; Ok(Response { data: resp }) }) }关键点拆解:
tokio::runtime::Runtime::new()?创建多线程运行时,?把创建失败传播为anyhow::Error;rt.block_on(...)同步阻塞当前线程直到异步闭包完成,从而让main保持同步签名;- 异步闭包内部可以正常使用
.await,reqwest的?错误(reqwest::Error)会被自动转换为anyhow::Error; - 返回值
Ok(Response {...})会经serde序列化为 JSON 输出。
从执行器源码看,这种"同步外壳 + 内部 runtime"的模式与 gen_cargo_crate 生成的包装代码完全兼容:worker 生成的main.rs从args.json读取参数、调用你的main、把返回值写入result.json,整个调用链是同步的,因此任何异步逻辑都必须收敛在main内部完成。
五、底层执行流程:从源码到二进制缓存
理解底层机制有助于排查编译慢、缓存失效等问题。Rust 脚本的一次运行在 worker 端经历以下阶段(见 handle_rust_job):
- 计算缓存键:
compute_rust_hash对"源码 + 依赖锁文件 + 关联模块"计算哈希(见 rust_cache_key),并附加工作区注册表后缀; - 查缓存:若哈希对应的二进制已在本机缓存目录或对象存储中(
_rustbin/前缀),直接软链接复用,跳过编译; - 生成 Cargo 工程:
gen_cargo_crate写入三份文件——合并后的Cargo.toml、读取args.json/写result.json的main.rs包装器、包含你业务代码与__WINDMILL_ARGS__参数结构体的inner.rs; - 编译:调用
cargo build(正式运行加--release,preview 为 debug 构建);启用沙箱时(默认 nsjail)编译在隔离环境中进行; - 运行:在 nsjail 沙箱(或非沙箱的直接进程)中执行编译产物
/tmp/main,注入环境变量与保留变量,超时由 job 配置决定; - 产出结果:从
result.json读取返回值;同时缓存二进制供后续运行秒级复用。
值得注意的两个工程细节:
- 构建目录策略:
get_build_dir(rust_executor.rs)在沙箱开启时为"工作区@路径@创建者"创建独立构建目录以兼顾缓存命中率与安全,并用cargo sweep --maxsize(默认 25GB,可用CARGO_SWEEP_MAXSIZE环境变量调整)异步清理膨胀的缓存; - Cargo registry 可配置:通过 write_cargo_config 支持工作区级覆盖
cargo_registries配置,写入.cargo/config.toml,便于私有镜像源场景。
六、CLI 工作流:编写、预览、同步与部署
在本地用wmillCLI 开发 Rust 脚本时,遵循 write-script-rust 技能文档 的约定,可以避免"为了测试而误部署"的常见错误:
| 命令 | 用途 | 何时使用 |
|---|---|---|
wmill script preview <path> | 直接运行本地文件,不部署 | 迭代本地脚本的默认选择;有本地改动想"试一下"时用它 |
wmill script run <path> | 运行工作区中已部署的版本 | 仅在用户明确要测试部署版本、或没有本地改动时使用 |
wmill generate-metadata | 重新生成本地.script.yaml(输入 schema)与.lock(依赖锁),并刷新wmill-lock.yaml内容哈希 | 每次编辑脚本(尤其是增删 import 或改动main参数)后执行,保证元数据与代码一致 |
git push/wmill sync push | 将本地改动部署到工作区 | 仅当用户明确要求部署/发布/推送时 |
元数据同步是关键一步
wmill-lock.yaml为每个条目记录内容哈希。修改脚本内容——尤其是增删 import 或改变main的参数——会使哈希失效,导致.lock、.script.yaml输入 schema 与哈希行全部过期。此时应运行wmill generate-metadata(可加路径参数收窄范围,如wmill generate-metadata f/foo),否则 git-sync 与 CI 中会出现虚假 diff。该命令只写本地文件、不部署,但会重新解析依赖,可能升级未固定版本的依赖(与从 UI 部署行为一致),因此运行后应 diff 一下.lock,确认依赖版本没有意外跳动;若改动面超出预期,可先wmill generate-metadata --dry-run查看每个过期条目的原因(content changed或depends on <path>)。
main函数参数在 Rust 中直接以fn main(param1: String, ...)形式声明,preview 传参格式为wmill script preview <path> -d '<args>'。
七、总结
在 Windmill 中编写 Rust 脚本的要点可以浓缩为四句话:
- 写
main:同步函数、owned 参数、anyhow::Result<T>返回、T可Serialize; - 声明依赖:在脚本顶部
//!doc comment 内嵌cargo代码块,serde 免声明; - 做异步:在同步
main里创建 tokio Runtime 并block_on; - 本地迭代:编辑后先
wmill script preview验证,再wmill generate-metadata同步元数据,最后才按用户意图git push/wmill sync push部署。
通过 rust_executor.rs 与 windmill-parser-rust 的源码可以看到,平台在"简单脚本语法"背后封装了完整的 Cargo 工程生成、签名解析、二进制缓存与沙箱执行链路——理解这条链路,你就能更从容地写出既符合规范、又能在生产环境高效运行的 Rust 脚本。
【免费下载链接】windmillOpen-source developer platform to power your entire infra and turn scripts into webhooks, workflows and UIs. Fastest workflow engine (13x vs Airflow). Open-source alternative to Retool and Temporal.项目地址: https://gitcode.com/GitHub_Trending/wi/windmill
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考