lab2 正式进入到了底层工具库的编写,在进入 lab2 时我又碰到了一个预料之外的问题但勉强解决了,在实际编写代码时我发现仅仅是 lab0 中的知识还不足以支撑 lab2 的要求,Rust by Example
虽然内容并不少,但只是一个入门教程。
预料之外的依赖错误
按照课程描述在 merge lab2 的内容后执行 make 安装检查依赖没有报错就可以开始写代码了,但是我在执行 make 时遇到了一个依赖错误。简单来说是包core_io
依赖包rust_version
,而core_io
的更新缓慢和rust_version
删除旧版本共同造成了core_io
依赖了一个已经被删除的早期版本,我寻找解决方法时在core_io
的 issue 中还看到了一位疑似也在学 CS3210 的朋友。
虽然解决的过程花了不少时间,但是这里就不再赘述。考虑到原作者更新缓慢,最终解决这个依赖问题的方式是 fork 自己修。我的 fork 版本合并了两个有更新内容的分支。
1A StackVec
StackVec 就是一个非常基础的定长栈,但是第一次用 Rust 实现起来就没有那么轻松了。if
不加括号,返回值可以省略return
之类的语法特性虽然很简洁,但让人有些不习惯,这些都是无伤大雅的问题。在这个模块里最有趣的就是下面三个函数
pub struct StackVec<'a, T: 'a> {
storage: &'a mut [T],
len: usize
}
// 返回可变切片及失去所有权
pub fn into_slice(self) -> &'a mut [T] {
&mut self.storage[..self.len]
}
// 返回不可变切片
pub fn as_slice(&self) -> &[T] {
&self.storage[..self.len]
}
// 返回可变切片
pub fn as_mut_slice(&mut self) -> &mut [T] {
&mut self.storage[..self.len]
}
这里涉及到的问题就是函数返回隐含了释放其拥有的变量。
相比这里,实现Deref
DerefMut
IntoIterator
几个 trait 的代码难度更大,也让我对 trait 和生命周期的理解加深了不少,但目前还不能够清晰地表达出来,后面再提。
1B volatile
这个模块没有要求补全代码,而是讲解了 Rust 中裸指针(raw pointer)的使用。
前面提到过很多有关 Rust 中所有权体系的内容,而在 Rust 中解引用裸指针是无法在编译阶段追踪的不安全操作,解裸指针操作必须在unsafe
块中进行,裸指针和 C 语言相同需要程序员自行管理。
- 不能保证指向有效的内存,甚至不能保证是非空的
- 没有任何自动清除,所以需要手动管理资源
- 是普通旧式类型,也就是说,它不移动所有权,因此 Rust 编译器不能保证不出像释放后使用这种 bug
- 缺少任何形式的生命周期,不像&,因此编译器不能判断出悬垂指针
- 除了不允许直接通过*const T 改变外,没有别名或可变性的保障
在应用级编程中可能不需要使用裸指针,但在系统级编程中裸指针是不可避免的,使用裸指针时经常还会用到 volatile。在 C 中声明指针时可以添加volatile
关键字,在 Rust 中则是有read_volatile
和write_volatile
两个方法。
volatile 用于在不改变代码优化级别的前提下禁止对指定变量或操作进行编译器优化,一个比较好理解的例子就是 lab1 中的代码,编译器并不知道某个地址究竟是普通的寄存器还是会产生代码之外的效果的寄存器,所以在编译器看来,对一个地址仅写不读是没有意义的,是可以删除掉来提高代码质量的,是一种代码优化行为。但 lab1 中涉及到的三个地址都是与 GPIO 配置有关的寄存器,写地址会影响 GPIO 的工作状态,在这个场景中 volatile 就是用来防止写操作被“优化”的。
这个模块是对read_volatile
和write_volatile
的封装,简化使用阶段的代码,模块中同时使用了 macro 和 trait 两个特性。代码理解起来有一些困难,代码的作用还是比较容易看出来的。