Rust 输出实现:从底层到宏的完整解析
一、结构概览
整个实现分为三层:
- 底层输出的实现(
struct Stdout和impl Write) - 连接函数(
fn print) - 便利的宏(
print!和println!)
二、底层输出的实现
use crate::sbi::console_putchar;
use core::fmt::{self, Write};
struct Stdout;
impl Write for Stdout {
fn write_str(&mut self, s: &str) -> fmt::Result {
for c in s.chars() {
console_putchar(c as usize);
}
Ok(())
}
}
这是整个模块的基石:它把 Rust 的标准格式化系统(core::fmt)与底层的 SBI 调用连接起来。
关键点解析
-
use core::fmt::{self, Write};从core库(no_std 环境下可用)导入fmt::WriteTrait。 Trait 类似于其他语言中的 接口,定义了一组必须实现的方法。 -
struct Stdout;定义一个空结构体,没有任何字段。它仅作为“标准输出”这个概念的载体存在。 -
impl Write for Stdout为Stdout实现了WriteTrait。 -
fn write_str(&mut self, s: &str) -> fmt::ResultTrait 要求必须实现该方法。 它接收一个字符串切片s,然后“把它写到某个地方去”。 -
for c in s.chars() { console_putchar(c as usize); }实现具体输出逻辑。遍历字符串中的每个字符,逐个调用console_putchar将其输出。 -
Ok(())表示写入成功。fmt::Result是一个别名,等价于Result<(), fmt::Error>。
小结
我们创建了一个 Stdout,并告诉它如何将字符串输出到屏幕上(通过 SBI 调用实现)。
三、连接函数
pub fn print(args: fmt::Arguments) {
Stdout.write_fmt(args).unwrap();
}
这个函数是宏与底层实现之间的桥梁(胶水层)。
解析
-
pub fn print(args: fmt::Arguments)这是公开函数,接收一个特殊类型fmt::Arguments。 -
fmt::Arguments是由编译器内置宏format_args!创建的。 它是一个“格式化任务包”,包含:- 格式化字符串(如
"Hello, {}!") - 对应的参数(如
2025)
它的设计目标是避免运行时创建临时字符串,从而高效构建格式化输出。
- 格式化字符串(如
-
Stdout.write_fmt(args).unwrap();逐步解释:- 创建一个
Stdout实例。 - 调用
write_fmt(这是WriteTrait 自带的高级方法)。 write_fmt解析fmt::Arguments,并自动调用我们实现的write_str。.unwrap()表示假设写入不会失败,否则 panic。
- 创建一个
小结
print 接收一个格式化任务包,然后命令 Stdout 去执行它的写入任务。
四、便利的宏
宏是使用层面的关键,它让 print! 与 println! 看起来像普通函数调用。
/// print string macro
#[macro_export]
macro_rules! print {
($fmt: literal $(, $($arg: tt)+)?) => {
$crate::console::print(format_args!($fmt $(, $($arg)+)?));
}
}
/// println string macro
#[macro_export]
macro_rules! println {
($fmt: literal $(, $($arg: tt)+)?) => {
$crate::console::print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?));
}
}
宏基础回顾
macro_rules! 是 Rust 的编译期元编程工具。
它在编译时执行匹配与替换操作。
基本形式:
macro_rules! 名字 {
(匹配模式) => (替换内容);
}
print! 宏详解
-
#[macro_export]使宏可以在 crate 外部模块中使用(例如在main.rs中)。 -
匹配模式:
($fmt: literal $(, $($arg: tt)+)?)拆解如下:
语法 含义 $fmt: literal匹配一个字面量字符串,如 "Hello"$( ... )?整段为可选项 ,匹配逗号 $($arg: tt)+匹配一个或多个 Token Tree 举例:
print!("Year: {}", 2025)匹配后:$fmt→"Year: {}"$arg→2025
-
替换部分:
$crate::console::print(format_args!($fmt $(, $($arg)+)?));展开过程:
format_args!创建fmt::Arguments(格式化任务包)$crate::console::print(...)调用前面定义的print函数
println! 宏详解
println! 与 print! 几乎相同,只在一点上不同:
concat!($fmt, "\n")
concat! 是编译期宏,用于拼接字符串字面量。
因此在编译阶段 "Hello" 会被拼接成 "Hello\n"。
示例展开
当你写下:
println!("Hello");
编译器展开为:
$crate::console::print(format_args!(concat!("Hello", "\n")));
进一步合并为:
$crate::console::print(format_args!("Hello\n"));
于是最终输出带有换行符。
五、一次 println! 的完整旅程
-
你写下:
println!("Page {}", 1); -
编译期宏展开
$crate::console::print(format_args!("Page {}\n", 1)); -
运行时调用
print接收fmt::Arguments包(包含"Page {}\n"与1)。- 调用
Stdout.write_fmt(...)。
-
格式化执行
write_fmt根据格式生成"Page 1\n"。- 调用我们实现的
write_str。
-
底层输出
-
write_str逐个字符调用:P, a, g, e, , 1, \n -
每个字符都通过
sbi::console_putchar()输出。
-
-
SBI 调用
console_putchar执行ecall,请求 M-Mode 固件打印字符。
-
最终结果
-
在 QEMU 控制台显示:
Page 1 -
光标换行。
-
六、总结
| 层级 | 作用 | 关键技术 |
|---|---|---|
| 底层输出 | 负责字符输出到控制台 | SBI 调用 |
| 连接函数 | 连接格式化系统与底层输出 | fmt::Arguments + write_fmt |
| 宏层 | 用户接口层,方便调用 | macro_rules! + format_args! + concat! |
通过这三层的协作,println! 宏最终实现了从高层格式化字符串到底层硬件输出的完整通路。