Rust 输出实现:从底层到宏的完整解析


一、结构概览

整个实现分为三层:

  1. 底层输出的实现struct Stdoutimpl Write
  2. 连接函数fn print
  3. 便利的宏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::Write Trait。 Trait 类似于其他语言中的 接口,定义了一组必须实现的方法。

  • struct Stdout; 定义一个空结构体,没有任何字段。它仅作为“标准输出”这个概念的载体存在。

  • impl Write for StdoutStdout 实现了 Write Trait。

  • fn write_str(&mut self, s: &str) -> fmt::Result Trait 要求必须实现该方法。 它接收一个字符串切片 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(这是 Write Trait 自带的高级方法)。
    • 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! 名字 {
    (匹配模式) => (替换内容);
}

  • #[macro_export] 使宏可以在 crate 外部模块中使用(例如在 main.rs 中)。

  • 匹配模式:

    ($fmt: literal $(, $($arg: tt)+)?)
    

    拆解如下:

    语法 含义
    $fmt: literal 匹配一个字面量字符串,如 "Hello"
    $( ... )? 整段为可选项
    , 匹配逗号
    $($arg: tt)+ 匹配一个或多个 Token Tree

    举例:

    print!("Year: {}", 2025) 匹配后:

    • $fmt"Year: {}"
    • $arg2025
  • 替换部分:

    $crate::console::print(format_args!($fmt $(, $($arg)+)?));
    

    展开过程:

    1. format_args! 创建 fmt::Arguments(格式化任务包)
    2. $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! 的完整旅程

  1. 你写下:

    println!("Page {}", 1);
    
  2. 编译期宏展开

    $crate::console::print(format_args!("Page {}\n", 1));
    
  3. 运行时调用

    • print 接收 fmt::Arguments 包(包含 "Page {}\n"1)。
    • 调用 Stdout.write_fmt(...)
  4. 格式化执行

    • write_fmt 根据格式生成 "Page 1\n"
    • 调用我们实现的 write_str
  5. 底层输出

    • write_str 逐个字符调用:

      P, a, g, e,  , 1, \n
      
    • 每个字符都通过 sbi::console_putchar() 输出。

  6. SBI 调用

    • console_putchar 执行 ecall,请求 M-Mode 固件打印字符。
  7. 最终结果

    • 在 QEMU 控制台显示:

      Page 1
      
    • 光标换行。


六、总结

层级 作用 关键技术
底层输出 负责字符输出到控制台 SBI 调用
连接函数 连接格式化系统与底层输出 fmt::Arguments + write_fmt
宏层 用户接口层,方便调用 macro_rules! + format_args! + concat!

通过这三层的协作,println! 宏最终实现了从高层格式化字符串到底层硬件输出的完整通路。