3个不稳定版本

0.2.0 2023年4月15日
0.1.1 2023年4月15日
0.1.0 2023年4月15日

#153无标准库

每月31次下载

BSD-3-Clause

25KB
247

增强切片和索引的实用工具

简介

static_slicing库提供了一组编译时检查切片和索引的有用实用工具。

背景

不想阅读这些内容?这里有一个简短总结:在某些常见情况下,切片和索引可能会很奇怪,所以这个库使这些事情变得不那么奇怪。现在你可以跳转到安装部分

我最初开发这个库是为了供伍斯特理工学院的团队在2023年MITRE嵌入式Capture the Flag比赛中使用。

在为嵌入式系统安装的固件开发过程中,发现了一个有趣的语言痛点:数组切片和索引的编译时推断不足。

例如,这个函数无法编译(亲自尝试

fn test() {
	let a = [0u8, 1u8, 2u8, 3u8];
	let x = a[4];	// there are only 4 elements in `a`! no good...
}

幸运的是,编译器知道访问一个4元素数组的第4个索引永远不会成功,并发出一个unconditional_panic警告,默认情况下该警告会被转换为错误。

然而,这个几乎完全相同的函数可以成功编译,并在运行时崩溃(再次,亲自尝试

fn test() {
	let a = &[0u8, 1u8, 2u8, 3u8];
	let x = a[4];	// there are only 4 elements in `a`! no good...
}

编译器知道a是一个指向一个4元素u8数组的引用 - 换句话说,&[u8; 4] - 但是已经无法指出任何潜在的问题。

编译器对切片的分析(或缺乏分析)也可能有问题。这个新用户可能会期望编译的函数实际上不能编译

fn test() {
	let a = &[0u8, 1u8, 2u8, 3u8];
	let x: &[u8; 2] = &a[1..3];	// *x should be [a[1], a[2]], which is a 2-element array.
}

编译器的解释

3 |     let x: &[u8; 2] = &a[1..3];
  |            --------   ^^^^^^^^ expected array `[u8; 2]`, found slice `[u8]`
  |            |
  |            expected due to this

这也无法工作

fn test() {
	let a = &[0u8, 1u8, 2u8, 3u8];
	let x: &[u8; 2] = &a[1..3].into();
}

然而,可以工作

fn test() {
	let a = &[0u8, 1u8, 2u8, 3u8];
	let x: &[u8; 2] = &a[1..3].try_into().unwrap();
}

我并不是特别喜欢在所有东西的末尾都使用 .try_into().unwrap(),但我确实是超级喜欢解决像这样的奇怪问题——这就是为什么这个库存在的原因!

安装

要安装当前版本的 static_slicing,请将以下内容添加到您的 Cargo.toml 文件的 dependencies 部分

static-slicing = "0.2.0"

可以通过禁用默认功能来启用 no_std 支持

static-slicing = { version = "0.2.0", default-features = false }

注意:此库需要 Rust 1.59 或更高版本。

它是如何工作的?

不想读这个?跳到示例部分。

这个库引入了两种新的索引类型,利用了 const generics 的力量

  • StaticIndex<INDEX> 用于获取/设置 单个 元素,以及;
  • StaticRangeIndex<START, LENGTH> 用于获取/设置 范围 的元素。

对于固定大小的数组(即 [T; N],其中 T 是元素类型,N 是数组长度),库提供了 IndexIndexMut 的实现,这两个实现都接受静态索引类型。通过 const panic 支持(以及一点类型系统黑客技术),可以防止无效的索引操作编译!

要了解这是如何实现的,请查看 IsValidIndexIsValidRangeIndex 的实现。

对于其他类型(切片、Vec 等),需要使用 SliceWrapper。由于我在开发过程中遇到了 Rust 的孤儿规则的一些问题,因此 SliceWrapper 是一个不幸的权宜之计。无论如何,SliceWrapper 被设计得尽可能“正常”。它可以传递给接受切片引用的函数,本质上看起来就像它所封装的数据。

性能

包含 Criterion 基准测试,用于固定大小数组索引。在我的计算机上(12核心 AMD Ryzen 9 5900X),内置索引功能和本库提供的功能之间没有观察到显著的 负面 性能差异。也就是说,这不会使你的代码变慢 200000 倍——实际上,任何性能影响在最坏的情况下也应该是微不足道的。

以下是单个基准测试运行的结果

基准类型 编译时检查 运行时检查
单个索引 428.87 ps 428.07 ps
范围索引 212.19 ps 212.69 ps

(在某些其他运行中,编译时检查的单个索引比运行时检查的单个索引略快。)

示例

以下是如何使用此库的一些示例。

示例 1:从一个固定大小数组获取元素和切片

use static_slicing::{StaticIndex, StaticRangeIndex};

fn main() {
	let x = [513, 947, 386, 1234];

	// get the element at index 3
	let y = x[StaticIndex::<3>];

	// get 2 elements starting from index 1
	let z: &[i32; 2] = &x[StaticRangeIndex::<1, 2>];
	// this also works:
	let z: &[i32] = &x[StaticRangeIndex::<1, 2>];

	// prints: y = 1234
	println!("y = {}", y);

	// prints: z = [947, 386]
	println!("z = {:?}", z);
}

示例 2:使用静态索引进行修改

use static_slicing::{StaticIndex, StaticRangeIndex};

fn main() {
	let mut x = [513, 947, 386, 1234];

	assert_eq!(x[StaticIndex::<1>], 947);
	x[StaticIndex::<1>] = 1337;
	assert_eq!(x[StaticIndex::<1>], 1337);
	assert_eq!(x, [513, 1337, 386, 1234]);

	x[StaticRangeIndex::<2, 2>] = [7331, 4040];

	assert_eq!(x[StaticRangeIndex::<2, 2>], [7331, 4040]);
	assert_eq!(x, [513, 1337, 7331, 4040]);
}

示例 3:防止越界

use static_slicing::{StaticIndex, StaticRangeIndex};

fn main() {
	// read Background to understand why 
	// `x` being an array reference is important!
	let x = &[513, 947, 386, 1234];

	// this block compiles...
	{
		let y = x[5];
		let z: &[i32; 2] = &x[2..5].try_into().unwrap();
	}

	// ...but not this one!
	{
		let y = x[StaticIndex::<5>];
		let z = x[StaticRangeIndex::<2, 3>];
	}
}

示例 4:从 SliceWrapper 中读取

use static_slicing::{StaticIndex, StaticRangeIndex, SliceWrapper};

fn main() {
	let x = SliceWrapper::new(&[513, 947, 386, 1234][..]);
	
	{
		let y = x[StaticIndex::<3>];
		let z = x[StaticRangeIndex::<2, 2>];

		// prints: y = 1234
		println!("y = {}", y);

		// prints: z = [386, 1234]
		println!("z = {:?}", z);
	}

	{
		// both of these would panic at runtime 
		// since they're performing invalid operations.
		let _ = x[StaticIndex::<5>];
		let _ = x[StaticRangeIndex::<2, 4>];
	}
}

示例 5:向 SliceWrapper 中写入

use static_slicing::{StaticIndex, StaticRangeIndex, SliceWrapper};

fn main() {
	// SliceWrappers are mutable under the following conditions:
	// 1. the wrapper itself has been declared as mutable, AND;
	// 2. the wrapper owns its wrapped data OR contains 
	//	  a mutable reference to the data.
	let mut x = SliceWrapper::new(vec![513, 947, 386, 1234]);
	
	assert_eq!(x[StaticIndex::<3>], 1234);
	x[StaticIndex::<3>] = 1337;
	assert_eq!(x[StaticIndex::<3>], 1337);

	assert_eq!(x[StaticRangeIndex::<1, 2>], [947, 386]);
	x[StaticRangeIndex::<1, 2>] = [5555, 6666];
	assert_eq!(x[StaticRangeIndex::<1, 2>], [5555, 6666]);
}

局限性

有一些局限性需要注意

  • rust-analyzer似乎无法显示由库使用const panic生成的错误。
  • SliceWrapper在任何情况下都无法进行编译时检查,即使提供了具有已知长度的数组引用。这可能会在特化稳定之前被阻塞。
  • SliceWrapper的存在本身就是一个限制(至少在我心中是这样),除非对孤儿规则和一致性执行进行重大更改,否则它不太可能消失。(基本问题是无法为Index(Mut)同时实现[T; N][T]。)

否则,这应该是一个相当容易处理的库 - 它确实让我的队友和我处理事情容易了很多!

无运行时依赖