Rust in Everything
基于 tauri 2.12.5

async commands that contain references as inputs must return a Result

主线位置:调用主线上「#[command] 包装」这一步的编译期。async 命令的包装把命令返回的 future 交给 FutureTag::future 或 ResultFutureTag::future,再 spawn 到 async_runtime 上;前者要求 future 是 'static,后者不要求。宏和这条约束的完整分析在 #[command] 与 generate_handler! 的「async 命令借用参数」一节,本页只讲怎么认出它、怎么改。

flowchart TB
  fnq["async 命令"] --> refq{"参数里有引用或生命周期"}
  refq -->|没有| okn["任何返回类型都行"]
  refq -->|有| retq{"返回类型"}
  retq -->|"Result"| okn
  retq -->|"其他类型"| e1["E0277 加 E0597"]
  retq -->|"没写"| e2["compile_error 加 找不到宏"]

本页实验 crate 是 pit1-async:src/bin/ 下两个编不过的 bin,src/main.rs 里是改好的命令和一个 MockRuntime 测试。M2 Max、macOS 27、rustc 1.99.0,tauri 2.12.5。

现象

一:要 State,返回 String

很自然的一种写法:命令要一个托管状态,顺手用 &str 收参数。

use tauri::State;

struct Db;

#[tauri::command]
async fn save(name: &str, _db: State<'_, Db>) -> String {
  format!("saved {name}")
}
error[E0277]: async commands that contain references as inputs must return a `Result`
 --> src/bin/bad.rs:6:50
  |
6 | async fn save(name: &str, _db: State<'_, Db>) -> String {
  |                                                  ^^^^^^ the trait `AsyncCommandMustReturnResult` is not implemented for `std::string::String`
  |
help: the trait `AsyncCommandMustReturnResult` is implemented for `Result<A, B>`
 --> src/bin/bad.rs:6:50
  |
6 | async fn save(name: &str, _db: State<'_, Db>) -> String {
  |                                                  ^^^^^^
  = note: required for the cast from `&std::string::String` to `&dyn AsyncCommandMustReturnResult`

error[E0597]: `__tauri_message__` does not live long enough
   --> src/bin/bad.rs:5:1
    |
  5 | #[tauri::command]
    | ^^^^^^^^^^^^^^^^-
    | |               |
    | |               `__tauri_message__` dropped here while still borrowed
    | borrowed value does not live long enough
    | argument requires that `__tauri_message__` is borrowed for `'static`
...
 13 |     .invoke_handler(tauri::generate_handler![save])
    |                     ------------------------------ in this macro invocation
    |
note: requirement that the value outlives `'static` introduced here
   --> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.12.5/src/ipc/command.rs:298:38
    |
298 |       F: Future<Output = T> + Send + 'static,
    |                                      ^^^^^^^
    = note: this error originates in the macro `__cmd__save` which comes from the expansion of the macro `tauri::generate_handler` (in Nightly builds, run with -Z macro-backtrace for more info)

Some errors have detailed explanations: E0277, E0597.
For more information about an error, try `rustc --explain E0277`.
error: could not compile `pit1-async` (bin "bad") due to 2 previous errors

第一条指向返回类型,第二条指向 #[tauri::command] 那一行和 generate_handler!,再指到 tauri 源码里的一行约束。两条说的是同一件事。

二:没写返回类型

#[tauri::command]
async fn log_line(line: &str) {
  println!("{line}");
}
error: async commands that contain references as inputs must return a `Result`
 --> src/bin/bad_unit.rs:2:19
  |
2 | async fn log_line(line: &str) {
  |                   ^^^^

error: cannot find macro `__tauri_command_name_log_line` in this scope
 --> src/bin/bad_unit.rs:8:21
  |
8 |     .invoke_handler(tauri::generate_handler![log_line])
  |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: this error originates in the macro `tauri::generate_handler` (in Nightly builds, run with -Z macro-backtrace for more info)

error: cannot find macro `__cmd__log_line` in this scope
 --> src/bin/bad_unit.rs:8:46
  |
8 |     .invoke_handler(tauri::generate_handler![log_line])
  |                                              ^^^^^^^^

error: could not compile `pit1-async` (bin "bad_unit") due to 3 previous errors

这次错误指向参数,后面两条「cannot find macro」是连带的:宏只输出了 compile_error!,命令函数和它的两个辅助宏都没生成,generate_handler! 找不到它们。只看最后两条会以为是 generate_handler! 写错了。

出处

宏的 wrapper 检查 async 命令的参数:类型是引用,或者是带生命周期参数的路径类型(State<'_, T>、tauri::ipc::Request<'_> 这一类),就要求返回 Result。有返回类型时,检查是一个只为 Result 实现的 trait,报 E0277;没有返回类型时直接 compile_error!。

真正的约束在运行时那一侧。返回普通值的 async 命令走 FutureTag::future,要求 F: Future<Output = T> + Send + 'static;save(name, db) 返回的 future 借用了包装里的局部变量 __tauri_message__,不是 'static,所以 E0597。返回 Result 的走 ResultFutureTag::future,没有 'static,借用在外层 async 块里闭合。宏的检查只是把 E0597 提前翻译成一句人话。为什么两条路径的约束不一样,见 #[command] 与 generate_handler!。

同步命令没有这个限制:它在主线程上当场执行完(分发),借用活不过这次调用也没关系。

修法

两种改法,选一种:

// 修法一:借用参数保留,返回 Result
#[tauri::command]
async fn save(name: &str, _db: State<'_, Db>) -> Result<String, String> {
  Ok(format!("saved {name}"))
}

// 修法二:参数改成拥有所有权的类型,返回什么都行
#[tauri::command]
async fn log_line(line: String) {
  println!("log_line: {line}");
}

要 State 的 async 命令只能用修法一,State<'_, T> 本身带生命周期。用 MockRuntime 调一次(cargo test --bin pit1-async -- --nocapture,只编这一个 bin,跳过那两个编不过的):

running 1 test
save {"name":"rie"} => Ok(Json("\"saved rie\""))
log_line: hi
log_line {"line":"hi"} => Ok(Json("null"))
test tests::fixed_commands ... ok

错误类型 E 只要实现了 Into<InvokeError> 就行,String 最省事;想让 JS 拿到结构化的错误,见 参数、返回值与 InvokeResolver 的错误一节。修法一里 Result 的 Err 一次都不返回也没关系,这个约束只看类型。

怎么确认没中招

  • 报错里出现 AsyncCommandMustReturnResult、__tauri_message__ does not live long enough 或 cannot find macro __cmd__<命令名>,都先看这个命令是不是 async、参数里有没有 & 或 '_。
  • 写 async 命令时默认返回 Result<T, String>(或者自己的错误类型),以后加 State 参数不用再改签名。
  • 想在 async 命令里保留借用、又不想改返回类型,没有第三种办法:Cow<str> 这类省略生命周期的写法会先被 rustc 以 E0726 拒绝,写成 Cow<'_, str> 又会被宏认出来。

小结

  • async 命令的参数里有引用或带生命周期的类型(包括 State<'_, T>)时,返回类型必须是 Result。
  • 有返回类型时是 E0277 加 E0597 两条;没有返回类型时是一条 compile_error! 加两条「cannot find macro」。
  • 原因在 FutureTag::future 的 'static 约束;返回 Result 走的 ResultFutureTag::future 没有这个约束。
  • 改成返回 Result,或者参数改成 String 这类拥有所有权的类型。

💬 关联讨论

发起讨论
加载中...