开发者文档/开发指南

Backend 与架构

创建类型化 Rust Backend、生成客户端,并理解底层 Wire Protocol v1。

核对日期:2026 年 9 月 11 日

本页目录

什么时候需要 Backend#

UI-only Curio 已能通过 host.xxx 使用底座能力。自己的原生计算、长任务和跨插件公开服务可以放入 Backend。

Host 管理安装、受管进程、窗口、连接身份、路由、期限和取消。公开服务属于常驻 Backend,不属于窗口;它们可按需启动,等待 ready 后接受调用,窗口关闭不结束已安装的公开提供者。Backend-only Curio 不需要 UI。

私有 on-demand Backend 默认随窗口关闭。若它承载必须脱离 UI 继续运行的任务,可以在私有契约声明无参数方法 backgroundStatus,返回 { "active": boolean }。Host 只在已连接的 Backend 上查询:true 保留 Backend,false 执行正常清理;退出 Host、禁用或卸载 Curio、结束开发会话仍会停止它。返回值必须来自 Backend 的真实任务状态,不能使用 UI 状态代替。

私有业务结构由作者自行定义。下面分别说明可选私有 RPC、公开服务和 Host 进程连接。

配置进程#

运行向导,名称选择 Native Tool、模板选择 Rust,并安装前端依赖。需要 Rust/Cargo 和 macOS 编译工具链:

sh
trove new
cd native-tool
trove generate
trove check
trove dev

trove generate 从 Rust 类型导出 Contract,再生成 src/generated/backend.ts;首次只读 check 前先生成。之后 dev 和 pack 会自动准备。

模板的 backend/src/main.rs 负责业务,vendor/trove-backend-0.0.1-alpha.1 负责协议,其中的 contract/ 是共享 Rust 合约声明库。tools/build.mjs 构建原生入口 dist/backend/curio-backend 和 UI。backend.contract 指向生成的 contracts/backend.json。trove kit 更新匹配套件,保留业务文件。

需要自己连接进程时使用 external 开发模式;设置 TROVE_DEV_SESSION_FILE 为本次 .trove/dev-session.json 路径,kit 会读取连接信息。结束会话时外部 Backend 收到 shutdown;Host 不强制杀死你手动启动的进程。Host 不分发 Node/Python 运行时,其他语言仍可自行实现 Wire。

通过 Wire Protocol v1 连接#

受 Host 管理的 Backend 通过 Unix Domain Socket 接入 Wire v1,完成 hello → welcome → ready。stdout/stderr 留给日志,不能当协议通道。

Wire v1 完整参考 包含环境变量、长度前缀、握手字段、全部消息、双向调用、身份边界、期限/取消、订阅、容量限制和退出流程,并提供可下载的跨语言实现。仅阅读消息 JSON Schema 无法推导出这些运行规则。

Rust Kit 处理这一连接层。其他语言可自行实现;应用内部已有的 HTTP/WebSocket 层可以并存。

实现私有请求#

在生成的 Rust 工程中,以下是完整的 backend/src/main.rs:

rust
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use trove_backend::{Backend, Context, Result, Service};

#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
struct Input { text: String }
#[derive(Serialize, JsonSchema)]
struct Output { text: String }

#[tokio::main]
async fn main() -> Result<()> {
    Backend::new(Service::new("dev.local.native-tool.backend", "0.0.1-alpha.1")
        .method("uppercase", 30_000, |_ctx: Context, input: Input| async move {
            println!("Converting {} characters", input.text.chars().count());
            Ok(Output { text: input.text.to_uppercase() })
        }))
        .run().await
}

kit 从实际 Input/Output 类型导出契约,并处理分帧、并发、期限、取消和关闭。作者维护 Rust 类型和业务处理函数,JSON Schema 与前端类型都是生成物。前端生成后直接调用:

ts
import { createClient } from "./generated/backend.js";

const device = createClient();
console.log((await device.uppercase({ text: "hello" })).text);

私有契约只服务 @self。公开能力需要 .provide(service)、导出该服务 Contract,并在 services.provides 声明;仅写 backend.contract 不会公开。

处理函数可通过 Context 查询 caller、remaining、is_cancelled 或等待 cancelled。Context::request 转发父上下文以继承追踪和剩余期限;Host 将反向调用认证为实际发起的 Backend,不保留原调用者身份。阻塞 FFI 与 CPU 工作应由业务工作线程执行并配合取消。其他语言的响应必须保持请求 ID,result/error 恰有一个,支持乱序完成与取消。

验证协议与生命周期#

在 Rust starter 中运行:

sh
trove test

基本模式检查 hello、分片帧、八个并发未知方法请求的结构化错误和正常 shutdown/goodbye。它不证明业务成功、执行取消、事件或反向调用正确。

完整模式使用 trove test --cases protocol-cases.json。用例必须描述真实成功、错误、可等待取消和反向调用;声明 subscribe 的 Backend 还需订阅用例。可下载 fixture 源码 与 完整用例,在独立测试工程使用,详见 协议验收。

内部通信:建议与自由度#

Trove 不规定必须存在 backend.xxx,也不规定 UI 业务代码要怎样分层。你可以:

  • 采用模板的可选私有契约生成器,把 createClient 结果命名为 device、api、tasks 或其他领域名称;调用路由到 @self,省去连接和类型维护。
  • 保留自己的 HTTP、WebSocket、消息总线和应用服务层,自行处理其认证、连接、取消和类型;不必为每条内部操作写 Trove Contract。
  • 完全不提供私有 RPC,只提供公开 Backend 服务;或让 UI 只调用 Host。

backend.contract 只描述通过 @self 暴露的私有 RPC,bindings.backend 只决定可选生成文件,二者不决定插件的内部架构。跨插件仍然只路由 services.provides 中声明的公开 Backend 能力,不能调用 @self,也不能通过服务调用拉起别人的 UI。

当前 Host 管理的进程仍须完成 Wire 生命周期连接,即使内部业务走 HTTP。Rust Backend::default().run() 可承担这个连接而不添加私有服务。纯 HTTP 进程直接作为 entry、却不做 Wire 握手,当前会等待启动超时;尚不存在免 Wire 的受管进程模式。

只提供公开服务#

以下是完整的 Rust main.rs,不需要伪造一个私有 Service:

rust
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use trove_backend::{Backend, Context, Result, Service};

#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
struct Input { text: String }
#[derive(Serialize, JsonSchema)]
struct Output { text: String }

#[tokio::main]
async fn main() -> Result<()> {
    Backend::default()
        .provide(Service::new("dev.example.text-tools", "0.0.1-alpha.1")
            .method("uppercase", 5_000, |_ctx: Context, input: Input| async move {
                Ok(Output { text: input.text.to_uppercase() })
            }))
        .run().await
}

将 Curio ID 设为 dev.example.text-tools,并用 services.provides 声明同名服务、版本和 contracts/text-tools.json。上例仅提供 uppercase 方法,不包含其他页面示例中的 progress 事件。编译后导出实际契约:

sh
./dist/backend/curio-backend --export-contract contracts/text-tools.json --service dev.example.text-tools

把导出命令加入项目的 bindings.generate 流程,并把契约与可执行文件列入 package.files。消费者据此生成自己的类型并按 Curio ID 获取引用。

Backend::new(private) 仍用于选择私有 RPC;Backend::default().provide(public) 用于仅公开服务。Backend::contract() 返回 Result<Value>,没有私有服务时返回 self.unavailable;公开契约由 services 导出路径取得。非 Rust Backend 的独立导出方式见 Rust 合约;合约语言不限制业务实现语言。