Safely handling a buffer from C

Viewed 157

I have a Rust function like this:

pub fn get_buffer() -> &[u8] {
    
    // returns *mut c_char
    let ptr = unsafe { get_buffer_from_c_code() }; 
    
    // returns &[u8]
    let buf = unsafe { core::slice::from_raw_parts(ptr as *const u8, 10) };

    buf
}

It generates this error:

pub fn get_buffer() -> &[u8] {
   |                        ^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
   |
19 | pub fn get_buffer() -> &'static [u8] {
   |                        ~~~~~~~~

I understand the error. It makes sense.

Question: should I take the compiler's suggestion and add a static lifetime specifier?

I'm connecting Rust to a C library that allocates memory internally and returns a pointer to it. Later, the C library takes care of de-allocating that memory on its own. I'd like the Rust code to be able to read what's in the memory, but otherwise leave it alone.

Rust is allocating the slice, though, in its own memory, and the slice itself (the pointer and the length) need to be dropped eventually.

Does a static lifetime do what I need it to do? Will Rust drop the slice, but not try to free the underlying buffer?

3 Answers

Question: should I take the compiler's suggestion and add a static lifetime specifier?

No. If your function return a static reference then you're promising to your caller that it can keep the reference around (and read through it) as long as it likes, which is only true if the buffer is never deallocated and never modified.

I'm connecting Rust to a C library that allocates memory internally and returns a pointer to it. Later, the C library takes care of de-allocating that memory on its own.

The solution to this problem depends entirely on when the deallocation happens. You need to ensure that there is some lifetime of a borrow such that there is no possibility to cause the deallocation until the borrow ends. You wrote in a comment

When my Rust code returns then C deallocates stuff.

That's key to picking the solution. That means that the reference should be obtained when the Rust code is called. That is:

extern "C" wrapper_called_from_c_code() {
    let ptr = unsafe { get_buffer_from_c_code() }; 
    let buf = unsafe { core::slice::from_raw_parts(ptr as *const u8, 10) };

    // Constrain the lifetime of the slice to be the duration of
    // this call by passing it through a lifetime-generic function.
    // (<'a> is just for explicitness and could be elided.)
    fn shim<'a>(buf: &'a [u8]) {
        safe_rust_code(buf);
    }
    shim(buf);

    // Now, after the function call returns, it's safe to proceed with
    // allowing the C code to deallocate the buffer.
}

fn safe_rust_code(buf: &[u8]) {
    // write whatever you like here
}

safe_rust_code can do whatever it likes in its function body, but the borrow checker will ensure it cannot hang onto the &'a [u8] slice reference longer than is safe.

The shim function exists to ensure that what wrapper_called_from_c_code needs (that the slice reference is being passed to a lifetime-generic function and not one that accepts &'static [u8]) inside wrapper_called_from_c_code rather than to explain it as a constraint on another function. I consider this good practice to keep invariants in the narrowest scope possible, to reduce the chances that they're broken by merely editing safe code without reading the comments.

Wrap the buffer in a struct that frees the buffer when the struct is dropped. The struct can then own the buffer, much like a Vec owns a block of data on the heap. When it hands out references their lifetimes will naturally be tied to the lifetime of the struct.

pub struct Buffer {
    ptr: *const u8,
    len: usize,
}

impl Buffer {
    pub fn new() -> Self {
        Self {
            ptr: unsafe { get_buffer_from_c_code() },
            len: 10,
        }
    }
}

impl Drop for Buffer {
    fn drop(&mut self) {
        unsafe {
            free_buffer_from_c_code(self.ptr);
        }
    }
}

impl Deref for Buffer {
    type Target = [u8];
    
    fn deref(&self) -> &[u8] {
        // SAFETY: The C library must not modify the contents of the buffer
        // for the lifetime of the slice.
        unsafe { slice::from_raw_parts(self.ptr, self.len) }
    }
}

The solution I ended up going with was something like this:

pub fn get_buffer<'a>(foo: u32) -> &'a [u8] {
    let ptr = unsafe { get_buffer_from_c_code() };
    let buf = unsafe { core::slice::from_raw_parts(ptr as *const u8, 10) };
    buf
}

Adding a parameter and then defining a lifetime on the function itself works. Essentially, it says that the returned slice lives as long as the foo parameter.

You can also do this:

pub fn get_buffer(foo: &u32) -> &[u8] {

If foo is a reference then you can elide the lifetime. The system makes the slice live as long as the foo reference. I don't know why it can do this with a reference and not a value parameter, but there it is.

Related