Another crazy idea I share with this website.
I was developing a game and an engine in Rust, so I was reading many articles, most of which criticize the ‘borrow checker’.
I know that Rust is a big agenda language, and the extreme ‘borrow checker’ shows that, but if it weren’t for the checker, Rust would be a straight-up better C++ for Game development, so I thought: “Why not just use unsafe
?”, but the truth is: unsafe
is not ergonomic, and so is Refcell<T>
so after thinking for a bit, I came up with this pattern:
let mut enemies = if cfg!(debug_assertions) {
// We use `expect()` in debug mode as a layer of safety in order
// to detect any possibility of undefined bahavior.
enemies.expect("*message*");
} else {
// SAFETY: The `if` statement (if self.body.overlaps...) must
// run only once, and it is the only thing that can make
// `self.enemies == None`.
unsafe { enemies.unwrap_unchecked() }
};
You can also use the same pattern to create a RefCell<T>
clone that only does its checks in ‘debug’ mode, but I didn’t test that; it’s too much of an investment until I get feedback for the idea.
This has several benefits:
1 - No performance drawbacks, the compiler optimizes away the if
statement if opt-level
is 1 or more. (source: Compiler Explorer)
2 - It’s as safe as expect()
for all practical use cases, since you’ll run the game in debug mode 1000s of times, and you’ll know it doesn’t produce Undefined Behavior
If it doesn’t crash.
You can also wrap it in a “safe” API for convenience:
// The 'U' stands for 'unsafe'.
pub trait UnwrapUExt {
type Target;
fn unwrap_u(self) -> Self::Target;
}
impl<T> UnwrapUExt for Option<T> {
type Target = T;
fn unwrap_u(self) -> Self::Target {
if cfg!(debug_assertions) {
self.unwrap()
} else {
unsafe { self.unwrap_unchecked() }
}
}
}
I imagine you can do many cool things with these probably-safe APIs, an example of which is macroquad’s possibly unsound usage of get_context()
to acquire a static mut
variable.
Game development is a risky business, and while borrow-checking by default is nice, just like immutability-by-default, we shouldn’t feel bad about disabling it, as forcing it upon ourselves is like forcing immutability, just like Haskell does, and while it has 100% side-effect safety, you don’t use much software that’s written in Haskell, do you?
Conclusion: we shouldn’t fear unsafe
even when it’s probably unsafe, and we must remember that we’re programming a computer, a machine built upon chaotic mutable state, and that our languages are but an abstraction around assembly.
I see now, that you were misunderstood in some parts.
This may be problematic for several reasons: it may be hard to reproduce, the more complicated the state, the harder; bug may rely on some race condition that may be much rarer in Debug because of speed difference; UB is notorious for causing things that should (seemingly) never happen, like returning from infinite loops, proving false statements true, and such, so it may be hard to understand what at all happened and why.
Regarding optimisations, it might still be better to try to profile the code (I will be honest, I don’t do that until the moment when I can’t go further without optimisation, and I haven’t reached that with Rust) and see what are the real hot spots that require optimisations. I hope that someday you will be able to upgrade your machine, and hope that your game will be a good example of something that really runs anywhere