Is adding a `Sendable` requirement a breaking change?

Hello,

Is it a breaking change to add Sendable requirements to public apis? By "breaking change" I mean that userland code would stop compiling with an error in any of the concurrency checking modes (minimal, targeted, complete). Warnings are not a breaking change.

I'm thinking about this kind of changes:

 // Library interface
-public protocol MyProtocol { ... }
+public protocol MyProtocol: Sendable { ... }

-public func f(_ closure: @escaping () -> Void) { ... }
+public func f(_ closure: @escaping @Sendable () -> Void) { ... }

-public func g<T>(_ closure: @escaping () -> T) { ... }
+public func g<T: Sendable>(_ closure: @escaping () -> T) { ... }

A little bit of context may clarify my question.

I maintain the library GRDB which currently requires Swift 5.7+ / Xcode 14+. It ships with some asynchronous functions, but has not been fully revised for sendability. Many apis that should eventually require Sendable input still allow non sendable values and closures. To be clear, I have audited the code and know what it would take, but I didn't make the changes because I was not sure they would be 100% backward compatible.

As time passes, more and more concurrency-related features ship in the compiler, and more and more users are compiling their apps with strict concurrency checkings. This is good. Recently, SE-0412 has just changed the landscape. GRDB is now a library that introduces warnings in user applications (warnings that the user can not fix in a satisfying way):

extension Author {
  // Warning: Static property 'books' is not concurrency-safe
  // because it is not either conforming to 'Sendable' or
  // isolated to a global actor; this is an error in Swift 6.
  static let books = hasMany(Book.self)
}

I want to preserve the excellent reputation of the library, so I am eager to remove those warnings. This means adding Sendable to a bunch of public protocols and closures.

So far, I could see that those changes introduce new warnings in userland code, such as the ones below:

  • non-final class 'UserClass' cannot conform to 'Sendable'; use '@unchecked Sendable'
  • Capture of 'value' with non-sendable type 'UserClass' in a @Sendable closure
  • Converting non-sendable function value to '@Sendable () -> Void' may introduce data races

I could not come up with userland code that would stop compiling with a hard compiler error. But I'm not 100% sure that no user application will stop compiling. I don't know if adding sendability annotations are a breaking change or not. I don't know if fixing GRDB warnings will force me to bump the major version, or not.

Can anyone help answering this question? Thanks in advance.

3 Likes

More context and clarifications in this Mastodon thread. I don't care if those warnings will become errors in Swift 6. I only care about the present - if I'm sure all user applications will keep on compiling today, I can just bump the minor version, and be ready when Xcode 15.3 is no longer a beta.

What I want to avoid is a user who upgrades to the next minor version, and discovers that their app stops compiling. That's the definition of a breaking change.

1 Like

May I directly ask @Douglas_Gregor and @John_McCall? As designers of Swift concurrency, you have clear ideas about the intended compiler warnings and errors.

I do not know the answer to your question, which I think is excellent!

However, I do want to say that I think questions along these lines will come up a lot. There are no-doubt many library authors facing similar problems.

This is making me think a lot about how libdispatch approached this problem. I realize this is a special case, because it is also itself a concurrency system. And that possibly skews things. But, what that library did was first express the truth in their API via needed Sendable requirements. However, they also added unsafe escape hatches to give users a warning-free path.

I'm not sure unsafe paths are actually helpful here. But, I think expressing the real concurrency requirements in your API, even if they are ultimately difficult to satisfy client-side, is the correct approach. Which makes the answer to your question particularly important for all library authors.

2 Likes

It might be worth making this a major version bump anyway, to preserve the ability to have a separate fork (in essence) to support Swift 5 mode indefinitely, that you might want to apply patches to (critical bug fixes etc). Hopefully the Swift community transitions rapidly to Swift 6 [mode] when it is released, but we can't know for sure that there won't be a Python 3 moment.

4 Likes

I think you are correct :innocent: Now, I have just discovered that since the upgrade to Sonoma, I can no longer run Xcode 14. I have no choice but to raise the requirements of the library (and hence bump the major version: Test what you ship!). The initial question remains, but I'm no longer waiting for the answer ;-)

EDIT: Oh no, the initial question remains quite crucial: I'd like to be able to add the Sendable requirements I missed on my first pass, without bumping again the major version!

1 Like

All Sendable diagnostics are warnings only in Swift 5 mode, even with -strict-concurrency=complete, so adding @Sendable or : Sendable annotations should never cause errors in clients building in Swift 5 mode. This is deliberate to allow incrementally annotating APIs that were designed prior to the introduction of Swift's concurrency programming model.

These warnings will become errors in Swift 6. However, once Swift 6 is available and your clients adopt it, you can still stage in Sendable annotations without breaking clients by annotating your API with @preconcurrency.

If you find cases where adding a Sendable annotation creates an error message in an adopter in Swift 5 mode, that's a compiler bug!

11 Likes

Thank you very much! Your answer lifts all remaining doubts! :+1:

2 Likes

We've been through this in Vapor. For almost all cases adding Sendable requirements is a non-breaking change. There have been a couple of instances where we've had to do some funky stuff (looking at you AnyHashable) and some edge cases where things could break for we've managed to update Vapor to build with no warnings with complete concurrency checking on.

5 Likes

Ha ha !! Gorgeous:

protocol P { }

extension P {
    func f() where Self: Sendable { }
    //  new: ~~~~~~~~~~~~~~~~~~~~
}

class C: P { }

func f() {
    // Error or warning? 🥁
    // 🌟 Warning: Type 'C' does not conform to the 'Sendable' protocol
    C().f()
}

This exquisite delicacy was unexpected, I'm delighted to see it! :yum:

4 Likes

With pleasure :yum: Wrong compiler error regarding conditional conformances that involve Sendable · Issue #71544 · apple/swift · GitHub

3 Likes

Yeah, I think it'd be nice if you could use protocol compositions with structs and marker protocols so you can write AnyHashable & Sendable.

3 Likes

I just found an issue that is far less-severe, but I figured I'd drop it in here too just in case.

2 Likes

For reference here's our AnySendableHashable implementation - console-kit/Sources/ConsoleKitTerminal/Utilities/AnySendableHashable.swift at main · vapor/console-kit · GitHub

It works for the most part but we don't get access to all of the fun compiler parts to avoid using @unchecked Sendable and make it work in every case. I tried copying the full implementation but that has special compiler access that you can't resolve outside of the stdlib

1 Like

Unless your organization requires Warnings are Errors turned on so warnings aren’t ignored (in large orgs they tend to build up and become technical debt otherwise). Then it breaks the build.

1 Like

i’m surprised anyone is still able to enforce Warnings as Errors considering CommandLine.arguments is not concurrency-safe still exists. i remember having to compromise on that policy months ago.

Yep.
I brought up the warnings that can’t be disabled issue ( SE-0337: Incremental Migration to Concurrency Checking - #6 by haikuty ) back when the incremental migration to concurrency plan was being proposed and being discussed and got the following reply:

Curious if this technique work in the situation you describe to allow reenabling warnings as errors? (Sorry I’m not up to speed on this stuff; I’ve been out for an extended period).

1 Like

The purpose of warnings-as-errors is to force a project to address warnings immediately. If the project cannot actually tolerate new warnings because addressing warnings immediately is impractical, the project should not be using warnings-as-errors. I think the act of accepting warnings-as-errors as your policy means you are accepting that updating library dependencies or installing new tools can break your build because those things can and will introduce new warnings. The specific warnings we're talking about in this thread are data race safety issues, which seems like exactly the kind of issues that warnings-as-errors is meant to enforce addressing immediately because those can turn into real runtime bugs in the project.

This is only a problem if there is no possible way to address a warning. For libraries that have not yet been updated to include concurrency annotations, @preconcurrency is designed to suppress diagnostics in client projects, which will later return once an explicit annotation has been added or the library has adopted Swift 6. There are other suppression tools such as @unchecked Sendable and nonisolated(unsafe) that can also be judiciously applied when you've taken synchronization into your own hands or just need a temporary opt out from actor isolation checking.

If you encounter a case where you cannot address a concurrency warning, please file a GitHub issue.

6 Likes

In Xcode's "Build Phases" there's "Compiler Flags" column which allows to set compiler flags per file. I tried "-warnings-as-errors" in there but that didn't work. What could be set in there?

(The thing I am heading to is somewhat the opposite: enable -warnings-as-errors on the whole project but disable them for certain files.)

1 Like

Last I checked that only applied to clang settings. And I don't think the Swift compiler has a way to turn off -warnings-as-errors (there's no equivalent to clang's -Wno functionality), only enable it.

2 Likes