I can't remember who said this before, but basically the bottom line about Rust is: Freedom from undefined behavior, even in the presence of bugs in safe code.. This is in contrast to, say, C++, which only protects you from UB in the absence of bugs, period.
So yes. Unsafe code cannot rely on invariants being upheld by arbitrary code. It can only rely on invariants being held by specific code. e.g.
pub fn get(slice: &[i64], index: usize) -> i64 {
assert!(index < slice.len());
unsafe {
slice.as_ptr().offset(index).read()
}
}
The unsafe code above relies on:
- The precondition checked above it. (i.e. safe code in the same crate)
- The invariants of
&[T]
.
- The correctness of the implementation of
&[T]
in the standard library. (i.e. upstream code)
If it turns out that e.g. the third bullet fails to hold, we can simply fix the standard library. But we can't fix downstream code.
Now contrast with BTreeMap
. Ideally, an implementation of Ord::cmp
should:
- Be transitive.
- Be antisymmetric.
- Have reflexive equality.
- Have definitions of e.g.
lt
and eq
that match
...and it probably shouldn't panic. But unsafe code in BTreeMap
cannot rely on any of these invariants! If somebody writes a rogue Ord
impl, the implementation of BTreeMap still must not invoke undefined behavior.
However, let's say you really do have some unsafe
code that needs to rely on an invariant being held by arbitrary code. For instance, suppose you wanted some FastBTreeMap
that was able to assume that the Ord
impl was correct. To this end, Rust has unsafe trait
s:
pub struct FastBTreeMap<T> {
...
}
/// Marker trait for types that correctly uphold
/// the invariants of Ord, and whose Ord impls
/// do not panic.
///
/// # Safety
/// It is undefined behavior to implement this trait
/// on a type whose Ord impl panics, or that fails
/// to be transitive or antisymmetric.
pub unsafe trait TrustedOrd: Ord { }
impl<T: TrustedOrd> FastBTreeMap<T> {
// all the standard map methods...
}
Now, if somebody writes a rogue Ord impl and wants to use it in FastBTreeMap, they have to use unsafe
:
struct Naughty;
impl PartialEq for Naughty {
fn eq(&self, other: &Naughty) -> bool { false }
}
impl PartialOrd for Naughty { /* similar nonsense */ }
impl Eq for Naughty { }
impl Ord for Naughty { /* similar nonsense */ }
unsafe impl TrustedOrd for Naughty { }
fn main() {
// use Naughty in a FastBTreeMap
}
And so as you can see, this is no longer the case of unsafe
code relying on the correctness of just any arbitrary code. The downstream code was forced to forced to write unsafe
and acknowledge the threat of undefined behavior.