This is an interesting idea IMO. There’s certainly value in marking blocks as “the code in here in particular is involved in ensuring some safety-pre/post-conditions for unsafe code, pay attention when modifying it”. This kind of annotation can then apply to potentially quite large blocks of code, without obfuscating the detail of where exactly the unsafe operation actually happened.
This kind of block might as well just be an ordinary block with a SAFETY
comment though, IDK. Following your proposal of two levels of unsafe
, I thought back to this idea of mine:
and I think it might combine quite well. If we have
- a feature for
unsafe
keyword directly on unsafe operations themself e.g. as presented in the linked post above - a lint that disallows unsafe operations directly within an
unsafe
block without using another instance of theunsafe
keyword on the operation itself - perhaps also a lint that makes it so that
unsafe
blocks are still required
this would enforce a coding style similar to the example code you gave above, but the syntax would look like this instead:
fn safe() {
safe_operation1();
safe_operation2();
// SAFETY: This is safe because we're going to uphold all the invariants.
let foo = unsafe { // ← still doesn’t directly allow unsafe operations
let x = safe_preparation_operation(MAGIC_SAFEWORD);
x.prepare_even_more_safely();
// SAFETY: This is safe because x has been prepared so safely.
let result1 = x.unsafe this_is_where_the_unsafe_magic_happens();
// ↖ still required despite the outer `unsafe` block
gimme_that_result(result1);
// SAFETY: Our ffi is all good.
let final_result = x.unsafe this_is_where_the_unsafe_ffi_happens();
// (admitted, you only know that this is about FFI, when looking at the docs
// of `this_is_where_the_unsafe_ffi_happens`, but at least it’s very
// clear where exactly you have to look)
final_result
};
/* and now, let's completely ignore and */ drop(foo) /*, yay!! */;
safe_operation3();
}
If instead of using unsafe { … }
, marking the block here is done with comments alone, it wouldn’t be too bad either IMO. You’d just need a new convention, e.g.
fn safe() {
safe_operation1();
safe_operation2();
// SAFETY-CRITICAL: this block ensures the pre-conditions
// of two unsafe operations, be careful when modifying it
let foo = {
let x = safe_preparation_operation(MAGIC_SAFEWORD);
x.prepare_even_more_safely();
// SAFETY: This is safe because x has been prepared so safely.
let result1 = x.unsafe this_is_where_the_unsafe_magic_happens();
gimme_that_result(result1);
// SAFETY: Our ffi is all good.
let final_result = x.unsafe this_is_where_the_unsafe_ffi_happens();
// (admitted, you only know that this is about FFI, when looking at the docs
// of `this_is_where_the_unsafe_ffi_happens`, but at least it’s very
// clear where exactly you have to look)
final_result
};
/* and now, let's completely ignore and */ drop(foo) /*, yay!! */;
safe_operation3();
}
Using some kind of conventional marker in comments like “SAFETY-CRITICAL
”, you can mark blocks, or perhaps entire function bodies (probably commonly relevant for unsafe fn
implementations) or even entire modules (in particular when we don’t have unsafe fields yet) with this marker and add a note what one must look out for when modifying any code within the relevant block, function, or module.
In particular such a SAFETY-CRITICAL
comment would also imply that in the example above, anything that happens outside of the block is not relevant for ensuring the soundness of the unsafe
method calls. If e.g. only ensuring the safety conditions of one of multiple unsafe calls within such a block was completely handled within that block, then a SAFETY-CRITICAL
would explain which of the operations that is, e.g.
/// # Safety
/// You must ensure XYZ when calling this function
unsafe fn foo() {
// SAFETY-CRITICAL: in this function body qux is safe to call because
// of the safety conditions of `foo` above.
bar();
// SAFETY-CRITICAL: baz() and f() below together are relevant for g() to be safe to call
{
baz();
// SAFETY: [explain why exactly qux() is safe to call because of XYZ]
unsafe qux();
f();
// SAFETY: [explain why exactly g() is safe to call because the previous calls to baz() and f()]
unsafe g();
}
}
so the comments above would indicate that
- care must be taken when changing anything in the definition of
foo()
to make sure thatqux()
stays safe to call - additionally, care must be taken when changing anything inside of the block to make sure that
g()
stays safe to call