Generically coercing the lifetime parameters of a type

Hi :slight_smile:

I am looking for a way to coerce the lifetime parameters of a type to a shorter lifetime.

The following function coerces the lifetime annotation of taker to the lifetime annotation of giver:

fn coerce_lifetime<'g, 't, G, T>(_giver : &'g G, taker : &'t T) -> &'g T 
where 't : 'g
{
    taker
}

However I would like to also coerce the possible lifetime parameters of taker to the lifetime annotations of giver.
For example if I am using the following type:

struct Wrapper<'a, T> {
    inner : &'a T,
}

Is something like this possible?

Why do I want to achieve this?
I am writing a crate as a Rust API for an existing C framework.
The C framework dynamically loads and unloads so called bundles (basically cdylibs). When two bundles are interacting it is enforced that during the interaction neither of the two bundles is unloaded. This is done by a callback that is given to the framework.
A callback could look like this:

struct Usage<'a, 'b,> {
    func : fn(&UseData, &ProviderStruct),
    data : &UseData,
}

extern "C" fn(user : *mut void, provider : *mut void) {
    let usage = user.cast::<Usage>().as_mut().unwrap();
    let provision = provider.cast::<Provision>().as_mut().unwrap();
    (usage.func)(usage.data, provision)
}

When one bundle wants to use another bundle, this callback is now feed into the framework together with a pointer to the Usage struct.

However outside of the callback it is possible that one of the two bundles gets unloaded. I need to make sure that no reference to another bundle lifes longer than during this callback.
For example it could be that the providing bundle feeds a 'static reference to the using bundle. This reference is only 'static in the perspective of the providing bundle. For the using bundle it is only valid during the providing bundle is present.

There's no trait bound for supertype coercion or the like, so it's not possible to write something like this directly:

// Made-up non-available functionality
//                              vvv
fn coerce_lifetime<'t: 'u, 'u, T: U, U>(t: &'t T) -> &'u U { t }

And there's no "higher-ranked type parameters" or the like either; type parameters like T always represent a single type (e.g. with a single set of lifetimes).

You'd have to encapsulate covariance in a trait or write out the specific types involved or the like.


I don't know that I really understand your use case though. It sounds like you'd want to force reference lifetimes to be the same, or perhaps to capture references. Maybe.

If you're accepting a &'long Thing from somewhere uncontrolled, and Thing might go away before 'long expires, you've already lost the soundness game. The caller still has their copy of the &'long Thing.

You can't retroactively make someone else's existing borrows shorter.

1 Like

Thanks for your reply!
I'll try to make the use case more clear.

I have two instances with totally unrelated lifetimes. However for the duration of the callback I know that both instances are present. So during the callback it is totally fine if both instances interact with each other.
But I need to make sure that both instances do not permanently contaminate each other during this callback.
I can achieve this by coercing the lifetimes of such an interaction (which might be called inside of this callback:

fn interact<'cb>(&'cb a : Instance1, &'cb b : Instance2) -> &'cb InteractionResult;

Here b could be stored in a (by interior mutability in a). To enforce that b is removed from a before the callback ends, it is possible to coerce the lifetime of the instances to the lifetime of the callback. Extending the example from my previous post would give us something like this:

use core::ffi::c_void;

struct Instance;
struct OtherInstance;
struct InteractionResult;

fn coerce_lifetime<'g, 't, G, T>(_giver : &'g G, taker : &'t T) -> &'g T 
where 't : 'g
{
    taker
}

struct Interaction<'a> {
    func : for <'b> fn(&'b Instance, &'b OtherInstance) -> &'b InteractionResult,
    data : &'a Instance
}

fn interact<'cb>(_a : &'cb Instance, _b : &'cb OtherInstance) -> &'cb InteractionResult {
    &InteractionResult
}

unsafe extern "C" fn callback(interaction : *mut c_void, other_instance : *mut c_void) {
    // casting the C pointers back to the Rust types
    let interaction = interaction.cast::<Interaction>().as_ref().unwrap();
    let other_instance = other_instance.cast::<OtherInstance>().as_ref().unwrap();
    // creating a variable with a lifetime
    let lifetime = ();
    // coercing the lifetime of both instances to the lifetime of the created variable
    let data = coerce_lifetime(&lifetime, interaction.data);
    let other_instance = coerce_lifetime(&lifetime, other_instance);
    // calling the interaction function pointer
    (interaction.func)(data, other_instance);
}

fn main() {
    let my_data = Instance;
    let mut interaction = Interaction { func : interact, data : &my_data};
    let cb = callback;
    let _interaction_ptr : *mut Interaction = &mut interaction;
    let _cb_ptr : *mut Option<unsafe extern "C" fn(*mut c_void, *mut c_void)> = &mut Some(cb);
    // unsafe { let_it_be_called_from_C(interaction_ptr as *mut c_void, cb_ptr as *mut c_void) }
}

However for this to work as you pointed out it is necessary that Instance and OtherInstance are fully concrete types, meaning they are not generic over some T that might have internal borrows.

After some further investigation I think that the following might work (you also mentioned something like this):

  • Creating an unsafe trait that expresses that a types lifetime parameters can be coerced
  • Implementing this trait for primitive types
  • making this trait derivable for a type if and only if all of its members also implement it (meaning all of its lifetime parameters must be known as well); it must also have no 'static references internally
unsafe trait LifetimeCoercible<'g> {
    type Coerced;
    fn coerce_lifetime<G>(self, _giver : &'g G) -> Self::Coerced;
}

unsafe impl<'a, 'g, T> LifetimeCoercible<'g> for &'a T 
where 
    'a : 'g,
    T : LifetimeCoercible<'g>
{
    type Coerced = &'g T;
    fn coerce_lifetime<G>(self, _giver : &'g G) -> Self::Coerced {
        self
    }
}

Thinking further, something like this could be useful for other ffi contexts as well. The nomicon mentions unbounded lifetimes. It recommends to bound the lifetimes that were created from thin air by using a function like coerce_lifetime. However for internal borrows there is no generic function possible that does this. Bounding of internal borrows is only possible with a fully concrete type.

Perhaps you just need the returned lifetime to be within the intersection of the input lifetimes?

fn intersection<'a, 'b, 'c, A, B>(a: &'a A, b: &'b B) -> &'c Callback
where
    'a: 'c,
    'b: 'c,
{
    todo!()
}

Or even just

fn inter<'a, 'b, A, B>(a: &'a A, b: &'b B) -> InterResult<&'a A, &'b B> {
    todo!()
}

(etc.)

These don't make 'a containable or 'b or vice-versa, they just assert that 'a and 'b have some intersection where both are valid -- which is trivially true if the function is callable[1] (the intersection is at least the length of the function call).

I have doubts about making any of that sound. You can make a sound version for concrete types without unsafe. But I also have doubts that variance is actually the answer to your use case.


Ultimately I think the difficulty comes down to, you're trying to model dynamic properties (the presence of some "bundle") with a static property (lifetime analysis). You might want to look into how scope threads work or into the GhostCell concepts around using a callback or visitor pattern to encapsulate invariants and see if they can fit your use case.

When I see things like

I see flags that you may be abusing lifetimes and playing with fire, since making a &'static _ is a promise that it would be around forever. It's hard to make that sound when it's not actually true, and probably you should just be using raw pointers and only creating short-lived references.

Perhaps another implementation to look into is that of thread locals. Things to note include

  • how every method has a caveat around panicking, because "is the thread destructing/destructed" is not a statically checkable property
  • how with and friends are the callback/visit pattern again
  • reasoning in the source

Or maybe see how libloading handles dynamic libraries.


That was all pretty vague advice, but I'm afraid I don't have very concrete advice for your goal which I'm confident is correct. Maybe someone else will.

Perhaps request a code review when you reach a more complete milestone.


  1. assuming you haven't already hit UB ↩ī¸Ž

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.