Introduction
With the recent introduction of opt-in strict memory safety checking (SE-0458), Swift gained the @unsafe attribute. When strict safety mode is enabled, any interaction with unsafe types or functions requires an explicit unsafe effect at the call site:
extension Array<Int> {
func sum() -> Int {
withUnsafeBufferPointer { buffer in
unsafe sumIntBuffer(buffer.baseAddress, buffer.count, 0)
}
}
}
Motivation
Historically, in non-strict modes, Swift has relied on a naming convention where "unsafe" is included as a substring in identifier names to warn developers of potential risks. A recent pitch highlighted this by proposing to rename certain symbols to reflect their underlying unsafety.
However, relying on naming conventions presents two major issues:
- API Churn: Renaming a symbol inherently breaks backward compatibility. Furthermore, if the underlying source of unsafety is eventually resolved (such as an LLVM fix in the linked pitch), removing "unsafe" from the name causes yet another breaking change.
- C++ Interoperability: In C++ interop, certain functions are inherently dangerous to use because Swift lacks the lifetime information necessary to keep the backing storage of returned views alive. Previously, we attempted to communicate this risk by automatically renaming these functions to include "Unsafe". This heuristic is often confusing, obfuscates the original API, and severely hinders discoverability.
Now that Swift has dedicated syntax to communicate unsafety, we have better tools at our disposal than renaming symbols.
Proposed Solution
I propose addressing both of these problems by introducing a stronger, unconditional variant of @unsafe, spelled as @unsafe(always).
Functions or types annotated with @unsafe(always) would mandate the unsafe keyword at their use site across all language modes, not just when strict memory safety is enabled.
This approach entirely avoids the churn of renaming symbols. Once the underlying safety issue is mitigated, the @unsafe(always) annotation can simply be removed without breaking backward compatibility.
Example: Improving C++ Interop
To illustrate the impact, let us look at how C++ interoperability would improve. Previously, due to the renaming heuristics designed to enforce Swift's naming conventions, a user would need to know that renaming had occurred. The exact spelling of the imported function was undiscoverable:
// C++
class MyIntVector {
int &getRawRef() { return data; }
};
// Swift caller (Before):
let _ = myVec.getRawRef() // Error: no such function
let _ = myVec.__getRawRefUnsafe() // Compiles, but is highly unintuitive
Under this proposal, no confusing renaming would occur. Instead, the inherent unsafety must be explicitly acknowledged via the unsafe effect:
// Swift caller (After):
let _ = myVec.getRawRef() // Error: function requires an explicit 'unsafe' acknowledgment
let _ = unsafe myVec.getRawRef() // Compiles
This change would make C++ interop significantly more intuitive for newcomers, and regular Swift APIs could similarly benefit in scenarios like the one linked above.
Conclusion
Because this proposal introduces the unsafe keyword into regular Swift (even when strict memory safety is disabled), the intention is to use @unsafe(always) sparingly. It should be reserved for scenarios where there is a compelling need to highlight significant API footguns.
What do you think? Is this a viable approach for the language?
I implemented a prototype of this change, feel free to experiment with it: