Skip to content

Rust 插件开发上手教程


本文面向 熟悉 Rust 基础,但第一次接触 WASI / WIT / Component Model 的开发者。 目标不是“讲全”,而是 快速建立正确心智模型 + 跑通最小闭环

如果你正在 Vibe Coding,请使用智力足够的模型(推荐 gpt-5.2-codex / gemini-3-pro),最好使用 Agent 类模型,并将此文喂给它。


AstroBox 插件 = 一个 Rust lib,被编译成 wasm32-wasip2 的 WebAssembly Component

它绝对他妈的不是:

  • 普通 CLI 程序
  • WebAssembly for Web
  • Tokio Server / 后端服务

而是:

一个运行在 AstroBox 宿主里的“受控 Rust 组件” 通过 WIT 定义好的接口,和宿主进行 强类型、异步、安全 的交互。


必须记住下面这三个概念喵!不然我会在喝完粉色魔爪后用小刀刀捅死你的喵!

一、Host / Plugin 不是“进程”,而是“组件边界”

Section titled “一、Host / Plugin 不是“进程”,而是“组件边界””
  • Host(AstroBox):提供能力(UI、设备、系统、通信)
  • Plugin(你写的 Rust):实现逻辑、处理事件、调用 Host

二者之间 没有共享内存、没有直接 syscall:一切交互都必须写在 WIT 接口里


二、WIT = 插件和宿主之间的“接口契约”

Section titled “二、WIT = 插件和宿主之间的“接口契约””

WIT 文件定义了:

  • 可以调用哪些函数
  • 数据结构长什么样
  • 哪些是同步 / 哪些是异步
  • 哪些事件会回调给插件

不需要自己写 ABI / FFI wit-bindgen 会帮你把它们变成 Rust 代码。

详见 WIT 文件


WIT / Component Model 里:

  • future<T>跨组件边界的异步承诺
  • Rust 侧表现为:FutureReader<T>

这就是为什么你会看到:

fn on_event(...) -> FutureReader<String>

而不是:

async fn on_event(...) -> String

👉 https://www.rust-lang.org/learn/get-started

Windows 用户按提示装好 MSVC 即可。


我们在插件模板中准备了一个使用 Python 编写的脚本,方便你快速执行构建和打包等操作,你需要安装 Python 3 来使用它。

👉 https://www.python.org/downloads/


Terminal window
rustup target add wasm32-wasip2

AstroBox V2 当前基于 WASI Preview 2

未来会支持 wasi-p3,但旧插件无需修改即可继续工作


Terminal window
git clone --recurse-submodules https://github.com/AstralSightStudios/AstroBox-NG-Plugin-Template-Rust
cd AstroBox-NG-Plugin-Template-Rust

.
├── Cargo.toml
├── scripts # 预置的构建辅助脚本
├── src
│ ├── lib.rs # 插件入口(你主要改的地方)
│ └── logger.rs # tracing 日志初始化
└── wit # (submodule)Host / Plugin 的 WIT 接口定义

⚠️ wit/submodule,包含 wit 接口定义文件。详见 WIT 文件

AstroBox 升级时,只会 新增接口,不会破坏旧接口


你不是一个肉编器(也许?),先执行一次实实在在的编译操作应该能加深你对项目结构的理解。

在上文中说过,我们在插件模板中准备了一个使用 Python 编写的脚本,方便你快速执行构建和打包等操作,这是它的用法:

Terminal window
# Debug 构建到 dist 文件夹
python scripts/build_dist.py
# Release 构建到 dist 文件夹并打 abp 包
python scripts/build_dist.py --release --package

非常简单,不是吗?


先别被一大坨宏和impl吓到,来看这三段关键代码:

wit_bindgen::generate!({
path: "wit",
world: "psys-world",
generate_all,
});

它帮你做了三件事:

  1. 把 Host 的 WIT 接口导入成 Rust 模块

    psys_host::dialog::show_dialog(...)
  2. 生成你必须实现的 Guest trait

    lifecycle::Guest
    event::Guest
  3. 生成异步桥接所需的运行时代码FutureReader / spawn / block_on


impl lifecycle::Guest for MyPlugin {
fn on_load() {
logger::init();
tracing::info!("Hello AstroBox V2 Plugin!");
}
}
  • 插件 被加载时自动调用

  • 是同步函数

  • 非常适合做:

    • 日志初始化
    • 设备扫描
    • register 各种事件

三、事件入口:on_event / on_ui_event

Section titled “三、事件入口:on_event / on_ui_event”
impl event::Guest for MyPlugin {
fn on_event(...) -> FutureReader<String> { ... }
fn on_ui_event(...) -> FutureReader<String> { ... }
}

这是插件 90% 逻辑调用的入口


为什么不能直接发动锈术释放 async fn

Section titled “为什么不能直接发动锈术释放 async fn?”

除了我不喜欢你,我什么都没做错。知名博主 LexBurner 曾在不经意间被吓一跳释放忍术🥷,在使用 Rust 编写 AstroBox 插件时,你肯定也会忍不住被吓一跳释放锈术,把 async fn 扔给 FutureReader

好吧上面是在玩梗,但你的确不能这么做

因为:

宿主和插件可能不在同一个 executor / runtime / 线程模型里

所以 WIT 定义的是:

on-event: func(...) -> future<string>

                              ———XQC

所以实际上这才是正确写法:

let (writer, reader) = wit_future::new::<String>(|| "".to_string());
wit_bindgen::spawn(async move {
// 这里可以 await host 接口
writer.write("result".to_string()).await.unwrap();
});
reader

你可以把它理解为:

oneshot channel + promise

  • reader:马上还给宿主(“我以后会给你结果”)
  • writer:某个时刻把结果填回去

wit_bindgen::spawn(async { ... });

杂鱼杂鱼,这才不是什么 tokio 呢~

这是 Component Model 自带的最小 async runtime——足够用,且 WASI 下最稳定


在同步函数里调用异步 Host 接口

Section titled “在同步函数里调用异步 Host 接口”

on_load 是同步的,但 Host 接口几乎都是 future<T>

如果你熟悉Rust开发,你肯定马上要发动tokio::block_on之力了——没错,但hold on,把它换成wit_bindgen::block_on才是真没错:

wit_bindgen::block_on(async {
let result = psys_host::dialog::show_dialog(...).await;
});

原则

  • ✅ 生命周期函数里可以 block_on
  • ⚠️ 但是请无论如何都不要把 dialog 之类需要等待用户操作的异步操作在 on_loadblock_on
  • ❌ 事件回调里不要阻塞,直接返回 FutureReader

第一个完整调用闭环:弹一个 Dialog

Section titled “第一个完整调用闭环:弹一个 Dialog”
psys_host::dialog::show_dialog(
DialogType::Alert,
DialogStyle::System,
&DialogInfo {
title: "Plugin Alert".into(),
content: "插件正在运行".into(),
buttons: vec![DialogButton {
id: "ok".into(),
primary: true,
content: "OK".into(),
}],
},
).await;

你可以从中总结出 WIT → Rust 的映射规律

WITRust
enumRust enum
recordRust struct
list<T>Vec<T>
stringString
future<T>.await / FutureReader<T>

Register → Event:事件是“订阅制”的

Section titled “Register → Event:事件是“订阅制”的”

正确流程永远是:

  1. on_load

    • register_xxx(...)
  2. on_event

    • 收到对应事件
    • 执行业务逻辑

UI 接口:声明式,而不是模板式或 DOM

Section titled “UI 接口:声明式,而不是模板式或 DOM”

逃离使用命令式 UI 的 Qt 和尤雨溪统治的 <template> 帝国,让我们来拥抱一些真正现代、真正 Next-Gen 的东西——声明式 UI。人人都喜欢 React 和 SwiftUI,除了那些仍在玩弄 WPF 或使用 Vanilla HTML 编写上世纪级页面的老登。

ui::element 是一个 链式 Builder API

let btn = ui::element::new(
ElementType::BUTTON,
Some("Click me".into())
)
.on(event::Event::CLICK, "btn-click");
ui::render(btn);
  • 没有 HTML
  • 没有 JS
  • 没有 CSS

去他妈的 XSS 注入。


Crates.io 生态兼容:你不需要重新发明轮子

Section titled “Crates.io 生态兼容:你不需要重新发明轮子”

虽然前面还没提到,但也许你已经发现,模板里的 logger.rs 做了三件事:

  • stdout 打印(带 [Plugin] 前缀)
  • 文件滚动日志(logs/app.log

并且,它直接使用了目前 Rust 桌面应用采用的主流方案 tracing 库。它并没有为 WebAssembly 特别开发,但由于我们使用了 WASI,大部分 std 操作都得以实现,这些来自 Rust 现存生态的第三方库也能被直接使用。

因此

WASI ≠ 只能写玩具代码

绝大多数纯 Rust 库 可以直接用


  • WASM / WASI 下 tokio 是 子集支持
  • full feature 会展现黑曼巴精神直接坠机
  • 定时器、IO 行为大概率没适配 wasi,也不让你过编译

So,优先使用:

  • wit_bindgen::spawn
  • FutureReader
  • Host 提供的能力

你现在已经可以:

  • 写一个可加载的 AstroBox 插件
  • 调用 Host UI / Device / Transport 接口
  • 注册并接收事件
  • 正确处理跨组件异步
  • 使用标准 Rust 日志与库

接下来只剩两件事:

  1. 业务逻辑
  2. 设计好你的插件 UX

发挥你的创造力,我们迫不及待地想看看你能在这个充满可能性的平台上做些什么!