Rust代码安全库no-panic的使用:确保函数永不触发panic的编译时检查工具
Rust代码安全库no-panic的使用:确保函数永不触发panic的编译时检查工具
一个Rust属性宏,要求编译器证明函数永远不会发生panic。
[dependencies]
no-panic = "0.1"
use no_panic::no_panic;
#[no_panic]
fn demo(s: &str) -> &str {
&s[1..]
}
fn main() {
println!("{}", demo("input string"));
}
如果函数确实发生panic(或者编译器无法证明该函数不会panic),程序将无法编译,并出现一个链接器错误,标识出函数名称。让我们通过传递一个无法在第一个字节处切片的字符串来触发这种情况:
fn main() {
println!("{}", demo("\u{1f980}input string"));
}
Compiling no-panic-demo v0.0.1
error: linking with `cc` failed: exit code: 1
|
= note: /no-panic-demo/target/release/deps/no_panic_demo-7170785b672ae322.no_p
anic_demo1-cba7f4b666ccdbcbbf02b7348e5df1b2.rs.rcgu.o: In function `_$LT$no_pani
c_demo..demo..__NoPanic$u20$as$u20$core..ops..drop..Drop$GT$::drop::h72f8f423002
b8d9f':
no_panic_demo1-cba7f4b666ccdbcbbf02b7348e5df1b2.rs:(.text._ZN72_$LT$no
_panic_demo..demo..__NoPanic$u20$as$u20$core..ops..drop..Drop$GT$4drop17h72f8f42
3002b8d9fE+0x2): undefined reference to `
ERROR[no-panic]: detected panic in function `demo`
'
collect2: error: ld returned 1 exit status
错误信息并不完美,但请注意末尾的ERROR[no-panic]部分,它提供了违规函数的名称。
注意事项
-
需要一定程度的优化才能证明不会panic的函数,在被标记为
#[no_panic]
后,可能在调试模式下不再编译。 -
Panic检测发生在整个依赖关系图的链接时,因此任何不调用链接器的Cargo命令都不会触发panic检测。这包括库crate的
cargo build
以及二进制和库crate的cargo check
。 -
在使用
panic = "abort"
构建的代码中,该属性是无用的。代码必须使用panic = "unwind"
(默认值)构建,以便检测任何panic。在确认没有panic后,您当然仍然可以将软件作为panic = "abort"
构建发布。 -
不支持常量函数。如果放在
const fn
上,该属性将无法编译。
如果您发现代码需要优化才能通过#[no_panic]
,要么将no-panic设为仅在发布构建中启用的可选依赖项,要么在您的Cargo.toml或.cargo/config.toml中添加以下部分,以在调试构建中启用非常基础的优化。
[profile.dev]
opt-level = 1
如果您需要证明不会panic的代码调用了来自不同crate的非泛型非内联函数,您可能需要启用thin LTO,以便链接器推断这些函数不会panic。
[profile.release]
lto = "thin"
如果thin LTO不够用,接下来可以尝试使用单个代码生成单元的fat LTO:
[profile.release]
lto = "fat"
codegen-units = 1
如果您希望no_panic仅假设您调用的某些函数不会panic,并在运行时发生panic时获得未定义行为,请参阅dtolnay/no-panic#16;尝试将该调用包装在unsafe extern "C"
包装器中。
致谢
链接器错误技术基于Kixunil的crate dont_panic
。请查看该crate以获取其他方便的要求无panic的方法。
许可证
根据Apache License, Version 2.0或MIT license中的任一许可,由您选择。
除非您明确声明,否则根据Apache-2.0许可定义,您有意提交包含在此crate中的任何贡献均应如上所述双重许可,无需任何附加条款或条件。
完整示例代码
// 引入no_panic宏
use no_panic::no_panic;
// 使用#[no_panic]属性标记函数,要求编译器证明该函数不会panic
#[no_panic]
fn safe_slice(s: &str) -> &str {
// 这个切片操作在运行时可能panic,但编译器会检查
&s[1..]
}
fn main() {
// 正常情况下的调用
println!("{}", safe_slice("hello"));
// 以下代码会导致编译错误,因为无法证明不会panic
// println!("{}", safe_slice("")); // 空字符串会导致panic
// println!("{}", safe_slice("🍕unicode")); // 多字节字符会导致panic
}
// 另一个示例:数学计算函数
#[no_panic]
fn safe_math(a: i32, b: i32) -> i32 {
// 这个计算不会panic
a.wrapping_add(b).wrapping_mul(2)
}
// 注意:以下函数无法通过#[no_panic]检查
// #[no_panic]
// fn unsafe_division(a: i32, b: i32) -> i32 {
// a / b // 除零会panic,无法通过编译
// }
Rust代码安全库no-panic的使用指南
简介
no-panic是一个Rust编译时检查工具,用于确保被标记的函数永远不会触发panic。它通过在编译时分析函数代码,检测可能引发panic的操作,从而帮助开发者编写更加安全的无panic代码。
使用方法
1. 添加依赖
在Cargo.toml中添加:
[dependencies]
no-panic = "0.1"
2. 基本用法
使用#[no_panic]
属性标记需要确保无panic的函数:
use no_panic::no_panic;
#[no_panic]
fn safe_addition(a: u32, b: u32) -> u32 {
a + b
}
#[no_panic]
fn get_array_element(arr: &[i32], index: usize) -> Option<&i32> {
arr.get(index)
}
3. 处理可能panic的情况
对于可能产生panic的代码,需要显式处理:
#[no_panic]
fn safe_division(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
#[no_panic]
fn safe_indexing<T>(slice: &[T], index: usize) -> Option<&T> {
if index < slice.len() {
Some(&slice[index])
} else {
None
}
}
4. 编译时检查
当标记了#[no_panic]
的函数包含可能panic的代码时,编译将会失败:
#[no_panic]
fn unsafe_division(a: f64, b: f64) -> f64 {
a / b // 编译错误:可能除零panic
}
#[no_panic]
fn unsafe_indexing<T>(slice: &[T], index: usize) -> &T {
&slice[index] // 编译错误:可能越界panic
}
5. 与unsafe代码配合使用
#[no_panic]
unsafe fn safe_unsafe_operation(ptr: *mut i32) -> Option<i32> {
if ptr.is_null() {
None
} else {
Some(*ptr)
}
}
完整示例demo
// 引入no_panic宏
use no_panic::no_panic;
// 基本用法示例:安全的加法函数
#[no_panic]
fn safe_addition(a: u32, b: u32) -> u32 {
a + b // 整数加法不会panic
}
// 安全的数组元素获取
#[no_panic]
fn get_array_element(arr: &[i32], index: usize) -> Option<&i32> {
arr.get(index) // 使用get方法避免越界panic
}
// 处理可能panic的情况:安全除法
#[no_panic]
fn safe_division(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None // 显式处理除零情况
} else {
Some(a / b)
}
}
// 安全索引访问
#[no_panic]
fn safe_indexing<T>(slice: &[T], index: usize) -> Option<&T> {
if index < slice.len() {
Some(&slice[index]) // 手动检查边界
} else {
None
}
}
// 与unsafe代码配合使用
#[no_panic]
unsafe fn safe_unsafe_operation(ptr: *mut i32) -> Option<i32> {
if ptr.is_null() {
None // 检查空指针
} else {
Some(*ptr) // 安全的解引用
}
}
// 会导致编译错误的示例(注释掉以避免编译错误)
/*
#[no_panic]
fn unsafe_division(a: f64, b: f64) -> f64 {
a / b // 编译错误:可能除零panic
}
#[no_panic]
fn unsafe_indexing<T>(slice: &[T], index: usize) -> &T {
&slice[index] // 编译错误:可能越界panic
}
*/
fn main() {
// 测试安全函数
println!("安全加法: {}", safe_addition(10, 20));
let arr = [1, 2, 3, 4, 5];
println!("数组元素: {:?}", get_array_element(&arr, 2));
println!("安全除法: {:?}", safe_division(10.0, 2.0));
println!("安全索引: {:?}", safe_indexing(&arr, 3));
// 测试unsafe操作
let mut value = 42;
let ptr = &mut value as *mut i32;
unsafe {
println!("安全unsafe操作: {:?}", safe_unsafe_operation(ptr));
}
}
注意事项
- no-panic只能检测编译时可知的panic路径
- 对于外部函数调用或动态分发,可能无法完全检测
- 建议在性能关键和对稳定性要求极高的代码中使用
- 与
#[inline(always)]
等属性同时使用时需要注意兼容性
适用场景
- 嵌入式系统开发
- 实时系统
- 安全关键代码
- 需要绝对稳定性的库函数
这个工具通过编译时检查帮助开发者编写更加健壮的代码,避免运行时意外panic的发生。