LIIGO: Why say NO to tauri

tauri

LIIGO写在前面的话:

以下文字是从《A 2025 Survey of Rust GUI Libraries》一文中摘抄出来的,作者是Melody Horn。对于他评价Tauri的观点,我深度的感同身受。一句话概述,Tauri严重的人为的割裂了桌面APP内部的前端和后端。我此前的博文《初次体验Tauri和Sycamore(1)Tauri 2.0》也曾经隐晦地表达过类似的不满(后续同系列博文也体现出我逐步倾向于Dioxus)。在一个桌面APP内部,Rust前端和Rust后端,在Tauri架构下居然有强烈的割裂感,双方要依靠进程间通讯(IPC)进行交互。我(LIIGO)前一段时间做过专项调查,发现Electron和Dioxus Desktop都没有这种割裂感,与Tauri形成强烈反差。原生于Rust社区的Tauri框架对Rust用户如此不友好属实不应该,我建议其下一代3.0架构应参考Dioxus Desktop(所有Rust代码编译为目标系统原生代码而非部分使用WebAssembly(WASM))。


Do you like Electron but wish it was Rust? Tauri is that. To their credit, they’ve also swapped out the bundled Chromium for just binding to the system’s inherent web browser, whether that’s WebView2 on Windows, WebKit on macOS, or WebKitGTK on Linux, so Tauri applications don’t fill your hard drive with a dozen copies of the same web browser. Unfortunately, they have not touched the architecture; you still have a host process running outside of the browser and an independent frontend running inside the browser.

Building that frontend is not something Tauri is concerned with; you have to decide for yourself what you want your stack to look like. I want to write Rust, so in the new project wizard I pick Rust as my frontend language (rather than JavaScript/TypeScript or, for all three diehard Blazor fans, C#). I’m then asked which Rust frontend framework I want, and I don’t really want to have to pick between Dioxus and Leptos and Sycamore and Yew right now, so I pick “Vanilla” assuming that it’ll give me bare web-sys to make the same DOM API calls as vanilla JS but in Rust; I’ve done this before, and it’s not very good, but at this scale it’d be completely tolerable. Instead, though, “Vanilla” means vanilla JS even if I selected Rust, and since I selected Rust I don’t even get an option of vanilla TS instead. This was reported a year and a half ago and ignored.

If I’m stuck making a choice, I need an excuse to ignore three of the four provided options. I already looked at Dioxus, so that’d be boring to use again. Yew’s 0.22 release was announced in October 2024, added to the changelog in December 2024, and released on crates.io literally never (as of April 2025); that’s not great. The Leptos book says that it’s “most similar to frameworks like Solid and Sycamore”; maybe that’s a sign that it doesn’t matter, or maybe it’s a sign that I should try both.

I guess the main difference between these two Web frameworks that we can see from here is that Sycamore has a bind: modifier for attributes (very good!) but can’t quite handle input type= because type is a Rust keyword and requires r#type instead (very bad!). The largest issue, though, is something I haven’t captured here, because today’s task is so trivial it can be done entirely within the frontend, and I let that count for GTK so I have to let it count here too.

If I add the requirement that the new value of the label be printed to standard output when the label changes (as a stand-in for, say, performing some file I/O), in most frameworks it suffices to either add a println! to the existing event handler or subscribe to a two-way-bound state with a println!. Even Dioxus, built on the same WebView2/WebKitGTK library as Tauri, will do the right thing if I println! from an event handler. In Tauri, though, a println! from the frontend will be completely ignored, and if we want to be able to println! we need inter-process communication between the frontend and the host process.

In the host, this is nice and easy due to the magic of proc macros:

#[tauri::command]
fn print(text: &str) {
    println!("{}", text);
}

In the frontend, however, there is a lot more boilerplate:

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])]
    async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}

#[derive(Serialize, Deserialize)]
struct PrintArgs<'a> {
    text: &'a str,
}

#[component]
pub fn App() -> View {
    // ...

    create_effect(move || {
        let label = label.get_clone();
        spawn_local_scoped(async move {
            let args = serde_wasm_bindgen::to_value(&PrintArgs { text: &label }).unwrap();
            invoke("print", args).await;
        })
    });

    // ...
}

This makes me sad for two reasons. The first is a question of principle: this arbitrary boundary drawn through the middle of my application means that if I discover I need a new piece of functionality I may need to move a substantial chunk of code from the frontend to the backend, and if it’s something load-bearing within the frontend I’m going to have a real motherfucker of a time pivoting my architecture on short notice for no reason. The second is a question of type safety: as you may have noticed, the IPC interface in the frontend takes an &str for the command name and a JsValue for the command arguments, meaning frontend IPC calls have no type checking. Indeed, if I rename the argument in the host from text to text_to_print and don’t update the PrintArgs in the frontend, I get not even a warning at compile time, and at runtime I get

panicked at src\app.rs:6:1:
unexpected exception: JsValue(“invalid args textToPrint for command print: command print missing required key textToPrint”)

Uncaught RuntimeError: unreachable

If I then change the label, I get

panicked at C:\Users\Melody.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\wasm-bindgen-futures-0.4.50\src\task\singlethread.rs:103:37:

already borrowed: BorrowMutError

and this is all just misery upon misery. Half the point of Rust is the sheer quantity of bugs that it can catch at compile time, and if your IPC is just tossing strings around and praying at runtime, you may as well be just writing vanilla JavaScript. (I checked, and even if your frontend is TypeScript, the invoke IPC boundary just takes a string rather than a union of the actual legal values.)

Hilariously, the Tauri docs claim that the command mechanism is “for reaching Rust functions with type safety”, as distinct from their event system, which is even less type safe. With events, you’re tossing strings and arbitrary JavaScript payloads around in both the frontend and the host, so technically it’s not false to claim that only doing that on one end is more type safe, but type checking only at one end is like putting a lock on your bike but not running it through the bike rack: you aren’t tying two things together, you’re just tying one thing to itself and praying. Even to the limited extent that half of type safety could be useful, though, they’ve picked the wrong half: there’s inherently only one implementation of the command, but there can be many calls to it, so it’d be far more valuable to have type safety at the call sites than at the implementation site. Commands only go from the frontend to the host, so type checking in the host and YOLOing in the frontend is being picky at the receive end and sloppy at the send end, which is the exact opposite of Postel’s law.

The common formulation of Postel’s law is “be conservative in what you emit and liberal in what you accept”, and it’d be more recognizable if I used those terms instead of “picky” and “sloppy”. I haven’t done that, though, because you should be conservative never and liberal very rarely. Be radically leftist in everything, even technical blog posts that aren’t intrinsically political.

If there was an unnecessary IPC boundary but it was type safe, or if there was bad stringly typed nonsense somewhere but no unnecessary IPC boundary, I might find it within myself to forgive that, but the combination of the entirely unnecessary split-brain architecture with the absolute lack of type safety at the boundary means that I think I genuinely hate Tauri.

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值