January 15, 2025·6 min read·engineeringswiftmemory-safetyinteroperability

Swift's strict memory safety vision makes unsafe boundaries explicit

Swift has accepted a direction for optional strict memory safety. The difficult work lies in safe interfaces over unsafe implementations and in describing the lifetimes of imported memory.

An unsafe operation need not have an alarming name. The Swift memory safety vision uses memcpy as an example. Its pointer parameters permit mistakes about storage and byte counts, yet a call can contain neither unsafe nor unchecked. Naming conventions help readers recognize many dangerous operations, but they cannot establish that a module avoids them. That gap explains the proposed compiler checking more precisely than a general ambition to make Swift safer.

On January 15, the Language Steering Group accepted the optional strict memory safety vision. Doug Gregor wrote the document. Acceptance endorses its goals and broadly supports its approach. Individual language changes still require ordinary evolution review and can change or fail that review. The announcement explicitly says that this checking is not intended to become the default in a future language mode.

I find Swift's value model and explicit optionals well designed, while recognizing that a struct containing references does not eliminate aliasing. An optional way to identify unsafe operations fits that assessment. It gives a project a more precise account of where the language stops checking its assumptions without requiring every application to adopt the same policy.

Safe execution can still fail

The vision's December revision, available before this acceptance, separates memory safety into lifetime, bounds, type, initialization, and thread safety. These properties concern whether a program can access dead storage, exceed an allocation, interpret a value through an incompatible type, read uninitialized data, or race on shared state. A program can satisfy them and still produce an incorrect result or terminate.

Swift already supplies mechanisms for these properties. Reference counting manages reference lifetimes, and exclusivity constrains overlapping access. Collections check bounds. Definite initialization prevents reads before a value has been initialized. The vision places Swift 6 strict concurrency checking alongside those mechanisms because data races can undermine the other guarantees.

Optional unwrapping makes the distinction concrete. A force unwrap checks for absence and traps when its operand is nil. That is a runtime failure with defined behavior. The vision contrasts it with unsafelyUnwrapped, whose release-build behavior omits the check. Removing a check can permit the program to violate assumptions about initialized values and their lifetimes. Strict memory safety is consequently not a proposal to reject every operation that can crash.

That distinction matters for interpreting diagnostics. A bounds check that stops an invalid array access preserves the safety property. A pointer operation that continues beyond the allocation does not. Treating both as equivalent failures loses the reason a security-sensitive component may accept a controlled termination while rejecting unchecked access to memory.

Declarations carry the information names cannot

The proposed checking would recognize unsafe language constructs and declarations. Examples include unowned(unsafe), nonisolated(unsafe), and @unchecked Sendable. The document also sketches an @unsafe annotation for APIs whose use requires obligations the compiler cannot establish. Pointer types and unchecked operations would acquire that classification.

An API involving an unsafe type would itself count as unsafe. That rule explains how the compiler could recognize the problem with memcpy even when a caller sees only its ordinary C name. The relevant evidence is in the imported declaration. A reviewer does not have to infer its status from the spelling of the call.

This is still a design direction, so the annotation must not be presented as newly available syntax. The same applies to proposed mechanisms for acknowledging unsafe implementation code. The acceptance announcement reserves their details for separate proposals. A source example using those mechanisms today would illustrate the vision, not demonstrate a feature delivered by this announcement.

The distinction between an unsafe declaration and a safe wrapper is essential. A function classified as unsafe could invoke other unsafe operations. Its callers would then see the classification. Another function could use those same operations internally but establish enough invariants to offer callers a safe interface. Such a wrapper needs an explicit acknowledgement of its internal obligations without classifying every caller as unsafe.

Safe wrappers retain implementation obligations

The standard library's collections explain why banning every unsafe implementation would be impractical. A safe collection still needs a way to manage underlying storage. The vision proposes locally acknowledged unsafe code, with possible syntax including a block or an unchecked safety annotation. It leaves the choice of spelling open.

A bounds-checked subscript over an underlying buffer illustrates the obligation. The wrapper must establish that the position is nonnegative and less than the buffer's count before accessing storage. Its safe status depends on that implementation being correct. An acknowledgement tells the compiler that this part requires human reasoning. It does not prove the reasoning sound.

The vision therefore includes auditing across dependencies. Tooling should identify modules that did not enable strict checking and locate acknowledged unsafe regions in modules that did. A project could use that information to require additional review or restrict unsafe implementation to selected libraries. Merely enabling a compiler option would not establish that every dependency contains no unsafe code.

I value that explicit distinction between compiler checking and a library author's promise. A useful safety report should identify both, because a safe public signature can conceal implementation obligations. Otherwise the report risks describing the caller accurately while encouraging an unsupported conclusion about the whole executable.

Contiguous memory needs a safe representation

Stricter diagnostics alone would make some legitimate low-level work harder. The vision identifies contiguous storage as an important gap. Existing unsafe buffer access can expose an allocation through a closure, but the pointer representation does not establish that it stays within the closure's lifetime. Unsafe buffer indexing also lacks release-build bounds checking.

The proposed Span direction addresses both properties. A span provides bounded access to contiguous memory, while non-escapable types constrain where the value can go. Lifetime dependencies would express relationships between borrowed storage and the owner that keeps it valid. These mechanisms would let APIs describe access to existing memory without requiring callers to assume responsibility for an untracked pointer.

Bounds and lifetime are independent obligations. Carrying an element count helps detect an invalid index, but the count does not keep the allocation alive. Conversely, keeping an allocation alive does not establish that an index falls inside it. An interface intended to replace unsafe buffer access must represent both relationships.

The practical consequence reaches beyond projects that enable strict checking. An API that expresses these relationships can be easier to use correctly in ordinary Swift too. The vision treats safer representations as part of the implementation work because diagnostics become more useful when programmers have a suitable replacement for the operation being diagnosed.

Imported APIs need more than a pointer count

The C interoperability section describes an averaging function whose arguments include a pointer and an element count. Those parameters are related, but an ordinary pointer declaration does not encode that relationship. A caller can provide a count inconsistent with the allocation. A bounds annotation could let Swift import the pair as one bounded value.

That change alone cannot establish a safe temporary borrow. The C function could retain the pointer and use it after the Swift caller's storage has ceased to be valid. A non-escaping annotation supplies another part of the contract. Together, the declarations could support a Swift interface using a span instead of exposing an unsafe pointer and a separate count.

Returning a pointer into an argument introduces a further relationship. The returned value remains valid only while the original storage remains valid. Lifetime annotations would express that dependency, including analogous cases in C++. Their purpose is to preserve a useful reference without manufacturing an unnecessary copy or silently losing the relationship to its owner.

These annotations describe obligations that the foreign implementation must honor. They do not make the Swift compiler verify arbitrary C code. Incremental adoption therefore retains trusted implementation boundaries. The benefit is that Swift callers can operate under explicit contracts instead of repeatedly reconstructing pointer conventions from documentation.

Optional adoption preserves existing interfaces

The document proposes adoption at module boundaries without requiring dependencies or clients to adopt first. Its sketched unsafe annotation would not become part of a declaration's type. A module could acknowledge unsafe use locally or propagate its classification through an unsafe declaration. Clients that did not opt into checking would receive no new diagnostics from that policy alone.

The vision also says that the checking would not require a changed binary interface. That supports adding annotations without making deployment to existing systems depend on a new ABI. Existing pointer-based APIs remain an important constraint because replacing them can break source or binary clients. Safe alternatives need time to become available across an ecosystem.

For the small Swift applications I write, this is a useful direction without an immediate migration obligation. The accepted document deliberately preserves Swift's current default. Its concrete development work concerns representing borrowed storage, classifying unsafe declarations, and making acknowledged implementation code visible to auditing tools. Each corresponding proposal will still need to establish the rules developers can rely on.