[Pitch/Discussion] Introduce `@unsafe(always)` to require explicit unsafe in all language modes

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:

  1. 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.
  2. 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:

9 Likes

How would this interact syntactically with swift-evolution/proposals/0458-strict-memory-safety.md at main · swiftlang/swift-evolution · GitHub? I think it's desirable to be able to define "unsafe to implement" protocols, that are seen as unsafe in all language modes. The future direction mentions @unsafe(conforms) as a syntax for that which clashes with this suggestion, maybe something like @unsafe(always, to: conform) in that case is ok?

I think this would be nice, but isn't anything annotated with @unsafe already this way? I can't think of good guidance for when to choose an annotation. Undermining memory safety is one of "the biggest" footguns imo, you'll lose more than a foot... Afaik, @unsafe in swift isn't just for "may be surprising if you hold it wrong", or even "won't do what it says if you don't check invariants" which can be communicated with naming conventions and is still safe (this is a common misconception in Rust, it is totally safe to do unexpected things even if its unpleasant), but "will lead to memory unsafety if you hold it wrong, and I will not / cannot check invariants to determine if that will happen". What kinds of @unsafe functions are not "big footguns" to use wrong?

1 Like