Making a struct outlive a parameter given to a method of that struct
Asked Answered
S

1

2

I am looking for a way to ensure a struct outlives the parameter given to a method of that struct. Even if the struct doesn't hold a reference to that data after leaving the method.

This is for wrapped raw pointers fed to an FFI. I want to guarantee that the struct implementing the FFI outlives the Option<&'a Any> I use to feed the Rust object to the pointer wrapper.

Context is the FFI wrapper. Data holds different types that map to FFI types. The FFI functions copies all these types immediately before returning.

Except raw pointers.

So I add a lifetime specifier to Context just for those and use that in send_data().

But somehow this is not enough. I expected below code to not compile.

Edit: someone one the Rust Discord suggested making &self mutable in send_data(). This has the desired effect but my FFI is thread safe (and stateless) and send_data() is time critical. So I would very much like to avoid this.

use std::any::Any;
use std::marker::PhantomData;

struct IntegerArray<'a> {
    data: &'a [i32],
}

struct WrappedRawPointer<'a> {
    ptr: *const std::ffi::c_void,
    _marker: PhantomData<&'a ()>,
}

impl<'a> WrappedRawPointer<'a> {
    fn new(data: Option<&'a dyn Any>) -> Self {
        Self {
            ptr: data
                .map(|p| p as *const _ as *const std::ffi::c_void)
                .unwrap_or(std::ptr::null()),
            _marker: PhantomData,
        }
    }
}

enum Data<'a, 'b> {
    IntegerArray(IntegerArray<'a>),
    WrappedRawPointer(WrappedRawPointer<'b>),
}

struct Context<'a> {
    ctx: u32,
    _marker: PhantomData<&'a ()>,
}

impl<'a> Context<'a> {
    fn new() -> Self {
        Self {
            ctx: 0, // Call FFI to initialize context
            _marker: PhantomData,
        }
    }

    fn send_data(&self, data: Data<'_, 'a>) {
        match data {
            Data::IntegerArray(_i) => (),      // Call FFI function
            Data::WrappedRawPointer(_p) => (), // Call FFI function
        }
    }
}

fn main() {
    let ctx = Context::new();

    {
        let some_float: f32 = 42.0;
        ctx.send_data(
            Data::WrappedRawPointer(
                WrappedRawPointer::new(
                    Some(&some_float)
                )
            )
        );

        // I would like rustc to complain 
        // here that some_float does not
        // outlive ctx
    }

    // Explicitly drop outside
    // the previous block to 
    // prevent rustc from being
    // clever
    drop(ctx);
}
Sleepless answered 14/6, 2020 at 15:7 Comment(0)
S
2

Making send_data take &mut self instead of &self works because it makes the type of the self parameter invariant with respect to the type Self. Subtyping and Variance is described in the Rustonomicon, as well as other questions here on Stack Overflow (see below).

Since you want invariance even when self is an immutable reference, that suggests that the variance of Context<'a> itself is wrong: it is covariant in 'a, but it should be invariant. You can fix this by changing the type argument to PhantomData to something that is also invariant in 'a:

struct Context<'a> {
    ctx: u32,
    _marker: PhantomData<*mut &'a ()>,  // or Cell<&'a ()>, or fn(&'a ()) -> &'a (), etc.
}

PhantomData is not just something you add mechanically to make the compiler not yell at you. The specific form of the type argument to PhantomData tells the compiler how your struct is related to its type and lifetime parameters (when the compiler can't figure it out by itself). In this case you want to tell the compiler that a Context<'some_long_lifetime> can't be substituted for a Context<'a_much_shorter_lifetime> even though its fields would all allow that substitution.

Some more questions on variance

Subvene answered 14/6, 2020 at 15:54 Comment(2)
Awesome, cheers! I just re-read the section of the Rustonomicon on Subtyping & Variance but that answer would still not have occurred to me. On that note: I didn't added PhantomData to make the compiler yell at me, not the opposite. :)Sleepless
When using this in my crate I now get an unexpected error when I do not call drop() explicitly (see here: github.com/virtualritz/nsi/blob/…): 140: foo dropped here while still borrowed. Borrow might be used here, when c is dropped and runs the Drop code for type Context While on the playground the modified example compiles w/o drop() (play.rust-lang.org/…) I do not understand what am I doing different here.Sleepless

© 2022 - 2024 — McMap. All rights reserved.