Rust in Everything
基于 tauri 2.12.5

#[command] 与 generate_handler!

主线位置:调用主线的中段。Webview::on_message 通过 ACL 检查后,把 Invoke 交给 Builder::invoke_handler 注册的那个闭包;这个闭包是 generate_handler! 生成的,按命令名 match 到某个命令的包装代码;包装代码由 #[command] 生成,逐个取参数、调用你的函数、把返回值交给 InvokeResolver。本页讲这两个宏生成了什么;on_message 之前的路见 on_message 与分发,参数怎么从 JSON 里反序列化、结果怎么编码回去、命令在哪个线程上跑,见参数、返回值与 InvokeResolver。

flowchart TB
  onmsg["Webview::on_message"] --> handler["generate_handler! 闭包"]
  handler -->|"match 命令名"| wrap["__cmd__greet 包装"]
  handler -->|"没有匹配"| nf["返回 false"]
  wrap --> args["CommandArg::from_command"]
  args --> userfn["greet 原函数"]
  userfn --> kind{"同步还是 async"}
  kind -->|同步| block["kind.block 就地 respond"]
  kind -->|async| spawn["respond_async_serialized"]

本页的实验 crate 是 build-cmd:从模板 base 复制,写了七个命令,用 tauri::test 的 MockRuntime 逐个调用(不建真窗口);build-cmd-err 放四个编译不过的 async 命令,每个一个 bin。依赖 tauri =2.12.5,--features tauri/custom-protocol。平台:M2 Max、macOS 27、rustc 1.99.0,cargo-expand 1.0.104。build-cmd 的命令:

use std::sync::atomic::{AtomicU32, Ordering};

#[derive(Default)]
struct Counter(AtomicU32);

#[tauri::command]
fn greet(name: &str) -> String {
  format!("hello, {name}")
}

#[tauri::command]
async fn word_count(text: String) -> usize {
  text.split_whitespace().count()
}

#[tauri::command]
fn bump<R: tauri::Runtime>(counter: tauri::State<'_, Counter>, app: tauri::AppHandle<R>) -> u32 {
  let _ = app.package_info();
  counter.0.fetch_add(1, Ordering::SeqCst) + 1
}

#[tauri::command(rename_all = "snake_case")]
fn snake(user_name: String) -> String {
  user_name
}

#[tauri::command(rename = "greetUser")]
fn greet_user(user_name: String) -> String {
  user_name
}

#[tauri::command]
async fn borrow_ok(name: &str) -> Result<String, String> {
  Ok(name.to_uppercase())
}

#[tauri::command]
async fn peek(counter: tauri::State<'_, Counter>) -> Result<u32, String> {
  Ok(counter.0.load(Ordering::SeqCst))
}

fn handlers<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::Builder<R> {
  builder.invoke_handler(tauri::generate_handler![greet, word_count, bump, snake, greet_user, borrow_ok, peek])
}

#[command] 产出什么

wrapper 不改你的函数,原样输出,然后在旁边加两个 macro_rules! 宏:

  quote!(
    #async_command_check

    #maybe_allow_unused
    #function

    // Command name macro used by the handler for pattern matching.
    // This macro returns the command name string literal (renamed or original).
    #maybe_allow_unused
    #maybe_macro_export
    #[doc(hidden)]
    macro_rules! #command_name_macro_ident {
      () => {
        #command_name_value
      };
    }

    #maybe_allow_unused
    #maybe_macro_export
    #[doc(hidden)]
    macro_rules! #wrapper {
      // double braces because the item is expected to be a block expression
      ($path:path, $invoke:ident) => {
        // The IIFE here is for preventing stack overflow on Windows,
        // see https://github.com/tauri-apps/tauri/issues/12488
        {
          move || {
            #[allow(unused_imports)]
            use #root::ipc::private::*;
            // prevent warnings when the body is a `compile_error!` or if the command has no arguments
            #[allow(unused_variables)]
            let #root::ipc::Invoke { message: #message, resolver: #resolver, acl: #acl } = $invoke;

            #maybe_span

            #body
          }
        }()
      };
    }

    // allow the macro to be resolved with the same path as the command function
    #[allow(unused_imports)]
    #visibility use {#wrapper, #command_name_macro_ident};
  )
  • __tauri_command_name_greet!() 展开成命令名字面量:默认是 stringify!(greet),用了 rename 就是新名字。
  • __cmd__greet!(路径, invoke) 展开成真正的包装代码。它收两个参数:原函数的路径 $path 和 Invoke 变量名,在 generate_handler! 的调用处展开,包装代码里的 $path(..) 就是对原函数的调用。
  • 最后一行 use {..} 把两个宏按函数的可见性重新导出,于是 commands::greet 旁边就有 commands::__cmd__greet。函数是 pub 或 pub(..) 时两个宏还带 #[macro_export]。

cargo expand 不显示 macro_rules! 定义,#[command] 处只剩原函数和那一行 use:

fn greet(name: &str) -> String {
    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("hello, {0}", name))
    })
}
#[allow(unused_imports)]
use {__cmd__greet, __tauri_command_name_greet};

包装代码要到 generate_handler! 调用这个宏的地方才展开。

一个参数怎么取

parse_arg 对每个参数生成同一种表达式,不看参数的类型:

  Ok(quote!(#root::ipc::CommandArg::from_command(
    #root::ipc::CommandItem {
      plugin: #plugin_name,
      name: #command_name,
      key: #key,
      message: &#message,
      acl: &#acl,
    }
  )))
  • key:参数模式里的名字(r#type 这类原始标识符会去掉 r#),默认转成 lowerCamelCase,rename_all = "snake_case" 时转成 snake_case;只接受这两个值,别的写法编译报「expected "camelCase" or "snake_case"」。_ 得到空字符串。只允许标识符、_、结构体和元组结构体模式,结构体模式取的是类型名的最后一段。
  • plugin:读编译时的 CARGO_PKG_NAME,以 tauri-plugin- 开头就取剩下的部分,否则 None。应用里的命令都是 None。
  • name:命令名,只在错误信息里用。
  • message、acl:包装代码从 Invoke 里解构出来的 InvokeMessage 和 ACL 检查得到的 Option<Vec<ResolvedCommand>>。

要什么值,全靠参数类型上的 CommandArg 实现决定。实现了 Deserialize 的类型走通用实现,把 CommandItem 当作 serde 的 Deserializer,从请求体的 JSON 对象里按 key 取值;其余几种不看请求体,从 message 里拿:

参数类型实现拿到什么
T: Deserializecommand.rs请求体 JSON 里 key 对应的值
State<'_, T>state.rsmanage 过的状态,没有就报「state not managed for field ...」
AppHandle<R>app.rs发起调用的 webview 所属的应用
Window<R> / Webview<R> / WebviewWindow<R>window/mod.rs、webview/mod.rs、webview_window.rs发起调用的窗口 / webview
ipc::Request<'_>ipc/mod.rs原始请求体和请求头
ipc::Channel<T>channel.rs先按 key 反序列化出一个字符串,再解析成通道 id
CommandScope<T> / GlobalScope<T>authority.rs允许这次调用的能力带来的作用域

所以宏不需要认识 State:bump 的两个参数照样各生成一个 key("counter"、"app"),只是这两个实现不读它(State 的错误信息里会引用它)。展开出来的 bump 分支:

                "bump" => {
                    ({
                        move || {
                            #[allow(unused_imports)]
                            use ::tauri::ipc::private::*;
                            #[allow(unused_variables)]
                            let ::tauri::ipc::Invoke {
                                message: __tauri_message__,
                                resolver: __tauri_resolver__,
                                acl: __tauri_acl__,
                            } = __tauri_invoke__;
                            let result = bump(
                                match ::tauri::ipc::CommandArg::from_command(::tauri::ipc::CommandItem {
                                    plugin: ::core::option::Option::None,
                                    name: "bump",
                                    key: "counter",
                                    message: &__tauri_message__,
                                    acl: &__tauri_acl__,
                                }) {
                                    Ok(arg) => arg,
                                    Err(err) => {
                                        __tauri_resolver__.invoke_error(err);
                                        return true;
                                    }
                                },
                                match ::tauri::ipc::CommandArg::from_command(::tauri::ipc::CommandItem {
                                    plugin: ::core::option::Option::None,
                                    name: "bump",
                                    key: "app",
                                    message: &__tauri_message__,
                                    acl: &__tauri_acl__,
                                }) {
                                    Ok(arg) => arg,
                                    Err(err) => {
                                        __tauri_resolver__.invoke_error(err);
                                        return true;
                                    }
                                },
                            );
                            let kind = (&result).blocking_kind();
                            kind.block(result, __tauri_resolver__);
                            return true;
                        }
                    })()
                }

参数按声明顺序求值,第一个失败的参数直接 invoke_error 并返回,后面的不再尝试。JSON 怎样变成参数值、错误信息怎么拼,见参数、返回值与 InvokeResolver;&str 能借用请求体里的字符串,是 serde 的借用反序列化,见 serde 卷的生命周期 'de 与借用。

📝NOTE
没有蠢问题

问: bump 一开始写的是 app: tauri::AppHandle,cargo run 能编译,加上 MockRuntime 测试就报「the trait serde_core::de::Deserialize<'_> is not implemented for AppHandle」。AppHandle 明明有自己的 CommandArg 实现,怎么扯到 Deserialize 上了?

答: 不写泛型参数的 AppHandle 是 AppHandle<Wry>:AppHandle<R> 上的 #[default_runtime(crate::Wry, wry)] 在开了 wry feature 时给 R 加上默认值。而包装代码是对 R 泛型的,测试里 R 是 MockRuntime,AppHandle<Wry>: CommandArg<'_, MockRuntime> 没有专门的实现,编译器只剩那个 D: Deserialize 的通用实现可试,于是报它不满足。完整的报错:

error[E0277]: the trait bound `AppHandle: CommandArg<'_, R>` is not satisfied
  --> src/main.rs:16:1
   |
16 | #[tauri::command]
   | ^^^^^^^^^^^^^^^^^ the trait `serde_core::de::Deserialize<'_>` is not implemented for `AppHandle`
...
38 |   builder.invoke_handler(tauri::generate_handler![greet, word_count, bump, snake, greet_user, borrow_ok])
   |                          ------------------------------------------------------------------------------- in this macro invocation
   |
   = help: the following other types implement trait `serde_core::de::Deserialize<'de>`:
...
   = note: required for `AppHandle` to implement `CommandArg<'_, R>`

改成 fn bump<R: tauri::Runtime>(.., app: tauri::AppHandle<R>) 就过了。State、Window 这类参数的报错如果提到 Deserialize,先查运行时参数对不对得上。

同步和 async 两种生成

#[command] 看函数有没有 async,或者属性里有没有 async(#[command(async)] 用于同步函数),选 body_blocking 还是 body_async。上面 bump 是同步的:参数用 match 取,取完直接调用函数,kind.block(result, resolver) 当场回应。word_count 是 async 的:

                "word_count" => {
                    ({
                        move || {
                            #[allow(unused_imports)]
                            use ::tauri::ipc::private::*;
                            #[allow(unused_variables)]
                            let ::tauri::ipc::Invoke {
                                message: __tauri_message__,
                                resolver: __tauri_resolver__,
                                acl: __tauri_acl__,
                            } = __tauri_invoke__;
                            __tauri_resolver__
                                .respond_async_serialized(async move {
                                    let result = word_count(
                                        ::tauri::ipc::CommandArg::from_command(::tauri::ipc::CommandItem {
                                            plugin: ::core::option::Option::None,
                                            name: "word_count",
                                            key: "text",
                                            message: &__tauri_message__,
                                            acl: &__tauri_acl__,
                                        })?,
                                    );
                                    let kind = (&result).async_kind();
                                    kind.future(result).await
                                });
                            return true;
                        }
                    })()
                }

区别有三处:参数提取挪进了 async move 块,失败用 ? 返回;word_count(..) 返回的 future 由 kind.future(..) 去 .await;整个块交给 respond_async_serialized,它要求 future 是 Send + 'static,在 async_runtime 上运行。message 被 move 进 async 块,参数从块里的这个局部变量借用,所以 &str 参数在 async 命令里也能工作,前提见下一节。

(&result).blocking_kind() 和 (&result).async_kind() 是自动引用特化:ipc::private 里给不同的类型各实现一个同名方法,方法解析先试接收者 &T 本身、再试 &&T,于是更具体的实现先命中:

返回值类型同步命中async 命中结果
Result<T, E>ResultKind(Self = Result)Err 转成 InvokeError
其他 T: IpcResponseResponseKind(Self = &T)总是成功
Future<Output = Result<T, E>>ResultFutureKind(Self = F)await 后同上
其他 Future<Output = T>FutureKind(Self = &F)await 后总是成功

宏不知道返回值的类型(它只看到语法),所以把选择推给类型检查。#[command(async)] 标在同步函数上时,生成的也是 body_async,只是函数立即返回一个值而不是 future:这时命中的是前两行的 async_kind,tracing 的 span 里 kind 记作 sync_threadpool。

async 命令借用参数

wrapper 里专门有一段检查:函数是 async,并且某个参数的类型是引用,或者是带生命周期参数的路径类型(State<'_, T> 就是),就要求返回 Result:

        if let Some(span) = ref_argument_span {
          if let syn::ReturnType::Type(_, return_type) = &function.sig.output {
            // To check if the return type is `Result` we require it to check a trait that is
            // only implemented by `Result`. That way we don't exclude renamed result types
            // which we wouldn't otherwise be able to detect purely from the token stream.
            // The "error message" displayed to the user is simply the trait name.
            let diagnostic = quote!(#[diagnostic::on_unimplemented(message = "async commands that contain references as inputs must return a `Result`")]);

            async_command_check = quote_spanned! {return_type.span() =>
              #[allow(unreachable_code, clippy::diverging_sub_expression, clippy::used_underscore_binding)]
              const _: () = if false {
                #diagnostic
                trait AsyncCommandMustReturnResult {}
                impl<A, B> AsyncCommandMustReturnResult for ::std::result::Result<A, B> {}
                let _check: #return_type = unreachable!();
                let _: &dyn AsyncCommandMustReturnResult = &_check;
              };
            };
          } else {
            return quote_spanned! {
              span => compile_error!("async commands that contain references as inputs must return a `Result`");
            }.into();
          }
        }

有返回类型时,检查是一个只在 Result 上实现的 trait:返回类型被别名改了名也认得出来,错误信息由 #[diagnostic::on_unimplemented] 换成人话。没写返回类型时直接 compile_error!,并且整个输出就只有这一句,原函数和两个宏都不生成了。build-cmd-err 的四个 bin 各写一个:

#[tauri::command]
async fn bad(name: &str) -> String {
  name.to_string()
}

fn main() {
  let _ = tauri::Builder::default().invoke_handler(tauri::generate_handler![bad]);
}

e1(上面这个)的报错:

error[E0277]: async commands that contain references as inputs must return a `Result`
 --> src/bin/e1.rs:2:29
  |
2 | async fn bad(name: &str) -> String {
  |                             ^^^^^^ the trait `AsyncCommandMustReturnResult` is not implemented for `std::string::String`
  |
help: the trait `AsyncCommandMustReturnResult` is implemented for `Result<A, B>`
 --> src/bin/e1.rs:2:29
  |
2 | async fn bad(name: &str) -> String {
  |                             ^^^^^^
  = note: required for the cast from `&std::string::String` to `&dyn AsyncCommandMustReturnResult`

error[E0597]: `__tauri_message__` does not live long enough
   --> src/bin/e1.rs:1:1
    |
  1 | #[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`
...
  7 |   let _ = tauri::Builder::default().invoke_handler(tauri::generate_handler![bad]);
    |                                                    ----------------------------- 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__bad` which comes from the expansion of the macro `tauri::generate_handler` (in Nightly builds, run with -Z macro-backtrace for more info)

两条错误,第一条是宏的检查,指向返回类型;第二条才是真正的原因,指向 tauri 源码里的一行约束。返回 String 的 async 命令命中的是 FutureKind,它的 FutureTag::future 要求 F: Future<Output = T> + Send + 'static;而 bad(name) 返回的 future 借用了 __tauri_message__(async 块里的局部变量),不是 'static。返回 Result 时命中的 ResultFutureTag::future 只要求 F: Future<Output = Result<T, E>> + Send,没有 'static,借用在 async 块内部闭合,外层 future 照样是 'static。宏的检查只是把第二条错误提前翻译成第一条。

其余三个 bin 的函数签名分别是 async fn bad(name: &str)(没有返回类型)、async fn bad(n: tauri::State<'_, u32>) -> u32、async fn bad(name: std::borrow::Cow<str>) -> String,报错(中间有删节):

===== e2
error: async commands that contain references as inputs must return a `Result`
 --> src/bin/e2.rs:2:14
  |
2 | async fn bad(name: &str) {
  |              ^^^^

error: cannot find macro `__tauri_command_name_bad` in this scope
 --> src/bin/e2.rs:7:52
...
error: cannot find macro `__cmd__bad` in this scope
 --> src/bin/e2.rs:7:77
...
===== e3
error[E0277]: async commands that contain references as inputs must return a `Result`
 --> src/bin/e3.rs:2:43
  |
2 | async fn bad(n: tauri::State<'_, u32>) -> u32 {
  |                                           ^^^ the trait `AsyncCommandMustReturnResult` is not implemented for `u32`
...
error[E0597]: `__tauri_message__` does not live long enough
...
===== e4
error[E0726]: implicit elided lifetime not allowed here
 --> src/bin/e4.rs:2:20
  |
2 | async fn bad(name: std::borrow::Cow<str>) -> String {
  |                    ^^^^^^^^^^^^^^^^^^^^^ expected lifetime parameter
  • e2 没有返回类型,compile_error! 指向参数;因为函数和宏都没生成,generate_handler! 那里又多出两条「cannot find macro」。
  • e3 说明 State<'_, T> 也在检查范围内:async 命令要 State 就得返回 Result,和 &str 一样。build-cmd 里的 peek 返回 Result<u32, String>,能编译、能调用。
  • e4 想用省略生命周期的 Cow<str> 绕过检查(语法上没有生命周期参数),rustc 先拦下了:async 函数的参数里不允许隐式省略的生命周期,写成 Cow<'_, str> 又会被宏的检查认出来。

rename_all 与 rename

snake 用了 rename_all = "snake_case",键是 user_name;greet_user 用了 rename = "greetUser",match 分支变成 "greetUser",键仍按默认规则是 userName。展开里 greet_user 的那一段:

                "greetUser" => {
                    ({
                        move || {
                            // ...
                            let result = greet_user(
                                match ::tauri::ipc::CommandArg::from_command(::tauri::ipc::CommandItem {
                                    plugin: ::core::option::Option::None,
                                    name: "\"greetUser\"",
                                    key: "userName",
                                    message: &__tauri_message__,
                                    acl: &__tauri_acl__,
                                }) {

name 多了一对引号。parse_arg 里写的是 quote!(stringify!(#r)),而 #r 在解析属性时已经是字符串字面量 "greetUser" 了,stringify! 再把它(连引号)变成字符串;没改名时 #command 是标识符,stringify!(greet) 才得到 "greet"。name 只用于错误信息,match 用的是另一个宏 __tauri_command_name_greet_user!(),所以调用本身不受影响,影响的是错误信息(见本页的动脑筋)。

MockRuntime 上逐个调用的结果(build-cmd 的 cargo test -- --nocapture):

greet {"name":"rie"} => Ok(String("hello, rie"))
word_count {"text":"a b c"} => Ok(Number(3))
bump {} => Ok(Number(1))
snake {"user_name":"s"} => Ok(String("s"))
snake {"userName":"s"} => Err(String("invalid args `user_name` for command `snake`: command snake missing required key user_name"))
greetUser {"userName":"u"} => Ok(String("u"))
greet_user {"userName":"u"} => Err(String("Command greet_user not found"))
borrow_ok {"name":"rie"} => Ok(String("RIE"))
greet {"name":42} => Err(String("invalid args `name` for command `greet`: invalid type: integer `42`, expected a borrowed string"))
peek {} => Ok(Number(1))

改名之后旧名字就调不到了:greet_user 落进 match 的 _ 分支。

❗IMPORTANT
动脑筋:用 {"user_name":"u"} 调用 greetUser(它期望 userName),返回的错误字符串里,命令名写成什么样?

generate_handler!

Handler 解析逗号分隔的路径列表,每项可以带属性(比如 #[cfg(..)]),列表前可以有 #![plugin(name)]。对每个路径,把最后一段 greet 换成 __cmd__greet 得到包装宏的路径,换成 __tauri_command_name_greet 得到命令名宏的路径,然后生成:

    quote::quote!(move |#invoke| {
      let #cmd = #invoke.message.command();
      match #cmd {
        #(#(#attrs)* #command_name_macros => #wrappers!(#paths, #invoke),)*
        _ => {
          return false;
        },
      }
    })

命令名宏用在模式位置上,展开成字符串字面量,所以这是一个普通的字符串 match。build-cmd 展开后的骨架:

        .invoke_handler(move |__tauri_invoke__| {
            let __tauri_cmd__ = __tauri_invoke__.message.command();
            match __tauri_cmd__ {
                "greet" => {
                    ({
                        move || {
                            // ...
                        }
                    })()
                }
                // "word_count"、"bump"、"snake"、"greetUser"、"borrow_ok"、"peek"
                _ => {
                    return false;
                }
            }
        })

闭包返回 bool:true 表示命令被认领了(结果会通过 resolver 回去,哪怕是错误),false 表示没有这个命令,on_message 据此 reject 一句「Command {command} not found」。plugin: 前缀的请求不进应用的这个闭包,交给对应插件注册的处理函数(插件里通常也是一个 generate_handler!),见 on_message 与分发。

每个分支是一个立即调用的闭包 { move || { .. } }(),而不是直接把包装代码写在分支里。这和栈有关:

📝NOTE
冷知识:每个分支为什么包一层立即调用的闭包

一个 match 有几十个分支、每个分支都内联了参数的反序列化时,整个闭包的栈帧要容纳所有分支的局部变量。#13217(首次发布于 tauri 2.5.0)把每个分支包进一个立即调用的闭包,各分支的局部变量就落在各自的栈帧里;CHANGELOG 原文是「Fix large number of commands with large structs as parameters causing stack overflow on debug build on Windows」。同一个 PR 还给闭包加了 #[cfg_attr(not(debug_assertions), inline(always))](提交说明里的一条是「Format and inline iife in release build」),release 构建里闭包被强制内联回 match,各分支的局部变量又合进同一个栈帧。#14170(首次发布于 tauri 2.9.0,即 tauri-macros 2.5.0)删掉了这个属性,注释里的「on Windows debug build」也改成了「on Windows」,CHANGELOG:「Fix the stack overflow when having too many commands in a single invoke handler in release build」。

generate_handler! 解析完列表后还会调 filter_unused_commands:只有 build script 写了 OUT_DIR/allowed-commands.json(build > removeUnusedCommands,见 tauri-build)才生效,把不在任何能力里的命令从列表里删掉,并在编译时打印「Removed unused commands from ...」。应用自己的命令只在有应用 ACL 清单时才过滤,否则全保留;插件名取 #![plugin(..)],没有就看 CARGO_PKG_NAME 是否以 tauri-plugin- 开头。被删掉的命令函数仍然编译,只是没有分支调用它;#[command] 在同样条件下给函数和两个宏加了 #[allow(unused)],源码注释说这是「for dead code elimination」。

动脑筋答案

命令名带着引号:

greetUser {"user_name":"u"} => Err(String("invalid args `userName` for command `\"greetUser\"`: command \"greetUser\" missing required key userName"))

Debug 打印把字符串里的引号转义成了 \",JS 侧拿到的错误字符串是 invalid args `userName` for command `"greetUser"`: command "greetUser" missing required key userName。两处命令名都来自 CommandItem::name:外层的「invalid args ... for command ...」是 crate::Error::InvalidArgs(name, arg, e) 拼的,内层的「command ... missing required key ...」是 CommandItem 作为 Deserializer 找不到键时拼的。对照没改名的 snake,同样的错误里是 `snake` 和 command snake,没有引号。原因是上一节的 stringify!(#r) 作用在一个字符串字面量上。这是 tauri-macros 2.7.4 的一个小 bug:只影响错误信息,不影响分发和 ACL。

小结

  • #[command] 原样保留函数,另生成 __cmd__<名字> 和 __tauri_command_name_<名字> 两个宏,并按函数可见性 use 导出,generate_handler! 只凭函数路径就能找到它们。
  • 每个参数生成一次 CommandArg::from_command(CommandItem { plugin, name, key, message, acl });宏不认识 State、AppHandle 这些类型,取什么由类型上的实现决定。键默认 camelCase,rename_all = "snake_case" 可改。
  • 同步命令就地取参、调用、kind.block 回应;async 命令把这些放进 async move 块交给 respond_async_serialized。返回值是不是 Result、是不是 future,靠自动引用特化在类型检查时选。
  • async 命令有引用参数(含 State<'_, T>)就必须返回 Result:非 Result 的 future 走 FutureTag,那里要求 'static;宏的检查把这条借用错误翻译成「async commands that contain references as inputs must return a Result」。
  • generate_handler! 展开成 move |invoke| match invoke.message.command() { .. },每个分支一个立即调用的闭包(防栈溢出),没有匹配返回 false;开了 removeUnusedCommands 时在这里删掉未授权的命令。
  • rename 之后错误信息里的命令名多一对引号,这是 stringify! 用在字符串字面量上的结果。

💬 关联讨论

发起讨论
加载中...