Ensuring value lives for its entire scope

Viewed 316

I have a type (specifically CFData from core-foundation), whose memory is managed by C APIs and that I need to pass and receive from C functions as a *c_void. For simplicity, consider the following struct:

struct Data {
    ptr: *mut ffi::c_void,
}

impl Data {
    pub fn new() -> Self {
        // allocate memory via an unsafe C-API
        Self {
            ptr: std::ptr::null(), // Just so this compiles.
        }
    }

    pub fn to_raw(&self) -> *const ffi::c_void {
        self.ptr
    }
}

impl Drop for Data {
    fn drop(&mut self) {
        unsafe {
            // Free memory via a C-API
        }
    }
}

Its interface is safe, including to_raw(), since it only returns a raw pointer. It doesn't dereference it. And the caller doesn't dereference it. It's just used in a callback.

pub extern "C" fn called_from_C_ok(on_complete: extern "C" fn(*const ffi::c_void)) {
    let data = Data::new();

    // Do things to compute Data.
    // This is actually async code, which is why there's a completion handler.

    on_complete(data.to_raw()); // data survives through the function call
}

This is fine. Data is safe to manipulate, and (I strongly believe) Rust promises that data will live until the end of the on_complete function call.

On the other hand, this is not ok:

pub extern "C" fn called_from_C_broken(on_complete: extern "C" fn(*const ffi::c_void)) {
    let data = Data::new();
    // ...
    let ptr = data.to_raw(); // data can be dropped at this point, so ptr is dangling.
    on_complete(ptr);        // This may crash when the receiver dereferences it.
}

In my code, I made this mistake and it started crashing. It's easy to see why and it's easy to fix. But it's subtle, and it's easy for a future developer (me) to modify the ok version into the broken version without realizing the problem (and it may not always crash).

What I'd like to do is to ensure data lives as long as ptr. In Swift, I'd do this with:

withExtendedLifetime(&data) { data in
    // ...data cannot be dropped until this closure ends...
}

Is there a similar construct in Rust that explicitly marks the minimum lifetime for a variable to a scope (that the optimizer may not reorder), even if it's not directly accessed? (I'm sure it's trivial to build a custom with_extended_lifetime in Rust, but I'm looking for a more standard solution so that it will be obvious to other developers what's going on).

Playground


I do believe the following "works" but I'm not sure how flexible it is, or if it's just replacing a more standard solution:

fn with_extended_lifetime<T, U, F>(value: &T, f: F) -> U
where
    F: Fn(&T) -> U,
{
    f(value)
}

    with_extended_lifetime(&data, |data| {
        let ptr = data.to_raw();
        on_complete(ptr)
    });
2 Answers

The optimizer is not allowed to change when a value is dropped. If you assign a value to a variable (and that value is not then moved elsewhere or overwritten by assignment), it will always be dropped at the end of the block, not earlier.

You say that this code is incorrect:

pub extern "C" fn called_from_C_broken(on_complete: extern "C" fn(*const ffi::c_void)) {
    let data = Data::new();
    // ...
    let ptr = data.to_raw(); // data can be dropped at this point, so ptr is dangling.
    on_complete(ptr);        // This may crash when the receiver dereferences it.
}

but in fact data may not be dropped at that point, and this code is sound. What you may be confusing this with is the mistake of not assigning the value to a variable:

    let ptr = Data::new().to_raw();
    on_complete(ptr);

In this case, the pointer is dangling, because the result of Data::new() is stored in a temporary variable within the statement, which is dropped at the end of the statement, not a local variable, which is dropped at the end of the block.

If you want to adopt a programming style which makes explicit when values are dropped, the usual pattern is to use the standard drop() function to mark the exact time of drop:

    let data = Data::new();
    ...
    on_complete(data.to_raw());
    drop(data); // We have stopped using the raw pointer now

(Note that drop() is not magic: it is simply a function which takes one argument and does nothing with it other than dropping. Its presence in the standard library is to give a name to this pattern, not to provide special functionality.)

However, if you want to, there isn't anything wrong with using your with_extended_lifetime (other than nonstandard style and arguably a misleading name) if you want to make the code structure even more strongly indicate the scope of the value. One nitpick: the function parameter should be FnOnce, not Fn, for maximum generality (this allows it to be passed functions that can't be called more than once).

Other than explicitly dropping as the other answer mentions, there is another way to help prevent these types of accidental drops: use a wrapper around a raw pointer that has lifetime information.

use std::marker::PhantomData;
#[repr(transparent)]
struct PtrWithLifetime<'a>{
   ptr: *mut ffi::c_void,
   _data: PhantomData<&'a ffi::c_void>,
}

impl Data {
    fn to_raw(&self) -> PtrWithLife<'_>{
        PtrWithLifetime{
            ptr: self.ptr,
            _data: PhantomData,
        }
    }
}

The #[repr(transparent)] guarantees that PtrWithLife is stored in memory the same as *const ffi::c_void is, so you can adjust the declaration of on_complete to

fn called_from_c(on_complete: extern "C" fn(PtrWithLifetime<'_>)){
    //stuff
}

without causing any major inconvenience to any downstream users, especially since the ffi bindings can be adjusted in a similar fashion.

Related