Earlier quoted context omitted.
In general: whenever you call into C, you get all the safety of C, because C code can trivially have memory corruption bugs that corrupt memory "belonging" to Rust as well, inducing undefined behavior across your whole program [0]. In addition, it's often considered permissible for C APIs to exhibit undefined behavior if their API contracts are violated. This means that a bug in Rust code that calls into a C API inco…
> The typical approach to using C libraries from Rust is to create a "safe wrapper" around the unsafe FFI calls that uses Rust's type system and lifetimes to enforce the safety invariants. Calling it a "safe" wrapper, when that safety is entirely dependent on (a) the correctness of the hand-written wrapper, (b) the safety of the underlying FFI code, has always been a huge stretch of terminology. It's more like a veil…
Fully agreed on that -- the unsafe keyword means it's unsafe.
> nor is it in any way special in being able to create statically-verifiable abstractions around an unsafe core.
This isn't really true. Rust's type system has:
- Affine types
- Lifetimes
- Borrowed and exclusive references
- Thread-safety
- Explicit delineation between safe and unsafe code -- with no UB in safe code, and a (cultural) design principle that unsafe code is not allowed to make unchecked assumptions about the behavior of safe code
Most mainstream languages do not have any one of these features, let alone all of them. Thus, Rust's ability to "create statically-verifiable abstractions around an unsafe core" is in practice far more powerful than in C, C++, or Zig. You certainly have the ability to create some such abstractions in other languages, but you cannot statically verify nearly as many properties. (And of course there are languages like SPARK that can verify even more statically than Rust.)
In my experience, as a systems programmer who heavily uses Rust to interact with FFI, hardware MMIO and DMA, interrupt handling, networking code, etc., Rust's ability to safely abstract unsafe primitives is easily the most practically useful aspect of the language. It's not a silver bullet that turns incorrect code into correct code, but it is incredibly good at verifying the correctness of a large application built from small low-level primitives. If you're interfacing with buggy C code, Rust certainly isn't going to help you much -- but it does makes a huge difference in preventing "you're holding it wrong" bugs when interacting with a C API.
An unsafe block declares an axiom: you're asserting to the compiler that you've verified (statically with the type system or dynamically with runtime assertions) all preconditions necessary for some primitive to be sound (free of UB). The compiler can then use that axiom to prove the soundness of all code that interacts with that primitive. When I write a driver to perform a DMA transfer, I only have to think through all the concurrency, alignment, lifetime, moveability, caching, and cancellation requirements once, and the compiler will check them for me every time I (or anyone else) use that driver in an application.