跳到主要内容

52 篇博文 含有标签「Rust」

查看所有标签

概述

trait-gen 是一个提供 trait 实现生成的属性宏的库。

它允许为多种类型生成 trait 实现,而无需自定义声明宏、代码重复或通用实现,从而使代码更易于阅读和维护。

使用示例

以下是一个简单的示例:

use trait_gen::trait_gen;

#[trait_gen(T -> u8, u16, u32, u64, u128)]
impl MyLog for T {
fn my_log2(self) -> u32 {
T::BITS - 1 - self.leading_zeros()
}
}

trait_gen 属性将 T 替换为给定的类型,生成如下代码:

impl MyLog for u8 {
fn my_log2(self) -> u32 {
u8::BITS - 1 - self.leading_zeros()
}
}
impl MyLog for u16 {
fn my_log2(self) -> u32 {
u16::BITS - 1 - self.leading_zeros()
}
}
// 其他类型依此类推

使用方法

该属性放置在伪泛型实现代码之前泛型参数首先给出,后跟右箭头->)和类型参数列表

#[trait_gen(T -> Type1, Type2, Type3)]
impl Trait for T {
// ...
}

属性宏会依次将代码中的泛型参数 T 替换为后续类型(Type1、Type2、Type3),生成所有实现。

所有以 T 开头的类型路径都会被替换。

例如,T::default() 会生成 Type1::default()Type2::default() 等, 但 super::T 保持不变,因为它属于另一个作用域。

代码必须与所有类型兼容,否则编译器将触发相关错误。例如,#[trait_gen(T -> u64, f64)] 不能应用于 let x: T = 0;,因为 0 不是有效的浮点字面量。

实际类型还会替换文档注释、宏和字符串字面量中的任何 ${T} 出现。

注意事项

  • 使用字母 "T" 不是强制性的,任何类型路径都可以。例如,gen::Type 也是可以的。但为了提高可读性,建议使用简短的大写标识符。
  • 可以链式使用两个或多个属性以生成所有组合。
  • trait_gen 也可以用于类型实现。

动机

生成多个实现的方法有几种:

  1. 手动复制
  2. 使用声明宏
  3. 使用通用实现

上面的实现示例可以通过声明宏实现:

macro_rules! impl_my_log {
($($t:ty)*) => (
$(impl MyLog for $t {
fn my_log2(self) -> u32 {
$t::BITS - 1 - self.leading_zeros()
}
})*
)
}

impl_my_log! { u8 u16 u32 u64 u128 }

但这种方法冗长且比原生代码难以阅读。

我们必须每次编写自定义宏,包括其声明、模式和一些元素的转换(如参数 $t)。

此外,IDE 通常无法提供上下文帮助或在宏代码中应用重构。

使用通用实现还有其他缺点

  • 除了同一 crate 中未被通用实现覆盖的类型外,禁止任何其他实现。
  • 找到对应的 trait 并不总是可能。虽然 num crate 对原始类型提供了很多帮助,但并不是所有情况都涵盖。
  • 即使操作和常量被 trait 覆盖,也很快需要一长串 trait 约束。

示例

以下是支持的替换示例,库的集成测试中还有更多示例。

第一个示例更多是说明什么被替换,什么不被替换,而不是实际实现:

#[trait_gen(U -> u32, i32, u64, i64)]
impl AddMod for U {
fn add_mod(self, other: U, m: U) -> U {
const U: U = 0;
let zero = U::default();
let offset: super::U = super::U(0);
(self + other + U + zero + offset.0 as U) % m
}
}

扩展为(我们只展示第一个类型,u32):

impl AddMod for u32 {
fn add_mod(self, other: u32, m: u32) -> u32 {
const U: u32 = 0;
let zero = u32::default();
let offset: super::U = super::U(0);
(self + other + U + zero + offset.0 as u32) % m
}
}

复杂示例

以下示例展示了如何使用类型参数:

struct Meter<U>(U);
struct Foot<U>(U);

trait GetLength<T> {
fn length(&self) -> T;
}

#[trait_gen(U -> f32, f64)]
impl GetLength<U> for Meter<U> {
fn length(&self) -> U {
self.0 as U
}
}

该属性可以与另一个属性组合,以创建泛型组合, 实现 Meter<f32>Meter<f64>Foot<f32>Foot<f64> 的 trait:

#[trait_gen(T -> Meter, Foot)]
#[trait_gen(U -> f32, f64)]
impl GetLength<U> for T<U> {
fn length(&self) -> U {
self.0 as U
}
}

这将扩展为:

impl GetLength<f32> for Meter<f32> {
fn length(&self) -> f32 { self.0 as f32 }
}
impl GetLength<f64> for Meter<f64> {
fn length(&self) -> f64 { self.0 as f64 }
}
impl GetLength<f32> for Foot<f32> {
fn length(&self) -> f32 { self.0 as f32 }
}
impl GetLength<f64> for Foot<f64> {
fn length(&self) -> f64 { self.0 as f64 }
}

多段路径(带有 :: 的路径)和路径参数(如 <f32>)也可以用于参数中。

例如,使用 gen::U 可以避免与已经定义的单字母类型混淆。

遗留格式

早期版本中使用了较短的格式,尽管仍然支持,但可能更难阅读:

#[trait_gen(Type1, Type2, Type3)]
impl Trait for Type1 {
// ...
}

在这里,Type1 的代码将按原样生成,然后 Type2 和 Type3 将替换 Type1 以生成它们的实现。 这是等效属性的快捷方式。

替代格式

当启用 in_format 特性时,还支持替代格式:

trait-gen = { version="0.3", features=["in_format"] }

在这里,使用 in 替代箭头 ->且参数类型必须放在方括号中

#[trait_gen(T in [u8, u16, u32, u64, u128])]
impl MyLog for T {
fn my_log2(self) -> u32 {
T::BITS - 1 - self.leading_zeros()
}
}

使用此格式会发出“已弃用”的警告, 可以通过在文件顶部添加 #![allow(deprecated)] 指令或在生成的代码中添加 #[allow(deprecated)] 来关闭。

限制

trait_gen 属性的过程宏无法处理作用域,因此不支持任何与泛型参数相同字面量的类型声明。

例如,以下代码因泛型函数冲突而无法编译:

#[trait_gen(T -> u64, i64, u32, i32)]
impl AddMod for T {
type Output = T;

fn add_mod(self, rhs: Self, modulo: Self) -> Self::Output {
fn int_mod<T: Num> (a: T, m: T) -> T { // <== 错误,冲突的 'T'
a % m
}
int_mod(self + rhs, modulo)
}
}

泛型参数必须是类型路径;不能是更复杂的类型,如引用或切片。

兼容性

trait-gen crate 在 Windows 64 位和 Linux 64/32 位平台上测试了 rustc 1.58.0 及更高版本。

链接

鱼雪

在 Rust 中,我编写了很多应用程序,因此发现自己经常使用 .unwrap(),这比我编写整洁的库时要多得多。

我经常遇到的问题是,过了一天或一周后,我总是记不清当初为什么要使用 .unwrap()。我真的希望在这种情况下让应用程序崩溃吗?还是我只是匆忙证明我的其他代码有效,想要稍后再实现错误处理?

我认为有三种不同的 unwrap,每种都有不同的语义,程序员应该以不同的方式对待它们。

作为 panic!() 的 Unwrap

第一种 unwrap 是显而易见的;我之所以 unwrap,是因为如果发生这种情况,我们就应该崩溃。

一个很好的例子是在某些 Web 服务器代码中:

let app = Router::new().route("/", get(get_info));
let address_str = format!("{address}:{port}");

// 如果我们给出的地址或端口无效,我们无法做任何事情。就崩溃吧!
let addr: SocketAddr = address_str.parse().unwrap();

// 如果无法打开 tcp 套接字,我们无法做任何事情。就崩溃吧!
let listener = TcpListener::bind(&addr).await.unwrap();

// 如果我们的 Web 服务器意外崩溃,我们也应该崩溃!
axum::serve(listener, app.into_make_service()).await.unwrap();

所有这些 .unwrap() 的目的是相同的。如果我们无法做到这一点,就崩溃。

这些 unwrap 也是故意存在的。它们是为了处理真实的错误情况,而不是错误处理的占位符。此外,所有这些错误情况都是可能发生的,我们只是不想去考虑它们。

作为 unreachable!() 的 Unwrap

第二种 panic 不太明显,但尤其在编写大量静态变量时会出现。

一个很好的例子是声明正则表达式:

// 我们在这里 unwrap 不是因为不关心错误情况,而是因为
// 我们的错误情况是绝对无法到达的!
static HEXADECIMAL_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("^[0-9a-f]*$").unwrap());

这个 unwrap 的目的与我们 Web 服务器示例中的不同。我们 unwrap 是因为这个错误情况根本不可能发生。尽管如此,这个 .unwrap() 是故意的,并不是错误处理的占位符。

作为 todo!() 的 Unwrap

每个在 Rust 中编写过应用程序的人都犯过使用 .unwrap() 的错误,心想“我稍后会处理错误,我只是想看看我的代码在正常路径上是否有效。”

实际上,任何在 Rust 中处理大型应用程序的人可能都花了大量时间追踪这些被遗忘的“临时”unwrap!

一个不错的例子是在快速而肮脏的 Rust 代码中:

// 啊,我稍后会更好地处理这个
let age: i32 = user_input.parse().unwrap();

// 或者...啊,这个文件存在,但我稍后会更好地处理。
let file: Vec<u8> = fs::read("data.txt").unwrap();

这段代码很肮脏,但在你只是想证明某些东西有效时,这种写法很常见。

这个 unwrap 的目的与其他的截然不同。我们 unwrap 是因为我们尚未实现错误处理。

那么,这有什么意义?

我所要指出的是,.unwrap() 在代码中可以有三种不同的原因:

  1. 如果我们无法做到这一点,我们就应该崩溃(类似 panic!())
  2. 这种情况是不可能的(类似 unreachable!())
  3. 我需要稍后处理错误(类似 todo!())

但这里绝对关键的问题是,这些信息并没有存储在代码中,而是存储在你的脑海中。

有些人会在周围写评论,比如 // TODO// cannot happen

有些人使用 .expect("todo").expect("must be valid regex")。 我认为这些都是肮脏的黑客行为,仍然无法准确保留我们为什么要 unwrap 的语义。

我们已经有了类似的“语义崩溃”的先例,使用 todo!()unreachable!() 宏。 为什么不在这里也使用它们呢?

你有什么建议?

我写了一份 RFC,提出了两个新的方法用于 ResultOption,旨在明确这些语义, 并防止对 unwrap 的混淆。

你可以在这里阅读提案,简单来说就是:

// unwrap 仍然用于类似 panic!() 的情况
TcpListener::bind(&addr).unwrap();

// 我们崩溃是因为错误处理尚未实现。
// 这种用例在原型应用程序中很常见。
let int: i32 = input.parse().todo();
let arg2 = std::env::args().nth(2).todo();
let data: Vec<u8> = fs::read("data.txt").todo();

// 这些错误状态是无法到达的。
// 这种用例在静态声明中很常见。
NonZeroU32::new(10).unreachable();
Regex::new("^[a-f]{5}$").unreachable();

它提议了 Option::todoOption::unreachableResult::todoResult::unreachable 函数。

这些函数分别与 todo!()unreachable!() 宏具有类似的目的。

如果在标准库中实现了这些功能,#[clippy::todo] 和其他特性可以指出不应进入生产环境的临时 unwrap。

如果这对你有用,或者这是你之前遇到过的问题,我很想在 RFC 中听听你的想法!对我个人来说,这些函数将带来很大的价值。

命名的附注

RFC 提到我们也可以将这些函数命名为 unwrap_todounwrap_unreachable。 我对此有些怀疑,因为 unwrap_todo 输入字符较多,我不确定在懒惰的上下文中,函数是否会被忽略。

我认为单独使用 .todo() 作为一个函数并不是特别令人困惑;它肯定没有比 .expect() 更令人困惑。

链接

鱼雪

Rust 提供了多种机制来定义全局常量和静态变量,其中 constlazy_static 是两种常见的选择。 它们各有优缺点,适用于不同的场景。

本文将详细分析 constlazy_static 的关系、优缺点及其使用场景,并提供示例代码帮助理解它们的用法。

1. constlazy_static 概述

const

  • 定义const 用于定义编译时常量。常量的值在编译时就已经确定,并且在代码中是不可变的。
  • 特性
    • 编译时初始化const 变量的值在编译时确定,内存分配也是在编译时完成的。
    • 不可变const 变量的值不可变,编译器会在编译时嵌入这些值到代码中。
    • 性能:由于在编译时初始化,const 变量不涉及运行时开销,性能较好。

lazy_static

  • 定义lazy_static 提供了在运行时初始化静态变量的功能。变量在第一次访问时被初始化,并且初始化过程是线程安全的。
  • 特性
    • 延迟初始化lazy_static 变量的初始化推迟到第一次访问时,这对于初始化代价高的变量尤其有用。
    • 线程安全lazy_static 使用同步原语(如 MutexRwLock)来确保多线程环境下的安全性。
    • 灵活性:支持在运行时进行复杂的初始化逻辑。

2. constlazy_static 的对比

2.1 性能

  • const:由于 const 变量在编译时就已确定其值,并且直接嵌入到代码中,因此不涉及运行时开销。适合那些需要高性能和确定性常量的场景。
  • lazy_static:涉及运行时初始化,因此会有初始化延迟和可能的同步开销。适用于需要复杂初始化的场景。

2.2 内存开销

  • const:常量直接嵌入到代码中,内存占用较少,开销可预测。
  • lazy_static:可能会导致较高的内存开销,尤其是存储大数据结构时。

2.3 灵活性

  • const:适用于简单、固定的值,无法处理复杂的初始化逻辑。
  • lazy_static:允许在运行时初始化变量,支持复杂的初始化逻辑和条件。

2.4 线程安全

  • const:不涉及线程安全问题,因为它们在编译时已经是不可变的。
  • lazy_static:提供线程安全的全局变量,适合多线程环境中的共享状态。

3. 示例代码与使用场景

示例 1:使用 const

// 定义一个编译时常量
const MAX_RETRIES: u32 = 5;

fn main() {
for attempt in 1..=MAX_RETRIES {
println!("Attempt {}", attempt);
}
}

使用场景

  • 常量值:适合定义那些在编译时即可确定的固定值,如数组的大小、固定的配置值等。

示例 2:使用 lazy_static

#[macro_use]
extern crate lazy_static;

use std::sync::Mutex;
use std::collections::HashMap;

lazy_static! {
static ref CONFIG: Mutex<HashMap<String, String>> = {
let mut map = HashMap::new();
map.insert("app_name".to_string(), "MyApp".to_string());
map.insert("version".to_string(), "1.0.0".to_string());
Mutex::new(map)
};
}

fn main() {
let config = CONFIG.lock().unwrap();
println!("App Name: {}", config.get("app_name").unwrap());
}

示例 3:使用 lazy_static 创建全局数据库连接池

在这个示例中,我们将展示如何使用 lazy_staticsqlx 创建一个全局的、线程安全的 PostgreSQL 数据库连接池。 代码还演示了如何在异步环境中执行查询操作。我们将使用 dotenv 来加载数据库连接信息。

代码示例

首先,在 Cargo.toml 文件中添加所需的依赖项:

[dependencies]
lazy_static = "1.4"
sqlx = { version = "0.5", features = ["postgres", "runtime-async-std"] }
tokio = { version = "1", features = ["full"] }
dotenv = "0.15"

接下来,创建一个 main.rs 文件:

use lazy_static::lazy_static;
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use std::env;
use tokio;

lazy_static! {
static ref DB_POOL: PgPool = {
// 加载环境变量
dotenv::dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

// 创建数据库连接池
PgPoolOptions::new()
.max_connections(5)
.connect_lazy(&database_url)
.expect("Failed to create pool")
};
}

#[tokio::main]
async fn main() {
// 获取数据库连接池
let pool = &*DB_POOL;

// 执行查询
let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
.fetch_one(pool)
.await
.expect("Failed to execute query");

println!("Number of users: {}", row.0);
}

代码说明

  1. 依赖项

    • lazy_static:用于定义全局静态变量。确保在多线程环境中安全地共享数据。
    • sqlx:Rust 的异步数据库库,支持多种数据库类型。这里我们使用 PostgreSQL 数据库的支持。
    • tokio:Rust 的异步运行时库,支持异步编程。
    • dotenv:从 .env 文件中加载环境变量,用于存储数据库连接信息。
  2. 环境变量

在项目根目录创建 .env 文件,并添加以下内容:

DATABASE_URL=postgres://username:password@localhost/database

其中,DATABASE_URL 是连接 PostgreSQL 数据库所需的连接字符串。请根据实际情况替换 usernamepasswordlocalhostdatabase 的值。

  1. 使用 lazy_static 创建全局数据库连接池

    • lazy_static!:定义一个全局静态变量 DB_POOL,这是一个线程安全的 PostgreSQL 连接池。
    • dotenv::dotenv().ok():加载 .env 文件中的环境变量,允许在运行时访问数据库连接 URL。
    • env::var("DATABASE_URL"):从环境变量中获取数据库连接 URL。如果未设置该环境变量,则会 panic。
    • PgPoolOptions::new().max_connections(5).connect_lazy(&database_url):使用 PgPoolOptions 创建一个连接池。max_connections(5) 设置池中最大连接数为 5,connect_lazy 方法会延迟连接,直到第一次使用时才进行实际的连接操作。
  2. 异步主函数

    • #[tokio::main]:标记 main 函数为异步,这允许使用 await 关键字。
    • &*DB_POOL:获取全局静态变量 DB_POOL 的实际值。&* 语法用于解引用 lazy_static 创建的静态变量。
    • sqlx::query_as("SELECT COUNT(*) FROM users"):执行 SQL 查询,获取 users 表中的记录数。query_as 方法将查询结果映射到一个元组 (i64,) 中。
    • .fetch_one(pool).await:异步地从数据库中获取一行结果。
    • println!("Number of users: {}", row.0):打印查询结果,即用户表中的记录数。

这个示例展示了如何使用 lazy_staticsqlx 创建一个全局的 PostgreSQL 连接池,并在异步环境中执行查询操作。

通过将连接池的创建和管理封装在 lazy_static 中,我们可以确保在多线程环境下安全地共享数据库连接池,同时利用 tokio 和异步编程模型来处理异步 I/O 操作

这种方式适合需要在整个应用程序中共享数据库连接的场景,并且需要进行复杂的初始化操作。

使用场景

  • 复杂初始化:适用于需要延迟初始化的全局状态,如配置文件缓存数据库连接等。

4. 结论

  • 使用 const:当需要在编译时确定值且这些值不会改变时,const 是一个合适的选择。 它具有较好的性能和较低的内存开销,但只能处理简单的、编译时已知的值。
  • 使用 lazy_static当需要在运行时进行初始化或需要复杂的初始化逻辑时lazy_static一个有效的解决方案。 它提供了线程安全的全局变量,但会引入一定的运行时开销。

在实际开发中,根据具体的需求选择合适的机制可以帮助优化性能、简化代码,并确保程序的正确性和安全性。

鱼雪

这是#[wasm_bindgen]的"Hello, world!"示例,展示如何设置项目, 将函数导出到JS,从JS调用并在Rust中调用警报功能。

1. 创建项目

cargo new hello-world --lib
cd hello-world

2. 添加依赖

cargo add wasm-bindgen

3. Cargo.toml

Cargo.toml 列出了 wasm-bindgen crate 作为一个依赖项。 值得注意的是,crate-type = ["cdylib"],这在当今主要用于 wasm 最终工件。

[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.92"
备注

[lib] 部分指定了库的类型为 cdylib,这意味着它将被编译为共享库

适合与其他语言(如 JavaScript)进行交互,特别是在 WebAssembly 的上下文中。

在 Rust 的 Cargo.toml 文件中,crate-type 可以指定以下几种类型:

  • lib: 默认类型,生成一个库文件(.rlib),用于其他 Rust 代码的依赖。
  • cdylib: 生成一个动态库,适合与其他语言(如 C、JavaScript)进行交互,通常用于 WebAssembly 项目。
  • rlib: 生成一个 Rust 静态库,供其他 Rust crate 使用。
  • dylib: 生成一个动态库,主要用于 Rust 生态内部的共享。
  • staticlib: 生成一个静态库,通常用于与 C/C++ 代码的交互。

你可以根据项目的需求选择合适的 crate-type

4. 编写src/lib.rs

src/lib.rs 文件包含了一个简单的函数,该函数使用 #[wasm_bindgen] 属性导出到 JavaScript。

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}

上述代码中,greet 函数使用 #[wasm_bindgen] 属性导出到 JavaScript。

alert 函数是一个外部函数,它在 JavaScript 中实现。使用 extern "C" 来声明它, 然后可以在Rust代码中调用从JavaScript导入的函数。

5. 编译为WebAssembly

wasm-pack build --target web

上述命令将生成一个 pkg 目录,其中包含了编译好的 WebAssembly 模块和 TypeScript(JavaScript) 包装器。 使用 wasm-pack build --target web 来生成一个可以在浏览器中运行的包, 是一个完整的 npm 包,直接按照包的方式直接导入到 JavaScript 项目中。

如果是wasm-pack build,则可以构建一个可以在 Node.js 中运行的包。

wasm-pack build生成pkg目录

6. 在JavaScript中调用

index.js
// /path/to/hello-world/index.js

import { greet } from './pkg';

greet('World');

7. 创建npm包

pnpm init
# 或
npm init

会在当前目录下生成一个package.json文件,此文件是npm包的配置文件

8. 添加依赖

pnpm add webpack webpack-cli webpack-dev-server html-webpack-plugin -D
# 或
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin -D

上述几个包的作用是:

  1. webpack Webpack 是一个模块打包工具,主要用于将多个模块(如 JavaScript、CSS、图片等)打包成一个或多个文件。它可以处理依赖关系,优化资源,并支持各种加载器和插件,以满足不同的构建需求。
  2. webpack-cli webpack-cli 是 Webpack 的命令行接口,允许用户通过命令行执行 Webpack 的构建任务。它提供了一些命令和选项,使得用户可以更方便地配置和运行 Webpack,而不需要直接在代码中进行设置。
  3. webpack-dev-server webpack-dev-server 是一个用于开发的服务器,提供了热模块替换(HMR)功能,可以在代码更改时自动刷新浏览器。它使得开发过程更加高效,能够快速查看修改后的效果,而无需手动刷新页面。
  4. html-webpack-plugin html-webpack-plugin 是一个 Webpack 插件,用于生成 HTML 文件。它可以自动将打包后的 JavaScript 和 CSS 文件插入到生成的 HTML 文件中,从而简化了手动管理 HTML 的过程。它还支持模板引擎,可以根据需要自定义生成的 HTML。

9. 创建webpack.config.js

webpack.config.js
// /path/to/hello-world/webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");

module.exports = {
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
},
plugins: [
new HtmlWebpackPlugin(),
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, ".")
}),
],
mode: 'development',
experiments: {
asyncWebAssembly: true
}
};

上述代码的作用是:

  • 引入 Node.js 内置的 path 模块,方便处理文件路径。
  • 引入 HtmlWebpackPlugin,用于生成 HTML 文件。
  • 引入 webpack,虽然在这个配置中没有直接使用,但通常用于访问 Webpack 的功能。
  • 引入 WasmPackPlugin,用于处理 Rust 编写的 WebAssembly 模块
  • entry: 指定应用程序的入口文件,这里是 index.js。即这里是我们在第6步中创建的文件。
  • output: 指定输出文件的配置:
  • path: 输出目录,使用 path.resolve 来确保路径正确,输出到 dist 文件夹。
  • filename: 输出的文件名,这里是 index.js
  • plugins: 配置使用的插件:
  • HtmlWebpackPlugin: 自动生成 HTML 文件,包含打包后的 JavaScript 代码。
  • WasmPackPlugin: 配置 WasmPack 插件,crateDirectory 指定 Rust crate 的路径,这里是当前目录。
  • mode: 设置为 development,表示当前环境是开发模式,启用一些开发时的功能(如更好的错误信息)。
  • experiments: 启用 Webpack 的实验功能,这里开启了 asyncWebAssembly,允许使用异步加载 WebAssembly 模块。

10. 配置webpack服务

{
"name": "hello-world",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack",
"serve": "webpack serve"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "^1.7.0",
"html-webpack-plugin": "^5.6.0",
"webpack": "^5.93.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4"
}
}

配置package.json文件,添加buildserve脚本,用于构建和启动服务。

11. 编译

pnpm run build
# 或
npm run build

使用pnpm构建代码

12. 运行

pnpm run server
# 或
npm run server

使用pnpm启动服务

打开浏览器,访问 http://localhost:8080,在控制台中可以看到输出 Hello, World!。 webpack默认的服务端口是8080,如果端口被占用,可以在webpack.config.js中配置。

浏览器中显示Hello, World!

整个项目代码结构

整个项目代码结构

链接

可能会遇到的问题

  1. wasm-pack build 时,可能会遇到以下错误:
wasm-pack build
[INFO]: 🎯 Checking for the Wasm target...
[INFO]: 🌀 Compiling to Wasm...
Compiling syn v2.0.72
Compiling wasm-bindgen-backend v0.2.92
Compiling wasm-bindgen-macro-support v0.2.92
Compiling wasm-bindgen-macro v0.2.92
Compiling wasm-bindgen v0.2.92
Compiling hello-world v0.1.0 (/Users/hal/tutorials/wasm-examples/hello-world)
Finished `release` profile [optimized] target(s) in 54.81s
[INFO]: ⬇️ Installing wasm-bindgen...
[INFO]: found wasm-opt at "/Users/hal/bin/binaryen/bin/wasm-opt"
[INFO]: Optimizing wasm binaries with `wasm-opt`...
dyld[46869]: Symbol not found: __ZTVN4wasm10PassRunnerE
Referenced from: <7458E7FE-3DC2-3048-B182-5DF54E45F6D4> /Users/hal/bin/binaryen/bin/wasm-opt
Expected in: <F032F917-10CC-3569-8C1E-2299E90E789F> /usr/local/lib/libbinaryen.dylib
Error: failed to execute `wasm-opt`: exited with signal: 6 (SIGABRT)
full command: "/Users/hal/bin/binaryen/bin/wasm-opt" "/Users/hal/tutorials/wasm-examples/hello-world/pkg/hello_world_bg.wasm" "-o" "/Users/hal/tutorials/wasm-examples/hello-world/pkg/hello_world_bg.wasm-opt.wasm" "-O"
To disable `wasm-opt`, add `wasm-opt = false` to your package metadata in your `Cargo.toml`.
Caused by: failed to execute `wasm-opt`: exited with signal: 6 (SIGABRT)
full command: "/Users/hal/bin/binaryen/bin/wasm-opt" "/Users/hal/tutorials/wasm-examples/hello-world/pkg/hello_world_bg.wasm" "-o" "/Users/hal/tutorials/wasm-examples/hello-world/pkg/hello_world_bg.wasm-opt.wasm" "-O"
To disable `wasm-opt`, add `wasm-opt = false` to your package metadata in your `Cargo.toml`
  • 解决方法1:

Cargo.toml 中添加 wasm-opt = false。 禁用 wasm-opt

[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.92"

[package.metadata.wasm-pack]
wasm-opt = false
  • 解决方法2:

安装 binaryen,并且将 binaryen 库的路径添加到动态库的环境变量中,DYLD_LIBRARY_PATH

brew install binaryen

在Mac系统上是这样,Linux系统上可能是 LD_LIBRARY_PATH。而且这里是使用Homebrew安装的,如果是其他方式安装的,可能路径不一样。

export DYLD_LIBRARY_PATH="/opt/homebrew/lib:$DYLD_LIBRARY_PATH"
鱼雪

本文详细介绍如何使用 Axum 框架在 Rust 中构建一个通用化的 Web 应用模板,包括:

  • 构建 RESTful API
  • 使用 SqlxPostgres 数据库实现数据库交互
  • 采用类似 Nest.js 的项目组织结构,以提升代码可维护性
  • 包含丰富的单元测试和集成测试
  • 使用 Github Actions 实现 CI/CD 流程

基础开发环境搭建

为了快速开始,可以参考我的 Rust 项目模板,点击这里获取项目代码。该模板包含基础项目结构和一些配置,帮助你迅速搭建开发环境。

类似 Nest.js 的项目组织方式

为了提升项目的可维护性和扩展性,本文中采用了类似 Nest.js 的项目组织结构:

├── docs                # 文档文件
├── fixtures # 必要文件,比如公私钥,测试 SQL 脚本
├── migrations # 数据库迁移文件,适用于 Sqlx
├── rest_client # VS Code REST Client 测试 API 文件
├── src # 源代码
│   ├── common # 公共模块,如加解密、错误处理、配置等
│   └── modules # 业务模块
│   ├── auth # 认证模块,包括: `handlers`, `services`, `dto`, `tests`, `middleware` 等
│   └── users # 用户管理模块,包括: `handlers`, `services`, `dto`, `tests`, `entity` 等

业务模块文件说明

  • mod.rs:模块的导入导出,创建当前模块的路由器
  • handlers.rs:处理用户请求的逻辑。
  • services.rs:处理业务逻辑以及与数据库的交互。
  • entity.rs:定义与数据库表对应的结构体,便于结构操作。
  • dto.rs:定义数据转换对象,用于请求和响应的序列化和反序列化。
  • tests.rs:包含单元测试和集成测试。
  • middleware.rs:定义模块的中间件,用于认证、权限控制等。
  • src/lib.rs:定义项目的路由器,加载配置文件、初始化全局状态、错误处理等。
  • src/main.rs:应用入口。

开发经验分享

在项目开发过程中,我积累了一些经验,分享如下,帮助大家更好地构建 Axum Web 应用:

1. 测试的排序和并发控制

  • 测试数量多或者测试较为复杂时,可能会遇到并发导致的错误,特别是在集成测试中。可以使用 serial_test 来对测试进行排序,防止这些问题。

2. 测试数据库的创建与销毁

  • 在测试时需要创建临时数据库,并在测试结束后销毁它。可以使用 sqlx-db-tester,并为测试初始化一个连接到测试数据库的全局状态。

3. 全局配置与数据库连接

  • 全局状态中应包含全局配置和数据库连接,这样可以统一管理数据库操作,并将所有 services.rs 中的数据库操作通过全局状态来执行,方便管理。

4. Github Actions CI 配置

  • 集成测试中,reqwest 默认依赖 OpenSSL,这在 CI 环境下可能会导致问题。因此,我们需要在 OpenSSLBoringSSL 之间做出选择。由于 jwt-simple 依赖于 BoringSSL,所以应禁用掉 reqwest 的默认 OpenSSL 依赖。

5. 公私钥生成

  • 将公私钥生成的逻辑放在 build.rs 中,这样每次执行 cargo buildcargo run 等构建操作时,都会自动生成证书文件到 fixtures 目录下,方便开发和部署。

6. 测试模块管理

  • 在 Rust 中,可以将单独文件当作一个模块(mod)。但需要注意的是,Rust 不会自动识别 integration_tests.rs,但会识别 tests.rs。因此,我将单元测试和集成测试都写在 tests.rs 中,并使用 util_testsintegration_tests 模块分别包裹,这样可以更清晰地查看测试日志。

7. 代码质量检查

  • 使用 pre-commit 执行各类代码检查工具,如 cargo-deny,确保代码规范和安全性,避免潜在的漏洞和错误。

8. Token 签名与安全性

  • 选择使用 Ed25519 作为 Token 的签名算法。公钥用于验证签名,私钥用于生成签名。这样可以将公钥公开,用于独立的服务来验证 Token,保证安全性。

9. 参数验证

  • 使用 validator crate 来验证请求参数,减少错误输入和代码量,简化开发过程。

CI/CD 集成(Github Actions)

在本项目中,使用 Github Actions 进行持续集成(CI)和持续部署(CD)。你可以通过配置 .github/workflows/ci.yml 文件来实现每次代码推送后的自动化测试和构建,确保代码的稳定性和质量。

示例配置文件

name: Rust CI

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Run tests
run: cargo test

Rust Axum Web 应用的最佳实践

为了提升代码的可维护性和扩展性,建议:

  1. 模块化设计:保持代码模块化,采用类似 Nest.js 的项目结构,有利于团队合作和项目扩展。
  2. 自动化测试:编写单元测试和集成测试,确保各个模块的功能和整体系统的稳定性。
  3. 持续集成:通过 Github Actions 等工具实现持续集成,保证代码的高质量。

链接与资源

项目展示效果

以下是单元测试和集成测试的部分运行效果展示:

Axum单元测试与集成测试

鱼雪

wasm-bindgen 是一个强大的工具,它提供了多种方式来实现 Rust 和 JavaScript 之间的互操作性。 本文将总结 wasm-bindgen 的几种常见用法,并结合代码案例进行说明。

wasm-bindgen

1. 使用 JavaScript 模块中的自定义函数

你可以在 Rust 中使用 extern "C" 块来声明从 JavaScript 导入的函数,然后在 Rust 中调用它们。 这种方式非常适合需要调用现有 JavaScript 函数的场景。

示例

  • JavaScript 部分 (foo.js)
export function js_add(a, b) {
return a + b;
}
  • Rust 部分
use wasm_bindgen::prelude::*;

// Declare the JavaScript function
#[wasm_bindgen(module = "/js/foo.js")]
extern "C" {
fn js_add(a: i32, b: i32) -> i32;
}

// Define a Rust function that uses the imported JavaScript function
#[wasm_bindgen]
pub fn add_in_rust(a: i32, b: i32) -> i32 {
js_add(a, b)
}

2. 使用 JavaScript 基础功能

如果你需要使用 JavaScript 的基础功能,可以依赖 js-sys crate。 js-sys 提供了对 JavaScript 标准库的绑定,例如 MathDateArray 等。

示例

use wasm_bindgen::prelude::*;
use js_sys::Math;

#[wasm_bindgen]
pub fn random_number() -> f64 {
Math::random()
}

3. 使用 DOM 内容

如果你需要操作 DOM,可以依赖 web-sys crate。 web-sys 提供了对 Web API 的绑定,例如 documentwindowElement 等。

示例

use wasm_bindgen::prelude::*;
use web_sys::window;

#[wasm_bindgen]
pub fn set_document_title(title: &str) {
let window = window().expect("no global `window` exists");
let document = window.document().expect("should have a document on window");
document.set_title(title);
}

4. 在 Rust 中导出给 JavaScript 使用

在 Rust 中,只要添加 #[wasm_bindgen] 宏并且将函数或类型声明为 pub, 它们就可以导出给 JavaScript 使用。

示例

  • Rust 中的代码:
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}

#[wasm_bindgen]
pub struct Person {
name: String,
age: u32,
}

#[wasm_bindgen]
impl Person {
#[wasm_bindgen(constructor)]
pub fn new(name: &str, age: u32) -> Person {
Person {
name: name.to_string(),
age,
}
}

pub fn greet(&self) -> String {
format!("Hello, my name is {} and I am {} years old.", self.name, self.age)
}
}
  • 在 JavaScript 中使用:
import init, { greet, Person } from './pkg/your_project_name.js';

async function run() {
await init();
console.log(greet("World")); // Should output "Hello, World!"

let person = new Person("Alice", 30);
console.log(person.greet()); // Should output "Hello, my name is Alice and I am 30 years old."
}

run();

5. 异步函数

你可以使用 wasm-bindgen-futures crate 来处理异步函数, 使得 Rust 中的异步函数可以在 JavaScript 中以 Promise 的形式使用。

示例

use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::window;

#[wasm_bindgen]
pub async fn fetch_data() -> Result<JsValue, JsValue> {
let window = window().expect("no global `window` exists");
let response = JsFuture::from(window.fetch_with_str("https://api.example.com/data")).await?;
let response: web_sys::Response = response.dyn_into().unwrap();
let json = JsFuture::from(response.json()?).await?;
Ok(json)
}

6. 错误处理

使用 Result 类型和 JsValue 来捕获和处理 JavaScript 异常。

示例

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn catch_js_error() -> Result<(), JsValue> {
let result = js_sys::Reflect::get(&JsValue::NULL, &JsValue::from_str("non_existent_property"));
match result {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}

7. 内存管理

注意 WebAssembly 和 JavaScript 之间的内存管理, 特别是当你在 Rust 中创建大型数据结构并传递给 JavaScript 时。

  1. 线性内存

    • WebAssembly 使用线性内存模型,这意味着内存是一个连续的字节数组。
    • WebAssembly 模块可以通过 memory 对象来访问和操作这段内存。

2.内存分配:

  • 当从 JavaScript 传递数据到 WebAssembly 时,通常需要在 WebAssembly 内存中分配空间。
  • 可以使用 wasm-bindgen 提供的 wasm_bindgen::memory() 函数来访问 WebAssembly 内存,并手动进行内存分配和释放。
  1. 内存释放:

    • WebAssembly 没有垃圾回收机制,因此需要手动管理内存。
    • 确保在不再需要数据时,及时释放内存以避免内存泄漏。
  2. 传递大型数据结构:

    • 在传递大型数据结构(如数组、字符串等)时,尽量避免不必要的拷贝。
    • 可以使用共享内存或其他优化技术来提高性能。

示例

  • Rust 中的代码:
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use js_sys::Uint8Array;

#[wasm_bindgen]
pub fn allocate_memory(size: usize) -> *mut u8 {
let mut buffer = Vec::with_capacity(size);
let ptr = buffer.as_mut_ptr();
std::mem::forget(buffer); // Prevent Rust from deallocating the memory
ptr
}

#[wasm_bindgen]
pub fn deallocate_memory(ptr: *mut u8, size: usize) {
unsafe {
let _ = Vec::from_raw_parts(ptr, size, size); // Reclaim the memory
}
}

#[wasm_bindgen]
pub fn process_data(ptr: *mut u8, size: usize) -> Result<JsValue, JsValue> {
let data = unsafe { Vec::from_raw_parts(ptr, size, size) };
let sum: u8 = data.iter().sum();
Ok(JsValue::from(sum))
}
  • JavaScript 中的代码:
import init, { allocate_memory, deallocate_memory, process_data } from './pkg/your_project_name.js';

async function run() {
await init();

const size = 10;
const ptr = allocate_memory(size);
const memory = new Uint8Array(wasm_bindgen.memory.buffer, ptr, size);

for (let i = 0; i < size; i++) {
memory[i] = i + 1;
}

const result = process_data(ptr, size);
console.log(result); // Should output the sum of the array

deallocate_memory(ptr, size);
}

run();

8. 宏简化

使用 #[wasm_bindgen(module = "...")]#[wasm_bindgen(inline_js = "...")] 等宏来简化模块导入和内联 JavaScript 代码。

示例

  • 内联 JavaScript 代码:
use wasm_bindgen::prelude::*;

#[wasm_bindgen(module = "/js/foo.js")]
extern "C" {
fn foo();
}

#[wasm_bindgen(start)]
pub fn main() {
foo();
}
  • foo.js 中:
export function foo() {
console.log("Hello from JavaScript!");
}

综合示例

以下是一个综合示例,展示了如何结合这些用法:

  • 在 Rust 中:
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{console, window, Element};

#[wasm_bindgen(module = "/js/foo.js")]
extern "C" {
fn js_add(a: i32, b: i32) -> i32;
}

#[wasm_bindgen]
pub fn add_in_rust(a: i32, b: i32) -> i32 {
js_add(a, b)
}

#[wasm_bindgen]
pub async fn fetch_data() -> Result<JsValue, JsValue> {
let window = window().expect("no global `window` exists");
let response = JsFuture::from(window.fetch_with_str("https://api.example.com/data")).await?;
let response: web_sys::Response = response.dyn_into().unwrap();
let json = JsFuture::from(response.json()?).await?;
Ok(json)
}

#[wasm_bindgen(start)]
pub fn main() {
console::log_1(&"Wasm module initialized".into());
}
  • foo.js 中:
export function js_add(a, b) {
return a + b;
}

通过这种方式,你可以充分利用 wasm-bindgen 的功能,构建强大且高效的 WebAssembly 应用程序。

#[wasm_bindgen(start)]宏来指定一个初始化函数,这个函数会在 WebAssembly 模块加载时自动调用。

总结

wasm-bindgen 提供了多种方式来实现 Rust 和 JavaScript 之间的互操作性。 通过这些方式,你可以轻松地在 Rust 和 JavaScript 之间传递数据、调用函数、处理异步操作等。 这些功能使得 WebAssembly 成为一个强大的工具,可以用于构建高性能的 Web 应用程序。

鱼雪

掌握 Cargo.toml 的格式规则,避免挫败感

在 JavaScript 和其他语言中,我们称令人惊讶或不一致的行为为“Wat!”(即“什么!?”)。 例如,在 JavaScript 中,空数组加空数组会产生一个空字符串,[] + [] === ""。Wat!

在另一个极端,某种语言有时会表现出令人惊讶的一致性。我称之为“Wat Not”。

Rust 通常比 JavaScript 更加一致。然而,一些与 Rust 相关的格式会带来惊喜。 具体来说,本文将介绍 Cargo.toml 中的九个 wats 和 wat nots。

回想一下,Cargo.toml 是定义 Rust 项目配置和依赖项的清单文件。 其格式 TOML(Tom's Obvious, Minimal Language)表示嵌套的键/值对和/或数组。 JSON 和 YAML 是类似的格式。与 JSON 不同的是,TOML 被设计为易于人类阅读和编写。

这九个 wats 和 wat nots 的旅程不会像 JavaScript 的怪癖那样有趣(谢天谢地)。 然而,如果你曾经对 Cargo.toml 的格式感到困惑,我希望本文能让你感觉更好。 最重要的是,当你了解了这九个 wats 和 wat nots 后,希望你能更轻松有效地编写 Cargo.toml

本文不是关于“修复” Cargo.toml。该文件格式在其主要用途上非常出色:指定 Rust 项目的配置和依赖项。 相反,本文旨在理解其格式及其怪癖。

Wat 1:依赖项 vs. 配置文件部分名称

你可能知道如何在 Cargo.toml 中添加 [dependencies] 部分。这样的部分指定了发布依赖项,例如:

[dependencies]
serde = "1.0"

同样,你可以使用 [dev-dependencies] 部分指定开发依赖项,使用 [build-dependencies] 部分指定构建依赖项

你可能还需要设置编译器选项,例如优化级别和是否包含调试信息。你可以通过发布开发构建的配置文件部分来设置这些选项。

你能猜出这三个部分的名称吗?是 [profile][dev-profile][build-profile] 吗?

不!它们是 [profile.release][profile.dev][profile.build]。Wat!

[dev-profile] 会比 [profile.dev] 更好吗?[dependencies.dev] 会比 [dev-dependencies] 更好吗?

我个人更喜欢带点的名称。(在“Wat Not 9”中,我们将看到点的强大之处。)然而,我愿意记住依赖项和配置文件的工作方式不同。

Wat 2:依赖项继承

你可能会认为点适用于配置文件,而连字符更适用于依赖项,因为 [dev-dependencies] 继承自 [dependencies]。换句话说,[dependencies] 中的依赖项在 [dev-dependencies] 中也可用。那么,这是否意味着 [build-dependencies] 也继承自 [dependencies]

不![build-dependencies] 不继承自 [dependencies]。Wat!

我发现这种 Cargo.toml 的行为既方便又令人困惑。

Wat 3:默认键

你可能知道,可以这样写:

[dependencies]
serde = { version = "1.0" }

也可以这样写:

[dependencies]
serde = "1.0"

这里的原则是什么?一般的 TOML 中如何指定一个键为默认键?

你不能!一般的 TOML 没有默认键。Wat!

Cargo TOML 对 [dependencies] 部分中的 version 键进行了特殊处理。这是 Cargo 特有的功能,而不是一般的 TOML 功能。据我所知,Cargo TOML 没有其他默认键。

Wat 4:子功能

使用 Cargo.toml[features],你可以创建依赖项不同的项目版本。这些依赖项本身的功能也可能不同,我们称之为子功能

在这里,我们创建了两个项目版本。默认版本依赖于带有默认功能的 getrandomwasm 版本依赖于带有 js 子功能的 getrandom

[features]
default = []
wasm = ["getrandom-js"]

[dependencies]
rand = { version = "0.8" }
getrandom = { version = "0.2", optional = true }

[dependencies.getrandom-js]
package = "getrandom"
version = "0.2"
optional = true
features = ["js"]

在这个例子中,wasm 是我们项目的一个功能,依赖于依赖项别名 getrandom-rs,它代表带有 js 子功能的 getrandom crate 版本。

那么,如何在避免冗长的 [dependencies.getrandom-js] 部分的情况下给出相同的规范?

[features] 中,将 getrandom-js 替换为 "getrandom/js"。我们可以这样写:

[features]
default = []
wasm = ["getrandom/js"]

[dependencies]
rand = { version = "0.8" }
getrandom = { version = "0.2", optional = true }

一般来说,在 Cargo.toml 中,功能规范(如 wasm = ["getrandom/js"])可以列出:

  • 其他功能
  • 依赖项别名
  • 依赖项
  • 一个或多个依赖项“斜杠”一个子功能

这不是标准的 TOML,而是 Cargo.toml 特有的简写。

附加:你如何用简写表示你的 wasm 功能应包括带有两个子功能的 getrandom:js 和 test-in-browser

答案:列出依赖项两次。

wasm = ["getrandom/js","getrandom/test-in-browser"]

Wat 5:目标的依赖项

我们已经看到如何指定发布调试构建的依赖项。

[dependencies]
#...
[dev-dependencies]
#...
[build-dependencies]
#...

我们已经看到如何指定各种功能的依赖项:

[features]
default = []
wasm = ["getrandom/js"]

你会怎么猜测我们如何为各种目标(例如某个版本的 Linux、Windows 等)指定依赖项?

我们在 [dependencies] 前加上 target.TARGET_EXPRESSION 前缀,例如:

[target.x86_64-pc-windows-msvc.dependencies]
winapi = { version = "0.3.9", features = ["winuser"] }

按照一般 TOML 的规则,我们也可以这样说:

[target]
x86_64-pc-windows-msvc.dependencies={winapi = { version = "0.3.9", features = ["winuser"] }}

我觉得这种前缀语法很奇怪,但我无法提出更好的替代方案。不过,我确实想知道为什么功能不能以相同的方式处理:

# 不允许
[feature.wasm.dependencies]
getrandom = { version = "0.2", features=["js"]}

Wat Not 6:目标 cfg 表达式

这是我们的第一个“Wat Not”,即它是一种让我感到惊讶的一致性。

除了具体目标(如 x86_64-pc-windows-msvc),你还可以在单引号中使用 cfg 表达式。例如:

[target.'cfg(all(windows, target_arch = "x86_64"))'.dependencies]

我不认为这是一个“wat!”。我认为这很棒。

回想一下,cfg 是“配置”的缩写,是 Rust 通常用于条件编译代码的机制。例如,在我们的 main.rs 中,我们可以这样说:

if cfg!(target_os = "linux") {
println!("This is Linux!");
}

Cargo.toml 中,在目标表达式中,几乎支持整个 cfg 迷你语言。

  • all()any()not()
  • target_arch
  • target_feature
  • target_os
  • target_family
  • target_env
  • target_abi
  • target_endian
  • target_pointer_width
  • target_vendor
  • target_has_atomic
  • unix
  • windows

cfg 迷你语言唯一不支持的部分(我认为)是你不能使用 --cfg 命令行参数设置值。此外,一些 cfg 值(如 test)没有意义。

Wat 7:目标的配置文件

回想一下 Wat 1 中,你可以通过 [profile.release][profile.dev][profile.build] 设置编译器选项。例如:

[profile.dev]
opt-level = 0

你如何为特定目标(如 Windows)设置编译器选项?是这样吗?

[target.'cfg(windows)'.profile.dev]
opt-level = 0

不。相反,你需要创建一个名为 .cargo/config.toml 的新文件,并添加以下内容:

[target.'cfg(windows)']
rustflags = ["-C", "opt-level=0"]

Wat!

一般来说,Cargo.toml 只支持 target.TARGET_EXPRESSION 作为依赖项部分的前缀。你不能为配置文件部分加前缀。然而,在 .cargo/config.toml 中,你可以有 [target.TARGET_EXPRESSION] 部分。在这些部分中,你可以设置环境变量来设置编译器选项。

Wat Not 8:TOML 列表

Cargo.toml 支持两种列表语法:

  • 内联数组
  • 表数组

这个例子使用了两者:

[package]
name = "cargo-wat"
version = "0.1.0"
edition = "2021"

[dependencies]
rand = { version = "0.8" }
# 内联数组 'features'
getrandom = { version = "0.2", features = ["std", "test-in-browser"] }

# 表数组 'bin'
[[bin]]
name = "example"
path = "src/bin/example.rs"

[[bin]]
name = "another"
path = "src/bin/another.rs"

我们可以将表数组更改为内联数组吗?可以!

# 内联数组 'bin'
bins = [
{ name = "example", path = "src/bin/example.rs" },
{ name = "another", path = "src/bin/another.rs" },
]

[package]
name = "cargo-wat"
version = "0.1.0"
edition = "2021"

[dependencies]
rand = { version = "0.8" }
# 内联数组 'features'
getrandom = { version = "0.2", features = ["std", "test-in-browser"] }

我们可以将功能的内联数组更改为表数组吗?

不可以。简单值(此处为字符串)的内联数组不能表示为表数组。然而,我认为这是一个“wat not”,而不是“wat!”,因为这是一般 TOML 的限制,而不仅仅是 Cargo.toml 的限制。

附带说明:YAML 格式与 TOML 格式一样,提供两种列表语法。然而,YAML 的两种语法都适用于简单值。

Wat Not 9:TOML 内联、部分和点

这是一个典型的 Cargo.toml。它混合了部分语法(如 [dependencies])和内联语法(如 getrandom = {version = "0.2", features = ["std", "test-in-browser"]})。

[package]
name = "cargo-wat"
version = "0.1.0"
edition = "2021"

[dependencies]
rand = "0.8"
getrandom = { version = "0.2", features = ["std", "test-in-browser"] }

[target.x86_64-pc-windows-msvc.dependencies]
winapi = { version = "0.3.9", features = ["winuser"] }

[[bin]]
name = "example"
path = "src/bin/example.rs"

[[bin]]
name = "another"
path = "src/bin/another.rs"

我们可以将其完全重写为 100% 内联吗?可以。

package = { name = "cargo-wat", version = "0.1.0", edition = "2021" }

dependencies = { rand = "0.8", getrandom = { version = "0.2", features = [
"std",
"test-in-browser",
] } }

target = { 'cfg(target_os = "windows")'.dependencies = { winapi = { version = "0.3.9", features = [
"winuser",
] } } }

bins = [
{ name = "example", path = "src/bin/example.rs" },
{ name = "another", path = "src/bin/another.rs" },
]

我们也可以将其重写为最大部分:

[package]
name = "cargo-wat"
version = "0.1.0"
edition = "2021"

[dependencies.rand]
version = "0.8"

[dependencies.getrandom]
version = "0.2"
features = ["std", "test-in-browser"]

[target.x86_64-pc-windows-msvc.dependencies.winapi]
version = "0.3.9"
features = ["winuser"]

[[bin]]
name = "example"
path = "src/bin/example.rs"

[[bin]]
name = "another"
path = "src/bin/another.rs"

最后,让我们谈谈点。

在 TOML 中,点用于分隔嵌套表中的键。例如,a.b.c 是表 a 中表 b 中的键 c。我们可以用“很多点”重写我们的例子吗?可以:

package.name = "cargo-wat"
package.version = "0.1.0"
package.edition = "2021"
dependencies.rand = "0.8"
dependencies.getrandom.version = "0.2"
dependencies.getrandom.features = ["std", "test-in-browser"]
target.x86_64-pc-windows-msvc.dependencies.winapi.version = "0.3.9"
target.x86_64-pc-windows-msvc.dependencies.winapi.features = ["winuser"]
bins = [
{ name = "example", path = "src/bin/example.rs" },
{ name = "another", path = "src/bin/another.rs" },
]

我欣赏 TOML 在部分、内联和点方面的灵活性。我认为这种灵活性是一个“wat not”。你可能会发现所有这些选择令人困惑。然而,我喜欢 Cargo.toml 让我们使用 TOML 的全部功能。

结论

Cargo.toml 是 Rust 生态系统中的一个重要工具,提供了简单性和灵活性的平衡,既适合初学者也适合经验丰富的开发人员。通过我们探讨的九个 wats 和 wat nots,我们看到了这个配置文件有时会因其特性而令人惊讶,但同时也因其一致性和强大而令人印象深刻。

理解这些怪癖可以让你避免潜在的挫败感,并使你能够充分利用 Cargo.toml。从管理依赖项和配置文件到处理特定目标的配置和功能,这些见解将帮助你编写更高效和有效的 Cargo.toml 文件。

总之,虽然 Cargo.toml 可能有其独特之处,但这些特性往往源于实用的设计选择,优先考虑功能性和可读性。

接受这些怪癖,你会发现 Cargo.toml 不仅能满足你的项目需求,还能提升你的 Rust 开发体验

鱼雪

本文将介绍如何使用 Axum 框架实现 JWT 授权,包括从生成密钥对、创建和验证 Token 到在 Axum 中实现授权中间件,全面讲解构建安全 API 的流程。

Axum 是一个基于 Hyper 的 Rust Web 框架,它提供了高效、灵活的方式来构建现代 Web 应用。在大多数实际应用中,我们需要对 API 端点进行授权,以确保只有授权用户才能访问受保护的资源。**JWT(JSON Web Token)**是一种流行的授权机制,非常适合用于这种场景,能够实现无状态、跨平台的身份验证。

本文大体分为三个部分:

  1. 生成一个新的 Ed25519 公私钥对
  2. 生成和验证 JWT Token
  3. 在 Axum 中集成授权中间件

1. 生成 Ed25519 的公私钥

在我们的授权系统中,我们使用 Ed25519 算法来生成公私钥对。这是一种现代的、安全的数字签名算法,提供了高安全性和高性能,非常适合用在 JWT 授权中。

生成公私钥的代码示例

use anyhow::Result;
use jwt_simple::prelude::*;
use std::fs::File;

fn main() -> Result<()> {
generate_and_save_keys()?;
Ok(())
}

fn generate_and_save_keys() -> Result<()> {
let key_pair = Ed25519KeyPair::generate();

// 保存私钥
let private_key_pem = key_pair.to_pem();
let mut private_key_file = File::create("private_key.pem")?;
private_key_file.write_all(private_key_pem.as_bytes())?;

// 保存公钥
let public_key_pem = key_pair.public_key().to_pem();
let mut public_key_file = File::create("public_key.pem")?;
public_key_file.write_all(public_key_pem.as_bytes())?;

Ok(())
}

代码说明:

  • 使用 Ed25519KeyPair::generate() 生成密钥对,并分别保存公钥和私钥。
  • 私钥用于生成 Token,必须严格保密,而公钥用于验证 Token,可以公开发布。
  • PEM 格式是一种常用的密钥存储格式,易于管理。
提示

将公钥和私钥分开存储可以保证系统安全性。私钥通常只在生成 Token 时使用,而公钥则用于对 Token 的验证,可以公开提供给需要验证身份的服务。

2. 生成 JWT Token 和验证 Token

在这一步,我们会学习如何生成和验证 JWT Token,帮助我们在 Web 应用中实现授权控制。

生成 Token 的实现

fn sign(user: impl Into<User>) -> Result<String> {
let private_key_pem = read_to_string("private_key.pem")?;
let key_pair = Ed25519KeyPair::from_pem(&private_key_pem)?;

let user = user.into();
let claims = Claims::with_custom_claims(user, Duration::from_secs(JWT_DURATION));
let claims = claims.with_issuer(JWT_ISS).with_audience(JWT_AUD);

let token = key_pair.sign(claims)?;
Ok(token)
}

代码说明:

  1. 从文件中读取私钥,并创建 Ed25519KeyPair
  2. 创建一个包含用户信息的 claims 对象,并设置 Token 的有效期、发行者和受众。
  3. 使用私钥对 claims 进行签名,生成 Token。

验证 Token 的实现

fn verify(token: &str) -> Result<User, Box<dyn std::error::Error>> {
let public_key_pem = read_to_string("public_key.pem")?;
let public_key = Ed25519PublicKey::from_pem(&public_key_pem)?;

let options = VerificationOptions {
allowed_issuers: Some(HashSet::from_strings(&[JWT_ISS])),
allowed_audiences: Some(HashSet::from_strings(&[JWT_AUD])),
..Default::default()
};

let claims = public_key.verify_token::<User>(token, Some(options))?;
Ok(claims.custom)
}

验证步骤:

  • 使用公钥验证 Token 的签名,并检查是否符合预期的发行者受众
  • 通过验证后,返回自定义的 User 数据。

要点:

  • 生成 Token 时使用私钥,验证 Token 时使用公钥。
  • 设置有效期发行者受众,以提高安全性。

3. 在 Axum 中集成 JWT 授权中间件

为了确保只有经过身份验证的用户才能访问受保护的 API,我们需要将 JWT 的生成和验证集成到 Axum 框架中。

定义 AuthUser 结构体并实现 FromRequestParts Trait

struct AuthUser(User);

#[async_trait]
impl<S> FromRequestParts<S> for AuthUser
where
S: Send + Sync,
{
type Rejection = StatusCode;

async fn from_request_parts<'life0, 'life1>(
parts: &'life0 mut Parts,
_state: &'life1 S,
) -> Result<Self, Self::Rejection>
where
'life0: 'async_trait,
'life1: 'async_trait,
Self: 'async_trait,
{
let token = parts
.headers
.get("Authorization")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or(StatusCode::UNAUTHORIZED)?;
let user = verify(token).map_err(|_| StatusCode::UNAUTHORIZED)?;
Ok(AuthUser(user))
}
}

该实现可以让我们方便地从请求中提取出认证信息。

创建授权中间件

async fn auth_middleware(
AuthUser(user): AuthUser,
req: Request<Body>,
next: Next,
) -> impl IntoResponse {
info!("Authenticated user: {}", user.username);
next.run(req).await
}

这个中间件会记录用户信息,然后继续处理请求,确保只有认证过的用户可以访问受保护的路由。

在 Axum 中使用中间件

let app = Router::new()
.route("/login", post(get_token))
.route("/protected", get(protected_route).layer(from_fn(auth_middleware)));

我们定义了两个路由:

  • /login:用于登录并获取 Token,不需要认证。
  • /protected:受保护的路由,需要通过授权中间件的认证。

登录并获取 Token 的实现

async fn get_token(
Json(payload): Json<TokenRequest>
) -> Result<Json<TokenResponse>, StatusCode> {
let user = User {
username: payload.username,
created_at: Utc::now(),
scope: vec!["read".to_string(), "write".to_string()],
};
let token = sign(user).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(TokenResponse { token }))
}

该函数用于处理登录请求,生成并返回 JWT Token。

受保护路由的实现

async fn protected_route(AuthUser(user): AuthUser) -> impl IntoResponse {
format!("Hello, {}! Your scopes are: {:?}", user.username, user.scope)
}

该路由只有在用户通过认证后才能访问,返回用户的相关信息。

要点:

  • 使用 FromRequestParts Trait 从请求中提取认证信息,便于集成 JWT 认证。
  • 中间件统一处理认证逻辑,提高了代码的模块化和可维护性。

测试 JWT 授权

使用 REST 客户端(例如 VSCode REST Client 插件)可以测试整个授权流程:

  1. 获取 Token:首先发送 POST 请求到 /login
  2. 访问受保护的路由:使用获得的 Token,设置 Authorization 请求头访问 /protected
### Get Token
POST http://localhost:3000/login
Content-Type: application/json

{
"username": "admin"
}

@token = {{signin.response.body.token}}

### Auth user
GET http://localhost:3000/protected
Authorization: Bearer {{token}}
备注

在这里,我们使用了 REST Client 插件来进行测试,方便地将获取的 Token 用于后续请求,避免手动复制粘贴。

完整代码示例

Cargo.toml 依赖库

[package]
name = "axum-jwt-auth"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.86"
axum = "0.7.5"
chrono = { version = "0.4.38", features = ["serde"] }
jwt-simple = "0.12.9"
serde = { version = "1.0.204", features = ["derive"] }
tokio = { version = "1.39.1", features = ["net", "rt", "rt-multi-thread"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

主要 Rust 代码示例

use anyhow::Result;
use chrono::{DateTime, Utc};
use jwt_simple::prelude::*;
use serde::{Deserialize, Serialize};
use tokio::net::TcpListener;

use tracing::{info, level_filters::LevelFilter};
use tracing_subscriber::{
fmt::Layer, layer::SubscriberExt,
util::SubscriberInitExt,
Layer as _
};

use std::{
collections::HashSet,
fs::{read_to_string, File},
io::Write,
net::SocketAddr,
};

use axum::{
async_trait, body::Body,
extract::FromRequestParts,
http::{request::Parts, Request,StatusCode},
middleware::{from_fn, Next},
response::IntoResponse,
routing::{get, post},
Json, Router
};

// 省略部分代码,可参考上文详细内容

结论

通过这篇文章,我们了解了如何使用 Axum 框架在 Rust 中实现基于 JWT 的授权系统。具体内容包括:

  • 使用 Ed25519 算法生成密钥对,确保私钥的安全性。
  • 生成和验证 JWT Token,以实现无状态的授权。
  • 将 Token 签发和验证集成到 Axum 中,确保对 API 端点进行保护。

这种方法为我们提供了一个灵活、安全的 API 保护机制。在实际应用中,你还可以引入更多的安全措施,如 Token 刷新权限管理密钥轮换等,以进一步提升安全性。

参考链接

鱼雪