1 个不稳定版本
0.1.0 | 2023年11月11日 |
---|
#1317 在 开发工具
76KB
352 行
AuToken
AuToken 是一个 Rust 静态分析工具,用于自动检查运行时的借用违规。
use autoken::MutableBorrow;
fn foo() {
let _my_guard = MutableBorrow::<u32>::new();
// Autoken statically issues a warning here because we attempted to call a function which could
// mutably borrow `u32` while we already have an active mutable borrow from `_my_guard`.
bar();
}
fn bar() {
let _my_guard_2 = MutableBorrow::<u32>::new();
}
warning: called a function expecting at most 0 mutable borrows of type u32 but was called in a scope with at least 1
--> src/main.rs:8:5
|
8 | bar();
| ^^^^^
检查项目
AuToken 是一个框架,可以将运行时借用的静态分析添加到您的crate中。如果您是具有 AuToken 集成的 crate 的最终用户,并希望使用此工具检查您的项目,这部分就是为您准备的!如果您正在构建 crate 并希望与 AuToken 集成,请跳转到 集成 AuToken 部分。
如果您想通过 cargo
安装此工具,应运行如下命令
cargo +nightly-2023-09-08 install cargo-autoken -Z bindeps
这可能需要您配置 rustup 工具链。由于这个过程可能因用户而异,因此最佳的工具链配置说明由 rustup、cargo 和 rust 提供。
如果您想从源代码安装,假设您当前的工作目录与 仓库 的 README 相同,可以使用以下方式安装 cargo-autoken
cargo install --path src/cargo -Z bindeps
您可以通过运行以下命令在目标二进制crate上运行 AuToken 验证
cargo autoken check
...在其目录中。
祝您玩得开心!
忽略假阳性
AuToken 本身非常保守。毕竟,它的整个工作就是确保在给定时间只有一个特定类型的借用,即使您可能同时从几个不同的来源进行借用!
let cell_1 = MyCell::new(1u32);
let cell_2 = MyCell::new(2u32);
let borrow_1 = cell_1.borrow_mut();
let borrow_2 = cell_2.borrow_mut();
warning: called a function expecting at most 0 mutable borrows of type u32 but was called in a scope with at least 1
--> src/main.rs:10:27
|
10 | let borrow_2 = cell_2.borrow_mut();
| ^^^^^^^^^^^^
如果您确定您正在执行的操作是安全的,您可以使用 assume_no_alias
方法忽略这些警告。
let cell_1 = MyCell::new(1u32);
let cell_2 = MyCell::new(2u32);
let borrow_1 = cell_1.borrow_mut();
let borrow_2 = autoken::assume_no_alias(|| cell_2.borrow_mut());
有关此函数的更多形式,请参阅 assume_no_alias_in
和 assume_no_alias_in_many
。
理解控制流错误
在使用 AuToken 时,您可能会遇到最奇怪的诊断信息之一
let cell_1 = MyCell::new(1u32);
let my_borrow = if some_condition {
Some(cell_1.borrow_mut())
} else {
None
};
warning: not all control-flow paths to this statement are guaranteed to borrow the same number of components
--> src/main.rs:9:21
|
9 | let my_borrow = if some_condition {
| _____________________^
10 | | Some(cell_1.borrow_mut())
11 | | } else {
12 | | None
13 | | };
| |_____^
这种错误发生是因为AuToken设计的根本局限性。AuToken通过遍历由rustc
生成的控制流图来分析您的程序,分析诸如借用检查等问题。每次它遇到对borrow_mutably
或borrow_immutably
的调用时,都会增加给定控制流块可能拥有的可变或不可变借用数的理论值,反之亦然,使用unborrow_mutably
和unborrow_immutably
。如果控制流因为if
语句或loop
而出现分歧,AuToken将分别访问和分析每条路径。
但当这两条路径再次合并时会发生什么?如果一条路径以可变方式借用u32
,而另一条路径根本不借用它,那么用户拥有多少借用?AuToken无法回答这个问题,只能随机猜测。因为这个猜测很可能是错误的,它发出警告告诉您,它真的无法处理这样的代码。
因此,如果这种类型的代码不能由AuToken分析,可以做什么呢?最佳解决方案是使用AuToken集成编写者强烈推荐实现的方法:borrow_on_loan
(或borrow_mut_on_loan
、get_mut_on_loan
……在文档中搜索_on_loan
即可!)此方法将借用与外部提供的MutableBorrow
实例相关联,该实例应在所有条件逻辑之外定义。
let cell_1 = MyCell::new(1u32);
let mut guard = MutableBorrow::<u32>::new();
let my_borrow = if some_condition {
Some(cell_1.borrow_mut_on_loan(&mut guard))
} else {
None
};
如果这太难管理,您还可以使用strip_lifetime_analysis
方法完全删除所有静态借用分析。然而,这非常危险,因为AuToken几乎忘记了该借用的存在,并且可能会让无效的借用悄悄通过。
let cell_1 = MyCell::new(1u32);
let my_borrow = if some_condition {
Some(cell_1.borrow_mut().strip_lifetime_analysis())
} else {
None
};
最后,如果事情变得非常糟糕,您可以使用assume_black_box
忽略整个部分。这个函数是一个非常最后的手段,因为它阻止了静态分析工具查看被调用闭包中的任何内容。在考虑触摸它之前,您应该阅读其文档以获取详细信息!
潜在借用
您可能会偶然发现您的本地AuToken集成crate中存在一个可错误的借用方法,它接受一个PotentialMutableBorrow
或PotentialImmutableBorrow
“借出”守卫。这些守卫的原因与我们为什么需要在其他有条件的创建的借用中需要借出守卫的原因相似,但有额外的限制,即因为这些借用守卫是与可错误的借用方法一起使用的,所以假设在运行时可以优雅地处理与现有借用的别名。由于这个假设,PotentialMutableBorrows
不会在作用域中已经存在另一个混淆的借用守卫时发出警告。
let my_cell = MyCell::new(1u32);
let mut my_loaner_1 = PotentialMutableBorrow::<u32>::new();
let borrow_1 = my_cell.try_borrow_mut(&mut my_loaner_1).unwrap();
// This should not trigger a static analysis warning because, if the specific cell is already
// borrowed, the function returns an `Err` rather than panicking.
let mut my_loaner_2 = PotentialMutableBorrow::<u32>::new();
let not_borrow_2 = my_cell.try_borrow_mut(&mut my_loaner_2).unwrap_err();
如果无法优雅地处理借用,可以创建一个MutableBorrow
或ImmutableBorrow
保护器,并将其downgrade
为PotentialMutableBorrow
或PotentialImmutableBorrow
保护器,这样静态分析器将再次开始报告这些可能存在问题的借用。
let my_cell = MyCell::new(1u32);
let mut my_loaner_1 = PotentialMutableBorrow::<u32>::new();
let borrow_1 = my_cell.try_borrow_mut(&mut my_loaner_1).unwrap();
// Unlike the previous example, this code cannot handle aliasing borrows gracefully, so we should
// create a `MutableBorrow` first to get the alias check and then downgrade it for use in the
// fallible borrowing method.
let mut my_loaner_2 = MutableBorrow::<u32>::new().downgrade();
let not_borrow_2 = my_cell.try_borrow_mut(&mut my_loaner_2).unwrap();
处理动态分派
AuToken通过收集所有可能的分派目标来处理动态分派,这些目标基于什么被转换成什么,并假设这些具体类型中的任何一个都可以被给定无尺寸类型的调用所调用。这有时可能会过于悲观。您可以通过使动态分派的特质更细粒度来帮助解决这个问题。例如,您可以使用FnMut(u32, i32, f32)
而不是使用FnMut(PhantomData<MyHandlers>, u32, i32, f32)
。同样,如果您有一个特质MyBehavior
,您可以通过一个标记泛型类型来参数化它,使其更加细粒度。
如果确实存在问题,您可以使用assume_black_box
来隐藏创建这些动态分派目标的解尺寸转换。再次强调,这绝对是一种最后的手段,您在考虑触摸它之前,绝对应该阅读其文档的详细信息!
处理外部代码
AuToken不知道如何处理外部代码,并且只是忽略它。如果您有一个外部函数调用了用户空间代码,您可以使用类似以下内容告诉AuToken该代码实际上是可到达的:
my_ffi_call(my_callback);
if false { // reachability hint to AuToken
my_callback();
}
集成AuToken
本节是为希望将静态分析添加到其动态借用方案的crate开发者而写的。如果您有兴趣使用这些crate之一,请参阅检查项目部分。
该库提供了四个基本借用函数
这些函数实际上什么也不做,并且在编译过程中被删除。然而,当通过自定义的AuToken rustc包装器进行检查时,它们几乎“借用”和“解除借用”一个全局类型的全局令牌T
,并在可能违反该虚拟全局令牌的XOR可变规则时发出警告。
通常,这些函数不会直接调用,而是通过它们的RAII对应物MutableBorrow
和ImmutableBorrow
间接调用。
这些原语可以用来自动引入额外的编译时安全性到动态检查的借用和锁定方案中。这里有一些例子
您可以为RefCell
创建一个安全的包装...
use autoken::MutableBorrow;
use std::cell::{RefCell, RefMut};
struct MyRefCell<T> {
inner: RefCell<T>,
}
impl<T> MyRefCell<T> {
pub fn new(value: T) -> Self {
Self { inner: RefCell::new(value) }
}
pub fn borrow_mut(&self) -> MyRefMut<'_, T> {
MyRefMut {
token: MutableBorrow::new(),
sptr: self.inner.borrow_mut(),
}
}
}
struct MyRefMut<'a, T> {
token: MutableBorrow<T>,
sptr: RefMut<'a, T>,
}
let my_cell = MyRefCell::new(1u32);
let _a = my_cell.borrow_mut();
// This second mutable borrow results in an AuToken warning.
let _b = my_cell.borrow_mut();
warning: called a function expecting at most 0 mutable borrows of type u32 but was called in a scope with at least 1
--> src/main.rs:33:22
|
33 | let _b = my_cell.borrow_mut();
| ^^^^^^^^^^^^
您可以为可重入性受保护的函数创建...
fn do_not_reenter(f: impl FnOnce()) {
struct ISaidDoNotReenter;
let _guard = autoken::MutableBorrow::<ISaidDoNotReenter>::new();
f();
}
do_not_reenter(|| {
// Whoops!
do_not_reenter(|| {});
});
warning: called a function expecting at most 0 mutable borrows of type main::do_not_reenter::ISaidDoNotReenter but was called in a scope with at least 1
--> src/main.rs:6:9
|
6 | f();
| ^^^
您甚至可以拒绝整个类别的函数,调用它们可能会很危险!
use autoken::{ImmutableBorrow, MutableBorrow};
struct IsOnMainThread;
fn begin_multithreading(f: impl FnOnce()) {
let _guard = MutableBorrow::<IsOnMainThread>::new();
f();
}
fn only_call_me_on_main_thread() {
let _guard = ImmutableBorrow::<IsOnMainThread>::new();
// ...
}
begin_multithreading(|| {
// Whoops!
only_call_me_on_main_thread();
});
warning: called a function expecting at most 0 mutable borrows of type main::IsOnMainThread but was called in a scope with at least 1
--> src/main.rs:6:9
|
6 | f();
| ^^^
真是太棒了。
处理限制
如果您像我之前要求的那样阅读了检查项目部分,您会了解到AuToken的四个相当重要的限制。虽然这些限制中的大多数可以通过AuToken提供的工具克服,但第二个限制——控制流错误——需要来自希望与AuToken集成的开发者的些许帮助。强烈建议您在阅读本节之前阅读该部分,因为它激励了这些特殊方法变体的必要性。
总之
- 为每个守卫对象提供一个类似于
MutableBorrow
的strip_lifetime_analysis
函数。 - 为每个守卫对象提供一种使用“贷款者”借用对象获取该对象的方法。此变体的推荐后缀是
on_loan
。这样做的方式可能与MutableBorrow
的loan
方法非常相似。 - 对于在执行之前检查其借用条件的条件借用方法,应将方法改为借出一个
PotentialMutableBorrow
或PotentialImmutableBorrow
。
所有这些方法都依赖于能够将RAII守卫的类型从最初借用类型转换为Nothing
——AuToken中的一个特殊标记类型,表示借用守卫实际上没有进行任何借用。这样做需要您在类型级别上跟踪借用类型,因为AuToken缺乏分析该运行时机制的能力。以下是如何实现这一点的示例
struct MyRefMut<'a, T, B = T> {
// ^ notice the addition of this special parameter?
token: MutableBorrow<B>,
sptr: RefMut<'a, T>,
}
有了这个额外的参数,我们可以实现第一个必需的方法:strip_lifetime_analysis
。它的实现相对简单
use autoken::Nothing;
struct MyRefMut<'a, T, B = T> {
token: MutableBorrow<B>,
sptr: RefMut<'a, T>,
}
impl<'a, T, B> MyRefMut<'a, T, B> {
pub fn strip_lifetime_analysis(self) -> MyRefMut<'a, T, Nothing<'static>> {
MyRefMut {
token: self.token.strip_lifetime_analysis(),
sptr: self.sptr,
}
}
}
let my_cell = MyRefCell::new(1u32);
let my_guard = if my_condition {
Some(my_cell.borrow_mut().strip_lifetime_analysis())
} else {
None
};
'static
生命周期在Nothing
中实际上没有意义。实际上,Nothing
中的生命周期纯粹是一个便利的生命周期,其效用将在我们实现第二个必需方法borrow_mut_on_loan
时变得更加明显。
编写此方法也相对简单
use autoken::Nothing;
struct MyRefCell<T> {
inner: RefCell<T>,
}
impl<T> MyRefCell<T> {
pub fn borrow_mut_on_loan<'l>(
&self,
loaner: &'l mut MutableBorrow<T>
) -> MyRefMut<'_, T, Nothing<'l>> {
MyRefMut {
token: loaner.loan(),
sptr: self.inner.borrow_mut(),
}
}
}
let my_cell = MyRefCell::new(1u32);
let mut my_loaner = MutableBorrow::<u32>::new();
let my_guard = if my_condition {
Some(my_cell.borrow_mut_on_loan(&mut my_loaner))
} else {
None
};
在这里,我们使用Nothing
中的占位符生命周期来限制贷款的寿命,直到loaner
的引用。相当方便。
最后,可以失败地实现与之前示例几乎相同的borrow
方法变体
use autoken::{Nothing, PotentialMutableBorrow};
struct MyRefCell<T> {
inner: RefCell<T>,
}
impl<T> MyRefCell<T> {
pub fn try_borrow_mut<'l>(
&self,
loaner: &'l mut PotentialMutableBorrow<T>
) -> Result<MyRefMut<'_, T, Nothing<'l>>, BorrowMutError> {
self.inner.try_borrow_mut().map(|sptr| MyRefMut {
token: loaner.loan(),
sptr,
})
}
}
let my_cell = MyRefCell::new(1u32);
let mut my_loaner_1 = PotentialMutableBorrow::<u32>::new();
let borrow_1 = my_cell.try_borrow_mut(&mut my_loaner_1).unwrap();
let mut my_loaner_2 = PotentialMutableBorrow::<u32>::new();
let not_borrow_2 = my_cell.try_borrow_mut(&mut my_loaner_2).unwrap_err();
多么令人兴奋!