Skip to main content

34 posts tagged with "Rust"

View All Tags

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"
note

[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 搭建后台服务,包括:

  • 构建 API
  • 使用 Sqlx 和 Postgres 数据库提供数据库服务
  • 采用类似 Nest.js 的项目组织结构,使得项目的文本文件负责单独的职责使得代码更信息易读
  • 包含单元测试和集成测试代码
  • Github Actions CI

基础开发环境搭建

可以参考我的 Rust 项目模版,地址

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

├── docs                # 放置文档
├── fixtures # 放置一些必要的文件,比如公私钥,测试SQL
├── migrations # Sqlx的目录
├── rest_client # VS Code REST Client的测试API文件
├── src # 源代码
│   ├── common # 放置公共模块,比如加解密,签名与验证,error模块,config模块等
│   └── 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: 做单元测试(util_tests)和集成测试(integration_tests)
  • middleware.rs: 适合模块自身的中间件
  • src/lib.rs: 定义总的路由器,加载配置文件,初始化全局状态,全局配置,全局错误等
  • src/main.rs: 应用入口

一点经验

  1. 如果测试过多或者复杂可能要给测试进行排序,防止并发导致错误,尤其在继承测试中,可以使用serial_test
  2. 为了便于测试时测试创建数据库,测试完删除数据库,可以使用sqlx-db-tester,并且在单独初始化全局状态时,为测试也初始化一个,连接测试数据库
  3. 为了方便数据库操作,在全局状态里包含全局配置数据库连接,并且可以将所有的services.rs关于数据库的操作都实现在全局状态下,便于操作
  4. 集成测试如果使用reqwest作为请求的客户端的话,在代码提交 Github Actions 时, 会由于 reqwest 默认依赖 OpenSSL 导致容器崩溃,可以参考我的Cargo.toml关于, 我们需要在OpenSSLboringSSL 之间二选一,由于jwt-simple也依赖boring, 所以要禁用掉默认的reqwest依赖的OpenSSL
  5. 我将生成公钥私钥的内容放在了build.rs,这样只要执行cargo build或者cargo run之类的构建操作, 就会自动生成证书文件在fixtures目录下,具体逻辑可以查看build.rs文件
  6. 关于测试,Rust 中可以将单独文件当做一个mod,但是 Rust 不会识别integration_tests.rs为一个测试模块,但是可以识别tests.rs, 所以我将单元测试与集成测试都写在模块的tests.rs中并且使用util_testsintegration_tests模块分别包裹, 这样测试日志可以明确看清楚是属于什么测试,并且按照模块写测试,便于出错后排查
  7. 使用pre-commit严格执行各类工具检查,使代码更加规范化,cargo-deny也会优化代码,让代码更合理
  8. Token 的签名算法使用Ed25519,这样便于将公钥与私钥分开,签名依赖私钥,验证签名依赖公钥,这样如果想将验证签名作为一个独立的服务很容易也更安全
  9. 使用validator验证请求参数,这样可以减少参数传递错误,也可以减少参数传递的代码量,避免不必要的错误

链接

一些效果

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 是一个基于 Hyper 的 Rust Web 框架,它提供了一种简单、高效的方式来构建 Web 应用。 在实际应用中,我们通常需要对 API 端点进行授权,以确保只有授权用户才能访问受保护的资源。 JWT(JSON Web Token)是一种流行的授权机制,它可以帮助我们实现这一目标。

本文大体分为三部分:

  • 生成一个新的密钥对
  • 生成和验证Token
  • 在Axum中使用授权中间件

Token的签发详细过程的时序图

1. 生成 Ed25519 的公私钥

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

首先,让我们看看生成密钥对的函数:

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(())
}

这个函数完成了以下几个步骤:

  1. 使用 Ed25519KeyPair::generate() 生成一个新的密钥对。
  2. 将私钥转换为 PEM 格式,并保存到 "private_key.pem" 文件中。
  3. 将公钥转换为 PEM 格式,并保存到 "public_key.pem" 文件中。

要点:

  • 密钥对只需要生成一次,然后可以重复使用。
  • 私钥必须保密,而公钥可以公开。
  • PEM 格式是一种常用的密钥存储格式,易于管理和使用。
tip

把公钥和私钥分开存储,是为了保证私钥的安全性。 私钥只有在生成 token 时才会用到,而公钥则用于验证 token 的签名。 可以将私钥存储在安全的地方,而公钥可以公开发布。 可以将授权系统的公钥存储在一个公开的地方,以便其他服务可以验证 token。

2. 生成 token 和验证 token

接下来,我们来看看如何生成和验证 JWT token。

生成 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. 从文件中读取私钥
  2. 创建一个包含用户信息的 claims 对象。
  3. 设置 token 的有效期发行者issuer)和受众audience)。
  4. 使用私钥签名 claims,生成 JWT 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)
}

验证函数执行以下步骤:

  1. 从文件中读取公钥。
  2. 设置验证选项,包括允许的发行者和受众。
  3. 使用公钥验证 token 的签名,并解析出 claims。
  4. 返回自定义的 User 数据。

要点:

  • 生成 token 时使用私钥,验证 token 时使用公钥。
  • 设置适当的 token 有效期、发行者和受众可以增加安全性。
  • 验证时要检查这些元数据以确保 token 的有效性。

3. 将 token 的签发和验证封装成中间件,在 Axum 中使用授权中间件

最后,我们来看看如何在 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受保护的路由,使用 auth_middleware 进行认证。

登录和获取 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 可以方便地从请求中提取认证信息。
  • 中间件可以用来统一处理认证逻辑,简化路由处理函数。
  • 将认证应用到特定路由,而不是全局应用,可以提供更灵活的控制。

测试

使用提供的 REST Client 代码,我们可以测试整个流程:

  1. 首先,发送 POST 请求到 /login 获取 token
  2. 然后,使用获得的 token 访问受保护的路由
### Get Token
# @name signin
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}}
note

这里使用了VSCode REST Client 插件编写的请求代码。 首先需要发送 POST 请求到 /login 获取 token,然后使用获得的 token 访问受保护的路由。

这里使用了VScode REST Client插件中定义变量的方式,可以在后续的多个请求中使用, 避免每次复制新的token。

完整代码示例

Cargo.toml依赖库

[package]
name = "axum-template"
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
};

const JWT_DURATION: u64 = 60 * 60 * 24;
const JWT_ISS: &str = "my_service";
const JWT_AUD: &str = "my_app";

#[derive(Debug, Serialize, Deserialize)]
struct User {
username: String,
created_at: DateTime<Utc>,
scope: Vec<String>,
}

struct AuthUser(User);

#[derive(Debug, Serialize, Deserialize)]
struct TokenRequest {
username: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct TokenResponse {
token: String,
}


#[tokio::main]
async fn main() -> Result<()> {
let layer = Layer::new().with_filter(LevelFilter::INFO);
tracing_subscriber::registry().with(layer).init();
let app = Router::new()
.route("/login", post(get_token))
.route("/protected", get(protected_route).layer(from_fn(auth_middleware)));

let addr: SocketAddr = "0.0.0.0:3000".parse().unwrap();
info!("Listening on {}", &addr);
let listener = TcpListener::bind(&addr).await?;
axum::serve(listener, app.into_make_service()).await?;

Ok(())
}

#[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 protected_route(AuthUser(user): AuthUser) -> impl IntoResponse {
format!("Hello, {}! Your scopes are: {:?}", user.username, user.scope)
}

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 }))
}

#[allow(unused)]
fn generate_and_verify_token() -> Result<()> {
// generate_and_save_keys()?;
let user = User {
username: "hal".to_string(),
created_at: Utc::now(),
scope: vec!["read".to_string(), "write".to_string()],
};
let token = sign(user)?;
println!("Token: {}", &token);
let claims = verify(&token).unwrap();
println!("Claims: {:?}", claims);
Ok(())
}


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

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)
}

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)
}

#[allow(unused)]
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(())
}

运行结果

Axum 命令行运行结果 VSCode REST Client运行结果

结论

通过这个实现,我们成功地在 Axum 中集成了基于 JWT 的授权系统。 这个系统包括了密钥生成、token 的签发和验证,以及在 Axum 中使用授权中间件。 这种方法提供了一个安全、灵活的方式来保护你的 API 端点。

在实际应用中,你可能还需要考虑更多的安全措施,如 token 刷新、权限管理、密钥轮换等。 但这个实现为你提供了一个坚实的基础,你可以在此基础上进行进一步的开发和优化。

链接

鱼雪

本文介绍使用datafusion读取parquet文件的相关代码和说明。

Cargo.toml依赖库

[package]
name = "datafusion_read_parquet"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.86"
datafusion = { version = "40.0.0", features = ["serde"] }
serde = { version = "1.0.204", features = ["derive"] }
tokio = { version = "1.38.1", features = ["rt", "rt-multi-thread"] }

代码说明

use anyhow::Result;
use datafusion::{arrow::array::AsArray, execution::context::SessionContext};

const PQ_FILE: &str = "../assets/sample.parquet";

#[tokio::main]
async fn main() -> Result<()> {
read_with_datafusion(PQ_FILE).await?;
Ok(())
}

async fn read_with_datafusion(file: &str) -> Result<()> {
let ctx = SessionContext::new();
ctx.register_parquet("stats", file, Default::default()).await?;

let ret = ctx
.sql("SELECT name::text name, email::text email FROM stats limit 3")
.await?
.collect()
.await?;

for batch in ret {
let names = batch.column(0).as_string::<i32>();
let emails = batch.column(1).as_string::<i32>();

for (name, email) in names.iter().zip(emails.iter()) {
let (name, email) = (name.unwrap(), email.unwrap());
println!("{}: {}", name, email);
}
}
Ok(())
}
  • 首先使用SessionContext::new()创建一个上下文会话(Session), 将数据转换成表以 及执行表查询都需要这个上下文对象ctx
  • SQL语句SELECT name::text name, email::text email FROM stats limit 3,这里获取 3条数据,每条数据包含nameemail,这些需要为这两个字段加上类型说明,否则执 行会报类型转换的错误"thread 'main' panicked at /path/to/.cargo/registry/src/index.crates.io-6f17d22bba15001f/arrow-array-52.1.0/src/cast.rs:769:29"
  • 另外需要注意batch.column(column_index)这里是根据列索引取数据,需要跟SQL语句 SELECT取的字段相对应,否则会出现信息对应错误的问题
  • Rust中迭代器以及可以同时迭代多个集合的zip方法

运行方式

# cd arrow-examples/datafusion_read_parquet
cargo run
  • 运行结果 Rust DataFusion库读取parquet文件运行结果

链接

鱼雪

Cargo.toml依赖库

[package]
name = "polars_read_parquet"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.86"
polars = { version = "0.41.3", features = ["parquet", "timezones", "sql", "lazy"] }

其中:

  • parquet features: 是用来加载parquet文件的特性
  • timizones features: 是用来处理数据中的时区
  • sql features: 是以SQL的方式操作数据
  • lazy features: 是加载数据是执行懒加载,不是从一开始就把数据都加载到内存,而 是需要数据做运算或者查询时才会加载数据,以LazyFrame替换DataFrame

代码

use anyhow::Result;
use polars::{prelude::*, sql::SQLContext};

const PQ_FILE: &str = "../assets/sample.parquet";

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

fn read_with_polars(file: &str) -> Result<()> {
let df = LazyFrame::scan_parquet(file, Default::default())?;
let mut ctx = SQLContext::new();
ctx.register("stats", df);
let df = ctx
.execute("SELECT name::text name, email::text email FROM stats")?
.collect()?;
println!("{:?}", df);

Ok(())
}
  • 首先是使用lazy方式扫描加载parquet文件,返回一个LazyFrame的对象
  • 使用SQLContext创建可以使用SQL操作DataFrame数据的上下文对象
  • ctx.register()方法可以将读取到的DataFrame与执行名称的"表名"关联起来,可以 理解为读取到DataFrame数据取一个名称,表格数据也可以直接当做表来操作,在后文中 的SQL语句中使用这个名字
  • 使用ctx执行SQL查询语句

运行方式

# cd arrow-examples/polars_read_parquet
cargo run
  • 运行结果如下 Polars读取parquet文件

链接

鱼雪