March 16, 2021·6 min read·engineeringswiftconcurrencylanguage-design

Sendable makes Swift concurrency check the values being shared

SE-0302 adds a marker protocol and a function attribute for values that can cross concurrency domains. The design builds on value semantics while accounting for references, closure captures and errors.

Putting mutable state inside an actor cannot protect that state if a method returns an ordinary mutable reference to it. The receiver then has another route to the same object. Swift's concurrency design needs a rule for the values that cross an isolation boundary, as well as a rule for executing code inside it. SE-0302 has been accepted on March 16 to provide that part of the model through Sendable and @Sendable.

The acceptance includes a naming change. John McCall reports that the Core Team chose @Sendable for the function attribute, matching the protocol name. The connection is substantive. A function value can cross a concurrency boundary only when the state it carries satisfies the required safety rules. Its captures participate in the same reasoning as the stored properties of another value.

I like Swift's emphasis on values whose mutation can be understood locally. This proposal strengthens that idea by checking composition. A struct containing a mutable class reference still shares the referenced object when copied. Spelling the outer declaration as a value type does not establish that everything reachable through it behaves as a value.

Transfer safety depends on the contents

Chris Lattner and Doug Gregor's accepted proposal revision describes passing values between concurrency domains. Arguments and results of actor calls are examples. Structured concurrency introduces similar transfers between tasks. The proposed checking concerns whether copying a value across such a boundary can introduce unprotected shared mutable state.

Simple values fit the rule directly. A structure of integers and strings can satisfy the protocol because its constituent values satisfy it. The same reasoning applies recursively to enum associated values. A stored reference to an unsynchronized mutable object requires a different answer, even when every other field is an ordinary value.

The following declarations illustrate the accepted design. They describe the new language facility, not syntax that an existing stable compiler necessarily supports today.

struct Message: Sendable {
    var subject: String
    var attempt: Int
}

struct Pair<Element> {
    var first: Element
    var second: Element
}

extension Pair: Sendable where Element: Sendable {}

The generic constraint matters because Pair alone says nothing about the behavior of its elements. Conditional conformance carries the element requirement into the composite type. The proposal applies the same approach to standard collections. An array can conform when its elements conform, and a dictionary requires suitable keys and values. This preserves useful generic programming without making every possible instantiation equally transferable.

Copy-on-write collections fit this model without requiring an eager copy of their backing storage at every transfer. The public behavior must preserve independent values as mutation occurs. That is a stronger condition than observing that two copies initially share an allocation. The accepted design evaluates the safety promised by the type, including how it handles mutation.

A marker records a semantic promise

Sendable has no methods that an implementation must call to send a value. It is a marker protocol, which the proposal treats as a compile-time concept without runtime requirements. Marker protocols cannot provide ordinary protocol requirements, and a runtime conditional cast cannot be used to discover whether an arbitrary value conforms.

This choice keeps transfer checking in the type system. A generic function can require a sendable argument without demanding that every value implement a new copying operation. The proposal discusses an earlier design with a custom send hook and rejects its additional complexity. A transfer annotation does not imply that the runtime should execute an arbitrary user-defined conversion.

Checked conformance for structs and enums requires access to their complete stored representation. Consequently, it must appear in the same source file as the type definition. Looking only at public properties would miss private storage containing unsafe references. Access control can hide a field from callers without making its shared mutable state harmless.

The proposal permits inference for qualifying internal value types, while public non-frozen types need an explicit commitment. Making inferred sendability part of a public contract would constrain later changes to storage. The acceptance discussion also asks for clarification about frozen types. That discussion is still being clarified, so the dependable API design point is the need to treat public transfer guarantees as a deliberate commitment.

Reference types need a justified sharing model

A reference can safely cross a concurrency boundary when the referenced object's behavior supports sharing. The accepted design permits compiler-checked conformance for a narrow class case, a final class whose stored properties are immutable and themselves sendable. Finality prevents an unknown subclass from adding a different state model beneath the same declaration.

An immutable field alone is insufficient if it contains a reference to mutable, unsynchronized storage. Preventing replacement of the reference does not prevent mutation of its target. Requiring the field's type to conform extends checking through that relationship. This is the same compositional argument used for structures, applied within a more restricted class model.

Classes that implement their own synchronization can state an @unchecked Sendable conformance. The compiler cannot generally verify a locking discipline, so the author takes responsibility for the promise. Such a declaration does not insert locks or change how callers access the object. Its validity depends on the implementation actually making the public operations safe across concurrency domains.

I regard that escape mechanism as necessary, but it changes the evidence behind a declaration. Checked conformance lets the compiler inspect a restricted representation. Unchecked conformance asks readers to rely on a separate synchronization argument. Keeping the annotation visible helps distinguish those cases when a library exposes a shared reference type.

Actor references have their own place in the model. The proposal makes actor types implicitly sendable because actors supply internal synchronization. Transferring an actor reference preserves that access discipline. Transferring an unrelated mutable reference obtained from inside the actor would need its own justification.

Closures carry state across the same boundary

A closure packages executable code with captured values. Its parameter and return types do not describe all the state it can access. The @Sendable function attribute requires its captures to conform to Sendable and restricts capture behavior to values. An immutable local can be captured by value implicitly, while another capture can use an explicit capture list.

For example, a predicate can capture an immutable string and use it to compare an argument. A closure that instead captures a mutable local by reference and increments it exposes shared mutable storage. Merely arranging for that closure to run in another task does not make the storage safe. The attribute makes the problematic relationship available to compiler checking.

@Sendable and @escaping describe different properties. The accepted text treats them as orthogonal. Escaping concerns whether a function value can outlive the call that receives it. Sendability concerns whether its captured state permits transfer across concurrency domains. One property cannot stand in for the other when declaring a callback API.

Context can infer the sendable attribute for a closure expression when the receiving parameter expects it. A closure without such context does not become sendable automatically. That choice makes an API declaration important to downstream checking. Requiring a sendable callback lets the compiler assess captures where the caller constructs the closure.

Nested functions require attention for the same reason. Their declaration syntax can conceal the fact that they also capture surrounding local variables. The proposal permits the attribute on such declarations, subjecting their captures to the relevant checks. Moving a closure body into a named local function therefore does not supply a way to avoid the transfer requirement. The function type must still meet the receiving API's constraint.

Thrown errors also transfer values

An error can escape an actor just as a normal result can. The proposal illustrates an error structure that holds mutable storage from the actor. Throwing that error would expose the storage to another concurrency domain even if ordinary method results were checked correctly. A throws annotation does not enumerate the concrete error types that all called functions might produce.

To account for that route, the accepted design makes Error inherit from Sendable. It also makes CodingKey inherit from it so that encoding and decoding errors can carry suitable keys. These changes show why transfer checking cannot stop at explicit return values. Failure paths also transport application objects.

The source compatibility section acknowledges that these inherited requirements affect existing declarations. It specifies warnings in earlier language modes and enforcement in a future Swift 6 mode. This is an adoption plan in the accepted proposal, not a statement that a future release has already shipped. The immediate result is a defined model in which normal values, callbacks and thrown failures all participate in checking the same isolation boundary.