- Proposal: SE-NNNN
- Authors: Doug Gregor, Becca Royal-Gordon
- Review Manager: TBD
- Status: Awaiting implementation
SE-0302 introduced the Sendable
protocol, which is used to indicate which types have values that can safely be copied across task boundaries or, more generally, into any context where a copy of the value might be used concurrently with the original. However, Swift 5.5 does not fully enforce Sendable
because interacting with modules which have not been updated for Swift Concurrency would be painful. We propose adding features to help developers migrate their code to support Sendable
checking and interoperate with other modules that have not yet adopted it.
Swift-evolution threads: [Pitch] Staging in Sendable
checking, Discussion thread topic for that proposal
Swift Concurrency seeks to “provide a mechanism for isolating state in concurrent programs to eliminate data races.” That mechanism is Sendable
checking. APIs which send data across task boundaries require their inputs to conform to the Sendable
protocol; types which are safe to send declare conformance, and the compiler checks that these types only contain Sendable
types, unless the type's author explicitly indicates that the type is implemented so that it uses any un-Sendable
contents safely.
This would all be well and good if we were writing Swift 1, a brand-new language which did not need to interoperate with any existing code. Unfortunately, we are instead writing Swift 6, a new version of an existing language with millions of lines of existing libraries and deep interoperation with C, Objective-C, and hopefully soon C++ headers. None of this code specifies any of its concurrency behavior in a way that Sendable
checking can understand, but until it can be updated, we still want to use it from Swift 6 code.
There are several areas where we wish to address adoption difficulties.
Many existing APIs should be updated to formally specify specify concurrency behavior that they have always followed, but have not been able to describe to the compiler until now. For instance, it has always been the case that most UIKit methods and properties should only be used on the main thread, but before the @MainActor
attribute, this behavior could only be documented and asserted in the implementation, not described to the compiler.
Thus, many modules should undertake a comprehensive audit of their APIs to decide where to add concurrency annotations. But if they try to do so with the tools they currently have, this will surely cause source breaks. For instance, if a method is marked @MainActor
, projects which have not yet adopted Swift Concurrency will be unable to call it even if they are using it correctly, because the project does not yet have the annotations to prove to the compiler that the call will run in the main actor.
In some cases, these changes can even cause ABI breaks. For instance, @Sendable
attributes on function types and Sendable
constraints on generic parameters are incorporated into mangled function names, even though Sendable
conformances otherwise have no impact on the calling convention (there isn't an extra witness table parameter, for instance). A mechanism is needed to enforce these constraints during typechecking, but generate code as though they do not exist.
Here, we need:
-
A formal specification of a "compatibility mode" for pre-concurrency code which imports post-concurrency modules
-
A way to mark declarations as needing special treatment in this "compatibility mode" because their signatures were changed for concurrency
The process of auditing libraries to add concurrency annotations will take a long time. We don't think it's realistic for each module to wait until all of its libraries have been updated before they can start adopting Sendable
checking.
This means modules need a way to work around incomplete annotations in their imports--either by tweaking the specifications of imported declarations, or by telling the compiler to ignore errors. Whatever mechanism we use, we don't want it to be too verbose, though; for example, marking every single variable of a non-Sendable
type which we want to treat as Sendable
would be pretty painful.
We must also pay special attention to what happens when the library finally does add its concurrency annotations, and they reveal that a client has made a mistaken assumption about its concurrency behavior. For instance, suppose you import type Point
from module Geometry
. You enable Sendable
checking before Geometry
's maintainers have added concurrency annotations, so it diagnoses a call that sends a Point
to a different actor. Based on the publicly-known information about Point
, you decide that this type is probably Sendable
, so you silence this diagnostic. However, Geometry
's maintainers later examine the implementation of Point
and determine that it is not safe to send, so they mark it as non-Sendable
. What should happen when you get the updated version of Geometry
and rebuild your project?
Ideally, Swift should not continue to suppress the diagnostic about this bug. After all, the Geometry
team has now marked the type as non-Sendable
, and that is more definitive than your guess that it would be Sendable
. On the other hand, it probably shouldn't prevent you from rebuilding your project either, because this bug is not a regression. The updated Geometry
module did not add a bug to your code; your code was already buggy. It merely revealed that your code was buggy. That's an improvement on the status quo--a diagnosed bug is better than a hidden one.
But if Swift reacts to this bug's discovery by preventing you from building a module that built fine yesterday, you might have to put off updating the Geometry
module or even pressure Geometry
's maintainers to delay their update until you can fix it, slowing forward progress. So when your module assumes something about an imported declaration that is later proven to be incorrect, Swift should emit a warning, not an error, about the bug, so that you know about the bug but do not have to correct it just to make your project build again.
Here, we need:
-
A mechanism to silence diagnostics about missing concurrency annotations related to a particular declaration or module
-
Rules which cause those diagnostics to return once concurrency annotations have been added, but only as warnings, not errors
SE-0297's __attribute__((swift_attr))
makes it possible to declare many Swift concurrency behaviors in Objective-C headers[1], and SE-0302 specifies structural inference rules for Sendable
conformances. However, neither proposal allows you to declare whether there should or shouldn't be a Sendable
conformance, so there is no way to override SE-0302's rules when the semantic behavior doesn't match the behavior implied by the structural inference rules.
For instance, a struct containing an int
would be inferred to be Sendable
because integers can generally be safely copied, but if that int
is really a file descriptor, you would only want the struct to be Sendable
if the APIs used that that file descriptor in a thread-safe way. Contrariwise, Objective-C classes can contain shared mutable state so they should not be Sendable
by default, but if a specific class is known to be synchronized or truly immutable, it should be possible to make it Sendable
. In both cases, we should be able to override the default behavior and specify the sendability of the type.
Here, we need:
- A way to add a
Sendable
conformance--whether available or explicitly unavailable--to imported types
[1] SE-0297 alone allows you to specify:
swift_attr("@MainActor")
on declarations and block types.swift_attr("nonisolated")
on declarations.swift_attr("@Sendable")
on block types, Objective-C method declarations, and C function declarations.
We propose a suite of features to aid in the adoption of concurrency annotations and Sendable
checking. These features are designed to enable the following workflow for adopting Sendable
checking:
-
Enable Swift 6 mode or the
-warn-concurrency
flag. This causes new errors or warnings to appear whenSendable
constraints are violated. -
Start solving those errors. If they relate to types from another module, a fix-it will suggest using
@predatesConcurrency import
; do that to silence those warnings. -
Once you've solved these errors, integrate your changes into the larger build.
-
At some future point, a module you import may be updated to add
Sendable
conformances and other concurrency annotations. If it is, and your code violatesSendable
constraints, you will see warnings telling you about these mistakes; these are latent concurrency bugs in your code. Correct them. -
Once you've fixed those bugs, or if there aren't any, you will see a warning telling you that the
@predatesConcurrency import
is unnecessary. Remove the@predatesConcurrency
attribute. AnySendable
-checking failures involving that module from that point forward will not suggest using@predatesConcurrency import
and, in Swift 6 mode, will be errors that prevent your project from building.
Achieving this will require several features working in tandem:
-
In Swift 6 mode, all code will be checked for missing
Sendable
conformances and other concurrency violations, with mistakes generally diagnosed as errors. The-warn-concurrency
flag will diagnose these violations as warnings in older language versions. -
When applied to a nominal declaration, the
@predatesConcurrency
attribute specifies that a declaration was modified to update it for concurrency checking, so the compiler should allow some uses in Swift 5 mode that violateSendable
checking, and generate code that interoperates with pre-concurrency binaries. -
When applied to an
import
statement, the@predatesConcurrency
attribute tells the compiler that it should only diagnoseSendable
-requiring uses of non-Sendable
types from that module if the type explicitly declares aSendable
conformance that is unavailable or has constraints that are not satisifed; even then, this will only be a warning, not an error. -
For Objective-C libraries,
__attribute__((swift_attr()))
will be extended to allow you to specify which types should beSendable
, including blanket non-Sendable
for regions of your headers that you have finished auditing.
When this proposal speaks of an error being emitted as a warning or suppressed, it means that the compiler will recover by behaving as though (in order of preference):
-
A nominal type that does not conform to
Sendable
does. -
A function type with an
@Sendable
or global actor attribute doesn't have it.
Every scope in Swift can be described as having one of three "concurrency checking modes":
-
Full concurrency checking: Missing
Sendable
conformances are diagnosed as errors. -
Strict concurrency checking: Missing
Sendable
conformances are diagnosed as warnings. -
Minimal concurrency checking: Missing
Sendable
conformances are diagnosed as warnings; on nominal declarations,@predatesConcurrency
(defined below) has special effects in this mode which suppress many diagnostics.
The top level scope's concurrency checking mode is:
-
Full when the module is being compiled in Swift 6 mode or later.
-
Strict when the module is being compiled in a language version mode before Swift 6, but the
-warn-concurrency
flag is used, or when the file being parsed is a module interface. -
Minimal otherwise.
A child scope's concurrency checking mode is:
-
Strict if the parent's concurrency checking mode is Minimal and any of the following conditions is true of the child scope:
-
It is a closure with an explicit
@actorIndependent
or global actor attribute. -
It is a closure or autoclosure whose type is
async
or@Sendable
. (Note that the fact that the parent scope is in Minimal mode may affect whether the closure's type is inferred to be@Sendable
.) -
It is a declaration with an explicit
@actorIndependent
,nonisolated
, or global actor attribute. -
It is a function, method, initializer, accessor, variable, or subscript which is marked
async
or@Sendable
. -
It is an
actor
declaration.
-
-
Otherwise, the same as the parent scope's.
Implementation note: The logic for determining whether a child scope is in Minimal or Strict mode is currently implemented in
swift::contextUsesConcurrencyFeatures()
.
Imported Objective-C declarations belong to a scope with Minimal concurrency checking.
To describe their concurrency behavior, maintainers must change some existing declarations in ways which, by themselves, could be source-breaking in pre-concurrency code or ABI-breaking when interoperating with previously-compiled binaries. In particular, they may need to:
- Add
@Sendable
or global actor attributes to function types - Add
Sendable
constraints to generic signatures - Add global actor attributes to declarations
When applied to a nominal declaration, the @predatesConcurrency
attribute indicates that a declaration existed before the module it belongs to fully adopted concurrency, so the compiler should take steps to avoid these source and ABI breaks. It can be applied to any enum
, enum case
, struct
, class
, actor
, protocol
, associatedtype
, var
, let
, subscript
, init
, func
, accessor, or deinit
declaration. It can also be applied to an extension, in which case it will be applied to all declarations of those kinds in the extension.
[Maybe you should be allowed to apply it to typealias
, where it would mean that any declaration that used that typealias in its signature would need to have @predatesConcurrency
. That way, if you changed a typealias for (T) -> U
into @Sendable (T) -> U
, you could add @predatesConcurrency
to the typealias to ensure that the APIs using that typealias were all updated.]
When a nominal declaration uses @predatesConcurrency
:
-
Its name is mangled as though it does not use any of the listed features.
-
At use sites whose enclosing scope uses Minimal concurrency checking, the compiler will suppress any diagnostics about mismatches in these traits.
-
The ABI checker will remove any use of these features when it produces its digests.
Objective-C declarations are always imported as though they were annotated with @predatesConcurrency
.
A type can be described as having one of the following three Sendable
conformance statuses:
-
Explicitly
Sendable
if it actually conforms toSendable
. -
Explicitly non-
Sendable
if aSendable
conformance has been declared for the type, but it is not available or has constraints the type does not satisfy, or if the type was declared in a scope that uses Full or Strict concurrency checking.[2] -
Implicitly non-
Sendable
if noSendable
conformance has been declared on this type at all.
[2] This means that, if a module is compiled with Swift 6 mode or the
-warn-concurrency
flag, all of its types are either explicitlySendable
or explicitly non-Sendable
.
The @predatesConcurrency
attribute can be applied to an import
statement to indicate that the compiler should reduce the strength of some concurrency-checking violations caused by types imported from that module. You can use it to import a module which has not yet been updated with concurrency annotations; if you do, the compiler will tell you when all of the types you need to be Sendable
have been annotated. It also serves as a temporary escape hatch to keep your project compiling until any mistaken assumptions you had about that module are fixed.
When an import is marked @predatesConcurrency
, the following rules are in effect:
-
If an implicitly non-
Sendable
type is used where aSendable
type is needed:-
If the type is visible through an
@predatesConcurrency import
, no diagnostic is emitted. -
Otherwise, the diagnostic is emitted normally, but a note is attached recommending that
@predatesConcurrency import
be used to work around the issue.
-
-
If an explicitly non-
Sendable
type is used where aSendable
type is needed:-
If the type is visible through an
@predatesConcurrency import
, a warning is emitted instead of an error, even in Full concurrency checking mode. -
Otherwise, the diagnostic is emitted normally.
-
-
If the
@predatesConcurrency
attribute is unused[3], a warning will be emitted recommending that it be removed.
[3] We don't define "unused" more specifically because we aren't sure if we can refine it enough to, for instance, recommend removing one of a pair of
@predatesConcurrency
imports which both import an affected type.
Swift will extend swift_attr("@Sendable")
to allow it to be applied to Objective-C interfaces and protocols, C record types (structs and unions), and C enum types. If an @Sendable
attribute is present on such a type, Swift will synthesize an unconditional, fully available Sendable
conformance.
Swift will implicitly add swift_attr("@Sendable")
to:
-
The block type of the completion handler parameter of a method which is imported as
async
. (That is, if Swift infers an imported method to beasync
, it will also modify the completion handler of the non-async
version of the method to be@Sendable
.) -
Declarations for types marked with the following attributes:
enum_extensibility
(NS_ENUM
)ns_error_domain
(NS_ERROR
)flag_enum
(NS_OPTIONS
)swift_wrapper
(NS_TYPED_ENUM
)
Warning: Marking
swift_wrapper
types asSendable
creates a small hole inSendable
checking because, if you returned anNSMutableString
from a Swift API that was imported as theswift_wrapper
type and later mutated it, this mutation would be visible from another thread. However, this is a serious misuse ofswift_wrapper
types even in non-concurrent scenarios.
ClangImporter will also add swift_attr("@_nonSendable")
, which can be applied to anything that allows swift_attr("@Sendable")
, including the type declarations mentioned above. When applied to a type declaration, it indicates that Swift should synthesize an explicitly unavailable Sendable
conformance for that declaration.
This attribute can also be specified as swift_attr("@_nonSendable(_assumed)")
. The two variants differ in how they behave when swift_attr("@Sendable")
has also been applied to the same declaration or type:
-
swift_attr("@_nonSendable")
causes any@Sendable
attribute on the same declaration or type to be ignored. -
swift_attr("@_nonSendable(_assumed)")
is ignored if there is a@Sendable
attribute on the same declaration or type.
That creates the following set of rules:
-
If the type inherits
Sendable
from a superclass, it has a@MainActor
attribute, or its superclass has a@MainActor
attribute, it is explicitlySendable
. -
If
swift_attr("@_nonSendable")
is present, the type is explicitly non-Sendable
. -
If
swift_attr("@Sendable")
is present (including if it's added because ofenum_extensibility
or one of the other attributes mentioned above), the type is explicitlySendable
. -
If
swift_attr("@_nonSendable(_audited)")
is present, the type is explicitly non-Sendable
. -
If the type meets one of the
Sendable
Objective-C type criteria described in SE-0302, it is explicitlySendable
. -
Otherwise, the type is implicitly non-
Sendable
.
__has_attribute(swift_attr)
is not sufficient for a header file to check if the swift_attr("@Sendable")
changes and new swift_attr("@_nonSendable")
attribute are supported by the Swift compiler importing it. To help with this, ClangImporter will define the __SWIFT_ATTR_SUPPORTS_SENDABLE_DECLS
macro to be 1
or greater when these features are supported.
Note: In the past, Foundation has provided macros with an
NS_
prefix for working more easily with ClangImporter features. However, Foundation is not part of the Swift Evolution process, so adding macros for these new features is out of scope for an Evolution proposal.
swift_attr
will be updated to allow it to be used with the clang attribute
pragma. With this change, it will be possible to define macros to do region-based Sendable
auditing (i.e. a pair of MY_ASSUME_UNSENDABLE_BEGIN
/_END
macros).
Here's a full set of macros as a sample:
#ifdef __SWIFT_ATTR_SUPPORTS_SENDABLE_DECLS
#define MY_SENDABLE __attribute__((swift_attr("@Sendable")))
#define MY_UNSENDABLE __attribute__((swift_attr("@_nonSendable")))
#define MY_MAIN_ACTOR __attribute__((swift_attr("@MainActor")))
#define MY_NONISOLATED __attribute__((swift_attr("nonisolated")))
#define MY_ASSUME_UNSENDABLE_BEGIN _Pragma("clang attribute MY_ASSUME_UNSENDABLE.push __attribute__((swift_attr(\"@_nonSendable(_assumed)\"))), defined_in = any(objc_interface, record, enum, function, objc_method)")
#define MY_ASSUME_UNSENDABLE_END _Pragma("clang attribute MY_ASSUME_UNSENDABLE.pop")
#else
#define MY_SENDABLE
#define MY_UNSENDABLE
#define MY_MAIN_ACTOR
#define MY_NONISOLATED
#define MY_ASSUME_UNSENDABLE_BEGIN
#define MY_ASSUME_UNSENDABLE_END
#endif
And an example of how they might be used:
// This struct would normally be inferred as Sendable because the imported types
// of its fields are all Sendable, but we know that `fd` is a file descriptor
// and `MyDatabaseHandle` does not attempt to synchronize its use across
// multiple clients, so using the same handle in several tasks would not be
// safe.
MY_UNSENDABLE struct MyDatabaseHandle {
int fd;
};
// If that struct were below this line, we would also import it as explicitly
// non-Sendable.
MY_ASSUME_UNSENDABLE_BEGIN
// Outside of an ASSUME_UNSENDABLE block, this struct would be inferred to be
// Sendable, but ASSUME_UNSENDABLE takes priority over that.
MY_SENDABLE struct MyPoint {
double x;
double y;
};
// This class would normally be inferred as non-Sendable, but we happen to know
// that it, all of its subclasses, and all objects accessible through it are
// truly immutable and can be used by multiple tasks simultaneously.
MY_SENDABLE @interface MyRecord : NSObject
@property (readonly) NSInteger recordID;
@property (readonly,copy) NSString *name;
// This method should only be called on the main actor.
- (void)validateOrPresentErrorUsingResponder:(NSResponder*)responder MY_MAIN_ACTOR;
@end
// The first closure is run on an unspecified background task; the second is on
// the main actor.
void runInBackgroundAndThenOnMainActor(
MY_SENDABLE void (^backgroundFn)(),
MY_MAIN_ACTOR void (^mainActorFn)()
);
MY_ASSUME_UNSENDABLE_END
This proposal is largely motivated by source compatibility concerns. Correct use of @predatesConcurrency
should prevent source breaks in code built with Minimal concurrency checking, and @predatesConcurrency import
temporarily weakens concurrency-checking rules to preserve source compatibility if a project adopts Full or Strict concurrency checking before its dependencies have finished adding concurrency annotations.
By itself, @predatesConcurrency
does not change the ABI of a declaration. If it is applied to declarations which have already adopted one of the features it affects, that will create an ABI break. However, if those features are added at the same time or after @predatesConcurrency
is added, adding those features will not break ABI.
@predatesConcurrency
's tactic of disabling Sendable
conformance errors is compatible with the current ABI because Sendable
was designed to not emit additional metadata, have a witness table that needs to be passed, or otherwise impact the calling convention or most other parts of the ABI. It only affects the name mangling.
This proposal should not otherwise affect ABI.
@predatesConcurrency
on nominal declarations will need to be printed into module interfaces. It is effectively a feature to allow the evolution of APIs in ways that would otherwise break resilience.
@predatesConcurrency
on import
statements will not need to be printed into module interfaces; since module interfaces use the Strict concurrency checking mode, where concurrency diagnostics are warnings, they have enough "wiggle room" to tolerate the missing conformances. (As usual, compiling a module interface silences warnings by default.)
If the evolution of a given module is tied to a version that can be expressed in @available
, it is likely that there will be some specific version where it retroactively adds concurrency annotations to its public APIs, and that thereafter any new APIs will be "born" with correct concurrency annotations. We could take advantage of this by allowing the module to specify a "concurrency epoch"—a particular version when it started ensuring that new APIs were annotated correctly—and automatically applying @predatesConcurrency
to APIs available before this cutoff.
This would save maintainers from having to manually add @predatesConcurrency
to many of the APIs they are retroactively updating. However, it would have a number of limitations:
-
It would only be useful for modules used exclusively on Darwin. Non-Darwin or cross-platform modules would still need to add
@predatesConcurrency
manually. -
It would only be useful for modules which are version-locked with either Swift itself or a Darwin OS. Modules in the package ecosystem, for instance, would have little use for it.
-
In practice, version numbers may be insufficiently granular for this task. For instance, if a new API is added at the beginning of a development cycle and it is updated for concurrency later in that cycle, you might mistakenly assume that it will automatically get
@predatesConcurrency
when in fact you will need to add it by hand.
Since these shortcomings significantly reduce its applicability, and you only need to add @predatesConcurrency
to declarations you are explicitly editing (so you are already very close to the place where you need to add it), we think a concurrency epoch is not worth the trouble.
Because all Objective-C declarations are implicitly @predatesConcurrency
, there is no way to force concurrency APIs to be checked in Minimal-mode code, even if they are new enough that there should be no violating uses. We think this limitation is acceptable to simplify the process of auditing large, existing Objective-C libraries.