Rust 中的编译时资源追踪:从运行时括号到类型层面的安全

发布: (2025年12月21日 GMT+8 06:50)
11 min read
原文: Dev.to

Source: Dev.to
Originally published on Entropic Drift

问题:运行时资源泄漏

资源泄漏是潜伏的隐患。一次忘记的 close()、一次缺失的 commit()、一次在异常路径中跳过清理。你的代码可以编译。它可以运行。它甚至可以工作 直到 连接池耗尽、文件句柄用完,或事务永远持有锁。

传统的 Rust 做法使用 RAII:将资源包装在结构体中,在 Drop 中进行清理。这对基于所有权的模式效果很好,但在以下情况下会显得不足:

  • 资源通过 async 边界传递。
  • 需要组合和链式的副作用。
  • 想要表达 协议(例如 begin → query → commit/rollback)。
  • 清理逻辑本身可能会失败并需要处理。

如果类型系统能够强制每个获取的资源在运行代码之前最终被释放,该怎么办?

Source:

基础:运行时 Bracket

Stillwater 自早期版本起就通过 bracket 模式提供运行时资源安全。
bracket 函数保证即使出现错误,清理工作也会执行:

use stillwater::effect::prelude::*;

let result = bracket(
    open_connection(),                          // Acquire
    |conn| async move { conn.close().await },   // Release (always runs)
    |conn| fetch_data(conn),                    // Use
)
.run(&env)
.await;

释放函数 始终 运行,无论使用阶段是成功、失败还是 panic。

Bracket 变体

变体描述
bracket基本的 acquire → use → release,保证清理
bracket2, bracket3管理多个资源;清理遵循 LIFO(后进先出)顺序
bracket_full返回 BracketError,其中包含使用阶段和清理阶段的显式错误信息
acquiring用于组合多个资源的流式构建器

流式构建器示例

// 使用流式构建器管理多个资源
let result = acquiring(open_conn(), |c| async move { c.close().await })
    .and(open_file(path), |f| async move { f.close().await })
    .with_flat2(|conn, file| process(conn, file))
    .run(&env)
    .await;

代码可以正常工作,且清理会自动进行。
然而,由于这种安全性是在 运行时 强制的,只有在程序执行时才能确认你的 bracket 是否平衡。

Stillwater 0.14.0 – 类型级资源跟踪

Stillwater 0.14.0 在运行时括号基础上加入了 编译时 资源跟踪。编译器现在可以在代码运行之前证明你的资源是平衡的。

use stillwater::effect::resource::*;
use stillwater::pure;

// The TYPE says: this acquires a `FileRes`
fn open_file(path: &str) -> impl ResourceEffect<Acquires = FileRes, Releases = Empty> {
    pure::(format!("handle:{}", path)).acquires::()
}

// The TYPE says: this releases a `FileRes`
fn close_file(handle: String) -> impl ResourceEffect {
    pure::(()).releases::()
}

ResourceEffect trait 在 Effect 的基础上扩展了两个关联类型:

  • Acquires – 该 effect 创建的资源。
  • Releases – 该 effect 消耗的资源。

这些关联类型既是文档说明,又可以让编译器进行验证。

括号模式:保证资源中性

真正的威力来自 resource_bracket。它强制一个操作 获取 资源、使用 资源、并 释放 资源:

fn read_file_safely(path: &str) -> impl ResourceEffect {
    bracket::()
        .acquire(open_file(path))
        .release(|handle| async move { close_file(handle).run(&()).await })
        .use_fn(|handle| read_contents(handle))
}
  • bracket::() 在第一次捕获资源类型,随后从链式方法调用中推断出其余所有信息。
  • 返回类型显示 Acquires = Empty, Releases = Empty,这意味着函数是 资源‑中性 的。
  • 如果括号不匹配(例如获取的资源与释放的资源不对应),代码将无法通过编译。

协议强制执行:数据库事务

考虑数据库事务。事务必须被打开、使用,然后要么 提交 要么 回滚。如果缺少最后一步就是一个 bug。我们把它变成编译时错误:

fn begin_tx() -> impl ResourceEffect {
    pure::("tx_12345".to_string()).acquires::()
}

fn commit(tx: String) -> impl ResourceEffect {
    pure::(()).releases::()
}

fn rollback(tx: String) -> impl ResourceEffect {
    pure::(()).releases::()
}

fn execute_query(tx: &str, query: &str) -> impl ResourceEffect {
    // Queries are resource‑neutral
    pure::(vec!["row1".to_string()]).neutral()
}

现在,一个未关闭的事务操作会导致类型错误:

// This function signature promises resource neutrality
fn transfer_funds() -> impl ResourceEffect {
    bracket::()
        .acquire(begin_tx())
        .release(|tx| async move { commit(tx).run(&()).await })
        .use_fn(|tx| {
            execute_query(tx, "UPDATE accounts SET balance = balance - 100 WHERE id = 1");
            execute_query(tx, "UPDATE accounts SET balance = balance + 100 WHERE id = 2");
            pure::("transferred".to_string())
        })
}

强制正确的事务关闭

类型签名 强制 事务被正确关闭。如果尝试在没有匹配的 release 的情况下返回 begin_tx(),代码将无法编译。

跟踪多个资源

真实系统会同时处理多种资源类型。跟踪组合如下:

// Acquire both a file and a database connection
let effect = pure::(42)
    .acquires::()
    .also_acquires::();

// Release both
let cleanup = pure::(())
    .releases::()
    .also_releases::();

类型系统将 Has<…> 视为类型层面的集合。联合操作会合并链式 effect 中的集合。

编译‑时断言

对于关键代码路径,显式断言资源中性:

fn safe_operation() -> impl ResourceEffect {
    let effect = bracket::()
        .acquire(open_file("data.txt"))
        .release(|h| async move { close_file(h).run(&()).await })
        .use_fn(|h| read_contents(h));

    // This is a compile‑time check, not a runtime assert
    assert_resource_neutral(effect)
}

注意: 如果 effect 并非真正的资源‑中性,这将在 编译时 失败。
该断言不产生运行时开销,因为它纯粹在类型层面上起作用。

自定义资源种类

为特定领域的跟踪定义您自己的资源标记:

struct ConnectionPoolRes;

impl ResourceKind for ConnectionPoolRes {
    const NAME: &'static str = "ConnectionPool";
}

fn acquire_connection() -> impl ResourceEffect {
    pure::("conn_42".to_string()).acquires()
}

fn release_connection(conn: String) -> impl ResourceEffect {
    pure::(()).releases()
}

内置标记(FileResDbResLockResTxResSocketRes)覆盖了常见情况,但您并不限于此。

零运行时开销

这是关键点:所有跟踪都在编译时完成。实现使用:

  • PhantomData 用于类型层级的注解(零大小)
  • 关联类型用于资源集合的跟踪(在编译时计算)

Tracked 包装器直接委托给内部 effect:

use std::marker::PhantomData;

pub struct Tracked<Eff> {
    inner: Eff,
    _phantom: PhantomData<()>, // Zero bytes
}

impl<Eff> Effect for Tracked<Eff>
where
    Eff: Effect,
{
    type Env = Eff::Env;
    type Output = Eff::Output;
    type Error = Eff::Error;

    async fn run(self, env: &Self::Env) -> Result<Self::Output, Self::Error> {
        self.inner.run(env).await // Just delegates
    }
}

没有运行时检查、没有分配、也没有间接层。跟踪纯粹是为了类型检查器。

比较:RAII vs Bracket vs 类型级别跟踪

方法泄漏检测异步安全协议强制运行时开销
RAII (Drop)运行时有限最小
Stillwater bracket运行时最小
Stillwater bracket::()编译时
  • RAII 在你直接拥有资源时有效。
  • bracket()(运行时)保证清理代码始终执行——适用于简单的获取/使用/释放模式。
  • bracket::()(类型级别)更进一步:获取‑使用‑释放协议被编码在类型签名中,并在编译时得到验证。

如何将它们一起使用

  • 在异步上下文中使用 运行时 bracket 以确保清理。
  • 添加 类型级别跟踪 以获得对更复杂协议的编译时验证。

这种组合既提供安全性(无泄漏),又提供对协议正确性的信心。

何时使用

类型级资源跟踪在以下情况下表现出色:

  • 资源泄漏是高危错误(例如,连接池、文件系统、关键区段)
  • 必须遵循协议(例如,begin → work → commit/rollback
  • 跨函数边界组合效果
  • 你想要永不失真的文档——类型始终是最新的

对于简单的单所有者资源,RAII 仍然是正确的选择。对于资源安全至关重要的复杂效果管道,类型级跟踪能够捕获运行时检查可能遗漏的错误。

入门指南

将 Stillwater 0.14.0 添加到你的 Cargo.toml 中:

[dependencies]
stillwater = "0.14"

导入资源跟踪模块:

use stillwater::effect::resource::*;
use stillwater::pure;

// 开始为你的 effect 添加注解
fn my_acquire() -> impl ResourceEffect {
    pure::("handle".to_string()).acquires()
}

现有的 Effect API 仍保持不变。资源跟踪是纯粹的增量功能,需要自行选择开启。

Summary

Stillwater 的资源管理故事现在有了两个互补的层面:

层面目的
运行时括号 (bracket, bracket2, acquiring)保证清理始终执行,即使出现错误或 panic
编译时追踪 (bracket::() builder, ResourceEffect)在代码运行前证明资源协议是平衡的

它们共同提供了深度防御:

  • 运行时括号 确保在生产环境中进行清理。
  • 类型层面的追踪 在开发阶段捕获协议违规。
  • 符合人体工学的 API 通过构建器模式(单一类型参数)。
  • 零运行时开销 通过 PhantomData 实现。
  • 可组合,可跨效应链和函数边界使用。

资源太重要,不能交给运行时的偶然性。先使用括号确保清理,然后在协议重要时加入类型层面的追踪。

About Stillwater

Stillwater 是一个用于验证、效应组合和函数式编程模式的 Rust 库。版本 0.14.0 在其已有的运行时括号模式之上添加了编译时资源追踪的类型层。

Follow & Explore

想要更多类似内容?

  • Dev.to 上关注我
  • 订阅 Entropic Drift 以获取关于 AI 驱动开发工作流、Rust 工具链和技术债务管理的文章。

开源项目:

Back to Blog

相关文章

阅读更多 »

Rust 把 'pub' 搞错了

!封面图片:Rust 把 “pub” 写错了 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s...