19 个版本
0.4.1 | 2023 年 10 月 20 日 |
---|---|
0.4.0 | 2023 年 10 月 16 日 |
0.3.1 | 2022 年 8 月 24 日 |
0.3.0 | 2022 年 7 月 28 日 |
0.1.2 | 2022 年 4 月 29 日 |
在 Rust 模式 中排名 #90
每月下载量 70,682
在 19 个 包中使用(直接使用 6 个)
55KB
346 行代码(不包括注释)
即使这是疯狂的行为,但其中也有方法。
更多信息
-
哈姆雷特:
先生,您会像我一样变老——如果您能像蟹一样向后走。
-
波洛涅斯:
即使这是疯狂的行为,但其中也有方法。
-
波洛涅斯,最终
::polonius-the-crab
在稳定 Rust 中使用更宽容的基于 Polonius 的借用检查器模式的工具。
理由:NLL 借用检查器的限制
参见以下问题
-
#92985 – LendingIterator 的过滤适配器需要 Polonius(这个问题涉及 GATs 和普遍的
LendingIterator
示例,可加分)。
所有这些示例都归结为以下规范实例
#![forbid(unsafe_code)]
use ::std::{
collections::HashMap,
};
/// Typical example of lack-of-Polonius limitation: get_or_insert pattern.
/// See https://nikomatsakis.github.io/rust-belt-rust-2019/#72
fn get_or_insert (
map: &'_ mut HashMap<u32, String>,
) -> &'_ String
{
if let Some(v) = map.get(&22) {
return v;
}
map.insert(22, String::from("hi"));
&map[&22]
}
错误信息
# /*
error[E0502]: cannot borrow `*map` as mutable because it is also borrowed as immutable
--> src/lib.rs:53:5
|
14 | map: &mut HashMap<u32, String>,
| - let's call the lifetime of this reference `'1`
15 | ) -> &String {
16 | if let Some(v) = map.get(&22) {
| --- immutable borrow occurs here
17 | return v;
| - returning this value requires that `*map` be borrowed for `'1`
18 | }
19 | map.insert(22, String::from("hi"));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
# */
解释
点击隐藏
现在,这个模式已知是有效的 / 来自当前借用检查器 NLL 的错误。
-
背后的技术原因是涉及借用中的 命名 / 函数签名生存期:与完全在体内的匿名借用相反,持续“命名”/外部泛型生存期的借用被认为持续 直到函数结束,跨所有可能的代码路径(即使是从借用开始就不可达的路径)。
- 注意到这种差异的一种方法是在可能的情况下,将函数重写为宏。由于它是语法内联的,它将涉及匿名生存期,不会引起任何麻烦。
解决方案
所以你可能会建议“只是使用不安全”。但这很复杂
-
你的用例 真的 是否适合这个规范示例?
- 或者是一个变体:随着代码的发展/面对代码重构,它是否仍然适合这个示例?
-
即使我们知道“我们可以使用
unsafe
”,实际上使用它很微妙且容易出错。由于在这种情况下,&mut
借用通常涉及其中,一个人可能会意外地将一个&
引用转换为一个&mut
引用,这是 始终 UB。 -
这两个问题都导致了人们对
unsafe
_code 的某种完全合法的过敏,以及对#![forbid(unsafe_code)]
-at-the-root-of-the-crate 模式的非常放心。
非 unsafe
的虽然麻烦但可行的 Polonius 问题解决方案
点击显示
-
如果可能,寻找专门的 API。例如,可以使用
.entry()
API 来实现get_or_insert()
示例#![forbid(unsafe_code)] use ::std::{ collections::HashMap, }; fn get_or_insert ( map: &'_ mut HashMap<u32, String>, ) -> &'_ String { map.entry(22).or_insert_with(|| String::from("hi")) }
遗憾的是,现实是您并不总是可以随时使用这样的便利 API。
-
否则,您可以执行连续的非习惯性查找,以避免借用时间过长
#![forbid(unsafe_code)] use ::std::{ collections::HashMap, }; fn get_or_insert ( map: &'_ mut HashMap<u32, String>, ) -> &'_ String { // written like this to show the "transition path" from previous code let should_insert = if let Some(_discarded) = map.get(&22) { false } else { true } ; // but `should_insert` can obviously be shortened down to `map.get(&22).is_none()` // or, in this very instance, to `map.contains_key(&22).not()`. if should_insert { map.insert(22, String::from("hi")); } map.get(&22).unwrap() // or `&map[&22]` }
-
最后,关于“这种情况只发生在具体的命名生命周期”的问题,一个巧妙但麻烦的非
unsafe
的方法来规避限制是使用 CPS / 回调 / 作用域 API#![forbid(unsafe_code)] use ::std::{ collections::HashMap, }; fn with_get_or_insert<R> ( map: &'_ mut HashMap<u32, String>, yield_: impl FnOnce( /* -> */ &'_ String ) -> R ) -> R { if let Some(v) = map.get(&22) { yield_(v) } else { map.insert(22, String::from("hi")); yield_(&map[&22]) } }
虽然您应该首先尝试这些解决方案并看看它们如何适用于您的代码库,但有时它们不适用或与“一点 unsafe
”相比过于麻烦。
在这种情况下,与所有已知的、被认为是安全的 unsafe
模式一样,理想的解决方案是将它分解成一个自己的小型且易于审查的 crate 或模块,然后使用因此公开的非 unsafe fn
API 👌。
登场 ::polonius-the-crab
其实施说明
点击显示
所以,回到那个“安全封装”的想法
-
让我们找到一个已知的、被认为是合理的、在 Polonius 下被接受的借用检查器问题的规范实例;
-
然后对其进行调整,使其可以作为大多数这些问题的通用工具进行重用。
如果我们仔细观察上面的借用检查器问题,我们可以看到有两个定义性的成分
- 一个显式的泛型生命周期参数(可能被省略);
- 一个分支,其中一个分支基于那个借用返回,而另一个则不再关心它。
问题在于第二个分支应该能够重新获得在第一个分支中借用的东西的访问权,但当前的借用检查器拒绝了它。
这就是我们在正确的时刻撒一些正确位置的 unsafe
,让“借用检查器看看别处”的时候。
因此,我们首先用伪代码给出(
fn polonius<'r, T> (
borrow: &'r mut T,
branch:
impl // generic type to apply to all possible scopes.
for<'any> // <- higher-order lifetime ensures the `&mut T` infected with it…
FnOnce(&'any mut T) // …can only escape the closure…
// vvvv … through its return type and its return type only.
-> Option< _<'any> > // <- The `Some` / `None` discriminant represents the branch info.
// ^^^^^^^
// some return type allowed to depend on `'any`.
// For instance, in the case of `get_or_insert`, this could
// have been `&'any String` (or `Option<&'any String>`).
// Bear with me for the moment and tolerate this pseudo-code.
,
) -> Result< // <- we "forward the branch", but with data attached to the fallback one (`Err(…)`).
_<'r>, // <- "plot twist": `'any` above was `'r` !
&'r mut T, // <- through Arcane Magic™ we get to transmute the `None` into an `Err(borrow)`
>
{
let tentative_borrow = &mut *borrow; // reborrow
if let Some(dependent) = branch(tentative_borrow) {
/* within this branch, the reborrow needs to last for `'r` */
return Ok(dependent);
}
/* but within this branch, the reborrow needs to have ended: only Polonius supports that kind of logic */
// give the borrow back
Err(borrow) // <- without Polonius this is denied
}
此函数忽略伪代码中未指定泛型返回类型 _<'…>
,确实代表了借用检查器问题的一个规范示例(如果没有 -Zpolonius
,它将拒绝 Err(borrow)
行,表明 borrow
需要借用以供 'r
使用,并且 'r
的范围延伸到函数的任何结束处(借用检查器错误)。
而使用 -Zpolonius
时,它会被接受。
神秘的魔法™
在此正确使用 unsafe
,以缓解 -Zpolonius
缺少的不足,是将
let tentative_borrow = &mut *borrow; // reborrow
改为
let tentative_borrow = unsafe { &mut *(borrow as *mut _) }; // reborrow
其中 unsafe { &mut *(thing as *mut _) }
是执行 借用生命周期 扩展 的规范方法:该 &mut
借用的生命周期不再以任何方式与 'r
或 *borrow
绑定。
- 有些人可能会被诱惑使用
mem::transmute
。虽然这确实有效,但它是一个更灵活的 API,在unsafe
的情况下,这意味着它是一个更危险的 API。例如,使用transmute
时,如果借用对象有自己的生命周期参数,这些参数也可能被擦除,而降级到指针再升级回引用的操作仅保证“擦除”借用的外部生命周期,而内部类型保持不变:这更安全。
借用检查器不再牵着我们的手,在重叠使用 borrow
和 tentative_borrow
方面(这将导致未定义行为)。现在必须由我们确保没有任何运行时路径可以导致这样的借用重叠。
确实如此,正如简单的分支所展示的
-
在
Some
分支中,dependent
仍然借用tentative_borrow
,因此,*borrow
。但在此分支中,我们不再使用borrow
,并且在调用者体中也不使用,只要使用dependent
。实际上,在签名上,我们确实表明该dependent
返回值,其类型为_<'r>
,是从*borrow
借用的,因为那个'r
名称的重复。 -
在
None
分支中,没有dependent
,并且不再使用tentative_borrow
,因此可以再次引用borrow
。
换句话说
虽然这是
unsafe
,但 't 中确实存在 正确性。
为了额外的预防措施,这个crate甚至通过一个cfg
-opt-out来保护对unsafe
的使用,这样当使用-Zpolonius
时,unsafe
会被移除,同时函数的主体以及其签名都可以正常编译(这一点在CI中通过一个特殊的test
得到了进一步强化)。
将其推广
Option<T<'_>>
变成了PoloniusResult<T<'_>, U>
结果是,我们不必限制branch
在返回None
时无数据,并且我们可以将其用作一个“通道”,通过这个通道传递非借用数据。
这导致用Option< T<'any> >
替换PoloniusResult< T<'any>, U>
- 注意,
U
不能依赖于'any
,因为它不能命名它(泛型参数是在'any
量化之前引入的)。
FnOnceReturningAnOption
技巧被一个ForLt
模式所取代
- (其中
FnOnceReturningAnOption
是上述Demo
代码段中使用的辅助特质)
实际上,基于FnOnceReturningAnOption
的签名在调用者的一侧可能会有问题,因为
-
它推断了闭包的实际实例被喂入时,闭包的更高阶
'any
受感染的返回类型; -
但是,闭包只有在API明确要求它时才会成为更高阶的
因此,这导致调用者和被调用者都期望对方明确指出闭包的更高阶返回值应该是什么,从而导致根本就没有更高阶性,或者出现类型推断错误。
- 注意,来自https://docs.rs/higher-order-closure的
hrtb!
宏,或者实际的for<…>
-闭包RFC中的polyfills,在这方面可能会有帮助。但据我所知,使用这些方法比上述任何一种解决方案都要复杂得多,违背了此crate的初衷。
因此,Ret<'any>
通过另一种方式实现。通过"higher kinded types",即通过“泛型泛型”/“本身就是泛型的泛型”
//! In pseudo-code:
fn polonius<'r, T, Ret : <'_>> (
borrow: &'r mut T,
branch: impl FnOnce(&'_ mut T) -> PoloniusResult<Ret<'_>, ()>,
) -> PoloniusResult<
Ret<'r>,
(), &'r mut T,
>
这不能直接用Rust编写,但你可以定义一个表示类型 <'_>
特性的trait(在这个crate中是 ForLt
),并且通过它(R: ForLt
),使用 R::Of<'lt>
作为 "feed <'lt>
" 操作符
// Real code!
use ::polonius_the_crab::{ForLt, PoloniusResult};
fn polonius<'input, T, Ret : ForLt> (
borrow: &'input mut T,
branch: impl for<'any> FnOnce(&'any mut T) -> PoloniusResult< Ret::Of<'any>, ()>,
) -> PoloniusResult<
Ret::Of<'input>,
(), &'input mut T,
>
# { unimplemented!(); }
我们已经达到了这个crate公开的实际 fn polonius
的定义!
现在,ForLt
类型仍然很难使用。如果我们回到那个返回 &'_ String
的 get_or_insert
例子,我们需要表达这个代表 <'lt> => &'lt String
的 "泛型类型",例如
# use ::polonius_the_crab::ForLt;
#
/// Invalid code for our API:
/// It is not `StringRefNaïve` which is a type, but `StringRefNaïve<'smth>`
/// (notice the mandatory "early fed" generic lifetime parameter).
type StringRefNaïve<'any> = &'any String;
/// Correct code: make `StringRef` a fully-fledged stand-alone type!
type StringRef = ForLt!(<'any> = &'any String);
// Note: the macro supports lifetime elision, so as to be able to instead write:
type StringRef2 = ForLt!(&String); // Same type as `StringRef`!
- 有关此区分的更多信息,请参阅 https://docs.rs/higher-kinded-types 的文档。
综合起来:get_or_insert
没有使用 .entry()
也没有双重查找
这个crate公开了一个具有 unsafe
体的 "raw" polonius()
函数,它非常强大,可以解决与缺乏polonius相关的问题。
use ::polonius_the_crab::{polonius, ForLt, Placeholder, PoloniusResult};
#[forbid(unsafe_code)] // No unsafe code in this function: VICTORY!!
fn get_or_insert (
map: &'_ mut ::std::collections::HashMap<i32, String>,
) -> &'_ String
{
// Our `BorrowingOutput` type. In this case, `&String`:
type StringRef = ForLt!(&String);
match polonius::<_, _, StringRef>(map, |map| match map.get(&22) {
| Some(ret) => PoloniusResult::Borrowing(ret),
| None => PoloniusResult::Owned {
value: 42,
// We cannot name `map` in this branch since `ret` borrows it in the
// other (the very lack-of-polonius problem).
input_borrow: /* map */ Placeholder,
},
}) { // 🎩🪄 `polonius-the-crab` magic 🎩🪄
| PoloniusResult::Owned {
value,
// we got the borrow back in the `Placeholder`'s stead!
input_borrow: map,
} => {
assert_eq!(value, 42);
map.insert(22, String::from("…"));
&map[&22]
},
// and yet we did not lose our access to `ret` 🙌
| PoloniusResult::Borrowing(ret) => {
ret
},
}
}
我们必须承认这确实 非常繁琐! 😵💫
因此,这个crate还提供了
方便的宏,用于便捷使用 😗👌
主要,是 polonius!
入口点,在其中你可以使用 polonius_return!
来 提前返回依赖值,或者使用 exit_polonius!
来 "break" / 离开 polonius!
块并返回一个 非依赖 值(注意这个借用检查器限制的 分支 本质是如何保持在API的骨子里的)。
polonius!
宏需要使用'polonius
受感染的返回类型——HKT标记(for<'polonius>
),对于那些跟随实现的人来说。
这导致以下 get_or_insert
使用方式
使用 Polonius The Crab 来娱乐和盈利™
#![forbid(unsafe_code)]
use ::polonius_the_crab::prelude::*;
use ::std::collections::HashMap;
/// Typical example of lack-of-Polonius limitation: get_or_insert pattern.
/// See https://nikomatsakis.github.io/rust-belt-rust-2019/#72
fn get_or_insert(
mut map: &mut HashMap<u32, String>,
) -> &String {
// Who needs the entry API?
polonius!(|map| -> &'polonius String {
if let Some(v) = map.get(&22) {
polonius_return!(v);
}
});
map.insert(22, String::from("hi"));
&map[&22]
}
依赖项
~59KB