Windmill 中的 Rust 脚本开发完全指南:main 函数约定、依赖声明与异步执行
2026/9/14 11:23:43 网站建设 项目流程

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:默认清单模板,内置了serdeserde_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函数必须遵守:

  1. 参数使用拥有所有权的类型(owned types):如Stringi32,而不是&str这类借用引用。这是因为 worker 会将 JSON 参数反序列化后按值传入。
  2. 返回类型必须可序列化:即实现#[derive(Serialize)]。脚本返回值最终会被序列化为 JSON,供后续流程步骤通过results.step_id引用。
  3. 返回类型包装在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~i128u8~u128isize/usizeInt整数
Stringstr&str&mut StringStr(None)字符串
boolBool布尔
f32/f64Float浮点
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-scriptcargo-eval项目的逻辑一致,见 find_embedded_manifest):

  1. 简写形式:首行非空注释为// cargo-deps: dep1, dep2时,按逗号拆分生成[dependencies]表;未写版本号的依赖自动补"*"
  2. 代码块形式:从文档注释中用 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保持同步签名;
  • 异步闭包内部可以正常使用.awaitreqwest?错误(reqwest::Error)会被自动转换为anyhow::Error
  • 返回值Ok(Response {...})会经serde序列化为 JSON 输出。

从执行器源码看,这种"同步外壳 + 内部 runtime"的模式与 gen_cargo_crate 生成的包装代码完全兼容:worker 生成的main.rsargs.json读取参数、调用你的main、把返回值写入result.json,整个调用链是同步的,因此任何异步逻辑都必须收敛在main内部完成。

五、底层执行流程:从源码到二进制缓存

理解底层机制有助于排查编译慢、缓存失效等问题。Rust 脚本的一次运行在 worker 端经历以下阶段(见 handle_rust_job):

  1. 计算缓存键compute_rust_hash对"源码 + 依赖锁文件 + 关联模块"计算哈希(见 rust_cache_key),并附加工作区注册表后缀;
  2. 查缓存:若哈希对应的二进制已在本机缓存目录或对象存储中(_rustbin/前缀),直接软链接复用,跳过编译;
  3. 生成 Cargo 工程gen_cargo_crate写入三份文件——合并后的Cargo.toml、读取args.json/写result.jsonmain.rs包装器、包含你业务代码与__WINDMILL_ARGS__参数结构体的inner.rs
  4. 编译:调用cargo build(正式运行加--release,preview 为 debug 构建);启用沙箱时(默认 nsjail)编译在隔离环境中进行;
  5. 运行:在 nsjail 沙箱(或非沙箱的直接进程)中执行编译产物/tmp/main,注入环境变量与保留变量,超时由 job 配置决定;
  6. 产出结果:从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 changeddepends on <path>)。

main函数参数在 Rust 中直接以fn main(param1: String, ...)形式声明,preview 传参格式为wmill script preview <path> -d '<args>'

七、总结

在 Windmill 中编写 Rust 脚本的要点可以浓缩为四句话:

  1. main:同步函数、owned 参数、anyhow::Result<T>返回、TSerialize
  2. 声明依赖:在脚本顶部//!doc comment 内嵌cargo代码块,serde 免声明;
  3. 做异步:在同步main里创建 tokio Runtime 并block_on
  4. 本地迭代:编辑后先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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询