【Rust】在no_std里进行一个print
本帖最后由 tangptr@126.com 于 2025-8-21 12:37 编辑# 前言
Rust的`print!`和`println!`在`#!`里是不可用的,得自己去实现。
# 格式化参数
在Rust中,函数是不可以有可变数量的参数的,但是宏可以!Rust提供了名为[`format_args`](https://doc.rust-lang.org/core/macro.format_args.html)的宏。它可以将输入进来的参数构造为一个[`Arguments`](https://doc.rust-lang.org/core/fmt/struct.Arguments.html)结构体。与这个结构体直接相关的,就是[`Write`](https://doc.rust-lang.org/core/fmt/trait.Write.html) trait。你需要实现这个trait里的`write_str`方法来把`Arguments`变成字符串。
对应到C语言,可以粗略地理解为`Arguments`是`va_list`,`format_args`是`va_start`,而`Write::write_str`是`vsnprintf`中针对不同类型的打印函数(你会发现Rust这个套路比C的`vsnprintf`好使多了)。
接下来实现`print`和`println`宏:
```Rust
macro_rules! print
{
($($arg:tt)*) =>
{
(internal_print(format_args!($($arg)*)))
};
}
macro_rules! println
{
() =>
{
print!("\n")
};
($($arg:tt)*) =>
{
print!("{}\n",format_args!($($arg)*))
};
}
```
而`internal_print`函数的实现方法稍后再说。
# 栈上Print
顾名思义,就是把`Arguments`写进栈上的字符串。这个方法会限制一次print的长度,但是可以避免内存分配,性能较高,而且在Windows驱动里的高IRQL的条件下非常实用。这里采用512是也是因为Windows内核的`DbgPrint`一次最多只能发送512个字节。[微软的windows-drivers-rs里的栈上print就是我实现的。](https://github.com/microsoft/windows-drivers-rs/pull/233)
首先我们定义一个栈上缓冲区:
```Rust
pub struct FormatBuffer
{
used:usize,
buffer:MaybeUninit<>
}
```
这里用`MaybeUninit`可以避免无意义的初始化行为(直接``的开销很大)。接下来实现一下`Default` trait用来初始化:
```Rust
impl Default for FormatBuffer
{
fn default() -> Self
{
Self
{
used:0,
buffer:MaybeUninit::uninit()
}
}
}
```
接下来实现`Write` trait:
```Rust
impl fmt::Write for FormatBuffer
{
fn write_str(&mut self, s: &str) -> fmt::Result
{
let remainder=unsafe{&mut self.buffer.assume_init_mut()};
let current=s.as_bytes();
if remainder.len()<current.len()
{
return Err(fmt::Error);
}
remainder[..current.len()].copy_from_slice(current);
self.used+=current.len();
Ok(())
}
}
```
顺便实现一下`as_str`方法,这样一来打印起来更直观:
```Rust
impl FormatBuffer
{
pub fn as_slice(&self)->&
{
unsafe
{
&self.buffer.assume_init_ref()[..self.used]
}
}
pub fn as_str(&self)->&str
{
unsafe
{
str::from_utf8_unchecked(self.as_slice())
}
}
}
```
以Windows平台为例,实现方法如下:
```Rust
static mut STDOUT_HANDLE: LazyCell<HANDLE>=LazyCell::new(|| unsafe{GetStdHandle(STD_OUTPUT_HANDLE)});
pub fn internal_print(args:fmt::Arguments)
{
let mut w=FormatBuffer::default();
let r=fmt::write(&mut w,args);
if r.is_ok()
{
let s=w.as_str();
let mut utf16_buff:MaybeUninit<>=MaybeUninit::uninit();
let mut written_length:u32=0;
let mut i:usize=0;
for c in s.encode_utf16()
{
unsafe
{
utf16_buff.assume_init_mut()=c;
}
i+=1;
}
unsafe
{
WriteConsoleW(*STDOUT_HANDLE,utf16_buff.assume_init_ref().as_ptr(),i as u32,&raw mut written_length,null());
}
}
}
```
注意,Rust的字符串是UTF-8的,不能直接传给`WriteConsoleA`函数,否则打印汉字什么的就操蛋了。需要先转为UTF-16然后传给`WriteConsoleW`。
这里用了LazyCell类型来存放`stdout`的句柄,可以降低初始化全局变量的复杂度。但注意,这里为了演示,用了`LazyCell`(多线程不安全,所以只能用`static mut`声明它)。涉及多线程开发时需要用`LazyLock`(很不幸,`no_std`环境里没有,需要自己实现锁),或者在程序启动时提前用`force`方法对其提前初始化。
# 堆上Print
顾名思义,就是把`Arguments`写进堆上的字符串,这种方法不限制一次print的最大长度。我们可以直接用`alloc`库里提供的`String`类型,因为它已经实现过`Write` trait了。我们实现一下`internal_print`就可以了:
```Rust
pub fn internal_print(args:fmt::Arguments)
{
let mut w=String::new();
let r=fmt::write(&mut w,args);
if r.is_ok()
{
let v:Vec<u16>=w.encode_utf16().collect();
let mut written_length:u32=0;
unsafe
{
WriteConsoleW(*STDOUT_HANDLE,v.as_ptr(),v.len() as u32,&raw mut written_length,null());
}
}
}
```
但是在`no_std`的环境下,需要实现一个内存分配器才能用堆上分配。因此需要实现[`GlobalAlloc` trait](https://doc.rust-lang.org/core/alloc/trait.GlobalAlloc.html)。以`HeapAlloc`为例:
```Rust
static mut PROCESS_HEAP: LazyCell<HANDLE>=LazyCell::new(|| unsafe{GetProcessHeap()});
struct WindowsAllocator;
unsafe impl GlobalAlloc for WindowsAllocator
{
unsafe fn alloc(&self,layout: Layout)->*mut u8
{
unsafe
{
HeapAlloc(*PROCESS_HEAP,0,layout.size()).cast()
}
}
unsafe fn dealloc(&self,ptr:*mut u8,_layout: Layout)
{
unsafe
{
HeapFree(*PROCESS_HEAP,0,ptr.cast());
}
}
unsafe fn realloc(&self,ptr:*mut u8,_layout: Layout,new_size:usize)->*mut u8
{
unsafe
{
HeapReAlloc(*PROCESS_HEAP,0,ptr.cast(),new_size).cast()
}
}
unsafe fn alloc_zeroed(&self,layout: Layout)->*mut u8
{
unsafe
{
HeapAlloc(*PROCESS_HEAP,HEAP_ZERO_MEMORY,layout.size()).cast()
}
}
}
```
其中,`realloc`和`alloc_zeroed`是可选的。如果你用的内存分配器没有专门的优化,就不需要实现。(比如`realloc`的默认实现是分配一个大的buffer,copy旧的过去之后,再free掉。)
注意,以上实现不考虑内存对齐的问题!但是对于字符串来说,对齐粒度无所谓。以后再单开一篇讲内存分配。
# 结语
你会发现,其实也就是`internal_print`函数需要根据平台不同而调用不同的API(以及堆上print需要的分配器),其他的都是通用的。
页:
[1]