Strict isolation with subclasses

Hello folks,

I am working on a Swift binding to a third party system (Godot - GitHub - migueldeicaza/SwiftGodot: New Godot bindings for Swift), and I am trying to make my binding be strict-concurrent safe.

I expose an object-oriented wrapper in Swift that matches the underlying object model of Godot, this is basically a 1:1 mapping to what they have to offer.

They have an entire hierarchy of objects, starting with Object, and while some of the classes are thread-safe and can be called from any thread, others are not.

For example these chains of classes are thread safe:

Object > Input
Object > CameraServer
Object > RefCounted > TextServer > TextServerExtension > TextServerAdvanced

But other chains are not, for example:

Object > Node
Object > Node > Viewport > Window

So I do need things like Node, Viewport and Window to be annotated as @MainActor, but do not want Object, or RefCounted to be annotated with it.

But the compiler informs me that I am not able to do this:

public class Object {
}

@MainActor
public class Node: Object {
}
Main actor-isolated class 'Node' has different actor isolation from nonisolated superclass 'Object'; this is an error in the Swift 6 language mode

I am at a loss on how to proceed.

5 Likes

You might be interested in SE-0434: Usability of global-actor-isolated types. Specifically this section: swift-evolution/proposals/0434-global-actor-isolated-types-usability.md at main · apple/swift-evolution · GitHub

5 Likes

Whoa!

Thank you so much Holly!

You folks are always five steps ahead.

Trying it out now.

1 Like

...well, I forgot to actually commit the patch that implements this section of the proposal. Sorry about that, and here it is! [Concurrency] Allow isolation on subclasses of non-Sendable, non-isolated superclasses. by hborla · Pull Request #73497 · apple/swift · GitHub

3 Likes

Thanks, I will try out the daily builds.

The 6.0 toolchain from a day or two ago is crashing for me for an issue unrelated to this, so I will keep my eye on the fresh builds, hoping this gets fixed.

In the meantime, I came up with an ugly hack, and I am wondering if this is a terrible idea, or if we think this is an acceptable workaround:

@MainActor 
class Base { 
    // This allows me to construct this from any actors:
    nonisolated init () { }
    nonisolated func demo () { 
       // No MainActor affinity 
    }
}

@MainActor 
class Derived: Base {
    public override init () {  
      // Instances of derived run on MainActor
    }
    func regular () {
       // This method only runs in MainActor
    }
}

Task {
    let a = Base () // This works, no await necessary
    let b = await Derived () // Need to use await here due to @MainActor
}

This seemed to work without warnings, but I have a feeling that I might have stublmed into a loophole.

best,
Miguel

1 Like