Pre-RFC: unsafe trait impls


#1

Right now if a trait provides a method that is safe, it is impossible to implement that trait with an unsafe method. Now, in general, this makes sense. Generic code has to know if it’s calling a safe function or not. However, this is a problem for magic traits in Rust.

In particular, all the operators in Rust like Deref, Fn, and Index can only be implemented safely. This means that unsafe code has to be more cumbersome to write than safe code, or is special-cased in the compiler (see: derefing rawptrs). Unsafe code is by definition the most important to write correctly. If you have to jump through too many hoops to write it at all, then that could very well reduce your ability to do it correctly.

Therefore, this Pre-RFC proposes the ability to specify an entire impl as unsafe. This would look like:

unsafe impl<T> Deref<T> for *const T {
 ...
}

This causes the compiler to treat all methods in the impl as unsafe, even the default ones (because they probably call the non-default ones). If a type T implements a trait unsafely, it cannot be offered to a function that is generic over that trait. To opt into allowing unsafe impls, a generic bound can use the Unsafe? modifier. This is exactly analogous to the Sized? modifier: it allows types of either kind, but forces the function to assume that they’re the more restrictive one. In this case, calling any methods on an Unsafe? would be assumed to be unsafe.

fn do_unsafe_work<D: Unsafe? Deref<uint>>(data: D) { ... }

Obviously, this should not be done in general. It should only be done in internal methods where the set of generic implementors and their safety constraints are known. Even then, this is mostly intended to only be used in cases where the type is concrete and known.

Downsides

  • More lang items, more complexity

Alternatives

  • Just let unsafe code be harder to write/use.
  • Provide Unsafe* variants of the language’s magic traits.
  • Allow some methods on an unsafe impl to not be unsafe by requiring methods to be individually marked as safe or unsafe. Of course, the compiler has to treat all of the methods as unsafe in a generic context, but in the concrete and documentation context, this can improve ergonomics.

#2

The syntax conflicts with the syntax for implementing unsafe traits in the accepted opt-in built-in traits RFC. Also, saying that Unsafe? is exactly analogous to Sized? is a bit of an exaggeration: Sized is a trait, while Unsafe is… nothing? It acts as a way of modifying/generalising a trait, not as a way of bounding a generic type. It’s also backwards: Sized is implicit, while !Unsafe is implicit for traits. It makes more sense to me for this to simply be unsafe? Trait.

I understand the motivation, however: I could imagine having an entire collection type that’s unsafe, and being forced not to implement the collection traits for it is simply annoying. However, I can’t imagine code actually opting in to allowing unsafe? traits in their generic code: normally unsafe functions require that some invariant be held, and if code is being generically unsafe it has no idea which invariants it must hold, and any code calling that function doesn’t know which invariants it does hold. This proposal also describes no way to allow a generic function to be unsafe if and only if one of the traits in the generic list is implemented unsafely. All these things combined makes generic code using unsafe? bounds practically useless, making the only use of unsafe traits be operator overloading.

Overall, this makes me think that the best solution would simply be to add specific Unsafe* traits for Deref &c., although this is quite cumbersome.


#3

Wow, some great points here, thanks! The conflict with opt-in is definitely problematic.

I agree that it’s mostly useless in generic programming. However I think there’s a usecase for local code deduplication. Traits are our primary mechanism for deduplicating similar but different pieces of code. It’s not uncommon in my experience to see a trait introduced or implemented internally just to make some convenience method that handles a few concrete types uniformly. In that case the function isn’t intended to be truly generic over all types that implement the trait, but just over a handful of known concrete types. In such a situation, I believe it to be sound to use unsafe impls.

One of my working usecases is the “Deref/DerefMut trick” where you can make a structure that holds a reference generically over & and &mut by making them implement Deref and DerefMut. With this you can implement the mutability-agnostic behaviour on <T: Deref>, but expose the mutability-specific behaviour for <T: DerefMut>.

With unsafe impls, this same trick could be done with rawptrs.


#4

An alternative to Unsafe? would be to allow passing unsafe impls to generic code but only from within an unsafe block. At least for the case of Deref/DerefMut, this seems to make the most sense, as a function that takes a generic instance only needs promises that the pointers involved point to real allocated memory - this promise is given by the use of an unsafe block, but it still makes perfect sense to call the generic function.