资讯动态

Rust 编译器中的 Async Closures(Coroutine-Closures)实现深度剖析

发布时间:2026/9/12 2:29:18 来源:尧图企业网站定制
Rust 编译器中的 Async ClosuresCoroutine-Closures实现深度剖析【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust导读Async closures异步闭包即async || {}语法是 Rust 1.85 引入的一等公民语言特性其底层由 rustc 中一套被称为 coroutine-closures 的通用机制驱动。本文基于 Rust 编译器开发指南 中同名章节结合当前 rustc 仓库源码完整讲解该特性从 HIR 降级、类型表示、trait 层级、MIR 合成到借用检查的全链路实现。读完本文你将理解 async closures 为何能返回借用自身捕获变量的 Future、编译器如何在AsyncFn*与Fn*两套 trait 之间桥接以及 borrowck 为何对 async closures开绿灯。该特性最初由 RFC 3668 提出该外部链接仅作为设计动机参考本文所有技术细节以当前仓库源码为准。本文是一篇技术性较强、垂直深入的章节——理想情况下它应被拆分进各相关章节但为了整体理解 async closuresrustc 开发者指南将其集中在一章中呈现。一、什么是 Coroutine-ClosuresCoroutine-closures协程闭包是 async closures 的推广它是返回协程coroutine的闭包表达式的特殊语法最关键的特性是允许从闭包的 upvar捕获变量中进行捕获。在本文写作时唯一可用的 coroutine-closure 种类就是 async closure且支持 async closures 正是当前实现的范围。未来可能支持gen || {}等更多种类本文描述的多数问题与特性适用于所有 coroutine-closures。由于实现代码具有一定的通用性文中会在 async closures 与 coroutine-closures 两个称呼间切换。async closure 返回的 Future 通常被称为 coroutine 或 child coroutine子协程。二、HIR 表示与降级Lowering2.1 两层hir::Closure嵌套Async closures以及未来其他协程形态在 HIR 中表示为hir::Closure。其 closure-kind 为ClosureKind::CoroutineClosure(_)内部包裹一个 async block该 async block 同样表示为hir::Closure其 closure-kind 为ClosureKind::Closure(CoroutineKind::Desugared(_, CoroutineSource::Closure))。在降级过程中编译器通过 compiler/rustc_ast_lowering/src/expr/closure.rs 将闭包标记为hir::ClosureKind::CoroutineClosure(coroutine_desugaring)注释明确指出这样 HIR typeck 才会知道- str这类输出类型实际表示返回 str 的协程而非直接返回str。2.2 无条件移动参数进函数体与async fn类似在降级 async closure 的函数体时需要无条件地将所有闭包参数移动进函数体以便被捕获这项工作由lower_coroutine_body_with_moved_arguments函数完成。该函数有一个值得注意的特殊之处最终生成的 async block 的捕获方式为CaptureBy::ByRef。这是因为虽然之后我们会强制所有闭包参数按值捕获但并不希望整个 async block 表现得像一个async move——否则 async closure 的自借用self-borrowing机制将失去意义。正是这种参数按值捕获、块本身按引用捕获的组合构成了 async closure 的核心语义基础。三、rustc_middle::ty类型表示3.1 新增TyKind::CoroutineClosure该特性引入的最核心内容是一个新的类型变体TyKind::CoroutineClosure同时 typeck 与 borrowck 中相关的枚举UpvarArgs、DefiningTy、AggregateKind也都有对应变体。在 compiler/rustc_type_ir/src/ty_kind.rs 中可以看到其定义与注释/// The anonymous type of a closure. Used to represent the type of async |a| a. /// /// Coroutine-closure args contain both the - potentially instantiated - generic /// parameters of its parent and some synthetic parameters. CoroutineClosure(I::CoroutineClosureId, I::GenericArgs),之所以新增一个类型变体而非泛化现有的TyKind::Closure是因为两者在类型表示上存在重大差异。理解差异的最佳途径是查看CoroutineClosureArgsParts——coroutine-closure 泛型参数的解包表示见 compiler/rustc_type_ir/src/ty_kind/closure.rspub struct CoroutineClosureArgsPartsI: Interner { /// 该类型检查根typeck root的泛型参数 pub parent_args: I::GenericArgsSlice, /// 闭包的最大调用能力closure_kind_ty pub closure_kind_ty: I::Ty, /// 存储协程签名各组成部分的类型 pub signature_parts_ty: I::Ty, /// 闭包捕获的 upvars在 upvar 分析HIR typeck 后期之前保持为推理变量 pub tupled_upvars_ty: I::Ty, /// 形如 forenv fn() - (env T, ...) 的函数指针 pub coroutine_captures_by_ref_ty: I::Ty, }3.2 与普通闭包的相似之处与闭包一样coroutine-closure 也有parent_args从定义所在函数体继承的泛型、closure_kind_ty最大调用能力即必须被消费才能调用如FnOnce还是可以按引用调用以及tupled_upvars_ty闭包自身捕获的 upvars。3.3 签名Signature的爆炸式存储传统闭包用fn_sig_as_fn_ptr_ty表示签名而 coroutine-closure 的签名采用拆分方式存储因为 coroutine-closure 根据调用时使用的AsyncFn*trait 不同存在两套签名按引用调用与按移动调用。概念上coroutine-closure 可视为根据调用方式by-ref / by-move拥有多个不同签名类型。为方便重建这两套签名signature_parts_ty存储了该 coroutine-closure 返回的协程的所有相关组成部分。这个签名部分类型的一般形态为fn(tupled_inputs, resume_ty) - (return_ty, yield_ty)其中resume_ty、return_ty、yield_ty分别是 coroutine-closure 返回的协程的对应类型。编译器主要与CoroutineClosureSignature类型打交道compiler/rustc_type_ir/src/ty_kind/closure.rs它通过从上述fn()指针类型中抽取相关类型而创建并提供构造 coroutine-closure 最终返回的协程的方法pub struct CoroutineClosureSignatureI: Interner { pub tupled_inputs_ty: I::Ty, pub resume_ty: I::Ty, pub yield_ty: I::Ty, pub return_ty: I::Ty, pub fn_sig_kind: FnSigKindI, }从signature_parts_ty到CoroutineClosureSignature的拆解由coroutine_closure_sig()完成——它把fn(tupled_inputs, resume_ty) - (yield_ty, return_ty)中的各个类型逐一取出。3.4 构造Coroutine返回类型所需的附加数据除了签名中存储的数据要构造作为返回值的TyKind::Coroutine还需要存储协程的 witness。那么返回的Coroutine的 upvars 呢对于AsyncFnOnce即按移动调用它就是协程返回的同一组 upvars但对于AsyncFnMut/AsyncFn返回的协程以给定的 environment 生命周期从 coroutine-closure借用数据。这对应AsyncFnMut/AsyncFn调用签名上的self生命周期以及ByRef的 GAT 生命周期。3.5 真正拿到协程返回类型构造 coroutine-closure 返回的Coroutine最简便的方式是使用CoroutineClosureSignature::to_coroutine_given_kind_and_upvars辅助函数compiler/rustc_type_ir/src/ty_kind/closure.rs 附近它可通过CoroutineClosureArgs获得。该函数的大多数参数都来自CoroutineArgs的组成部分唯一特殊的是goal_kind: ClosureKind参数——它控制返回哪种形态的协程传入ClosureKind::Fn | ClosureKind::FnMut时准备by-ref协程传入ClosureKind::FnOnce时准备by-move协程。在 compiler/rustc_mir_transform/src/coroutine/by_move_body.rs 的合成逻辑中可以看到它的实际调用方式coroutine_closure_sig()擦除边界区域后以ClosureKind::FnOnce为 goal、配合父闭包的 tupled upvars 与coroutine_captures_by_ref_ty构造 by-move 协程类型。四、Trait 层级并行的AsyncFn*家族4.1AsyncFn/AsyncFnMut/AsyncFnOnce该特性引入了一套与Fn*平行的 trait 层级其精确定义位于 library/core/src/ops/async_function.rs#[lang async_fn] pub trait AsyncFnArgs: Tuple: AsyncFnMutArgs { extern rust-call fn async_call(self, args: Args) - Self::CallRefFuture_; } #[lang async_fn_mut] pub trait AsyncFnMutArgs: Tuple: AsyncFnOnceArgs { type CallRefFuturea: FutureOutput Self::Output where Self: a; extern rust-call fn async_call_mut(mut self, args: Args) - Self::CallRefFuture_; } #[lang async_fn_once] pub trait AsyncFnOnceArgs: Tuple { type CallOnceFuture: FutureOutput Self::Output; type Output; extern rust-call fn async_call_once(self, args: Args) - Self::CallOnceFuture; }注意 trait 定义中extern rust-call的调用约定以及AsyncFnMut::CallRefFuture这个带生命周期参数的 GAT——它正是上一节提到的 ByRef 的 GAT 生命周期 所在。关键事实所有当前已稳定的可调用类型闭包、函数项、函数指针、dyn Fn*trait 对象只要实现了Fn*() - Fut且Fut: FutureOutput T就自动实现AsyncFn*() - T。这一 blanket 实现体现在 trait solver 的 structural trait 装配逻辑中compiler/rustc_next_trait_solver/src/solve/assembly/structural_traits.rs 中AsyncFn*相关分支。而 async closures 则依据其函数体实现AsyncFn*——例如若函数体消费或修改了 upvars就可能影响其是否实现AsyncFn与AsyncFnMut。同文件还包含F、mut F的转发实现以及一个内部细节 traitAsyncFnKindHelperGoalKindlibrary/core/src/ops/async_function.rs其关联类型Upvarsclosure_env, Inputs, Upvars, BorrowedUpvarsAsFnPtr用于延迟 tupled upvar 类型的投影——这一点在第五节详述。4.2 为何不直接使用LendingFn*未来可能将AsyncFn*迁移到更一般的LendingFn*trait 集合上但目前存在几个具体的技术限制阻碍编译器今天便捷地使用LendingFn闭包签名推断closure signature inference高阶 trait boundhigher-ranked trait bounds的限制错误信息质量的不足。这些限制加上底层 trait 不应影响 async closures 与 asyncFntrait bound 的用户体验促使当前实现选择了AsyncFn*。为了将来能够迁移到更一般的 trait精确的AsyncFn*trait 定义包括关联类型被保留为实现细节。4.3 async closures 何时实现普通Fn*普通可调用类型可以实现AsyncFn*的另一面问题是async closures 也能实现Fn*吗简短回答是在合法的时候——即从AsyncFn/AsyncFnMut返回的协程实际上没有任何从父 coroutine-closure 借出lent的 upvars 时。完整的详细回答见第八节的启发式分析。五、两个函数体的故事by-ref 与 by-move5.1 问题的本质当 async closures 通过AsyncFn/AsyncFnMut调用时返回的协程借用闭包但当通过AsyncFnOnce调用时我们消费了闭包不能再返回一个借用已 drop 数据的协程。为绕开这个限制编译器为可被按引用调用的 coroutine-closure合成一个独立的by-move MIR 函数体专门用于AsyncFnOnce::call_once。该函数体与正常协程返回的协程运行方式相同唯一的区别是 upvars 集合不同——必须把捕获从父 coroutine-closure移动进子协程。compiler/rustc_mir_transform/src/coroutine/by_move_body.rs 的模块文档用一个精妙的例子说明了这一第二重魔法let x vec![1, 2, 3]; let closure async move || { println!({x:#?}); };它被脱糖为类似let closure move || { async { println!({x:#?}); } };关键在于外层闭包把x: Veci32移动进自己的 upvars但内层 async 协程只是捕获了x的引用。这就是 async closures 的魔法——它们返回的 Future 允许借用父闭包的 upvars。而当通过AsyncFnOnce或FnOnce所有 async closures 都实现它调用时self被消费、父闭包被销毁就必须创建一个第二个async 协程函数体原本按引用捕获的x改为按值捕获从而持有全部捕获并在.await结束后释放。5.2 合成 by-move 函数体coroutine_by_move_body_def_id访问 by-move 函数体需通过coroutine_by_move_body_def_id查询compiler/rustc_mir_transform/src/coroutine/by_move_body.rs。该查询通过**复制协程的 MIR 函数体并插入额外的 deref 与字段投影field projections**来合成新 MIR 函数体以保持函数体语义。其核心是field_remapping映射的构建对父 coroutine-closure 的每个捕获与子协程对应捕获比对决定若父捕获是 by-ref则在该 place 前插入Deref投影若子捕获是 by-ref则在该 place 末尾**剥离一个 deref**因为 MIR 没有投影类型只能利用其对偶——在 place 出现时剥掉一个 deref若父与子都按引用捕获插入与剥离会同时发生但因精确捕获2021 edition 闭包捕获规则投影的位置不同插入在开头、剥离在末尾代码对此保持灵活。此外由于合成了一个新 def id该查询还负责喂给 MIR 函数体相关的大量其他查询HIR、codegen_fn_attrs、coverage_attr_on、constness等。它在mir_promoted查询期间被ensure()因为它操作的是协程已构建的 MIR。一个值得注意的边界条件若函数体已被错误污染tainted_by_errors或协程类型引用了错误则直接返回原始 def id跳过合成。5.3 一个有趣的断言在合成逻辑中还有一条断言值得玩味compiler/rustc_mir_transform/src/coroutine/by_move_body.rsassert!( parent_capture.is_by_ref() || coroutine_kind ! ty::ClosureKind::FnOnce, FnOnce coroutine-closures return coroutines that capture from \ their body; it will always result in a borrowck error! );它揭示了一个实现事实FnOnce形态的 coroutine-closure 返回的协程从其函数体捕获若再试图剥离 deref 必然导致借用检查错误且当 coroutine 本身是FnOnce时by-move 函数体就是原函数体本身见if coroutine_kind ty::ClosureKind::FnOnce分支。六、闭包签名推断Closure Signature Inferenceasync closures 的签名推断比传统闭包复杂。与闭包一样算法遍历所有可能相关的子句针对传入的期望类型见 compiler/rustc_hir_typeck/src/closure.rs。为抽取签名算法考虑两种情形AsyncFnOnce::Output投影谓词用于提取闭包的输入与输出类型。对应场景是存在F: AsyncFn*() - Tbound。FnOnce::Output投影谓词用于提取输入类型对于输出还会寻找相关的Future::Output投影谓词来推导输出。对应场景是存在F: Fn*() - T, T: FutureOutput Ubound。若没有Futurebound则输出使用一个全新的推理变量。这对应将 async closure 传给Option::map之类的组合子函数的情形——此时只需要FnOnce的能力输出类型可留待后续约束。支持第二种情形的目的是让用户即使在AsyncFn*一等 trait 出现之前设计的 API 中也能无缝替换使用async || {}语法。6.1 在闭包 kind 推断完成之前调用它编译器将 coroutine-closure kind最大调用模式AsyncFnOnce/AsyncFnMut/AsyncFn的推导推迟到 typeck 结束。但 typeck 结束前又需要能调用该 coroutine-closure因此必须在此之前得出其返回类型。与普通闭包不同其返回类型不随调用所用Fn*trait 改变coroutine-closures确实会根据调用所用的AsyncFn*trait 形态返回不同的协程类型。具体来说虽然返回协程的 def-id 不变但 upvars从父 coroutine-closure 借用或移动与协程 kind 都取决于调用模式。为此引入AsyncFnKindHelpertrait 来推迟两个问题的求解该 coroutine-closure 是否支持此调用模式——通过一个 trait goallibrary/core/src/ops/async_function.rs 的AsyncFnKindHelperGoalKindtrait 本身该调用模式的 tupled upvars 是什么——通过关联类型同文件 L146-L154 的Upvars关联类型它可经由把 coroutine-closure 的输入类型追加到 upvar 分析得到的 upvars 或 by ref upvars 上来计算。6.2 为什么要绕这么一圈必须承认这看起来有些曲折。但考虑什么都不做的替代方案把所有AsyncFn*goal 标记为模糊ambiguous直到 upvar 分析后才知道返回协程的 upvars 中该放什么。这对程序中的推断是非常有害的——文档给出了反例let c async || - String { .. }; let s c().await; // ^^^ 若无法将 {c} as AsyncFn::call() 投影为协程 // 则 .await 内部的 IntoFuture::into_future 调用会停滞 // s 的类型将作为推理变量不被约束。 s.as_bytes(); // ^^^ 这意味着无法对 coroutine-closure 的 await 结果调用任何方法——完全不行所以改用这个别名此处是一个投影AsyncFnKindHelper::Upvarsenv, ...来延迟tupled upvars的计算同时仍能返回TyKind::Coroutine一个刚性类型并成功确认所需的内建 trait此处即Future——因为Future的实现根本不依赖 upvars。七、Upvar 分析coroutine-closures 与其子协程的 upvar 分析大体与常规 upvar 分析相同但有几个针对 async closures 特殊性质的要点均位于 compiler/rustc_hir_typeck/src/upvar.rs。7.1 强制所有输入被捕获与 async fn 一样所有输入参数都被捕获。编译器显式强制这些输入按移动by-move捕获使得 async closure 返回的 Future 协程不依赖于输入是否被函数体使用——否则会引入微妙的 semver 隐患例如未来函数体开始使用某个之前未用的参数会悄悄改变捕获方式与生命周期行为。7.2 计算 by-ref 捕获对于支持AsyncFn/AsyncFnMut的 coroutine-closure还必须计算 coroutine-closure 与其子协程捕获之间的关系。具体而言coroutine-closure 可能把一个 upvar移动进自己的捕获而子协程可能只是借用该 upvar。coroutine_captures_by_ref_ty的计算方式是查看子协程的所有捕获并与父 coroutine-closure 的对应捕获比较。该类型最终表示为一个forenv fn() - captures...类型其中的绑定生命周期binder lifetime代表调用AsyncFn::async_call或AsyncFnMut::async_call_mut时的self生命周期在真正调用这些方法时才实例化该 binder。注意并非父 coroutine-closure 的每个 by-ref 捕获都会导致 lending 借用——详见第八节这直接影响 coroutine-closure 是否能实现Fn*家族 trait。7.3 By-move 函数体与FnOnce的怪癖有几种情况会导致闭包 upvar 分析为子协程推断出过于宽松的 upvars最终产生借用检查错误。文档给出两个典型例子fn force_fnonceT: async FnOnce()(t: T) - T { t } let x String::new(); let c force_fnonce(async move || { println!({x}); });这里x会被移动进 coroutine-closure但返回的协程只会借用x。然而force_fnonce强制 coroutine-closure 为AsyncFnOnce——它不是 lending 的因此必须强制按移动捕获。let x String::new(); let y String::new(); let c async move || { drop(y); println!({x}); };x会被移动进 coroutine-closure返回的协程只会借用x但因为我们还捕获并 drop 了ycoroutine-closure 被强制为AsyncFnOnce于是也必须强制x按移动捕获。与前例不同此例中协程 kind 的 closure-kind 尚未被约束因此必须分析 coroutine-closure 的函数体观察所有 upvars 的使用方式判断是否存在消费性使用——即会将其强制为FnOnce的使用方式。八、深入探讨async closures 何时实现普通Fn*首先所有 async closures 都实现FnOnce——因为它们至少可以被调用一次。对于Fn/FnMut详细答案归结为一个相关问题该 coroutine-closure 是否 lending如果是则它无法实现非 lending 的Fn/FnMut。判定 coroutine-closure 何时必须借出其 upvars由should_reborrow_from_env_of_parent_coroutine_closure辅助函数实现compiler/rustc_hir_typeck/src/upvar.rs 中should_reborrow_from_env_of_parent_coroutine_closure附近。具体而言需要判定两种情况1. 我们是否正在借用父闭包拥有的数据可通过检查父捕获是否为 by-move 来判定除非我们应用了一个 deref 投影——那意味着我们在重借用reborrow一个按移动捕获的引用let x 1i32; // 假设这个生命周期为 1。 let c async move || { println!({:?}, *x); // 尽管内层协程按引用借用我们只捕获了 *x 而非 x // 因此内层闭包被允许在 1 内重借用该数据。 };2. 如果协程正从父捕获进行可变借用那么该可变借用不能活得比父捕获或原 upvar 上的借用更久。因此必须始终以父 coroutine-closure 的 env 生命周期来借用子捕获let mut x 1i32; let c async || { x 1; // 父闭包以某个 1 mut i32 借用 x。 // 但当调用 c() 时会为 AsyncFnMut::async_call_mut 的签名 // 隐式自动引用autoref。设该生命周期为 call。 // 由于 call mut 1 mut i32 最多只能被重借用为 call mut i32 // 内层协程应以 coroutine-closure 的生命周期来捕获。 };若任一情况适用就应以父 coroutine-closure 的 env 生命周期捕获该借用。令人安心的是即便该函数判定错误程序也不会因此变得不安全——因为 borrowck 仍会验证这些选择唯一副作用是用户可能收到不必要的 borrowck 错误。九、Instance 解析Instance ResolutionInstance 解析逻辑位于 compiler/rustc_ty_utils/src/instance.rs。若 coroutine-closure 的 closure-kind 为FnOnce其AsyncFnOnce::call_once与FnOnce::call_once实现直接解析到 coroutine-closure 的函数体而返回协程的Future::poll解析到子闭包的函数体。若 coroutine-closure 的 closure-kind 为FnMut/FnAsyncFn与返回协程的Future实现同样直接解析但AsyncFnOnce::call_once/FnOnce::call_once使用MIR shim生成Fn::call/FnMut::call_mut实例如果存在同样走 shim。shim 由ConstructCoroutineInClosureShim表示定义于 compiler/rustc_middle/src/ty/instance.rs 中ConstructCoroutineInClosureShim附近其中的receiver_by_ref布尔值在实例为Fn::call/FnMut::call_mut时为 true所有这些实例返回的协程对应的是到此刻已合成好的by-move 函数体。十、借用检查Borrow-Checking意外地简单事实证明借用检查 async closures相当直接。只需两步新增DefiningTy::CoroutineClosure变体compiler/rustc_borrowck/src/universal_regions.rs其文档注释明确The MIR is a special kind of closure that returns coroutines.教会 borrowck 如何生成 coroutine-closure 的签名同文件universal_regions.rs中DefiningTy::CoroutineClosure(_, args)分支从coroutine_closure_sig()取得绑定变量并追加一个BoundRegionKind::ClosureEnv区域。完成这两步后borrowck 流程照常进行。一个重要细节我们不对为 by-move 协程合成的函数体进行借用检查——因为根据构造方式以及其来源的 by-ref 协程函数体的有效性它必然合法。十一、总结与展望通过本文可以清晰看到async closures 虽然对用户只是闭包加个 async在编译器内部却牵动了一条完整链条阶段核心机制关键代码位置降级两层hir::Closure嵌套参数无条件按值捕获块按引用捕获compiler/rustc_ast_lowering/src/expr/closure.rs类型表示新TyKind::CoroutineClosure签名爆炸式存储compiler/rustc_type_ir/src/ty_kind.rs、compiler/rustc_type_ir/src/ty_kind/closure.rsTrait 层级并行AsyncFn*家族 AsyncFnKindHelper延迟投影library/core/src/ops/async_function.rsMIR 合成复制协程体并插入/剥离 deref 与字段投影生成 by-move 体compiler/rustc_mir_transform/src/coroutine/by_move_body.rs签名推断两类投影谓词 延迟 kind 推导compiler/rustc_hir_typeck/src/closure.rsUpvar 分析强制输入按值捕获、计算 by-ref 捕获、FnOnce怪癖compiler/rustc_hir_typeck/src/upvar.rsInstance 解析FnOnce直接解析、by-ref 走ConstructCoroutineInClosureShimcompiler/rustc_ty_utils/src/instance.rs借用检查新增DefiningTy变体 签名生成跳过合成体检查compiler/rustc_borrowck/src/universal_regions.rs当前实现只为 async closures 服务未来若引入gen || {}等新形态本文描述的绝大多数机制双签名、by-move 体合成、AsyncFnKindHelper式延迟投影、lending 判定启发式都可以直接复用。对于希望深入 rustc 内部或为 coroutine-closures 贡献代码的开发者而言从 compiler/rustc_type_ir/src/ty_kind/closure.rs 的CoroutineClosureArgs/CoroutineClosureSignature入手再顺藤摸瓜阅读 upvar 分析与 by-move 体合成是最佳路径。【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价