tangptr@126.com 发表于 2024-11-21 02:29:53

【Rust】一些在no_std里开发的技巧

本帖最后由 tangptr@126.com 于 2024-11-21 02:37 编辑


# 前言
如果是在常规的用户态里玩Rust,那我极不建议放弃使用Rust的标准库。
使用no_std时,一般是因为:
1. 你需要在内核态或其他特殊环境里使用Rust
2. 你需要砍掉CRT依赖(注:Rust标准库几乎完全静态,仅产生CRT类的动态库依赖)

# 使用`no_std`
使用`no_std`时需要在你的crate里根部源码文件(`main.rs`或`lib.rs`)中使用`#!`进行声明。
声明了`no_std`后,意味着你放弃了Rust的`std` crate,但你仍然可以使用`core` crate,很多`std`里的东西也能在`core`里找到,但是像I/O以及线程之类的库就消失了。

## 堆内存
使用`no_std`后,堆内存类型(如`Vec`,`String`,`Box`等)仍然可以在`alloc` crate里找到,但你需要用`extern crate alloc`来声明使用`alloc` crate。
此外你还需要声明一个全局内存分配器(见后文的例子)才能使用堆内存。

## 调试输出
使用`no_std`后,控制台输出的那些宏(如`print!`,`println!`等)也就消失了。但是你仍然能自己实现调试输出的宏(见后文的例子)。

# 使用`windows-sys`库
微软官方同时发布了`windows`库和`windows-sys`库。注意,前者是依赖`std`的!所以当你使用`no_std`时,请使用后者。

## 全局内存分配器
在声明全局内存分配器时,需要声明一个用于分配堆内存的类型,并为其实现`GlobalAlloc`的trait,最后用`#`语句声明其为全局内存分配器。
以Windows为例,我们可以用系统提供的`HeapAlloc`(请不要用`VirtualAlloc`,因为它的分配粒度太大,并且还要走系统调用,每次分配内存都用这个函数就太慢太浪费内存了):
```Rust
use core::alloc::GlobalAlloc;
use windows_sys::Win32::System::Memory::*;

struct SysAlloc;

unsafe impl GlobalAlloc for SysAlloc
{
        unsafe fn alloc(&self, layout: core::alloc:: Layout) -> *mut u8
        {
                HeapAlloc(GetProcessHeap(),0,layout.size()).cast()
        }

        unsafe fn dealloc(&self, ptr: *mut u8, _layout: core::alloc:: Layout)
        {
                HeapFree(GetProcessHeap(),0,ptr.cast());
        }

        unsafe fn alloc_zeroed(&self, layout: core::alloc:: Layout) -> *mut u8
        {
                HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,layout.size()).cast()
        }

        unsafe fn realloc(&self, ptr: *mut u8, _layout: core::alloc:: Layout, new_size: usize) -> *mut u8
        {
                HeapReAlloc(GetProcessHeap(),0,ptr.cast(),new_size).cast()
        }
}

# static GLOBAL_ALLOCATOR:SysAlloc=SysAlloc;
```
注意,`alloc_zeroed`和`realloc`是可选的特征方法,可以不实现。

## 格式化字符串与输出
和C不同,Rust自己的ABI不允许函数有可变的参数数量,但是宏允许可变的参数数量!最常用的`print!`, `println!`等等其实都是宏,完全可以自己实现:
```Rust
# macro_rules! print
{
        ($($args:tt)*) =>
        {
                internal_print(format_args!($($args)*))
        };
}

# macro_rules! println
{
        ()=>
        {
                print!("\n")
        };
        ($($args:tt)*)=>
        {
                print!("{}\n",format_args!($($args)*))
        };
}
```
这个`internal_print`函数需要自己实现。这个`format_args!`宏是rust内置的宏,即使是在`no_std`里也可以用。它会返回一个(https://doc.rust-lang.org/core/fmt/struct.Arguments.html)的类型。
它虽然实现了一个`as_str`的方法,但这个方法很鸡肋:只有当优化器可以直接格式化这个字符串的时候才能返回字符串(比如`print!("{}+{}={}",1,2,3);`一定会被优化器优化成输出`1+2=3`),否则一定会返回`None`。
这里可以定义一个用于接收格式化字符串的类型,并为其实现[`Write` trait](https://doc.rust-lang.org/core/fmt/trait.Write.html)。
```Rust
use core::fmt;

struct FormatBuffer
{
        buffer:,
        used:usize
}

impl Default for FormatBuffer
{
        fn default()->Self
        {
                Self
                {
                        buffer:,
                        used:0
                }
        }
}

impl fmt::Write for FormatBuffer
{
        fn write_str(&mut self, s: &str) -> fmt::Result
        {
                let remainder=&mut self.buffer;
                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(())
        }
}
```
接下来就可以实现`internal_print`函数了!
```Rust
fn internal_print(args:fmt::Arguments)
{
        let mut w=FormatBuffer::default();
        let r=fmt::write(&mut w,args);
        if r.is_ok()
        {
                let b=&w.buffer;
                let h=unsafe{GetStdHandle(STD_OUTPUT_HANDLE)};
                if !h.is_null()
                {
                        let mut size:u32=w.used as u32;
                        let _=unsafe{WriteConsoleA(h,b.as_ptr(),size,&raw mut size,null())};
                }
        }
}
```
注意:虽然你可以直接在实现`Write` trait的时候就直接输出到控制台,但你会遇到竞态条件的问题。

# 入口函数
如果你的程序不是库,则需要同时用`#!`来标记没有Rust可识别的入口函数。
在MSVC中,以控制台为例,默认的入口函数是`mainCRTStartup`,但你也可以给链接器加上`/ENTRY`参数来修改入口函数名。
声明入口函数时,需要用`#`取消rust编译器mangle函数符号的行为,并用`extern "C"`标记这个函数使用C的ABI。
```Rust
# extern "C" fn start()
{
    // Entry point starts here.
}
```
与此同时,你需要在crate的根目录里创建一个`build.rs`文件(和`Cargo.toml`文件平级),来给链接器增加参数:
```Rust
fn main()
{
        println!("cargo:rustc-link-arg=/ENTRY:start");
}
```

# 使用外部库
当你使用外部库时,需要确认它们是否支持`no_std`的环境。一般而言,支持`no_std`的crate会在简介里就强调自己支持`no_std`。
此外还需要按照它们的说明来进行配置。比如指定`default-feature`为`false`。每个crate各不相同,有的可能无需配置就可以支持`no_std`了。
页: [1]
查看完整版本: 【Rust】一些在no_std里开发的技巧