实现格式化输出

只能使用 console_putchar 这种苍白无力的输出手段让人头皮发麻。如果我们能使用 print! 宏的话该有多好啊!于是我们就来实现自己的 print!宏!

我们将这一部分放在 src/io.rs 中,先用 console_putchar 实现两个基础函数:

1. // src/lib.rs

3. // 由于使用到了宏,需要进行设置
4. // 同时,这个 module 还必须放在其他 module 前
5. #[macro_use]
6. mod io;

8. // src/io.rs

10. use crate::sbi;

12. // 输出一个字符
13. pub fn putchar(ch: char) {
14. sbi::console_putchar(ch as u8 as usize);
15. }

17. // 输出一个字符串
18. pub fn puts(s: &str) {
19. for ch in s.chars() {
20. putchar(ch);
21. }
22. }

而关于格式化输出, rust 中提供了一个接口 core::fmt::Write ,你需要实现函数

1. // required
2. fn write_str(&mut self, s: &str) -> Result

随后你就可以调用如下函数(会进一步调用write_str 实现函数)来进行显示。

1. // provided
2. fn write_fmt(mut self: &mut Self, args: Arguments<'_>) -> Result

write_fmt 函数需要处理 Arguments 类封装的输出字符串。而我们已经有现成的 format_args! 宏,它可以将模式字符串+参数列表的输入转化为 Arguments 类!比如 format_args!("{} {}", 1, 2) 。

因此,我们的 print! 宏的实现思路便为:

  1. 解析传入参数,转化为 format_args! 可接受的输入(事实上原封不动就行了),并通过 format_args! 宏得到 Arguments 类;
  2. 调用 write_fmt 函数输出这个类;

而为了调用 write_fmt 函数,我们必须实现 write_str 函数,而它可用 puts 函数来实现。支持print!宏的代码片段如下:

1. // src/io.rs

3. use core::fmt::{ self, Write };

5. struct Stdout;

7. impl fmt::Write for Stdout {
8. fn write_str(&mut self, s: &str) -> fmt::Result {
9. puts(s);
10. Ok(())
11. }
12. }

14. pub fn _print(args: fmt::Arguments) {
15. Stdout.write_fmt(args).unwrap();
16. }

18. #[macro_export]
19. macro_rules! print {
20. ($($arg:tt)*) => ({
21. $crate::io::_print(format_args!($($arg)*));
22. });
23. }

25. #[macro_export]
26. macro_rules! println {
27. () => ($crate::print!("\n"));
28. ($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
29. }

由于并不是重点就不在这里赘述宏的语法细节了(实际上我也没弄懂),总之我们实现了 print!, println! 两个宏,现在是时候看看效果了! 首先,我们在 panic 时也可以看看到底发生了什么事情了!

1. // src/lang_items.rs

3. #[panic_handler]
4. fn panic(info: &PanicInfo) -> ! {
5. println!("{}", info);
6. loop {}
7. }

其次,我们可以验证一下我们之前为内核分配的内存布局是否正确:

1. // src/init.rs

3. use crate::io;
4. use crate::sbi;

6. #[no_mangle]
7. pub extern "C" fn rust_main() -> ! {
8. extern "C" {
9. fn _start();
10. fn bootstacktop();
11. }
12. println!("_start vaddr = 0x{:x}", _start as usize);
13. println!("bootstacktop vaddr = 0x{:x}", bootstacktop as usize);
14. println!("hello world!");
15. panic!("you want to do nothing!");
16. loop {}
17. }

make run 一下,我们可以看到输出为:

格式化输出通过

1. _start vaddr = 0x80200000 2. bootstacktop vaddr = 0x80208000 3. hello world! 4. panicked at 'you want to do nothing!', src/init.rs:15:5

我们看到入口点的地址确实为我们安排的 0x80200000 ,同时栈的地址也与我们在内存布局中看到的一样。更重要的是,我们现在能看到内核 panic 的位置了!这将大大有利于调试。

目前所有的代码可以在这里找到。