3个版本

0.1.2 2023年9月10日
0.1.1 2023年9月4日
0.1.0 2023年9月3日

#100 in FFI

Apache-2.0

30KB
351

fmtbuf

将格式化的字符串写入固定缓冲区。当您需要将用户提供的缓冲区写入时,这非常有用,尤其是在编写与C的FFI时,字符串需要以null终止符结尾。

用法

use fmtbuf::WriteBuf;
use std::fmt::Write;

fn main() {
    let mut buf: [u8; 10] = [0; 10];
    let mut writer = WriteBuf::new(&mut buf);
    if let Err(e) = write!(&mut writer, "🚀🚀🚀") {
        println!("write error: {e:?}");
    }
    let written_len = match writer.finish_with("\0") {
        Ok(len) => len, // <- won't be hit since 🚀🚀🚀 is 12 bytes
        Err(len) => {
            println!("writing was truncated");
            len
        }
    };
    let written = &buf[..written_len];
    println!("wrote {written_len} bytes: {written:?}");
    println!("result: {:?}", std::str::from_utf8(written));
}

🚀🚀

主要用例是实现像 strerror_r 这样的API,其中用户提供缓冲区。

use std::{ffi, fmt::Write, io::Error};
use fmtbuf::WriteBuf;

#[no_mangle]
pub unsafe extern "C" fn mylib_strerror(
    err: *mut Error,
    buf: *mut ffi::c_char,
    buf_len: usize
) {
    let mut buf = unsafe {
        // Buffer provided by a users
        std::slice::from_raw_parts_mut(buf as *mut u8, buf_len)
    };
    // Reserve at least 1 byte at the end because we will always
    // write '\0'
    let mut writer = WriteBuf::with_reserve(buf, 1);

    // Use the standard `write!` macro (no error handling for
    // brevity) -- note that an error here might only indicate
    // write truncation, which is handled gracefully be this
    // library's finish___ functions
    let _ = write!(writer, "{}", err.as_ref().unwrap());

    // null-terminate buffer or add "..." if it was truncated
    let _written_len = writer.finish_with_or(b"\0", b"...\0")
        // Err value is also number of bytes written
        .unwrap_or_else(|e| e);
}

特性

!#[no_std]

通过禁用默认功能并重新启用 "std" 功能来启用对 !#[no_std] 的支持。

fmtbuf = { version = "*", default_features = false }

常见问题解答

为什么不能写入 &mut [u8]

Rust标准库中的 std::io::Write trait为 &mut [u8] 实现,可以用来代替这个库。这种方法的问题是没有UTF-8编码支持(此外,在 #![no_std] 中也不可用)。

use std::io::{Cursor, Write};

fn main() {
    let mut buf: [u8; 10] = [0; 10];
    let mut writer = Cursor::<&mut [u8]>::new(&mut buf);
    if let Err(e) = write!(&mut writer, "rocket: 🚀") {
        println!("write error: {e:?}");
    }
    let written_len = writer.position() as usize;
    let written = &buf[..written_len];
    println!("wrote {written_len} bytes: {written:?}");
    println!("result: {:?}", std::str::from_utf8(written));
}

运行此程序将显示错误

write error: Error { kind: WriteZero, message: "failed to write whole buffer" }
wrote 10 bytes: [114, 111, 99, 107, 101, 116, 58, 32, 240, 159]
result: Err(Utf8Error { valid_up_to: 8, error_len: None })

问题是,""rocket: 🚀""被编码成一个12字节的序列——🚀表情符号在UTF-8中以4字节编码为b"\xf0\x9f\x9a\x80"——但我们的目标缓冲区只有10字节长。对光标的write!操作天真地切断了🚀的编码过程,使得编码后的字符串无效UTF-8,尽管它已经将光标前进了整个10字节。这是预期的,因为std::io::Write来自io,不知道任何关于字符串编码的信息;它在u8级别上操作。

可以使用std::str::Utf8Error来正确地截断buf。唯一的问题是性能。由于std::str::from_utf8向前扫描整个字符串,这需要O(n)的时间来测试,而fmtbuf只需在O(1)时间内完成,因为它只查看最后的几个字节。

关于Unicode的“奇怪格式字符”呢?

这个库只能保证目标缓冲区的内容是有效的UTF-8。它不对由于Unicode格式字符(特别是U+200DU+200EU+200F)截断而产生的语义做任何保证。

什么?

如果你不知道那些是什么,没关系。只要知道人类语言很复杂,Unicode有一套功能使事情成为可能,但当你没有足够的空间在固定大小的缓冲区中存储时,事情就会出错。如果你想了解更多细节,请参阅下面的小节。

U+200D:零宽度连接符

某些图形符号,如"🙇‍♀"(你可能将其视为两个单独的图形符号),由三个码点组成

  1. 🙇 U+1F647 "Person Bowing Deeply"
  2. U+200D "Zero Width Joiner"
  3. U+2640 "Female Sign"

因此,单个字符图形是10字节的序列 b"\xf0\x9f\x99\x87\xe2\x80\x8d\xe2\x99\x80"。问题是:如果缓冲区大小只有9字节怎么办?在截断时,此库将丢弃应作为修饰符的码点。此库将截断最后一个Unicode码点,留下 b"\xf0\x9f\x99\x87\xe2\x80\x8d" -- 一个鞠躬的人和一个零宽连接符与空内容连接,因为女性修饰符无法容纳。

U+200EU+200F:方向标记

考虑阿拉伯语,它是一种从右到左的语言

‏希望有一天Rust能取代C++。‎

根据您的文本编辑器或浏览器对从右到左呈现的兼容性,您可能会以任何一种方式看到文本(如果文本右侧有“آمل”,则表示呈现正常)。但请注意,借用的单词“Rust”和“C++”在从右到左的文本中仍然按从左到右的方式拼写(或者应该是这样)。这是通过编码 U+200E 左到右标记,然后写入借用文本,然后写入 U+200F 右到左标记 来实现的。

如果文本被反转,但缓冲区中没有足够的空间来翻转它,会发生什么?在截断时,此库可能会让您处于文本反转运行的中途。

埃及象形文字和其他类似语言的构造面临类似的问题。应该在何处截断?此库不知道“𓁪𓌍𓃻”和“𓁪𓌍”之间的区别。弄清楚这一点是更高层次结构的责任。

无运行时依赖