#格式 #二进制 #大小 #减少 #最终 #输出 #hifmt

hifmt-macros

在不包含Rust代码段的二进制文件中格式化输出,以减少最终二进制文件的大小

2 个版本

0.2.2 2024年5月17日
0.2.1 2023年11月14日

#1394过程宏

Download history 3/week @ 2024-04-15 3/week @ 2024-04-22 142/week @ 2024-05-13 22/week @ 2024-05-20 1/week @ 2024-06-03 5/week @ 2024-06-10

每月 118 次下载
用于 hifmt

MIT/Apache

16KB
368

hifmt - 不包含Rust代码段的二进制文件中格式化输出,以减少最终二进制文件的大小

orion_cfmt 重命名为 hifmt

在嵌入式系统中受限,hifmt 的目标是减少最终二进制文件的大小。使用 hifmt,可以将Rust格式化打印函数转换为C的格式化打印。

用法

格式化字符串的规范如下定义

format-spec = {:d|u|x|p|e|cs|rs|rb|cc|rc}
d: print int as digits, see %lld
u: print int as hexdecimals, see %llu
x: print int as hexdecimals a/b/c/d/e/f, see %llx
p: print pointer,see %p
e: print floating point numbers, see %e
cs: print C string pointers, see %s
rs: print Rust string &str, see %.*s
rb: print Rust slice &[u8], see %.*s
cc: print ASCII char into int type in C, see %c
rc: print Rust char into unicode scalar value, see %s

转换后的C函数定义为 dprintf(int fd, const char* format, ...),需要用户代码中实现。第一个参数是 fd。值 1 表示 stdout,值 2 表示 stderr。或者 snprintf(char* buf, int len, const char* format, . . . ) ;

宏的返回值与 'dprintf' 和 'snprintf' 相同。

hifmt 提供以下宏

// print to stdout, converted into dprintf(1, format, ...)
cprint!(format: &'static str, ...);
print!(format: &'static str, ...);

// append \n to cprint!, converted into dprintf(1, format "\n", ...)
cprintln!(format: &'static str, ...);
println!(format: &'static str, ...);

// print to stderr, converted into dprintf(2, format, ...)
ceprint!(format: &'static str, ...);
eprint!(format: &'static str, ...);

// append \n to ceprint!, converted into dprintf(2, format "\n", ...)
ceprintln!(format: &'static str, ...);
eprintln!(format: &'static str, ...);

//write to buf, converted into snprintf(buf.as_byte().as_ptr(), buf.len(), format, ...)
csprint!(buf: &mut str, format: &'static str, ...)
sprint!(buf: &mut str, format: &'static str, ...)

//write to buf, converted into snprintf(buf.as_ptr(), buf.len(), format, ...)
cbprint!(buf: &mut [u8], format: &'static str, ...)
bprint!(buf: &mut [u8], format: &'static str, ...)

Rust中的用法如下所示

#[link(name = "c")]
extern "C" {
	fn dprintf(fd: i32, format: *const u8, ...) -> i32;
	fn snprintf(buf: *mut u8, len: usize, format: *const u8, ...) -> i32;
}
fn main() {
    let s = vec![b'\0'; 100];
    let s = &mut String::from_utf8(s).unwrap();
    hifmt::sprint!(s, "sprint({:rs})", "hello snprintf");

    let b = &mut [0_u8; 100];
    hifmt::bprint!(b, "bprint({:rs})", "hello snprintf");

    hifmt::println!("d = {:d} u = {:u} x = {:x} e = {:e} p = {:p} cstr = {:cs} str = {:rs} bytes = {:rb}",
        100, 200, 300, 400.0, b, b, s, b);
}

经过 cargo expand 后,上述代码变为

#[link(name = "c")]
extern "C" {
    fn dprintf(fd: i32, format: *const u8, ...) -> i32;
    fn snprintf(buf: *mut u8, len: usize, format: *const u8, ...) -> i32;
}
fn main() {
    let s = ::alloc::vec::from_elem(b'\0', 100);
    let s = &mut String::from_utf8(s).unwrap();
    {
        {
            let _hifmt_1: &str = "hello snprintf";
        }
        let _hifmt_0: &mut str = s;
        let _hifmt_1: &str = "hello snprintf";
        unsafe {
            snprintf(
                _hifmt_0.as_bytes_mut().as_mut_ptr(),
                _hifmt_0.len() as usize,
                "sprint(%.*s)\0".as_bytes().as_ptr(),
                _hifmt_1.len() as i32,
                _hifmt_1.as_bytes().as_ptr(),
            );
        }
    };
    let b = &mut [0_u8; 100];
    {
        {
            let _hifmt_1: &str = "hello snprintf";
        }
        let _hifmt_0: &mut [u8] = b;
        let _hifmt_1: &str = "hello snprintf";
        unsafe {
            snprintf(
                _hifmt_0.as_mut_ptr(),
                _hifmt_0.len() as usize,
                "bprint(%.*s)\0".as_bytes().as_ptr(),
                _hifmt_1.len() as i32,
                _hifmt_1.as_bytes().as_ptr(),
            );
        }
    };
    {
        {
            let _hifmt_1 = (100) as i64;
        }
        {
            let _hifmt_2 = (200) as i64;
        }
        {
            let _hifmt_3 = (300) as i64;
        }
        {
            let _hifmt_4 = (400.0) as f64;
        }
        {
            let _hifmt_5 = (b) as *const _ as *const u8;
        }
        {
            let _hifmt_6 = (b) as *const _ as *const u8;
        }
        {
            let _hifmt_7: &str = s;
        }
        {
            let _hifmt_8: &[u8] = b;
        }
        let _hifmt_1 = (100) as i64;
        let _hifmt_2 = (200) as i64;
        let _hifmt_3 = (300) as i64;
        let _hifmt_4 = (400.0) as f64;
        let _hifmt_5 = (b) as *const _ as *const u8;
        let _hifmt_6 = (b) as *const _ as *const u8;
        let _hifmt_7: &str = s;
        let _hifmt_8: &[u8] = b;
        unsafe {
            dprintf(
                1i32,
                "d = %lld u = %llu x = %llx e = %e p = %p cstr = %s str = %.*s bytes = %.*s\n\0"
                    .as_bytes()
                    .as_ptr(),
                _hifmt_1,
                _hifmt_2,
                _hifmt_3,
                _hifmt_4,
                _hifmt_5,
                _hifmt_6,
                _hifmt_7.len() as i32,
                _hifmt_7.as_bytes().as_ptr(),
                _hifmt_8.len() as i32,
                _hifmt_8.as_ptr(),
            );
        }
    };
}

设计理由

在Rust/C混合使用时,无条件地将Rust的格式化打印转换为C的API,可以完全消除对Display/Debug特质的依赖,从而消除Rust格式化打印的开销,实现最优的尺寸。

理想情况下,格式化打印应遵循Rust中的以下规范

fn main() {
	let str = "sample";
	cprintln!("cprintln hex = {:x} digital = {} str = {}", 100_i32, 99_i64, str);
}

经过proc宏cprintln!展开后,变为

#[link(name = "c")]
extern "C" {
	fn printf(format: *const u8, ...) -> i32
}

fn main() {
	let str = "sample";
	unsafe {
		printf("cprintln hex = %x digital = %lld str = %.*s\n\0".as_bytes().as_ptr(), 100_i32, 99_i64, str.len() as i32, str.as_bytes().as_ptr());
	}
}

要实现上述功能,我们需要确保proc宏满足以下要求

  1. RUST字符串需要以\0结尾以适应C语言;
  2. RUST参数大小需要被proc宏识别,以便确定使用哪种C格式,例如,是%d还是%lld
  3. RUST参数类型需要被proc宏识别:如果是一个字符串,格式需要指定长度,并且将具有*const u8指针及其长度的char参数分别处理。

遗憾的是,proc宏无法实现所有这些。当它们展开时,并未进行解析以确定变量的类型。例如,以下代码中的i32类型

type my_i32 = i32;
let i: my_i32 = 100;
cprintln!("i32 = {}", i);

最好的情况下,proc宏只能告知i的类型是my_i32,而不知道实际上my_i32等同于i32

实际上,在更复杂的场景中,参数可能是变量,或者是从函数调用返回的值。因此,期望proc宏能识别某些参数的类型是不切实际的,这使得实现上述理想解决方案成为不可能。

Rust当前的实施通过将所有类型统一到Display/Debug特质来响应类型问题,并基于这些特质的接口进行转换。

我们的目标是进一步消除对Display/Debug特质的需要,因此我们必须根据格式字符串确定参数类型。实际上,Rust也使用特殊字符,如'?',来确定是使用Display还是Debug特质。遵循同样的原则,我们可以利用格式字符串如下

fn main() {
	cprintln!("cprintln hex = {:x} digital = {:lld} str = {:s}", 100_i32, 99_i64, str);
}

这使得依赖于proc宏成为可能。然而,上述方法存在问题,即格式字符串也限制了参数的大小。例如,{:x}表示int,而{:lld}表示long long int在C语言中。这要求程序员保证格式字符串和参数大小的一致性。否则,无效的地址访问可能会降低代码的安全性。在这方面,我们需要提供简化,即格式字符串仅定义数据类型,而不指定数据大小,从而在C中将数据类型统一为long long intdouble

fn main() {
	cprintln!("cprintln hex = {:x} digital = {:d} str = {:s}", 100_i32, 99_i64, str);
}

因此,proc宏生成以下代码

fn main() {
	unsafe {
		printf("cprintln hex = %llx digital = %lld str = %.*s\n\0".as_bytes().as_ptr(), 100_i32 as i64, 99_i64 as i64, str.len() as i32, str.as_bytes().as_ptr());
	}
}

这样,Rust代码的安全性可以得到保证:如果传递了错误的参数类型,编译器会拒绝它而不是隐藏问题。

字符串的特殊处理

对于字符串,必须传递长度信息,因此Rust中的参数将转换为两个,这会产生一些副作用。以下是如何表示的

cprintln!("str = {:s}", get_str());

生成的代码如下

unsafe {
	printf("str = %.*s\n\0".as_bytes().as_ptr(), get_str().len(), get_str().as_bytes().as_ptr());
}

请注意,get_str()已被调用两次,这类似于C语言宏中的副作用,当宏被多次展开时,其效果是未知的。需要避免这个问题。

简单来说,程序员需要确保字符串格式输出不能传递返回字符串而不是变量的函数调用。这样会降低可用性。

最佳选择是判断参数是否为函数调用。如果是,则生成一个临时变量。或者无条件地将每个字符串参数定义为临时变量,并在字符串参数不是&str时明确报告错误。

Rust字符的特殊处理

Rust字符使用unicode编码,其格式输出需要基于char::encode_utf8将其转换为字符串;然而,使用char::encode_utf8会自动引入core::fmt的符号,导致二进制大小膨胀。

为了避免引入core::fmt包,我们需要实现Rust char的转换。以下是实现的第一版本:

pub fn encode_utf8(c: char, buf: &mut [u8]) -> &[u8] {
    let mut u = c as u32;
    let bits: &[u32] = &[0x7F, 0x1F, 0xF, 0xFFFFFFFF, 0x00, 0xC0, 0xE0, 0xF0];
    for i in 0..buf.len() {
        let pos = buf.len() - i - 1;
        if u <= bits[i] {
            buf[pos] = (u | bits[i + 4]) as u8;
            unsafe { return core::slice::from_raw_parts(&buf[pos] as *const u8, i + 1); }
        }
        buf[pos] = (u as u8 & 0x3F) | 0x80;
        u >>= 6;
    }
    return &buf[0..0];
}

尽管没有明确执行任何操作,二进制仍然包含core::fmt中的相关符号。

h00339793@DESKTOP-MOPEH6E:~/working/rust/orion/main$ nm target/debug/orion | grep fmt
0000000000002450 T _ZN4core3fmt3num3imp52_$LT$impl$u20$core..fmt..Display$u20$for$u20$u64$GT$3fmt17h7afd8f52b570e595E
0000000000002450 T _ZN4core3fmt3num3imp54_$LT$impl$u20$core..fmt..Display$u20$for$u20$usize$GT$3fmt17h95817e498b69c414E
0000000000001d70 t _ZN4core3fmt9Formatter12pad_integral12write_prefix17h9921eded510830d2E
00000000000018f0 T _ZN4core3fmt9Formatter12pad_integral17hd85ab5f2d47ca89bE
0000000000001020 T _ZN4core9panicking9panic_fmt17h940cb25cf018faefE
h00339793@DESKTOP-MOPEH6E:~/working/rust/orion/main$

这些符号被添加到Rust中动态检查数组索引以防止缓冲区溢出。要消除二进制中的此类代码,我们需要禁用所有数组索引检查,如下所示:

pub fn encode_utf8(c: char, buf: &mut [u8; 5]) -> *const u8 {
    let mut u = c as u32;
    if u <= 0x7F {
        buf[0] = u as u8;
        return buf as *const u8;
    }
    buf[3] = (u as u8 & 0x3F) | 0x80;
    u >>= 6;
    if u <= 0x1F {
        buf[2] = (u | 0xC0) as u8;
        return &buf[2] as *const u8;
    }
    buf[2] = (u as u8 & 0x3F) | 0x80;
    u >>= 6;
    if u <= 0xF {
        buf[1] = (u | 0xE0) as u8;
        return &buf[1] as *const u8;
    }
    buf[1] = (u as u8 & 0x3F) | 0x80;
    u >>= 6;
    buf[0] = (u | 0xF0) as u8;
    return buf as *const u8;
}

这提醒我们,使用代码需要避免使用数组索引的动态检查,以避免引入core::fmt依赖项。


lib.rs:

hifmt-macros 用来解析格式字符串,parse/unescapeufmt 这里重用。

实现过程参考了UFMT的实现,重用了其部分代码,parse/unescape,涉及到格式化字符串的解析

依赖关系

~1.5MB
~35K SLoC