Reputation: 367
According to The Rust Programming Language Chapter 19.1, Safe traits impose no safety requirement on their implementors. More quite literally, it says:
A trait is unsafe when at least one of its methods has some invariant that the compiler can’t verify.
Well, before long rationale, simply put: does the phrase "some invariant" include safety condition for calls? Let me rephrase. Assume the documentation of an unsafe method f
in a safe trait T
has a "Safety" section that says "the call of f
is safe if and only if Condition A is satisfied." Then could it constrain us to implement the method f
so that it is sound to call as long as Condition A is satisfied, even though the trait T
is safe?
[EDIT: added paragraph] To make things clear, let me quote my rephrase from my comment. The trait Eq
requires (x == x) == true
, but implementors don't need to obey it for safety. I wrote below an example safe trait Foo
with an unsafe method foo
.
The trait Foo
requires that self.foo(x)
is sound provided x: u32
is even, but implementors don't need to obey it for safety...!? Or do they? That's my question.
[/EDIT]
I have long thought unsafe methods of safe traits can't have any trustworthy safety invariant. At least, that's what our TRPL says. Even if we are given a trait like the following:
pub trait Foo {
/// # Safety
/// This is safe to call if and only if `x` is even.
unsafe fn foo(&self, x: u32);
}
I thought the following impl would be valid, because, after all, Foo
is safe to implement:
#![deny(unsafe_op_in_unsafe_fn)]
use core::hint::unreachable_unchecked;
pub struct Bar;
impl Foo for Bar {
unsafe fn foo(&self, _: u32) {
unsafe {
unreachable_unchecked();
}
}
}
and that the following code would be unsound:
fn gen_call_foo<T: Foo>(t: &T) {
unsafe {
t.foo(2);
}
}
I guessed gen_call_foo
should be marked unsafe and should pass the callers of gen_call_foo
the obligation to check the soundness of concrete implementations <SomeStruct as Foo>::foo
. Because, I imagined, that's how safe traits work! Maybe Foo
's documentation is misleading and should receive rewording. It could say "Since this is a safe trait, this method lacks any safety guarantee for call. If you would like to wrap a call to this function in a safe code (with an unsafe block), you should instantiate the concrete implementation and consult the implementation's document. Nonetheless, the implementors are strongly recommended to make this safe to call where x is even".
However, I found two pieces of information on the Web that made me doubt my belief. The first one is an (unstable) safe trait Step
. It has forward_unchecked
. It is an unsafe method, and its document innocently says
It is undefined behavior for this operation to overflow the range of values supported by Self. If you cannot guarantee this will not overflow, use forward or forward_checked instead.
as if it's safe to call when no overflow occurs! It doesn't even give an notice about the "fact" that since this is a safe trait, no call of this unsafe method can be guaranteed to be safe unless you check individual implementations. I started to suspect I had misunderstood something (well, as I'm writing this, I'm beginning to suspect Step
's safety condition is maybe only a tautology, i.e. the term overflow could be defined to be the situation where a call to this function is unsound. I guess this question is worth clarifying anyway).
Also, we have comments for this answer to this question on this site ("When is it appropriate to mark a trait as unsafe, as opposed to marking all the functions in the trait as unsafe?"). I just quote the comments:
Since an unsafe trait and unsafe functions in a trait are orthogonal, can you think of any cases where you might have both? – Shepmaster Jul 26, 2015 at 13:12
@Shepmaster: I can't think of a practical example, but it's certainly not impossible to envisage an unsafe trait where a subset of its methods have an unsafe interface. Perhaps a marker trait (like Send or Sync) where you have unsafe code relying on the marker trait which also exposes a method dealing in raw pointers (which would thus be unsafe to call). – DK. Jul 26, 2015 at 13:17
This astonished me, for I had thought unsafe methods are one of the main reasons of marking a trait unsafe, for we want trait methods to be at least conditionally safe to call generically.
My belief is getting more and more fragile. Maybe, even with unsafe methods, Rust's default is safety. Probably, we can only conditionally lift the safety of the function by explicitly mentioning in the documentation. Safe traits impose no restrictions on implementations, but maybe that doesn't apply to the soundness of the function calls: the Safety section in the function's documentation say something. After all, we always assume unconditional safety of the calls to safe functions even in safe traits, and it poses no restriction on safe implementations.
However, I couldn't find a documentation for that. I'm at a loss.
I wrap this up and I state the question again. If the document of an unsafe function in a safe trait has some safety condition, can callers of this function trust it? Must implementors obey it for soundness? Additionally: if this question has a definite answer, a document for that answer would be greatly appreciated.
Upvotes: 0
Views: 604
Reputation: 2654
A trait being unsafe
means there are invariants that need to be upheld by the implementor, while a function being unsafe
means there are invariants that need to be upheld by the caller. Consider the following trait:
/// A trait to abstract over array-like containers.
/// For any in-bounds index, `std::ptr::eq(self.get(index), unsafe{ self.get_unchecked(index) }` must be true.
trait Arraylike<T> {
///returns the length of the container
fn len(&self) -> usize;
/// returns a reference to the element at index, panicking if the index isn't less than `self.len()`
fn get(&self, index: usize) -> &T;
/// returns a reference to the element at index
///# Safety
/// index must be less than `self.len()`
unsafe fn get_unchecked(&self, index: usize) -> &T;
}
By making get_unchecked
unsafe, a well-behaved implementation could use the invariant to elide a bound check and potentially speed things up. Whoever calls get_unchecked
must be aware of the potential for undefined behavior, an make sure to do the bounds check themselves. However, an implementation must still ensure that get_unchecked
doesn't cause undefined behavior if it is called with an index less than self.len()
, regardless of how that implementation is defined.
In contrast, since the trait isn't unsafe, the trait level invariant saying that get
and get_unchecked
must return the same value can't be relied upon for a program's soundness. An implementation that decided get
should be zero indexed and get_unchecked
should be one indexed is perfectly valid, so long as the value of self.len()
is large enough, and any programs making use of Arraylike
in a generic context need to be aware of this, and can't have their code's soundness be reliant upon the implementation being correct. Making Arraylike
an unsafe trait would lift that restriction, allowing users of the trait to rely on the implementation being correct for program soundness.
Upvotes: 2