前言
如果是在常规的用户态里玩Rust,那我极不建议放弃使用Rust的标准库。
使用no_std时,一般是因为:
- 你需要在内核态或其他特殊环境里使用Rust
- 你需要砍掉CRT依赖(注:Rust标准库几乎完全静态,仅产生CRT类的动态库依赖)
使用no_std
使用no_std
时需要在你的crate里根部源码文件(main.rs
或lib.rs
)中使用#![no_std]
进行声明。
声明了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,最后用#[global_allocator]
语句声明其为全局内存分配器。
以Windows为例,我们可以用系统提供的HeapAlloc
(请不要用VirtualAlloc
,因为它的分配粒度太大,并且还要走系统调用,每次分配内存都用这个函数就太慢太浪费内存了):
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()
}
}
#[global_allocator] static GLOBAL_ALLOCATOR:SysAlloc=SysAlloc;
注意,alloc_zeroed
和realloc
是可选的特征方法,可以不实现。
格式化字符串与输出
和C不同,Rust自己的ABI不允许函数有可变的参数数量,但是宏允许可变的参数数量!最常用的print!
, println!
等等其实都是宏,完全可以自己实现:
#[macro_export] macro_rules! print
{
($($args:tt)*) =>
{
internal_print(format_args!($($args)*))
};
}
#[macro_export] macro_rules! println
{
()=>
{
print!("\n")
};
($($args:tt)*)=>
{
print!("{}\n",format_args!($($args)*))
};
}
这个internal_print
函数需要自己实现。这个format_args!
宏是rust内置的宏,即使是在no_std
里也可以用。它会返回一个Arguments的类型。
它虽然实现了一个as_str
的方法,但这个方法很鸡肋:只有当优化器可以直接格式化这个字符串的时候才能返回字符串(比如print!("{}+{}={}",1,2,3);
一定会被优化器优化成输出1+2=3
),否则一定会返回None
。
这里可以定义一个用于接收格式化字符串的类型,并为其实现Write
trait。
use core::fmt;
struct FormatBuffer
{
buffer:[u8;512],
used:usize
}
impl Default for FormatBuffer
{
fn default()->Self
{
Self
{
buffer:[0;512],
used:0
}
}
}
impl fmt::Write for FormatBuffer
{
fn write_str(&mut self, s: &str) -> fmt::Result
{
let remainder=&mut self.buffer[self.used..];
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
函数了!
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的时候就直接输出到控制台,但你会遇到竞态条件的问题。
入口函数
如果你的程序不是库,则需要同时用#![no_main]
来标记没有Rust可识别的入口函数。
在MSVC中,以控制台为例,默认的入口函数是mainCRTStartup
,但你也可以给链接器加上/ENTRY
参数来修改入口函数名。
声明入口函数时,需要用#[no_mangle]
取消rust编译器mangle函数符号的行为,并用extern "C"
标记这个函数使用C的ABI。
#[no_mangle] extern "C" fn start()
{
// Entry point starts here.
}
与此同时,你需要在crate的根目录里创建一个build.rs
文件(和Cargo.toml
文件平级),来给链接器增加参数:
fn main()
{
println!("cargo:rustc-link-arg=/ENTRY:start");
}
使用外部库
当你使用外部库时,需要确认它们是否支持no_std
的环境。一般而言,支持no_std
的crate会在简介里就强调自己支持no_std
。
此外还需要按照它们的说明来进行配置。比如指定default-feature
为false
。每个crate各不相同,有的可能无需配置就可以支持no_std
了。