如何在没有标准库和分配器的情况下将输出格式化为字节数组?

16

我想要做类似于:

let x = 123;
let mut buf = [0 as u8; 20];
format_to!(x --> buf);
assert_eq!(&buf[..3], &b"123"[..]);

使用#![no_std],并且没有任何内存分配器。

据我所知,u64core::fmt::Display的实现,如果可能,我想使用它。

换句话说,我想做类似于format!(...)的事情,但是不使用内存分配器。如何做到这一点?

2个回答

17

让我们从标准版本开始:

use std::io::Write;

fn main() {
    let x = 123;
    let mut buf = [0 as u8; 20];
    write!(&mut buf[..], "{}", x).expect("Can't write");
    assert_eq!(&buf[0..3], b"123");
}

如果我们移除标准库:
#![feature(lang_items)]
#![no_std]

use core::panic::PanicInfo;

#[lang = "eh_personality"]
extern "C" fn eh_personality() {}

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    loop {}
}

fn main() {
    let x = 123;
    let mut buf = [0 as u8; 20];
    write!(&mut buf[..], "{}", x).expect("Can't write");
    assert_eq!(&buf[0..3], b"123");
}

我们遇到了错误

error[E0599]: no method named `write_fmt` found for type `&mut [u8]` in the current scope
  --> src/main.rs:17:5
   |
17 |     write!(&mut buf[..], "{}", x).expect("Can't write");
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

write_fmt 是由核心库中的 core::fmt::Write 实现的。如果我们自己实现,就能够传递该错误:

#![feature(lang_items)]
#![feature(start)]
#![no_std]

use core::panic::PanicInfo;

#[lang = "eh_personality"]
extern "C" fn eh_personality() {}

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    loop {}
}

use core::fmt::{self, Write};

struct Wrapper<'a> {
    buf: &'a mut [u8],
    offset: usize,
}

impl<'a> Wrapper<'a> {
    fn new(buf: &'a mut [u8]) -> Self {
        Wrapper {
            buf: buf,
            offset: 0,
        }
    }
}

impl<'a> fmt::Write for Wrapper<'a> {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        let bytes = s.as_bytes();

        // Skip over already-copied data
        let remainder = &mut self.buf[self.offset..];
        // Check if there is space remaining (return error instead of panicking)
        if remainder.len() < bytes.len() { return Err(core::fmt::Error); }
        // Make the two slices the same length
        let remainder = &mut remainder[..bytes.len()];
        // Copy
        remainder.copy_from_slice(bytes);

        // Update offset to avoid overwriting
        self.offset += bytes.len();

        Ok(())
    }
}

#[start]
fn start(_argc: isize, _argv: *const *const u8) -> isize {
    let x = 123;
    let mut buf = [0 as u8; 20];
    write!(Wrapper::new(&mut buf), "{}", x).expect("Can't write");
    assert_eq!(&buf[0..3], b"123");
    0
}

请注意,我们正在将io::Cursor的行为复制到此包装器中。通常,对&mut [u8]的多次写入会相互覆盖。这对于重用分配很好,但在连续写入相同数据时没有用处。
然后,如果您想要的话,只需要编写一个宏。
您还可以使用像arrayvec这样的crate,该crate已经为您编写了此代码。这是未经测试的:
#![feature(lang_items)]
#![feature(start)]
#![no_std]

use core::panic::PanicInfo;

#[lang = "eh_personality"]
extern "C" fn eh_personality() {}

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

use arrayvec::ArrayString; // 0.4.10
use core::fmt::Write;

#[start]
fn start(_argc: isize, _argv: *const *const u8) -> isize {
    let x = 123;
    let mut buf = ArrayString::<[u8; 20]>::new();
    write!(&mut buf, "{}", x).expect("Can't write");
    assert_eq!(&buf, "123");
    0
}

1
这需要在write_str()中加上 self.offset += bytes.len();,但其他方面都可以正常工作。想知道为什么格式化浮点数只打印了小数部分。 - Michael Böckling
请注意,这并不是直接复制io::Cursor,因为io::Cursor实现了std::io::Write,而这个实现了{core,std}::fmt::Write。这些Write特征在它们的结果类型上有所不同,但是write!宏愉快地接受任何实现了一个write_fmt方法(两个Write特征都有)并返回相应结果的东西。 - chrysn
@chrysn 当然可以;我的意思是只有行为。您可以根据需要选择实现两种特征。 - Shepmaster

2

使用 bare_io

use bare_io::{Cursor, Write};

let mut buf = [0 as u8; 256];
let mut cur = Cursor::new(&mut buf[..]);
write!(&mut cur, "hello world, stack buf, {}\n\0", 234).expect("!write");
unsafe { puts(buf.as_ptr()) };

使用 bare_iosmallvecalloc

use smallvec::{Array, SmallVec};

struct WriteSmallVec<A: Array<Item = u8>>(SmallVec<A>);
impl<A: Array<Item = u8>> Write for WriteSmallVec<A> {
    fn write(&mut self, buf: &[u8]) -> bare_io::Result<usize> {
        self.0.extend_from_slice(buf);
        Ok(buf.len())
    }
    fn flush(&mut self) -> bare_io::Result<()> {
        Ok(())
    }
}

let mut sv = WriteSmallVec(SmallVec::<[u8; 256]>::new());
write!(&mut sv, "hello world, SmallVec, prev len: {}\n\0", len).expect("!write");
unsafe { puts(sv.0.as_ptr()) };

使用 bare_io,修补的 inlinable_stringalloc:

use core::fmt::Write;
use inlinable_string::{InlinableString, StringExt};

let mut is = InlinableString::new();
write!(&mut is, "hello world, InlinableString, {}\n\0", 345).expect("!write");
unsafe { puts(is.as_ptr()) };

测试于Linux内核中,

cargo build --release -Z build-std=core,alloc --target=x86_64-linux-kernel

在此输入图片描述

同时,我们进行了一些基准测试,将简单数组与SmallVec和InlinableString进行了比较:https://gitlab.com/artemciy/lin-socks/-/blob/95d2bb96/bench/stack-string.rs

p.s. bare-io已被撤回,详情请参见此处


为什么这个被撤下了?链接的问题似乎与整个软件包的撤下实际上没有关系。 - tufelkinder
2
嘿,@tufelkinder!看起来它已经被重命名core2了。 - ArtemGr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接