Unsafe binder types

Motivation

Rust currently has no way of naming certain classes of existential lifetimes. For example, self-referential lifetimes:

struct DataAndView {
    x: Box<[u8]>,
    element: & /* '??? */ u8,
}

Often these lifetimes can be (unsafely) filled with 'static, and with judicious usage of transmute and some manual safety guarantees, they can be wrapped with a sound API. However, these 'static lifetimes can sometimes get in the way, since they often imply outlives-'static relationships with the data:

struct DataAndView<T> {
    x: Box<[T]>,
    element: &'static T,
    //~^ ERROR the parameter type `T` may not live long enough
}

This is further exacerbated by the presence of GATs and the Self: 'a bounds which are often required on them.

While a coherent design for self-referential lifetimes relies on major design work and possible changes to the way we represent places, borrows, etc., designing a coherent but unsafe opt-out to the problem of "what do we put in this lifetime position other than 'static" is a bit more tractable.

Design

The type: unsafe<...> T

I'm proposing a new kind of type which I'll call an "unsafe binder". It's written like unsafe<'a> &'a T, and has two parts:

  • the binder unsafe<'a, 'b, 'c>, which introduces a set of lifetimes like a for<'a> binder.
  • the type, which is any type and which may reference the lifetimes introduced by the unsafe binder.

An unsafe binder is a distinct class of type than the type it wraps. For example, unsafe<'a> &'a T is not a reference type, but a distinct type which wraps a reference. However, it inherits many of the properties of the type that it wraps (e.g. its Sizedness)[1].

The wrapped type of an unsafe binder may be any other type, including structs like Foo<'a, 'b>, slices, tuples, etc. The wrapped type may reference lifetimes from the unsafe binder or any other lifetime in scope, including 'static, so this type is valid: for<'a> fn(unsafe<'b> (&'a i32, &'b i32)).

Converting to and from unsafe<...> T

Since I mentioned above that unsafe binder types are distinct types from the type that they wrap, you may be wondering how we intercovert between these types. I'm proposing the addition of two new operators which are represented with macros: wrap_unsafe!(expr) and unwrap_unsafe!(expr). The former converts from, e.g., &'_ T to unsafe<'a> &'a T, and the latter converts in the other direction. These are distinct operators and not implemented with as casts since they

  1. Are place-preserving: wrap_unsafe! and unwrap_unsafe! operate on places, and will not perform moves unless the resulting expression is moved. This is useful when, for example, reading from an unsafe binder stored in a struct without moving out of the struct field.
  2. Guide inference: They use the type of the argument and the expectation to guide inference. For example, given some t: unsafe<'a> &'a i32, the unwrap_unsafe!(t) operator can eagerly evaluate to &'?0 i32. This prevents unnecessary type ascription and can help prevent inference ambiguity errors.

These operators are unsafe to use, since they are essentially a limited form of transmute between an unsafe binder and its wrapped type.

Here's using unsafe binders in practice:

struct SelfRef<T> {
  ptr: unsafe<'a> &'a T,
  data: Box<T>,
}

let s = SelfRef {
    ptr: unsafe { wrap_unsafe_binder!(&*data) },
    data,
};

let ptr = unsafe { unwrap_unsafe_binder!(s.ptr) };

This may not be a very motivating example, so here's a complete example which implements a Map combinator on async streams with async closures.

Why not just have some 'unsafe?

An unsafe lifetime introduces complications as it's not necessarily clear where the unsafety must be discharged. What constitutes an unsafe usage? Introducing a distinct typeclass which must be explicitly coerced to and from makes this very clear, since the unsafety must be discharged at the wrap_unsafe/unwrap_unsafe operators.

The semantics of 'unsafe embedded within normal types are not necessarily clear either. Do types which have 'unsafe as one of their lifetime substitutions still impelement traits normally? How do they work with implied bounds? Etc. This is further confused by NLL, which treats lifetimes in MIR as "locations" in the MIR; this isn't clearly extensible to an unsafe lifetime.

TODO:

  • Copy/clone impl that is dependent on the inner type

  • ManuallyDrop or Copy inner type

  • Remove all the MIR jank from dropping


  1. I won't go into detail about the coherence semantics or anything like that. A lot of this design is still in the air. I envision it to be a bit like a repr(transparent) wrapper around the wrapped type, conceptually at least. ↩︎