Hey, I’d be interested in feedback what the best approach here ought to be. Consider some code, e.g.
fn safe() {
safe_operation1();
safe_operation2();
// SAFETY: Lorem ipsum dolor sit amet, consectetur adipiscing elit,
// sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
let foo = unsafe {
let x = safe_preparation_operation(MAGIC_SAFEWORD);
x.prepare_even_more_safely();
let result1 = unsafe {
x.this_is_where_the_unsafe_magic_happens()
};
gimme_that_result(result1);
let final_result = unsafe {
x.this_is_where_the_unsafe_magic_happens_part_two()
};
final_result
};
/* and now, let's completely ignore and */ drop(foo) /*, yay!! */;
safe_operation3();
}
every operation is safe except for the two methods “…where_the_unsafe_magic_happens…
”. Now obviously there’s more unsafe
blocks here than necessary. But it’s not clear what the best approach is for improving the code. What would you change? What kind of warning/explanation would you expect/want the compiler to give you?
Some thoughts…
Explaning the redundancy
So there’s two possible ways of explaining here how/why one or two of the unsafe {}
blocks are redundant.
- The inner
unsafe
blocks are redundant: There already is a containingunsafe
block, so you don’t need to add another one for calling any unsafe method. Remove both innerunsafe
blocks (only the blocks, not their content) and you’re good! - The outer
unsafe
block is redundant: The outerunsafe
block is unused i.e. it doesn’t (directly) contain any actual unsafe code. If the outer block is removed (e.g. turned into an ordinary block) then the code will be fine!
A question of style?
Now, which solution should be taken? It’s not clear cut, here’s a few pros and cons…
Keeping the outer unsafe
blocks would group together unsafe operations with some safe operations for setup that might be crucial for the soundness of the overall endeavor. In this example, there’s also a SAFETY
comment explaining why the whole thing is sound. Would it be good style to keep a SAFETY
comment on a larger safe block that merely contains multiple unsafe operations in their own smaller unsafe blocks? Or do SAFETY
comments have to go directly in front of each and every unsafe
block?
Would the code perhaps be easier to understand if the inner blocks are kept, acting as a clear indication where exactly within the lengthy outer block the actually unsafe operations are that need some safety conditions to be fulfilled in order to be sound to call?
Keeping the inner unsafe
blocks also means less syntactical change to the code to resolve any warnings.
Keeping the outer unsafe
block is what you’re going to do if you’re following the warning that rustc
currently gives. (I don’t know how strong of an argument the status quo is for anything.)
Warnings, suggestions, etc…
Now let’s consider some possible ways that the compile could respond to this code. Let’s start with the status quo:
warning: unnecessary `unsafe` block
--> src/lib.rs:10:23
|
7 | let foo = unsafe {
| ------ because it's nested under this `unsafe` block
...
10 | let result1 = unsafe {
| ^^^^^^ unnecessary `unsafe` block
|
= note: `#[warn(unused_unsafe)]` on by default
warning: unnecessary `unsafe` block
--> src/lib.rs:15:28
|
7 | let foo = unsafe {
| ------ because it's nested under this `unsafe` block
...
15 | let final_result = unsafe {
| ^^^^^^ unnecessary `unsafe` block
And this is how the warning could look like instead, if the compiler only complained about the outer unsafe
block being unused:
warning: unnecessary `unsafe` block
--> src/lib.rs:7:15
|
7 | let foo = unsafe {
| ^^^^^^ unnecessary `unsafe` block
|
= note: `#[warn(unused_unsafe)]` on by default
Then there’s most likely some middle-ground, too. Let’s first point out some properties of these different warnings:
- the first one is longer / there are more warnings
- the second one does not point out all the unsafe blocks involved
Then some logical alternatives / variations include
- the second version could be used but it could get a
note
pointing out some contained unsafe blocks, e.g. “thisunsafe
blocks does contain unsafe operations but those operations are located within nestedunsafe
blocks” (and then give an example of one of the nested unsafe blocks). Pointing out one example is enough: If the user removed one of the nested unsafe block, the outer one becomes used, and further warnings will point out the remaining now redundant innerunsafe
blocks. - the first version could get a
note
that the containingunsafe
block is actually unused and that resolving the warning in this case could involve either removing the unnecessary unsafe block or the containing one.
A word about macros
There’s one particular and not uncommon usage of unsafe
blocks that makes, IMO, warning about the outer unsafe
block a better choice: If a macro contains some usage of unsafe
, and the inner unsafe
block in a setting as above comes from such a macro, then that inner unsafe
block might not report any warnings at all, either because the macro author (rightfully) placed an #[allow(unused_unsafe)]
around it, or because the unused_unsafe
warning doesn’t get reported at all for external macros. In order to have code like
unsafe {
some_safe_macro!("…");
more_safe_code();
}
not end up producing no warning at all when some_safe_macro
uses unsafe {}
internally in its expansion (think e.g. the futures::pin_mut
macro), there would need to be special-case logic detecting external macros (or maybe even macros in general) that changes the behavior accordingly in order to, ultimately, do report the outer unsafe
block as being unused in this case. OTOH, if the outer unsafe block is always the one that’s reported anyways, then no special handling of macros is required.
Finding the right warning behavior here is probably a (minor) language design decision about whether more coarse or more fine-grained unsafe
blocks are preferred, and to a large degree a compiler diagnostics design question on what the best approach should look like in detail. Ultimately, I can’t decide alone what approach should be taken, though I have a slight preference (in case you couldn’t tell that already). I’d to get as much feedback / as many opinions as possible here in order to get an indicator of what might be preferred by the community, also feel free to point out any aspects in this discussion that I’ve overlooked. This general question came up during review of #93678.