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
unsafeblocks are redundant: There already is a containingunsafeblock, so you don’t need to add another one for calling any unsafe method. Remove both innerunsafeblocks (only the blocks, not their content) and you’re good! - The outer
unsafeblock is redundant: The outerunsafeblock 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
notepointing out some contained unsafe blocks, e.g. “thisunsafeblocks does contain unsafe operations but those operations are located within nestedunsafeblocks” (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 innerunsafeblocks. - the first version could get a
notethat the containingunsafeblock 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.