如何为 C 库调用分配缓冲区

mcm*_*yer 1 heap-memory ffi rust

这个问题并不新鲜,据我所知,有两种方法:

  1. Vec<T>按照此处的建议使用
  2. 使用std::alloc::alloc自行管理堆内存,如下所示

我的问题是这是否确实是两个(好的)选择。

只是为了清楚地表明这一点:两种方法都有效。问题是是否还有另一种可能是首选的方式。下面的示例旨在确定 Vec 的使用效果不好,以及其他方法可能更好的地方。

让我们陈述一下问题:假设有一个 C 库需要写入一些缓冲区。例如,这可能是一个压缩库。最简单的方法是让 Rust 分配堆内存并对其进行管理,而不是在 C/C++ 中使用malloc/进行分配new,然后以某种方式将所有权传递给 Rust。

让我们以压缩为例。如果库允许增量(流)压缩,那么我需要一个缓冲区来跟踪某些偏移量。

按照方法 1(即:“滥用” Vec<T>),我将包装Vec并使用lenandcapacity来达到我的目的:

/// `Buffer` is basically a Vec
pub struct Buffer<T>(Vec<T>);

impl<T> Buffer<T> {
    /// Create new buffer of length `len`
    pub fn new(len: usize) -> Self {
        Buffer(Vec::with_capacity(len))
    }
    /// Return length of `Buffer`
    pub fn len(&self) -> usize {
        return self.0.len()
    }
    /// Return total allocated size of `Buffer`
    pub fn capacity(&self) -> usize {
        return self.0.capacity()
    }
    /// Return remaining length of `Buffer`
    pub fn remaining(&self) -> usize {
        return self.0.capacity() - self.len()
    }
    /// Increment the offset
    pub fn increment(&mut self, by:usize) {
        unsafe { self.0.set_len(self.0.len()+by); }
    }
    /// Returns an unsafe mutable pointer to the buffer
    pub fn as_mut_ptr(&mut self) -> *mut T {
        unsafe { self.0.as_mut_ptr().add(self.0.len()) }
    }
    /// Returns ref to `Vec<T>` inside `Buffer`
    pub fn as_vec(&self) -> &Vec<T> {
        &self.0
    }
}
Run Code Online (Sandbox Code Playgroud)

唯一有趣的函数是incrementas_mut_ptr

Buffer会像这样使用

fn main() {
    // allocate buffer for compressed data
    let mut buf: Buffer<u8> = Buffer::new(1024);
    loop {
        // perform C function call
        let compressed_len: usize = compress(some_input, buf.as_mut_ptr(), buf.remaining());
        // increment
        buf.increment(compressed_len);
    }
    // get Vec inside buf
    let compressed_data = buf.as_vec();
}
Run Code Online (Sandbox Code Playgroud)

Buffer<T>此处所示显然是危险的,例如,如果使用任何引用类型。即使T=bool也可能导致未定义的行为。但是,可以通过引入限制可能类型的特征来避免未初始化实例的T问题T

另外,如果对齐很重要,那么这Buffer<T>不是一个好主意。

但除此之外,这Buffer<T>真的是最好的方法吗?

似乎没有现成的解决方案。bytes crate 很接近,它提供一个“用于在连续内存片上存储和操作的容器”,但接口不够灵活。

kmd*_*eko 7

您完全可以使用aVec的剩余容量来手动写入。这就是为什么.set_len()可用。但是,compress()必须知道给定的指针指向未初始化的内存,因此不允许从中读取(除非首先写入),并且必须保证返回的长度是初始化的字节数。我认为这些规则在 Rust 和 C 或 C++ 之间在这方面大致相同。

用 Rust 写成这样:

pub struct Buffer<T>(Vec<T>);

impl<T> Buffer<T> {
    pub fn new(len: usize) -> Self {
        Buffer(Vec::with_capacity(len))
    }

    /// SAFETY: `by` must be less than or equal to `space_len()` and the bytes at
    /// `space_ptr_mut()` to `space_ptr_mut() + by` must be initialized
    pub unsafe fn increment(&mut self, by: usize) {
        self.0.set_len(self.0.len() + by);
    }

    pub fn space_len(&self) -> usize {
        self.0.capacity() - self.0.len()
    }

    pub fn space_ptr_mut(&mut self) -> *mut T {
        unsafe { self.0.as_mut_ptr().add(self.0.len()) }
    }

    pub fn as_vec(&self) -> &Vec<T> {
        &self.0
    }
}

unsafe fn compress(_input: i32, ptr: *mut u8, len: usize) -> usize {
    // right now just writes 5 bytes if there's space for them

    let written = usize::min(5, len);
    for i in 0..written {
        ptr.add(i).write(0);
    }
    written
}

fn main() {
    let mut buf: Buffer<u8> = Buffer::new(1024);
    let some_input = 5i32;

    unsafe {
        let compressed_len: usize = compress(some_input, buf.space_ptr_mut(), buf.space_len());
        buf.increment(compressed_len);
    }

    let compressed_data = buf.as_vec();
    println!("{:?}", compressed_data);
}
Run Code Online (Sandbox Code Playgroud)

你可以在操场上看到它。如果你通过 Miri 运行它,你会发现它没有发现任何未定义的行为,但如果你过度宣传你已经写入了多少内容(比如 return written + 10),那么它确实会产生一个错误,即检测到读取未初始化的内存。


没有开箱即用的类型的原因之一是因为Vec该类型:

fn main() {
    let mut buf: Vec<u8> = Vec::with_capacity(1024);
    let some_input = 5i32;

    let spare_capacity = buf.spare_capacity_mut();
    unsafe {
        let compressed_len: usize = compress(
            some_input,
            spare_capacity.as_mut_ptr().cast(),
            spare_capacity.len(),
        );

        buf.set_len(buf.len() + compressed_len);
    }

    println!("{:?}", buf);
}
Run Code Online (Sandbox Code Playgroud)

您的Buffer类型并没有真正增加任何便利性或安全性,第三方板条箱也无法这样做,因为它依赖于compress().


这样的 Buffer 真的是最好的方法吗?

是的,这几乎是提供写入缓冲区的成本最低的方法。查看生成的发布程序集,它只是一次分配调用,仅此而已。如果您多次执行此操作,则使用特殊的分配器或简单地预分配并重用分配可能会变得棘手(但请务必进行测量,因为内置分配器无论如何都会执行此操作,只是更一般而言)。