The key point imo is that "your implementation has to guarantee soundness so long as callers uphold those obligations" is a property of you calling unsafe
code, not of the method being unsafe
.
It is true that most of the time, it will end up being that a trait with an unsafe
method will itself end up being unsafe
. After all, if the method returns any sort of raw handle (be it pointer, index, or whatever), the caller needs to know something about its validity in order to be able to use it.
But if there's no further safety concerns for the output of a method, then the trait doesn't need to be unsafe
, because there's no need to flow guarantees in that direction.
As an illustrative example, consider
trait Get {
type Item;
fn get(&self, ix: usize) -> Option<&Item>;
unsafe fn get_unchecked(&self, ix: usize) -> &Item;
}
Now there are two reasonable implementations for slices:
// the simple, safe one:
impl<T> Get for &'_ [T] {
type Item = T;
fn get(&self, ix: usize) -> Option<&T> {
self.get(ix) // the inherent one
}
fn get_unchecked(&self, ix: usize) -> &Item {
self.get(ix).unwrap()
}
}
// or the one that uses the precondition:
impl<T> Get for &'_ [T] {
type Item = T;
fn get(&self, ix: usize) -> Option<&T> {
self.get(ix) // the inherent one
}
unsafe fn get_unchecked(&self, ix: usize) -> &Item {
unsafe { self.get(ix).unwrap_unchecked() }
}
}
The thing to consider here is the flow of promises:
Get::get_unchecked
is unsafe fn
, so the impl knows (on pain of UB) that the preconditions hold; that ix
is a valid index.
trait Get
is not unsafe trait
, so any (generic) callers of its methods have the type guarantees of the return value, but just that, and must not ever cause UB for any potential (safety variant upholding) return value.
Any caller MUST (on pain of UB) fulfill any documented preconditions on the trait. The callee MUST NOT add any preconditions to the trait impl. The callee MAY use the trait documented preconditions to fulfill the preconditions of any unsafe
functionality it itself calls. The caller MUST NOT rely on the correctness of the trait impl to ensure soundness of its own implementation. (The callee, however, must in fact be sound to call if the preconditions are held; if it is not, this is a soundness bug in the callee.)
I think the interesting point might be that soundness is a requirement of every Rust function, not just unsafe
ones. It's just that it's trivial to prove the soundness of a function that doesn't call unsafe
functionality -- all of the functions it calls are sound over all input, thus it is sound itself. Marking a function unsafe
gives it no extra responsibilities; it just gives it more information (the documented safety preconditions) in order to satisfy the global requirement of soundness.
A somewhat more practical example might be SIMD and CPU capabilities. You could have a trait abstracting over doing some work, and it has multiple entry point methods. Depending on the (unsafe
) method called, the implementation may unsafe
ly assume that the CPU supports some level of functionality not necessarily guaranteed by just the CPU target for the program as a whole.
The abstracting trait shouldn't be unsafe
; there's nothing more to it than FnOnce
, really. However, the functions that assume CPU functionality do need to be unsafe
, as they might execute undefined CPU instructions and cause undefined behavior if called on an unsupported CPU.