Swift 5.7/6 and Non-Final Class Opt-In to AtomicReference

Consider a class Cls, to which we'd like to be able to make an atomic reference via ManagedAtomic. This class is intentionally the root of a class hierarchy, and we'd like to be able to make references to it, and its subclasses:

import Atomics

// Swift 5.6
class Cls: AtomicReference {} // ✅

This works without complaint in Swift 5.5/5.6, but in Swift 5.7, new warnings are introduced by the conformance above:

// Swift 5.7
// warning: Non-final class 'Cls' cannot safely conform to protocol 'AtomicOptionalWrappable', which requires that 'Self.AtomicOptionalRepresentation.Value' is exactly equal to 'Self?'; this is an error in Swift 6
// warning: Non-final class 'Cls' cannot safely conform to protocol 'AtomicReference', which requires that 'Self.AtomicOptionalRepresentation' is exactly equal to 'AtomicOptionalReferenceStorage<Self>'; this is an error in Swift 6
// warning: Non-final class 'Cls' cannot safely conform to protocol 'AtomicValue', which requires that 'Self.AtomicRepresentation.Value' is exactly equal to 'Self'; this is an error in Swift 6
class Cls: AtomicReference {} // ⚠️

These warnings make perfect sense: as written, the constraints on the Atomic* types all require type equality, which will clearly be violated by subclasses. (In practice, I don't believe this matters, because as stored, Cls and its subclasses all have identical layouts—just a pointer to actual storage—but happy to be corrected if this is a nontrivial mistake.)

Is there guidance going forward on what to do here?

  • Will an exception somehow be made for these types, or will clients of swift-atomics no longer be able to do this?
    • If this is safe to do, will there be a way to silence these warnings in Swift 5.7? (Apologies if this is already possible, and I've missed that capability)
  • And if not, what's the right way to handle this? Should we be writing a generic final class AtomicWrapper<T: AnyObject>: AtomicReference to handle this type of storage, and make references to that? Drop down to AtomicReferenceStorage/AtomicOptionalReferenceStorage directly? Something else?

Apologies if there is already official guidance somewhere on how to handle this — in my searching, I haven't found anything definitive.

@lorentey, I'm wondering if there's a preferred approach here going forward — any guidance on what might be appropriate to do?

(Sorry if you're not the right person to ask; reaching out to you as apparent primary dev / package maintainer.)

Huh, interesting!

The diagnostic certainly seems correct, and the workaround probably best belongs in the Atomics package. AtomicReference already has a fixme about a compiler issue ([SR-10251] Unable to infer associated type T in types conforming to child protocol with requirement T == Self · Issue #52651 · apple/swift · GitHub) that may have the same root cause. :thinking:

One way to go at it might be to add a new associated type to the AtomicReference mixin protocol, along the lines of:

public protocol AtomicReference: AnyObject, AtomicOptionalWrappable
{
  associatedtype AtomicBaseClass: AtomicReference = Self
  where
    Self: AtomicBaseClass,
    AtomicRepresentation == AtomicReferenceStorage<AtomicBaseClass>,
    AtomicOptionalRepresentation == AtomicOptionalReferenceStorage<AtomicBaseClass>
}

(I don't know if this shape is currently expressible within the type system; I expect the requirements will need to be tweaked a bit.)

1 Like

(I filed https://github.com/apple/swift-atomics/issues/53 to track this issue.)

1 Like

For what it's worth, I don't see a reason why it wouldn't be possible to make an atomic strong reference to the root of a class hierarchy -- the underlying atomics machinery is expressed on AnyObject and it is happy to work with any strong reference.

The protocol is only needed to make these play well with the ManagedAtomic/UnsafeAtomic constructs, and to provide a minimal amount of control over what classes are expected to be used this way.

The primary limitation with applying this to a class hierarchy is that ManagedAtomic<Cls> would always return a Cls, never a subclass type -- but then again, that's also true for regular variables.

For a subclass of Cls called Derived, I think the approach above would also allow ManagedAtomic<Derived> to work, although the code might need to be a bit careful about always downcasting to the correct type. (Subclasses won't be able to customize their atomic representations, but that's not much of a hardship -- I don't expect anyone would want to roll their own AtomicRepresentation type, anyway.)

2 Likes

Fantastic — thanks for this, Karoy! I'm hoping the solution won't be too painful to express in the type system.

Thanks for confirming! I figured this was the case, and that the issue was expressing the right constraints to the compiler; sounds like we're on the same page.

Exactly, and in my case, downcasting to the right type won't be an issue. (I do expect to store ManagedAtomic<Derived> much more frequently than ManagedAtomic<Cls>, but that we'll get for free.)


Given some spare time, I'll see if I can poke about with the specific spelling of the constraints. Hopefully I can be of some use.

2 Likes

I just wanted to follow up on this since Swift 5.7 is now out and the warnings are here to stay — I tried a while back to find a way to spell the constraints in a way that the compiler would accept (some details in the issue), but no dice.

We can live with the warnings for now (though it's pretty frustrating that there's no way for us to silence them), but is there a good way to help prioritize the issue? Should I file Feedback, or is the GH issue sufficient? Should anyone get pulled in to see if we can find a clever spelling for the constraints that the compiler will be happy with?

My team and I can work with this for now, for sure, but if we don't get to resolving this before Swift 6, we're going to need to move back to regular locks, which feels like a real shame. Happy to do what I can to assist!

1 Like

I can't promise specific timeframes, but this is the top issue to resolve for the next release of Swift Atomics. (FWIW, the Collections package is currently making me very busy, but I'll schedule a bit of time to look into this and make a Swift Atomics release at the next appropriate context switch opportunity.)

If all else fails, one not-very-exciting way to do this would be to just implement a standalone, non-generic atomic strong reference construct that supports any class object, and let the client side do its own downcasting as needed. (Along the lines of AtomicLazyReference.) Ideally though we would be able to just tweak AtomicReference to support this.

1 Like

Thanks, Karoy!

Good to know! No expectations at all on my end — I know you're very busy!

Ah, yes! This would be a great intermediate workaround. Good call :smile:

I made a potential workaround: [Draft] Fix support for non-final classes in AtomicReference by lorentey · Pull Request #58 · apple/swift-atomics · GitHub

Unfortunately this is source-breaking, as it removes support for declaring atomic variables of subclasses of the conforming class:

class Base: AtomicReference {}
class Derived: Base {}

let ref = ManagedAtomic<Derived?>(nil)
  // before: OK
  // after: 'ManagedAtomic' requires the types 'Derived' and 'Base' be equivalent

Ideally we'd figure out an alternative workaround that would keep this working somehow.

If not, this will have to wait until a major version bump. The ownership model is becoming a thing now, and the introduction of a move-only Atomic<T> struct might be a good excuse to do it.

Thanks for the update, Karoy! The inability to declare a reference to a derived class is definitely a bummer, but workable. (The diagnostic is going to be really confusing, but I don't think there's much we can do about that :sweat:)

I'm still holding out hope that there'll be a way to convince the compiler to line these types up somehow. Thanks again for your work and exploration!