It is currently impossible for a dyn-compatible unsafe trait A to be able to express the semantics of "if T: A + B (where B is a safe trait), the B implementation must behave a certain way". Safe code can maliciously impl B for dyn C with a locally-definded trait C: A, so requiring this in the unsafe preconditions of A is insufficient. This came up when we were looking to stabilise a dyn-compatible Allocator, notably: `dyn Allocator` together with `Allocator + Clone` requirements is unsound, leading to UB with `Arc` · Issue #156920 · rust-lang/rust · GitHub.
Notably, breaking dyn compatiblity only makes Allocator sound in that implementors can rely on the orphan rule to reason about which impls may exist downstream; the underlying semantics of a sometimes-implemented safe trait affecting the correctness of an unsafe trait are currently inexpressible. The closest we could do would be making unsafe trait A: B, so always implemented.
Upon getting a vibe-check from libs(-api), lang, and a couple of people on types, this seems feasible. The proposed idea is approximately:
// -- in core --
#[unsafe(has_preconditions(Allocator))]
trait Clone {}
unsafe trait Allocator {}
#[unsafe(satisfies_preconditions(Allocator))] // this is new
impl<T: Clone> Clone for Box<T> {}
unsafe impl<A: Allocator> Allocator for Box<A> {}
// -- in downstream --
struct Foo;
// all good
impl Clone for Foo {}
struct Bar;
#[unsafe(satisfies_preconditions(Allocator))] // if we don't write this, error
impl Clone for Bar {}
unsafe impl Allocator for Bar {}
with the only bit of orphan-rule reasoning here being for implementability, not soundness: someone can't write a blanket impl Clone that breaks their downstream dependents, and if they somehow did it anyway said dependent would get a compiler error.
The one part of this we'd need to have on stable is the unsafe(satisfies_preconditions(_)) attribute. In fairness, we could ditch that as well and make it so it's just impossible to (stably) impl Clone on allocators, but that would be sad. The rest (e.g. the way we mark a safe trait as conditionally-unsafe-relevant) could be delegated to later syntax bikeshedding.
Imo, having e.g. #[marker] unsafe trait UseAllocator: Allocator + Clone to mark clones that correctly share the allocator is honestly reasonable. It's a difference between #[unsafe(satisfies_preconditions(Allocator))] on the Clone impl (which now can't be derived) or an unsafe impl for UseAllocator. The former doesn't seem much better than the latter to justify the new mechanism for marking impls as fulfilling safety guarantees.
For a bit of further context, the Storage proposal for generalizing allocators further has two concepts of cloning. Using my current local revision's naming:
DupStore, which indicates that the clone duplicates its backing storage and is independent from the source; and
UseStore, which indicates that the clone shares its backing storage with the source.
My draft also includes methods on Store to clone bound on these traits e.g.
final fn clone_dup(&self) -> Self
where
Self: DupStore,
{
self.clone()
}
I need to think through what cloning with just Store + Clone gives you further. I want it to be enough for Box::clone, i.e. can be used as a fresh new storage.
My problem with this is that #[unsafe(has_preconditions(Allocator))] introduces an obligation, but is too close of a syntax to #[unsafe(satisfies_preconditions(Allocator))] which discharges an obligation (opposite polarity).
unsafe fn and unsafe { .. } is the more frequent confusion, but at least one has braces and the other doesn't, and one is applied to an item and the other is an expression.. so it feels different in a way that #[unsafe(..something)] doesn't.
But I guess this ship sailed. (unsafe trait / unsafe impl is just as confusable)
Conceptually, at least, it does seem like it'd be nice to have something like this.
For example, TrustedLen wants to also put a requirement that ExactSizeIterator::len can't be garbage, but there's nothing on that trait impl (nor the len method) itself that acknowledges that.
Spitballing, this seems awkward:
because Clone is in core so if something that's not core -- such as external crates, if this wanted to become a "real" feature -- then they can't put something on Clone.
We have an existing way of expressing this kind of contract. Given that the Clone and Allocator impls for a type can come about completely independently via generic impls involving other traits defined in independent crates, I don't see how we can reasonably check in a local way that all involved Clone impls have the needed attribute.
Maybe we should have nicer syntax to define and use such traits, but they do look like fundamentally the right mechanism to me.
We spoke about this in the libs meeting today and concluded that we're going to do marker traits for now, and try to migrate to this backwards-compatibly if feasible. Effectively:
#[unsafe(has_preconditions(A))]
trait B {}
#[unsafe(satisfies_preconditions(A))]
impl<T: Whatever> B for T {}
can expand to:
unsafe trait Unsafe_A_and_B: A + B {}
trait B {}
unsafe impl<T: Whatever + A + B> Unsafe_A_and_B for T {}
impl<T: Whatever> B for T {}
with unsafe code just relying on Unsafe_A_and_B being implemented as part of its safety contract.
It seems like it should require unsafe trait CompatibleClone (changing Allocator + Clone to Allocator + CompatibleClone). Types implementing Clone should then also implement the CompatibleClone.
The precondition mechanism seems to create a parallel trait resolution mechanism. It's probably more convenient if you don't need the whole world to start implementing CompatibleClone, but feels like a workaround.
Maybe it could be done with maker traits and relaxed orphan rules that allow unsafely implementing marker traits on foreign types?