跳到主要内容

73 篇博文 含有标签「Rust」

查看所有标签

作为 Rust 项目的包管理与构建工具,Cargo 提供了众多子命令(subcommand)用于完成依赖管理、构建、测试、发布、文档生成、升级等各种任务。 本篇文章将通过对常用子命令的详细解读,带你系统、深入地掌握 Cargo 的使用。 同时,文中会着重介绍与 features(特性)相关的用法及注意事项。

目录

  1. 初识 Cargo 与常见工作流
  2. 特性 features 与 Feature Unification
  3. 常用子命令详解
  1. features 相关的命令及注意事项
  2. 总结

初识 Cargo 与常见工作流

  • Cargo.toml:项目的主配置文件,里面包含项目名、版本、依赖以及特性等信息。
  • Cargo.lock:记录当前项目锁定的依赖版本信息,用于保证构建的可重现性。
  • 常见流程
    1. cargo new myprojectcargo init:初始化一个新的 Rust 项目。
    2. cargo build / cargo run / cargo check:进行构建、运行或仅做语义检查。
    3. cargo test:运行测试用例。
    4. cargo doc:生成文档。
    5. cargo publish:发布到 crates.io(或私有 Registry)。

特性 features 与 Feature Unification

features 是 Cargo 中的一种可选依赖机制,用于按需启用或禁用代码的某些部分。

  • 默认特性default feature 会在没有指定其他 feature 时被自动启用。
  • 依赖特性:有时一个 Crate 内部的某些特性在另一个 Crate 里会被依赖并合并启用(也称为特性统一,Feature Unification)。简单来说,只要有一个地方启用了某个特性,最终编译时就会被全部启用。

在使用 features 时需要注意:

  1. 有时不同依赖会对同一个 Crate 启用不同特性,最终编译时将把所有特性汇合。
  2. 可以通过命令行参数 --features--no-default-features--all-features 等来控制启用哪些特性。

常用子命令详解

1. cargo initcargo new

  • 作用:用来初始化或创建全新的 Rust 项目目录。
  • 区别
    • cargo init: 在一个已存在的空目录里初始化一个 Cargo 项目(生成 Cargo.tomlsrc 目录)。
    • cargo new: 在一个不存在或为空的目录中新建项目。

常用参数

  • --bin:创建可执行项目(包含 src/main.rs)。
  • --lib:创建类库项目(包含 src/lib.rs)。
  • --edition <year>:指定 Rust Edition,如 2015, 2018, 2021, 2024 等。

示例

# 在当前目录初始化项目
cargo init

# 新建一个名为 my_cli 的二进制项目
cargo new my_cli --bin

# 新建一个库项目,并指定为 2021 edition
cargo new my_lib --lib --edition 2021

2. cargo add

  • 作用:向当前项目的 Cargo.toml 中添加或修改依赖。该命令在 nightly 之外也通常通过 cargo-edit 插件提供。
  • 常用参数
    • cargo add crate_name:添加一个依赖(默认加到 [dependencies])。
    • --dev: 添加到 [dev-dependencies] 中。
    • --build: 添加到 [build-dependencies] 中。
    • --path <path>:以本地路径添加依赖。
    • --git <url>:从 git 仓库添加依赖。
    • --features / -F:指定要启用的 feature;可以用 crate_name/feature_name 启用子特性。
    • --no-default-features:关闭默认特性。

示例

# 添加 regex 到 dependencies
cargo add regex

# 添加 trybuild 到 dev-dependencies
cargo add --dev trybuild

# 添加 serde & serde_json 并启用 serde/derive 特性
cargo add serde serde_json -F serde/derive

# 从 git 仓库添加依赖
cargo add --git https://github.com/user/repo crate_name

3. cargo tree

  • 作用:可视化查看依赖关系树,帮助分析依赖层级、重复依赖和特性启用情况等。
  • 常用参数
    • cargo tree:查看默认依赖树。
    • cargo tree --no-dedupe:不去重,重复出现的依赖也全部显示。
    • cargo tree -d / --duplicates:只显示被多次依赖、可能存在重复版本的依赖。
    • cargo tree -i <package> / --invert <package>:查看哪些包依赖了 <package>
    • cargo tree -e features:可视化显示各依赖所启用的特性。
    • cargo tree -e features -i <crate>:倒置依赖树并查看该包及其特性被如何启用。

示例

# 基础用法
cargo tree

# 查看启用的 features
cargo tree -e features

# 查看 crate "syn" 被谁依赖、启用了哪些特性
cargo tree -e features -i syn

4. cargo update

  • 作用:更新 Cargo.lock 中的依赖到最新的可用版本。如果没有 Cargo.lock 文件,则会创建一个新的锁文件。
  • 常用参数
    • cargo update:更新所有依赖到兼容的最新版本。
    • cargo update <crate1> <crate2>:仅更新指定的依赖包。
    • --precise <version>:将指定的依赖更新到一个明确版本(或 git SHA)。
    • --dry-run:只显示将要更新的结果,不实际写入 Cargo.lock

示例

# 更新全部依赖
cargo update

# 更新特定依赖
cargo update foo bar

# 更新 foo 到指定版本
cargo update foo --precise 1.2.3

5. cargo check

  • 作用:仅做语义和类型检查,不会生成最终的可执行文件或库文件,速度比 cargo build 更快。
  • 常用参数
    • cargo check:检查当前包。
    • --all-targets:检查所有目标(包括测试、bench 等)。
    • -p <package> / --package <package>:指定要检查的包。
    • --features <features> / --all-features / --no-default-features:选择启用的特性。

示例

# 对当前包进行语义和类型检查
cargo check

# 检查所有目标(包含测试目标、bench等)
cargo check --all-targets --profile test

6. cargo searchcargo info

  • cargo search

    • 作用:在 registry(默认 crates.io)上搜索 crate。
    • --limit <n>:限制搜索结果数。
    • --registry <name>:指定自定义 registry 进行搜索。

    示例

    # 在 crates.io 搜索 "serde"
    cargo search serde

    # 限制返回结果数量
    cargo search serde --limit 5
  • cargo info

    • 作用:查看(本地或远程) crate 的详细信息,如版本、依赖、README 等。
    • 可以结合 --registry 或直接查看本地 Cargo.toml 里指定的版本信息。

    示例

    # 查看 crates.io 上的最新 serde 的信息
    cargo info serde

    # 查看特定版本
    cargo info serde@1.0.0

7. cargo installcargo uninstall

  • cargo install

    • 作用:安装用 cargo build 构建的二进制包。
    • 常用参数
      • cargo install <crate_name>:安装对应的二进制包。
      • --bin <bin_name>:只安装特定的可执行文件。
  • cargo uninstall

    • 作用:卸载用 cargo install 安装的二进制包。
    • 常用参数
      • cargo uninstall <crate_name>:卸载对应的二进制包。
    • --bin <bin_name>:只卸载特定的可执行文件。

示例

# 安装 ripgrep
cargo install ripgrep

# 卸载 ripgrep
cargo uninstall ripgrep

# 只卸载 ripgrep 的可执行文件
cargo uninstall --bin rg

8. cargo fix

  • 作用:自动根据编译器输出的建议修改源代码,通常用于修复警告,或进行 edition 升级等自动化操作。
  • 常用场景
    • 升级 edition 时(比如从 2018 升到 2021):cargo fix --edition
    • 修复所有特性或风格相关的警告:cargo fix --edition-idioms
  • 常用参数
    • --broken-code:即使源代码本身有编译错误也尝试修复。
    • --allow-dirty / --allow-staged:在工作区不是干净状态或已经有 staged 修改时仍允许执行 fix。
    • --features / --all-features:若想对特定 feature 下的代码进行 fix,需要手动指定特性启用。

示例

# 在本项目中应用修复
cargo fix

# 帮助切换到下一版 edition(不会自动修改Cargo.toml)
cargo fix --edition

# 修复edition风格,并启用所有特性
cargo fix --edition-idioms --all-features

9. cargo doc

  • 作用:生成当前项目及其依赖的文档,默认输出到 target/doc
  • 常用参数
    • --open:生成完文档后自动在浏览器中打开。
    • --no-deps:只生成当前项目的文档,不包含依赖。
    • --document-private-items:包含非公有(private)项的文档(默认对二进制目标是开启的)。

示例

# 生成当前项目及依赖的文档
cargo doc

# 生成完文档立即打开
cargo doc --open

# 只生成当前项目自身文档
cargo doc --no-deps

10. cargo run

  • 作用:构建并运行当前包或指定包的二进制。
  • 常用参数
    • --bin <bin_name>:若有多个二进制目标,通过此选项指定要运行的目标。
    • --example <example_name>:运行 example 目标。
    • --release:使用 release 配置来构建并运行,启用优化。
    • --features <features> / --no-default-features / --all-features:在运行时启用或禁用特性。

示例

# 运行当前项目的主二进制
cargo run

# 运行名为server的可执行目标,并传递参数给它
cargo run --bin server -- --port 8080 --debug

# 运行 example 中的 exname
cargo run --example exname -- --exoption exarg1 exarg2

11. cargo rustc

  • 作用:编译当前包并向最终的 rustc 编译器传递额外参数。适合需要传递额外编译器选项或对编译过程做更精细控制。
  • 使用限制:只能针对单一目标使用传递参数,若需要对多目标同时编译时请使用全局编译参数(如 RUSTFLAGS 环境变量)。
  • 常用参数
    • cargo rustc --lib -- -D warnings:编译 library 并把所有 warning 当作错误处理。
    • --bin <bin_name>:编译特定二进制目标。
    • --crate-type <types>:手动指定 crate 类型(如 lib,cdylib)。
    • 其余与 cargo build 类似的参数,如 --release--features--all-features 也通用。

示例

# 对lib进行编译,并将所有warning视为错误
cargo rustc --lib -- -D warnings

# 覆盖 Cargo.toml 中 crate-type
cargo rustc --lib --crate-type lib,cdylib

# 使用 nightly 中的实验性命令行选项
cargo rustc --lib -- -Z print-type-sizes

  1. 启用或禁用默认特性

    • --no-default-features:完全禁用默认特性。
    • --features="foo,bar":显式启用某些特性。
    • --all-features:启用所有可用特性。
  2. 常见在命令中的使用

    • cargo build --features="foo"
    • cargo check --no-default-features --features="bar"
    • cargo run --features="server_mode,logging"
    • cargo tree -e features:可视化依赖树中的特性启用情况。
    • cargo fix --features mycrate/async --edition:自动修复在特定 feature 下的代码并进行 edition 修正。
  3. Feature Unification 特性合并
    当多个依赖对同一个 crate 启用了不同的特性,Cargo 会将这些特性统一在一起,也就是说只要有一个依赖启用了某 feature,就会对编译起作用。若对依赖项的特性启用不一致,需要注意这可能引入更多的编译代码,甚至在极端情况下导致依赖冲突。

  4. 排查特性冲突
    如果想知道某个 crate 的哪些特性是被哪些包或者自身项目启用的,可通过

    cargo tree -e features -i <crate_name>

    加上 --no-dedupe 进一步查看。


总结

在 Rust 的生态系统中,Cargo 几乎可以说是“万金油”般的存在——它负责管理项目结构、依赖、文档、编译、测试、发布的方方面面。 熟练掌握各个常用子命令,尤其是明白 features 如何运作与合并,对于提升开发效率、优化项目结构而言至关重要。

  • 日常操作cargo build / cargo run / cargo check / cargo test 构成了主要工作流。
  • 特性管理是 Cargo 中的高级功能,需要对 “Feature Unification” 有所了解,以防止出现不一致或冲突。
  • 通过 cargo tree, cargo update, cargo add 以及 cargo doc 等命令,能够更好地管理依赖、生成文档、快速定位问题。
  • 遇到需要自动修复或 edition 升级时,cargo fix 也能帮我们省力不少。

希望这篇文章能够帮助你在项目中轻松地掌握并运用 Cargo 的各种命令与特性,从而让 Rust 的开发体验更上一层楼!

鱼雪

2024年标志着Burn架构的重大演变。 传统的深度学习框架常常要求开发者在性能、可移植性和灵活性之间做出妥协;而我们的目标是超越这些权衡。 展望2025年,我们致力于将这一理念应用于整个计算栈,从嵌入式设备到数据中心,涵盖所有领域。

2024年回顾:突破硬件限制

重新定义内核开发

今年之初,我们面临一个限制:我们的WGPU后端依赖于基础的WGSL模板,限制了我们的适应能力。这个挑战促使我们创建了CubeCL [1],这是我们统一内核开发的解决方案。这项任务非常复杂——设计一个抽象层,适用于各种不同的硬件,同时保持顶级性能。我们的结果证明了这一策略的有效性,在大多数基准测试中,性能现在已匹配甚至超过LibTorch。

多后端架构

后端生态系统现已包括CUDA [2]、HIP/ROCm [3]以及支持WebGPU和Vulkan的先进WGPU实现 [4]。迄今为止最显著的成就是在相同硬件上实现不同后端的性能平衡。例如,无论是在CUDA还是Vulkan上执行矩阵乘法操作,性能几乎相同,这直接反映了我们平台无关优化的策略。

我们还引入了新的Router和HTTP后端:Router后端支持多后端的动态混合,而HTTP后端则支持跨多台机器的分布式处理。为了解决内存管理挑战,我们实施了池化和检查点机制,即使在反向传播期间也能实现操作融合。

硬件无关加速

我们的硬件加速策略标志着一个重要的技术里程碑。我们并不依赖于特定平台的库,如cuBLAS [5]或rocBLAS [6],而是开发了一套编译器栈,利用每个平台的最佳特性,同时确保跨平台的兼容性。这涉及克服代码生成和优化中的复杂挑战,尤其是对于矩阵乘法等操作,必须高效利用各种硬件架构的张量核心。

2025年路线图:拥抱极端

在2025年,我们将解决深度学习部署中的两个基本挑战。

小规模:量化

量化对于资源有限的计算至关重要。 我们的方法使用复杂操作的融合,通过“读取时融合”功能,实现如归约等任务在计算管道中的无缝集成。 这种融合策略自动处理操作的打包和解包,确保量化操作高效运行,无需手动调整。 结果是什么?高性能的量化操作在保持精度的同时,降低了资源需求。

大规模:可扩展的分布式计算

在另一端,是分布式计算。 通过利用我们的Router和HTTP后端构建强大的分布式训练基础设施,我们旨在创建一个流畅的分布式计算体验, 使工作负载能够在不同硬件和后端配置之间轻松流动,同时优化异构计算环境中的资源利用。

为了支持这种普遍兼容性的愿景,我们正在扩展我们的后端生态系统,包括:

  • 开发Metal后端,充分利用Apple Silicon的能力,超越当前WGPU的功能;
  • 在Rust中实现一个即时向量化的CPU后端,以增强CPU性能;
  • 开启新的后端可能性,如FPGA支持,确保Burn能够适应任何计算环境。

我们还将大量投资于开发者体验,提供全面的CubeCL文档,并推动Burn API的稳定化。这些改进将使开发者更容易利用我们跨平台能力的全部潜力。

在2024年,我们证明了跨平台性能不需要妥协。 展望2025年,我们将这一原则扩展到整个计算领域——从微控制器到服务器农场。 通过解决两个极端的技术挑战,我们致力于使深度学习在任何规模或硬件限制下都更加高效和易用。

参考文献

鱼雪

本文将深入探讨 Rust 语言中的 vec::Drain 及其 Drop 实现,作为所有权如何防止内存及其他微妙错误的一个例子。

目标读者:能够阅读 Rust 代码,并对其所有权语义及 Drop 特性有基本(且仅是基本)理解的开发者。

引言

在读《The Rust Programming Language》书籍时,偶然发现了 Vec::drain 方法。 常见的编程语言中,没有见过以 drain 命名的方法,这引起了我的好奇心。

什么是 Vec::drain

如果你不熟悉 Vec::drain,可以像下面这样使用它从 Vec抽取元素(类似的方法还存在于 StringHashMap 及其他多种集合类型中):

let mut values = vec![1, 2, 3, 4, 5];
for val in values.drain(1..3) {
println!("Removed: {}", val);
}
println!("Remaining: {:?}", values);

这段代码的输出为:

Removed: 2
Removed: 3
Remaining: [1, 4, 5]

文档描述

截至 Rust 1.83,文档对 Vec::drain 的描述如下(加粗部分为重点):

批量移除向量中指定范围的元素,返回所有被移除元素的迭代器。如果在完全消费之前迭代器被丢弃,它会丢弃剩余被移除的元素。

返回的迭代器保持对向量的可变借用,以优化其实现。

最后一句,特别是我加粗的部分,引起了我的注意,并促使我深入研究其实现。

Vec::drain 的内部实现

一种合理的实现方式

一种完全合理的实现方法是:接收 Vec,复制出所有要移除的元素并放入新的 Vec,更新原始 Vec 以移除这些元素,并返回由新分配的 Vec 支持的迭代器。

然而,这种方法对于计算机来说可能需要大量的前期工作。 如果 Vec 很大(包含数千或数百万个元素)且操作的是中间的一部分, 这将导致大量额外的内存分配和复制操作,甚至在确定是否使用这些值之前。

Rust 的独特实现

因此,Rust 在这里采取了完全不同的方法:它保留了对原始 Vec 的可变引用,并仅从原始存储中读取和更新。

这得益于 Rust 的所有权规则:

  • 只要 Vec::drain 返回的迭代器存在,其他任何地方都无法对原始 Vec 进行读写访问
  • 因此无法通过使迭代器或其支持的存储失效来使其处于错误状态(例如,改变 Vec 中的值,改变其长度等)

Rust 通过创建一个新的数据结构 Drain 来实现这一点,该结构合理地命名,并持有对原始 Vec 的可变引用以及一个用于访问 Vec 值的迭代器。 使用 Drain 上的迭代器方法时,它会转发到对切片的迭代器。这意味着它不需要自己实现迭代,而是可以使用与对切片迭代相同的(经过优化的)实现。 唯一的区别(也是关键的区别)是 drain 通过 unsafe std::ptr::read 调用立即从切片中返回值。

内存安全性保障

如果在 Drain 迭代器访问期间或之后有人能访问 Vec 中的值,或者 Vec 记住所有 Drain 访问的元素,这将是不安全的。

然而,如前所述,只要 Drain 迭代器存在,就无法访问 Vec,因为它通过可变引用持有自身

因此,对于熟悉 Rust 的开发者来说,这部分内容应该相当直接,std::ptr::read 是唯一不寻常的部分。

Drain 的 Drop 实现

Drop 实现的重要性

当迭代器被丢弃——无论是因为在 for 循环中遍历完毕,还是因为在迭代部分元素后手动丢弃——Drain 类型的 Drop 实现会接管。 这意味着 impl Drop for Drain 负责确保 Drain 的不安全清理保持合理,同时避免内存泄漏, 即 Drain 创建时 Vec 遗忘的内存(稍后会详细讨论)。

常见模式与安全性

这是 Rust 中一种常见且值得理解的模式,同时也非常巧妙。 我们将一步步详细讲解——包括所有实现中的代码(截至 Rust 1.85 nightly 版本)。 你可能需要将这些代码与本文并排查看,以便更好地理解上下文!

Drop Trait 的实现

impl<T> Drop for Drain<'_, T> {
fn drop(&mut self) {
// ...
}
}

需要注意的是,drop 接受 &mut self。这意味着我们无法做任何需要拥有 self 所有权的操作,这进一步引出了下一个内容。

DropGuard 结构体

/// 将未被 `Drain` 移除的元素移动回来,以恢复原始的 `Vec`。
struct DropGuard<'r, 'a, T>(&'r mut Drain<'a, T>);

这是一个内部数据结构,仅在此函数体内可用。其目的是确保在正确的位置将所有内容移回原始 Vec

impl<'r, 'a, T> Drop for DropGuard<'r, 'a, T> {
fn drop(&mut self) {
// 实现细节(稍后详细讨论)
}
}

内存操作与安全性

DropGuardDrop 实现确保即使在发生恐慌(panic)的情况下,也能正确地将元素移回原始位置,保持内存安全。

处理 Drain 的构造

Drain 被构造时,Vec::drain 方法通过以下代码行确保即使 Drain 被泄漏,也不会破坏安全性:

// set self.vec length's to start, to be safe in case Drain is leaked
self.set_len(start);

这实际上是截断 Vec,确保它不包含被 drain 移除的范围内的元素。 即使有人调用 std::mem::forget 来泄漏 Drain,也不会破坏安全性,只会导致内存泄漏,这是可控的。

DropGuard 的工作机制

当迭代器被丢弃时,DropGuard 确保所有未被消耗的元素被正确地丢弃,同时恢复原始 Vec 的长度。

这通过以下代码实现:

let iter = mem::take(&mut self.iter);
let drop_len = iter.len();

if drop_len == 0 {
return;
}

let _guard = DropGuard(self);

DropGuard 的存在确保即使在 drop_in_place 调用时发生恐慌,也能保持内存和 Vec 的有效性。

性能与安全性的平衡

Rust 的实现通过仅在必要时移动元素,最大限度地减少了计算和内存开销。 这对于处理大型 Vec 特别重要,因为它避免了不必要的内存分配和复制操作。

结论

通过深入了解 Vec::drain 及其 Drop 实现,我们可以看到 Rust 如何利用所有权语义来提供高性能和内存安全的保障。

具体来说:

  • 原始的 Vec 在使用 drain 期间及之后永远不会处于无效状态
  • Vec 的迭代器永远不会被无效化
  • 即使在面对不良行为的实现(只要没有不安全代码),上述两点依然成立
  • 最坏的情况是内存泄漏,但这依然是安全的

此外,DropGuard 的使用展示了 Rust 如何通过所有权和析构函数自动管理资源,而无需依赖特殊的语言结构。

参考资料

鱼雪

在现代桌面应用开发中,Tauri 框架凭借其轻量级、高性能和安全性,成为开发者们构建跨平台应用的首选工具之一。Tauri 允许开发者使用现代前端框架(如 React、Vue 或 Svelte)构建用户界面,同时利用 Rust 语言处理高效且安全的后端逻辑。然而,前后端之间的高效通信是构建功能丰富且稳定的 Tauri 应用的关键。本文将详细介绍 Tauri 中前后端通信的主要方式——Commands(命令)Events(事件)Channels(通道),并通过示例代码帮助您更好地理解和应用这些技术。

目录

  1. Tauri 简介
  2. 前后端通信方式概述
  3. Commands、Events 与 Channels 的对比
  4. 示例代码
  5. 权限配置示例
  6. 错误处理
  7. 最佳实践与安全性
  8. 性能优化
  9. 实际案例
  10. 总结
  11. 参考资源

Tauri 简介

Tauri 是一个用于构建跨平台桌面应用的框架,支持 WindowsmacOSLinux。它利用前端技术(如 HTML、CSS、JavaScript)构建用户界面,并使用 Rust 处理后端逻辑。与 Electron 相比,Tauri 生成的应用体积更小,性能更优,且具备更高的安全性。

前后端通信方式概述

在 Tauri 框架中,前端(通常使用 JavaScript 框架如 React、Vue 或 Svelte)与后端(Rust 编写)之间的通信是实现应用功能的核心。Tauri 提供了多种通信机制,主要包括 Commands(命令)Events(事件)Channels(通道)。除此之外,还有一些其他的通信方式,如在 Rust 中执行 JavaScript 代码。以下将详细介绍这些通信方式、它们的区别及适用场景。

Commands(命令)

Commands 是前端调用后端 Rust 函数的主要方式。通过命令,前端可以请求后端执行特定任务并获取结果。这种通信方式类似于前端发起的远程过程调用(RPC)。

使用场景

  • 执行复杂逻辑��需要后端处理的数据计算、文件操作、数据库交互等。
  • 获取后端数据:例如,从数据库获取数据并在前端展示。
  • 安全性需求:通过命令调用,能够在 Tauri 的安全模型下细粒度地控制权限。

实现步骤

  1. 在 Rust 后端定义命令

    使用 #[tauri::command] 宏定义一个可供前端调用的函数。

    src-tauri/src/lib.rs
    #[tauri::command]
    fn my_custom_command() {
    println!("我被 JavaScript 调用了!");
    }

    fn main() {
    tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![my_custom_command])
    .run(tauri::generate_context!())
    .expect("运行 Tauri 应用时出错");
    }

    :::note命令名称必须唯一。:::

    :::note由于胶水代码生成的限制,在 lib.rs 文件中定义的命令不能标记为 pub。如果将其标记为公共函数,您将看到如下错误:

    error[E0255]: the name `__cmd__command_name` is defined multiple times
    --> src/lib.rs:28:8
    |
    27 | #[tauri::command]
    | ----------------- previous definition of the macro `__cmd__command_name` here
    28 | pub fn x() {}
    | ^ `__cmd__command_name` reimported here
    |
    = note: `__cmd__command_name` must be defined only once in the macro namespace of this module

    :::

  2. 在前端调用命令

    使用 @tauri-apps/api 提供的 invoke 方法调用后端命令。

    前端 JavaScript 代码示例(如 React 组件)
    import { invoke } from '@tauri-apps/api/core';

    async function greetUser() {
    try {
    const greeting = await invoke('my_custom_command');
    console.log(greeting); // 输出: "我被 JavaScript 调用了!"
    } catch (error) {
    console.error('调用命令时出错:', error);
    }
    }

    // 在适当的生命周期钩子中调用 greetUser
  3. 配置权限

    tauri.conf.json 中,通过 CapabilitiesPermissions 配置命令的访问权限,确保命令的安全调用。

    tauri.conf.json 示例
    {
    "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/tooling/cli/schema.json",
    "package": {
    "productName": "Pomodoro Timer",
    "version": "0.1.0"
    },
    "tauri": {
    "windows": [
    {
    "label": "main",
    "title": "ToDo Pomodoro",
    "width": 800,
    "height": 600,
    "resizable": true,
    "fullscreen": false,
    "decorations": true,
    "transparent": false,
    "alwaysOnTop": false,
    "visible": true,
    "url": "http://localhost:3000",
    "webviewAttributes": {
    "webPreferences": {
    "nodeIntegration": false
    }
    }
    }
    ],
    "security": {
    "capabilities": [
    {
    "identifier": "greet-capability",
    "description": "Allows the main window to greet users.",
    "windows": ["main"],
    "permissions": ["core:default"]
    }
    ]
    },
    "bundle": {
    "active": true,
    "targets": "all",
    "identifier": "com.yuxuetr.pomodoro",
    "icon": [
    "icons/32x32.png",
    "icons/128x128.png",
    "icons/128x128@2x.png",
    "icons/icon.icns",
    "icons/icon.ico"
    ]
    }
    }
    }

Events(事件)

Events 是 Tauri 中实现后端向前端推送消息的机制。与 Commands 不同,Events 是单向的,适用于需要实时通知前端的场景。

使用场景

  • 状态更新通知:后端状态变化时通知前端
  • 长时间任务进度:报告后台任务的执行进度
  • 系统事件通知:如系统状态变化、文件变动等

实现步骤

  1. 在 Rust 后端发送事件

    src-tauri/src/lib.rs
    use tauri::Manager;

    #[tauri::command]
    async fn start_process(window: tauri::Window) {
    // 模拟一个耗时操作
    for i in 0..100 {
    window.emit("process-progress", i).unwrap();
    std::thread::sleep(std::time::Duration::from_millis(100));
    }
    }
  2. 在前端监听事件

    import { listen } from '@tauri-apps/api/event';

    // 监听进度事件
    await listen('process-progress', (event) => {
    console.log('Progress:', event.payload);
    });

Channels(通道)

Channels 提供了一种双向的、持久的通信通道,特别适合需要持续数据交换的场景。

使用场景

  • 流式数据传输:如实时日志、数据流
  • 长连接通信:需要保持持续通信的场景
  • 复杂的双向数据交换:需要前后端频繁交互的功能

实现示例

  1. 在 Rust 后端创建通道

    src-tauri/src/lib.rs
    use tauri::plugin::{Builder, TauriPlugin};
    use tauri::{Runtime, State, Window};
    use std::collections::HashMap;
    use std::sync::{Mutex, mpsc};

    #[derive(Default)]
    struct ChannelState(Mutex<HashMap<String, mpsc::Sender<String>>>);

    #[tauri::command]
    async fn create_channel(
    channel_id: String,
    state: State<'_, ChannelState>,
    window: Window,
    ) -> Result<(), String> {
    let (tx, mut rx) = mpsc::channel(32);
    state.0.lock().unwrap().insert(channel_id.clone(), tx);

    tauri::async_runtime::spawn(async move {
    while let Some(message) = rx.recv().await {
    window
    .emit(&format!("channel:{}", channel_id), message)
    .unwrap();
    }
    });

    Ok(())
    }
  2. 在前端使用通道

    import { listen } from '@tauri-apps/api/event';
    import { invoke } from '@tauri-apps/api';

    // 创建并使用通道
    async function setupChannel() {
    const channelId = 'my-channel';
    await invoke('create_channel', { channelId });

    await listen(`channel:${channelId}`, (event) => {
    console.log('Received:', event.payload);
    });
    }

在 Rust 中执行 JavaScript

Tauri 还支持从 Rust 后端直接执行 JavaScript 代码,这提供了另一种前后端交互的方式。

实现示例

src-tauri/src/lib.rs
#[tauri::command]
async fn execute_js(window: tauri::Window) -> Result<String, String> {
// 执行 JavaScript 代码
window
.eval("console.log('从 Rust 执行的 JavaScript')")
.map_err(|e| e.to_string())?;

// 执行带返回值的 JavaScript
let result = window
.eval("(() => { return 'Hello from JS'; })()")
.map_err(|e| e.to_string())?;

Ok(result)
}

Commands、Events 与 Channels 的对比

特性Commands(命令)Events(事件)Channels(通道)
调用方向前端 → 后��后端 → 前端双向
响应类型同步/异步异步异步
使用场景一次性请求响应状态通知持续数据交换
数据流单次请求单次响应单向推送双向持续
适用性通用操作状态更新流式传输

示例代码

Commands 示例

完整的文件操作示例:

src-tauri/src/main.rs
use std::fs;
use tauri::command;

#[command]
async fn read_file(path: String) -> Result<String, String> {
fs::read_to_string(path)
.map_err(|e| e.to_string())
}

#[command]
async fn write_file(path: String, contents: String) -> Result<(), String> {
fs::write(path, contents)
.map_err(|e| e.to_string())
}

fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![read_file, write_file])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
// 前端调用示例
import { invoke } from '@tauri-apps/api/tauri';

async function handleFileOperations() {
try {
// 写入文件
await invoke('write_file', {
path: 'test.txt',
contents: 'Hello, Tauri!',
});

// 读取文件
const content = await invoke('read_file', {
path: 'test.txt',
});
console.log('File content:', content);
} catch (error) {
console.error('File operation failed:', error);
}
}

Events 示例

文件监控示例:

src-tauri/src/main.rs
use notify::{Watcher, RecursiveMode, watcher};
use tauri::Manager;
use std::time::Duration;

#[tauri::command]
async fn watch_directory(window: tauri::Window, path: String) -> Result<(), String> {
let (tx, rx) = std::sync::mpsc::channel();

let mut watcher = watcher(tx, Duration::from_secs(2)).map_err(|e| e.to_string())?;

watcher.watch(&path, RecursiveMode::Recursive).map_err(|e| e.to_string())?;

tauri::async_runtime::spawn(async move {
for res in rx {
match res {
Ok(event) => {
window.emit("file-change", event).unwrap();
}
Err(e) => println!("watch error: {:?}", e),
}
}
});

Ok(())
}
// 前端监听示例
import { listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/tauri';

async function setupFileWatcher() {
// 启动文件监控
await invoke('watch_directory', {
path: './watched_folder',
});

// 监听文件变化事件
await listen('file-change', (event) => {
console.log('File changed:', event.payload);
});
}

Channels 示例

实时日志流示例:

src-tauri/src/main.rs
use tokio::sync::mpsc;
use std::collections::HashMap;
use std::sync::Mutex;

struct LogChannel(Mutex<HashMap<String, mpsc::Sender<String>>>);

#[tauri::command]
async fn start_log_stream(
channel_id: String,
state: tauri::State<'_, LogChannel>,
window: tauri::Window,
) -> Result<(), String> {
let (tx, mut rx) = mpsc::channel(100);
state.0.lock().unwrap().insert(channel_id.clone(), tx);

tauri::async_runtime::spawn(async move {
while let Some(log) = rx.recv().await {
window
.emit(&format!("log:{}", channel_id), log)
.unwrap();
}
});

Ok(())
}
// 前端实现
import { listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/tauri';

async function setupLogStream() {
const channelId = 'app-logs';

// 创建日志流通道
await invoke('start_log_stream', { channelId });

// 监听日志消息
await listen(`log:${channelId}`, (event) => {
console.log('New log:', event.payload);
});
}

权限配置示例

{
"tauri": {
"security": {
"capabilities": [
{
"identifier": "file-access",
"description": "允许读写文件",
"windows": ["main"],
"permissions": ["fs:default"]
},
{
"identifier": "log-stream",
"description": "允许访问日志流",
"windows": ["main"],
"permissions": ["event:default"]
}
]
}
}
}

错误处理

src-tauri/src/lib.rs
#[derive(Debug, thiserror::Error)]
enum Error {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid input: {0}")]
InvalidInput(String),
}

impl serde::Serialize for Error {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.to_string().as_str())
}
}

#[tauri::command]
async fn handle_with_error() -> Result<String, Error> {
// 业务逻辑
Ok("Success".to_string())
}
// 前端错误处理
import { invoke } from '@tauri-apps/api/tauri';

try {
await invoke('handle_with_error');
} catch (error) {
console.error('Operation failed:', error);
}

最佳实践与安全性

  1. 权限控制

    • 始终使用最小权限原则
    • 明确定义每个命令的权限需求
    • 使用 allowlist 限制可用 API
  2. 数据验证

    • 在前后端都进行数据验证
    • 使用强类型定义接口
    • 处理所有可能的错误情况
  3. 性能优化

    • 使用适当的通信方式
    • 避免频繁的小数据传输
    • 合理使用异步操作

性能优化

  1. 批量处理

    • 合并多个小请求
    • 使用数据缓存
    • 实现请求队列
  2. 数据压缩

    • 大数据传输时使用压缩
    • 选择适当的序列化格式
  3. 异步处理

    • 使用异步命令
    • 实现后台任务
    • 合理使用线程池

实际案例

  1. 文件管理器

    • 使用 Commands 处理文件操作
    • 使用 Events 监控文件变化
    • 使用 Channels 传输大文件
  2. 实时聊天应用

    • 使用 Channels 处理消息流
    • 使用 Events 处理状态更新
    • 使用 Commands 处理用户操作

总结

Tauri 提供了丰富的前后端通信机制,每种方式都有其特定的使用场景:

  • Commands 适合一次性的请求-响应模式
  • Events 适合单向的状态通知
  • Channels 适合持续的双向数据交换

选择合适的通信方式对应用的性能和用户体验至关重要。同时,始终要注意安全性,合理使用权限控制和数据验证。

参考资源

鱼雪

Rust 提供了强大的类型系统,但在可迭代特性上仍有一些可改进之处。

本文将讨论 CollectionIterable 特性的定义与实现,以及它们在提升代码通用性与可复用性上的作用。


背景

在 Rust 的核心库中,我们已经有了 IteratorIntoIterator 特性。 然而,对于可以多次迭代的集合类型(Collection)或通用的可迭代类型(Iterable),目前还缺乏统一的特性支持。 我们将深入探讨这些特性的重要性和实现方式。


核心概念

Iterator

Iterator 特性用于逐步遍历集合中的元素或计算结果。它是一种懒加载模型,仅在调用消费方法时才执行计算。

常见的 Iterator 方法包括:

  • 迭代器到迭代器的转换filtermapflat_map
  • 迭代器的消费方法collectreducefold
let numbers = vec![1, 2, 3];
let doubled: Vec<_> = numbers.iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // [2, 4, 6]

IntoIterator

IntoIterator 特性允许将类型转换为迭代器:

pub trait IntoIterator {
type Item;
type IntoIter: Iterator<Item = Self::Item>;

fn into_iter(self) -> Self::IntoIter;
}

它支持三种实现方式:

  1. 消费自身:x.into_iter()
  2. 消费不可变引用:(&x).into_iter()
  3. 消费可变引用:(&mut x).into_iter()

定义 Collection 和 Iterable 特性

Collection 和 CollectionMut 特性

Collection 表示可以反复生成共享引用迭代器的集合,CollectionMut 进一步扩展为支持生成可变引用迭代器。

trait Collection {
type Item;
type Iter<'i>: Iterator<Item = &'i Self::Item>
where
Self: 'i;

fn iter(&self) -> Self::Iter<'_>;
}

trait CollectionMut: Collection {
type IterMut<'i>: Iterator<Item = &'i mut Self::Item>
where
Self: 'i;

fn iter_mut(&mut self) -> Self::IterMut<'_>;
}

通过 IntoIterator 的实现,可以简洁地为所有符合条件的集合实现这些特性:

impl<X> Collection for X
where
X: IntoIterator,
for<'a> &'a X: IntoIterator<Item = &'a <X as IntoIterator>::Item>,
{
type Item = <X as IntoIterator>::Item;
type Iter<'i> = <&'i X as IntoIterator>::IntoIter;

fn iter(&self) -> Self::Iter<'_> {
<&X as IntoIterator>::into_iter(self)
}
}

impl<X> CollectionMut for X
where
X: IntoIterator,
for<'a> &'a mut X: IntoIterator<Item = &'a mut <X as IntoIterator>::Item>,
{
type IterMut<'i> = <&'i mut X as IntoIterator>::IntoIter;

fn iter_mut(&mut self) -> Self::IterMut<'_> {
<&mut X as IntoIterator>::into_iter(self)
}
}

Iterable 特性

Iterable 是对更广泛可迭代类型的定义,支持生成值迭代器,而不是引用迭代器。

trait Iterable {
type Item;
type Iter: Iterator<Item = Self::Item>;

fn iter(&self) -> Self::Iter;
}

实现方式如下:

impl<'a, X> Iterable for &'a X
where
&'a X: IntoIterator,
{
type Item = <&'a X as IntoIterator>::Item;
type Iter = <&'a X as IntoIterator>::IntoIter;

fn iter(&self) -> Self::Iter {
self.into_iter()
}
}

示例

Collection 示例

以下示例展示了如何使用 Collection 计算集合的统计信息:

fn statistics(numbers: &impl Collection<Item = i64>) -> Stats {
let count = numbers.iter().count() as i64;
let mean = numbers.iter().sum::<i64>() / count;
let sum_sq_errors: i64 = numbers.iter().map(|x| (x - mean) * (x - mean)).sum();
let std_dev = f64::sqrt(sum_sq_errors as f64 / (count - 1) as f64) as i64;

Stats { count: count as usize, mean, std_dev }
}

支持的集合类型包括:

  • 数组和向量
  • 标准库集合(如 HashSetVecDeque
  • 第三方集合(如 SmallVecArrayVec

CollectionMut 示例

以下示例展示了如何使用 CollectionMut 修改集合中的元素:

fn increment_by_sum(numbers: &mut impl CollectionMut<Item = i32>) {
let sum: i32 = numbers.iter().sum();
for x in numbers.iter_mut() {
*x += sum;
}
}

Iterable 示例

以下示例展示了使用 Iterable 的灵活性,包括对范围和自定义生成器的支持:

fn statistics(numbers: impl Iterable<Item = i64>) -> Stats {
/* 与 Collection 示例相同 */
}

statistics(7..21); // 支持范围
statistics(FibUntil(10)); // 自定义生成器

优势与结论

优势

  • 自动实现:无需额外配置,集合类型可以自动实现 CollectionCollectionMut
  • 广泛适用:支持标准集合、自定义集合和生成器类型。
  • 灵活性:通过 Iterable 扩展了迭代器的应用范围。

总结

通过引入 CollectionIterable 特性,我们显著提升了代码的复用性和通用性。 Rust 的类型系统为实现这些特性提供了极大的便利,再次展现了其强大的能力 ❤️🦀。

链接

鱼雪

2024 年 12 月 9 日,Dioxus 0.6 重磅发布!这次更新带来了许多全新的工具特性,显著提升了开发者体验,包括移动模拟器支持、热重载、交互式 CLI 等,助力开发者更高效地构建全栈应用。


什么是 Dioxus?

Dioxus 是一个全栈开发框架,支持通过单一代码库构建 Web、桌面和移动应用。我们的目标是打造一个比 Flutter 更强大的框架。Dioxus 专注于:

  • 一流的全栈 Web 支持
  • 类型安全的服务器/客户端通信
  • 极致的性能

0.6 版本的主要亮点

此次发布,我们重新设计了 Dioxus CLI,大幅提升了开发者体验,修复了许多长期存在的问题,并引入了一系列新功能。

1. 全新 Dioxus CLI

以下是 Dioxus CLI 的关键改进:

  • dx serve for mobile:支持在 Android 和 iOS 模拟器及设备上运行应用。
  • 神奇的热重载:支持格式化字符串、属性和嵌套的 rsx!{} 的热重载。
  • 交互式 CLI:借鉴 Astro 的交互式用户体验,重新设计了 Dioxus CLI。
  • 内联堆栈跟踪:直接在终端中捕获 WASM 崩溃和日志。
  • 桌面和移动端的服务器函数支持:为本地应用内联服务器 RPC。

2. 全框架开发者体验改进

我们还在框架的其他方面进行了大量优化:

  • 通知与加载屏幕:开发模式下新增通知与加载屏幕,提升调试体验。
  • 自动补全改进:大幅提升 RSX 的自动补全效果。
  • asset! 稳定化:稳定了集成于原生应用的基于链接器的资源系统。
  • 流式 HTML:支持从服务器到客户端的流式 Suspense 和错误边界。
  • 静态网站生成(SSG)与增量静态生成(ISG):支持更多静态网站构建模式。
  • 事件错误处理:在事件处理器、任务和组件中使用 ? 处理错误。
  • Meta 元素:新增 HeadTitleMetaLink 元素,用于设置文档属性。
  • 同步的 prevent_default:跨平台同步处理事件。
  • onresize 事件处理器:无需 IntersectionObserver 也能跟踪元素大小变化。
  • onvisible 事件处理器:无需 IntersectionObserver 也能跟踪元素可见性。
  • WGPU 集成:支持将 Dioxus 渲染为 WGPU 表面及子窗口的覆盖层。
  • dx bundle:全面支持 Web、iOS 和 Android 平台的打包。
  • JSON 模式:CLI 消息支持 JSON 输出,便于第三方工具和 CI/CD 流程使用。
  • 新模板:新增三个跨平台应用的启动模板。
  • 教程与指南:推出面向 Dioxus 0.6 及后续版本的新教程和指南。
  • 二进制补丁原型:基于纯 Rust 的热重载引擎原型。

重点改进详情

神奇的热重载

Dioxus 的热重载功能实现了前所未有的便利性,不仅支持常规的代码热更新,还能对嵌套的 rsx!{} 结构进行即时刷新。这极大提升了开发效率,尤其是在复杂 UI 开发场景中。

交互式 CLI

新版 CLI 借鉴了 Astro 的交互式设计,提供了更直观的用户体验。例如,在构建项目时,CLI 会根据用户选择动态更新配置,减少不必要的手动操作。

WASM 崩溃与日志捕获

通过内联堆栈跟踪功能,开发者可以直接在终端中查看 WASM 的崩溃原因及日志信息。这有助于快速定位问题,尤其是在调试复杂 Web 应用时。


新功能概览

特性描述
移动模拟器支持在 Android 和 iOS 上快速运行和测试应用。
流式 HTML 支持从服务器流式加载 Suspense 和错误边界,提升性能。
SSG 与 ISG 支持更灵活的静态网站生成和增量更新支持。
WGPU 集成在 WGPU 表面和子窗口中渲染 Dioxus 应用。
新教程与模板快速上手跨平台开发的全新模板和详细指南。
JSON 模式CLI 消息支持 JSON 格式,便于与 CI/CD 集成。
事件处理改进新增同步 prevent_defaultonresize 等功能。
二进制补丁原型纯 Rust 实现的热重载引擎,带来更快的开发迭代体验。

未来发展

Dioxus 将继续优化开发体验,增加对更多平台和场景的支持。我们希望通过 Dioxus,开发者能够更高效地构建现代化应用。

鱼雪

在本文中,我将分享如何使用 Rust 宏来解决复杂构建需求,同时探索 macro-by-exampleproc-macro 的实现方式。


背景故事

安德烈·乌涅洛·利赫内罗维茨提出用 Rust 重写 Kubernetes 服务的想法并获得了批准。 这让我在最近写了大量的 Rust 代码。 Rust 的宏系统是其中最复杂的部分之一,即使对于经验丰富的 Rust 开发者也是如此。

安德烈·乌涅洛·利赫内罗维茨的场景是构建一个工具,用来与多个内部服务通信。 这些服务的连接模式各异,例如 Basic Auth、Bearer Tokens 和 OAuth,每种模式都有不同的字段要求。 这显然是一个适合使用 构建者模式 的场景。


现有库的调研

在着手实现之前,我调研了几个现有的库:

  • derive_builder:支持删除 Option 字段,但会将其设为必填,不符合我的需求。
  • builder_macro:保留 Option 字段,但生成的代码不够整洁。

因此,我决定自己编写一个宏来实现自动化。


使用 macro-by-example

首先,我尝试了使用 macro_rules! 来实现自动化。以下是我的改进实现:

macro_rules! builder {
(@builder_field_type Option<$ftype:ty>) => { Option<$ftype> };
(@builder_field_type $ftype:ty) => { Option<$ftype> };
($builder:ident -> $client:ident { $( $fname:ident{$($ftype:tt)+} $(,)? )* }) => {
#[derive(Debug)]
pub struct $client {
$( $fname: $($ftype)+, )*
}

#[derive(Debug)]
pub struct $builder {
$( $fname: $crate::builder!(@builder_field_type $($ftype)+), )*
}

impl $builder {
$(
paste::paste! {
pub fn [<with_ $fname>](&mut self, $fname: $crate::builder!(@builder_field_setter_type $($ftype)+)) -> &mut Self {
self.$fname = Some($fname);
self
}
}
)*

pub fn build(&self) -> Result<$client, std::boxed::Box<dyn std::error::Error>> {
Ok($client {
$( $fname: $crate::builder!(@builder_unwrap_field self $fname $($ftype)+), )*
})
}
}

impl $client {
pub fn builder() -> $builder {
$builder {
$( $fname: None, )*
}
}
}
};
}

builder!(Builder -> Client {
field{bool},
});

最终效果是一个灵活的 Builder 宏,支持动态生成字段和方法。


使用过程宏(proc-macro

过程宏是另一种强大的实现方式,允许我们动态解析 TokenStream 并生成代码。

以下是一个基本的过程宏示例:

use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(Builder)]
pub fn builder_derive(input: TokenStream) -> TokenStream {
let expanded = quote! {
#[derive(Debug)]
pub struct Builder {
}
};
TokenStream::from(expanded)
}

更复杂的版本可以解析结构体的字段,并动态生成相应的 Builder 结构体和方法。

以下代码示例展示了如何提取字段类型并实现自定义构建逻辑:

fn inner_type(ty: &Type) -> (bool, &Type) {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.first() {
if segment.ident == "Option" {
if let syn::PathArguments::AngleBracketed(ref angle_bracketed) =
segment.arguments
{
if let Some(syn::GenericArgument::Type(ref inner_ty)) =
angle_bracketed.args.first()
{
return (true, inner_ty);
}
}
}
}
}
(false, ty)
}

最终,完整的 proc-macro 实现可以自动生成 Builder 模式的完整逻辑。


比较两种实现方式

特性macro-by-exampleproc-macro
易用性相对简单,适合快速实现需要更多代码和独立的 crate
灵活性受限于宏系统的规则,不能动态生成类型名可动态生成字段、方法和类型
适用场景适合固定结构或简单需求适合复杂的动态代码生成

总结

Rust 的宏系统强大而灵活,无论是 macro-by-example 还是 proc-macro 都各有用武之地。

在解决重复性代码生成时,这些工具可以大幅提升开发效率。希望这篇文章能帮助初学者更好地理解和使用 Rust 宏。

链接

鱼雪

本文旨在比较 Diesel 与其他连接关系型数据库的 Rust 库。

汇总对比表

特性DieselSQLxSeaORMtokio-postgres/mysql/rusqlite
稳定性稳定 (2.0 版发布于 2022 年)不稳定 (0.x 版本)稳定 (1.0 版发布于 2024 年)不稳定 (0.x 版本)
安全性编译时检查,类型系统支持编译时检查,需运行数据库部分语法检查,无类型验证仅语法验证
灵活性支持动态查询和扩展静态查询检查,不支持动态查询DSL 支持有限,扩展性弱支持所有 SQL 查询
可扩展性高,可自定义扩展 DSL/后端低,无法扩展核心功能低,无法扩展 DSL/后端不适用
可用性需 C 库支持 (SQLite)需 C 库支持 (SQLite)易于使用,无需额外依赖易于使用
性能高性能,支持查询流水线性能良好,不支持流水线性能一般,不支持流水线性能一般

详细分析

稳定性

Rust 库通常遵循语义版本控制(SemVer),其稳定性由版本决定。版本号高于 1.0 的库承诺在主版本升级前不做破坏性变更。以下是库的现状:

  • Diesel:1.0 发布于 2018 年,2.0 于 2022 年发布。
  • SeaORM:1.0 发布于 2024 年夏季。
  • 其他库仍处于 0.x 状态。

安全性保证

根据提供的安全级别,可以将这些库分为三类:

  1. 纯数据库接口:接受 SQL 字符串,但语法错误需用户自行处理。
  2. 未验证的查询生成器:提供 DSL,但无法验证类型和约束。
  3. 编译时检查:通过编译时检查验证查询。

Diesel 和 SQLx 的实现差异:

  • Diesel 使用 Rust 类型系统进行检查,支持动态构建查询。
  • SQLx 使用宏检查静态查询,但需要运行数据库实例。

灵活性

各库 API 的灵活性差异:

  • tokio-postgres、rusqlite 和 mysql:接受 SQL 字符串,因此支持所有查询。
  • SeaORM:DSL 支持常见 SQL 功能,但限制了高级查询(如超过 3 个表的联接)。
  • SQLx:宏检查静态查询,不支持基于运行时信息的动态查询。
  • Diesel:DSL 覆盖大部分常见 SQL 功能,并支持自定义扩展。

可扩展性

以下是库的可扩展性差异:

  • SeaORM:通过枚举实现 DSL,不支持自定义扩展。
  • SQLx:依赖宏,无法轻松扩展。
  • Diesel:广泛使用特性(traits),支持自定义 DSL 和后端扩展。

可用性

  • Diesel、Diesel-async、SQLx、SeaORM、tokio-postgres 等提供纯 Rust 实现,易于编译。
  • Diesel 和 rusqlite 的 SQLite 后端依赖 C 库,需预先安装。

性能

各库性能概述:

  1. 异步 SQLite 表现较差:SQLite 缺乏异步 API。
  2. 小数据输出性能相似,大数据输出差异明显:Diesel 在数据反序列化方面表现优异。
  3. 查询流水线:Diesel-async 和 tokio-postgres 支持查询流水线,可提升 PostgreSQL 性能 20%。

关于异步数据库库的必要性

异步库的性能优势主要体现在高网络延迟或需要中断请求的场景。 对于 SQLite,使用同步库更高效。 Diesel-async 可支持异步场景。

链接

鱼雪