1

I'm wrapping a C library that has context and device objects. The context object needs to outlive the device object because the device keeps an internal reference to the context.

To express this, I use a PhantomData field in the Device wrapper:

use std::marker::PhantomData;

struct Context;

impl Context {
    fn open_device<'a>(&'a self) -> Device<'a> {
        Device { _context: PhantomData, }
    }
}

struct Device<'a> {
    _context: PhantomData<&'a Context>,
}

Now, in my client code, I would like to have a struct that holds both the Context and Device objects. But because the Device holds a reference (a fake one) to the Context, I'm not able to do this (see this question). But this is an unnecessary restriction because the Device struct doesn't actually contain a reference to the Context.

So how can I tie the lifetime of the Device to the lifetime of the Context in a way that would allow me to hold both of them in a struct?

Community
  • 1
  • 1
awelkie
  • 2,422
  • 1
  • 22
  • 32

1 Answers1

3

As of today, Rust is not able to express a lifetime that refers to an object defined in the same struct, so it's impossible to declare the proper lifetime for the Device member.

Instead of storing both objects directly in the struct, could you instead store references to these structs? (This won't work if you want to make a function that returns that struct.)

struct Both<'a: 'b, 'b> {
    context: &'a Context,
    device: &'b Device<'a>,
}

Another option is to declare the Device as having a Context with 'static lifetime (I'm using 'static because it's the only lifetime with a name), but always using a method to "cast" the Device into one with appropriate lifetime parameters, rather than using the field directly.

struct Both {
    context: Context,
    device: Device<'static>,
}

impl Both {
    fn get_device<'a>(&'a self) -> &'a Device<'a> {
        &self.device
    }
}

Actually, thanks to lifetime elision, it is not necessary to specify the lifetime parameters explicitly on get_device (likewise for your open_device method, by the way):

impl Both {
    fn get_device(&self) -> &Device {
        &self.device
    }
}

There's just a gotcha: you need to use transmute to lie about the device's lifetime parameter when you initialize the struct.

use std::mem;

fn get_both() -> Both {
    let context = Context; // could also be a parameter
    let device: Device<'static> = unsafe { mem::transmute(context.open_device()) };
    Both {
        context: context,
        device: device,
    }
}

You might also want to consider having the Both struct containing a Device that has no lifetime parameter, then wrapping that in another struct that does have a lifetime parameter and returning that from a method.

use std::marker::PhantomData;
use std::mem;

struct Context;

impl Context {
    fn open_device(&self) -> Device {
        Device
    }
}

struct Device;

struct DeviceWrapper<'a> {
    _context: PhantomData<&'a Context>,
    device: &'a Device,
}

struct Both {
    context: Context,
    device: Device,
}

impl Both {
    fn get_device(&self) -> DeviceWrapper {
        DeviceWrapper { _context: PhantomData, device: &self.device }
    }
}

fn get_both() -> Both {
    let context = Context;
    let device = context.open_device();
    Both {
        context: context,
        device: device,
    }
}

(In fact, the DeviceWrapper probably doesn't need the _context member, since the DeviceWrapper's lifetime is tied to that of the Both already.)

Francis Gagné
  • 60,274
  • 7
  • 180
  • 155
  • Do you know where in the rfcs github the discussion about binding lifetime to that of another member in the same instance can be found? – Mahmoud Al-Qudsi Sep 12 '18 at 21:10