第二次的作业主要内容有:驱动、引导、壳,带领你熟悉 Rust,鉴赏课程作者 Sergio Benitez 的精妙之作!

这次作业拖了好久,要是没有 @codeworm96 大神 push 我,我还能拖得更久囧。因为前面挖了一个不锁编译器版本的坑,所以为了让这次作业能够在 rustc 1.28.0-nightly (cd494c1f0 2018-06-27) 上面通过,还花了蛮多时间的。

Phase 1: Ferris Wheel

这个系列任务是让你按照要求把给定的小程序改对(或者是改成不能编译),有修改的行数的限制,另外还给了一个 test.sh 自动测试。

我刚把仓库拉下来就跑了一次 test.sh,惊讶的发现竟然 compile-fail/ 里面的两个点直接就过了,这肯定不对。按照 test.sh 手动执行

$ rustc compile-fail/modules-1.rs -Z no-trans
error: unknown debugging option: `no-trans`

查了一下,这个 -Z no-trans 只检查错误不做代码的变换。在新版本的编译器中,所有的 trans 都被改成了 codegen。我觉得 codegen 确实是个好名字,一目了然就知道是生成代码,trans 没法一下子反应过来是做各种编译器变换、生成代码。

所以说,这里把 test.sh 里面的 -Z no-trans 改成 -Z no-codegen 就行了。下面罗列一下我在做这个系列的任务的时候觉得有意思的地方吧:

  • #[derive(Copy)] 要顺带 #[derive(Clone)],因为 Clone is a supertrait of Copy
  • Rust 也有 const function(很合理),不过还不稳定,需要 feature gate。
  • u128i128 类型已经稳定了,不需要 feature gate。
  • Rust 竟然允许像这样 let x = &mut 10; 借一个字面值,语义是创建一个临时的值,然后借走。
  • 一开始写 try.rs 的时候,感觉自己写的非常丑陋,发现我改完了比他的 diff budget 少了很多,而且这个文件的 diff budget 除了新增12行以外,还有额外的新增两个字符的限制,让我觉得我肯定少用了什么条件。然后想起来,以前在 Rust by Examples 上面看过错误类型的转换。用上了类型转换之后,就可以用 ? 操作符了,程序一下子就变得优雅了!
  • 要写一个又能接受 String 又能接受 &str 的函数真复杂。

Phase 2: Oxidation » Subphase A: StackVec

IntoIterator 的时候,一开始我还在纠结又要造一个 IntoIter 类,好烦。后来一想,我们的 StackVec 不就是在 slice 外面包了一层吗,那不如直接用 slice 的。于是美滋滋地把 impl<'a, T> IntoIterator for &'a [T] 抄了过来。这样顺带 Deref 什么的也可以直接照搬。

另外,因为查文档的时候老是看到同一个东西又在 core 里面又在 std 里面,特地查了一下,一般 std 里面同名的东西就是从 core 又导出了一次。这样其实在写 no_std 的程序的时候挺开心的,因为还是有很多常用的功能在 core 里面的。包括像后面 kprintln! 也不用自己写格式化了,直接照搬 println! 就行。

Phase 2: Oxidation » Subphase B: volatile

这部分是让你鉴赏课程作者 Sergio Benitez 的精妙之作。

首先是搞了三个 trait:Readable<T>Writeable<T>ReadableWriteable<T>,把裸指针的 unsafe 操作包了起来,而且分开了读和写的功能,顺带提供了一些实用的工具函数。

接着三个宏:readable!writeable!readable_writeable! 用来自动实现 trait。

然后就是定义和实现三个类:ReadVolatile<T>WriteVolatile<T>Volatile<T>。这三个类都标记了 Send,因为这里面都只包含了一个指针,没有特殊的含义,所以可以把所有权转交给别的线程。

UniqueVolatile<T>Volatile<T> 更复杂一些,多了一个 Unique<T>core::ptr::Unique 本身的处境就比较尴尬,先是被标记成了永远不稳定的特性,然后文档又被藏起来了。因为编译器内部实现用到了,所以实际上也还存在,没被删掉。尽管推荐使用 core::ptr::NonNull 替代,但是两者语义又不一样……

回到CS140e上面,作业问 UniqueVolatile<T> 有什么特别的地方,那就是 Unique<T> 多了 Sync文档是这么说的

/// `Unique` pointers are `Send` if `T` is `Send` because the data they
/// reference is unaliased. Note that this aliasing invariant is
/// unenforced by the type system; the abstraction using the
/// `Unique` must enforce it.
#[unstable(feature = "ptr_internals", issue = "0")]
unsafe impl<T: Send + ?Sized> Send for Unique<T> { }

/// `Unique` pointers are `Sync` if `T` is `Sync` because the data they
/// reference is unaliased. Note that this aliasing invariant is
/// unenforced by the type system; the abstraction using the
/// `Unique` must enforce it.
#[unstable(feature = "ptr_internals", issue = "0")]
unsafe impl<T: Sync + ?Sized> Sync for Unique<T> { }

但是我觉得我还是无法理解,可能是因为我对 SendSync 的理解还不到位吧。而且有趣的是,好像作者自己也没有用到 UniqueVolatile<T>……

另外,这一节作者特地教大家用 cargo doc --open 看文档,顺带其实也教了大家怎么写文档,感觉还是学到了。我顺带查了一下,原来 rustup doc 直接就能打开本地的 Rust 文档,包括 API 和官方的几本书。离线看文档还是很开心的,网页秒开。最惊喜的是连着几本书也一块带上了,要知道前不久为了在动车上看 The Rust Programming Language,我还特地开了热点先把所有章节打开……

Phase 2: Oxidation » Subphase C: xmodem

看代码的时候首先是看到了一个我已经遗忘掉的 Rust 语法糖了:

'next_packet: loop {
    for _ in 0..10 {
        match receiver.read_packet(&mut packet) {
            Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
            Err(e) => return Err(e),
            Ok(0) => break 'next_packet,
            Ok(n) => {
                received += n;
                into.write_all(&packet)?;
                continue 'next_packet;
            }
        }
    }

    return Err(io::Error::new(io::ErrorKind::BrokenPipe, "bad receive"));
}

可以直接跳出多重循环,还是非常实用的。

然后写代码的时候,我发现了 Rust 一个很捉急的设定:

error[E0503]: cannot use `self.packet` because it was mutably borrowed
   --> src/lib.rs:240:44
    |
240 |                 self.expect_byte_or_cancel(self.packet, "wrong packet number")?;
    |                 ----                       ^^^^^^^^^^^ use of borrowed `*self`
    |                 |
    |                 borrow of `*self` occurs here

让我想起来以前看到的一篇文章 Non-lexical lifetimes: introduction,上面解释了说这样的问题出现是因为 Rust 的 lifetime checker 是根据词法来分析的,而没有根据语义来分析。虽然很容易就可以改掉,先 let packet = self.packet; 就可以解决了,但是这篇文章的作者也觉得这样很不好。好消息是,non-lexical lifetimes 和 MIR-based borrow-checker 已经开始在开发了!

还有一点就是,因为这里涉及到了非常多的 IO 操作,就一定会有失败的处理。这个时候就深刻地体会到 Rust ? operator 的优越性了。要是让我写 C/C++,我估计要么就偷懒不判断了,要么就 if 然后 goto 了,非常烦(其实还可以写宏)。

运行起来之后,我又遇到了一个小问题:thread '<unnamed>' panicked at 'attempt to add with overflow'。这是在算校验码的时候,虽然根据 XModem 的规范,校验码计算就是自然溢出,但是 Rust 不知道,所以在 debug build 的情况下会自动 panic!。我发现有个 std::num::Wrapping 类型可以用,可惜这里没有 std,只好手动用 u8::wrapping_add(self, rhs: u8) 来算。

Phase 2: Oxidation » Subphase D: ttywrite

这里我遇到了一个小问题,就是怎么优雅地表达要么从文件输入、要么从 stdin 输入,我想要一个大概这样的效果:

let input: &mut CommonTraitOfStdinAndFile = 
    if opt.input_file {
        // construct file input
    } else {
        // stdin
    }

然而想想大概是做不到这样的,因为如果如果直接返回构造出了的对象,那 ifelse 返回的类型不同;而如果分别在 ifelse 块里面构造再返回引用,虽然 trait 是对的,但是构造出来的对象的生命周期比引用还短……所以唯一的办法就是把对象丢到堆上了……

Phase 3: Not a Seashell » Subphase A: Getting Started

首先我遇到的一个问题就是,我替换了 config.txt 之后,在 Raspberry Pi 3B+ 上面跑 act-led-blink.bin 跑不动,PWR 闪烁。查了一下说是供电不足,但是我插上了电源之后也不行……而我要是把课程给的 start.elf 复制上去后,就变回了 ACT 闪四下。这个事情我折腾了好久,我还去查了 device_tree= 是什么。

后来我发现,TTL线的RX/TX灯好像会稍微亮一下,赶紧用 screen 看了一眼,发现 PWR 灯亮的时候显示 On;PWR 灯灭的时候显示 Off。所实说实际上 act-led-blink.bin 是可以跑的,只不过迷之 PWR 灯和 ACT 灯搞反了……还好 TTY 还有输出,算破案了。

Phase 3: Not a Seashell » Hidden Subphase: rustc 1.28.0-nightly

自己挖的坑,自己就得填平……因为编译器版本比较新,os/kernel/ 里面 make 不出所料地挂了。在 @codeworm96 大神的鼓励下,经过了不懈努力,终于能够编译通了……有需要的读者可以直接 patch。下面是我遇到的各种问题:

首先最烦的一个事情是,每一次 make 都会从 core 开始全部编译一遍,哪怕是前面这些依赖之前都已经编译通过了。查了好久,最后发现把 Cargo.lock 删掉就好了,因为里面蜜汁有一个 std==1.0.0 的依赖,而我们这里的 std==0.1.0,我也不清楚为什么我会有这个 Cargo.lock

然后有时候会告诉我找不到 compiler_builtins,因为 ~/.xargo/lib/rustlib/aarch64-none-elf/lib 里面有不止一个候选……我也不清楚这是怎么造成的,反手直接 rm -rf……

#![feature(macro_reexport)] 已经#![feature(use_extern_macros)] 包含了

StrExtSliceExt 两个 trait 已经删掉了,内容并入了 str[T]

std_unicode 也类似,直接并入了 core

下面的三个文件变化较大,主要原因是很多原来的 std 实现的东西转移到了 core 里面,所以直接删掉就好了。我是直接 rustup doc 打开当前版本编译器文档看源代码,再跟课程给的内容做对比,然后复制粘贴删除一把梭:

到后面 kprintln! 用到了 panic! 之后,又遇到了非常神奇的链接错误:

build/kernel.a(kernel-06e9e5e237d25b27.kernel2.rcgu.o): In function `core::panicking::panic_fmt':
kernel2-30c9570e8bd6400576515b3e9ca4056b.rs:(.text._ZN4core9panicking9panic_fmt17hd5547619de8a957fE+0x38): undefined reference to `rust_begin_unwind'

找了半天也不知道咋办,后来瞎猫碰到死耗子,找到了 #[lang= "panic_impl"],竟然成功解决了:

#[lang = "panic_impl"]
#[no_mangle]
pub extern fn rust_begin_panic(info: &PanicInfo) -> ! {
    loop {}
}

原理应该是把 rust_begin_panic 替换掉了,可能原先调用了 rust_begin_unwind 吧。

Phase 3: Not a Seashell » Subphase B: System Timer

在实现 timer::current_time 的时候我有个问题,我只知道计数器的值,又不知道时钟频率,我怎么算时间呢?唯一找到相关的地方就是手册第199页:

the SP804 expects a 1MHz clock

我就只好假定时钟频率是1MHz,这样正好计数器每1纳秒跳一次。

Phase 3: Not a Seashell » Subphase C: GPIO

课程作者一上来先吹了一下说这个编译期间的状态机转移检查只有 Rust 能做,感觉不明觉厉。看到作者给了一个

pub struct Gpio<State> {
    pin: u8,
    registers: &'static mut Registers,
    _state: PhantomData<State>
}

之后去学习了一下 PhantomData<T>,感觉这是一个萌萌哒的结构,文档上面还有萌萌哒的emoji。之后瞬间明白了这个状态机怎么做了,无非就是不同的 State 有不同的 impl。感觉 Rust 的泛型还是非常厉害的。

一番包装之后,主程序就完全不需要 unsafe 了,非常神奇,感觉又一次鉴赏了课程作者 Sergio Benitez 的精妙之作!

Phase 3: Not a Seashell » Subphase D: UART

在实现 UART 驱动的时候同样遇到了不知道时钟频率的问题,因为手册上老是说 250MHz 就假装是 250MHz 好了。

Phase 4: Boot ‘em Up

在写 shell 之前,我觉得还是先把 bootloader 写好比较合理。在这个时候就发现了前面 XModem 各种没测出来的 bug……调好了之后,第一次看到进度条成功走完真的非常兴奋。先把那个 act-led-blink.bin 传了过去,发现真的能跑。终于不用再来回拔插TF卡了!(虽然还是得来回拔插USB-TTL线)

Phase 3: Not a Seashell » Subphase E: The Shell

把前面写的所有东西都融合一下就好了。第一次写这么长一段 Rust(其实也就不到50行),算是新手经验大涨吧……