NSObject related compile issue on Linux

The following code compiles fine on macOS and iOS but failed on Linux.

import Foundation

class Box<Delegate: NSObject> {
    var delegate: NSObject {
        return Delegate()
    }
}
DemoKit.swift:8:16: error: constructing an object of class type 'Delegate' with a metatype value must use a 'required' initializer
        return Delegate()
               ^~~~~~~~
Foundation.NSObject:2:12: note: selected non-required initializer 'init()'
    public init()
           ^
error: fatalError

Is this an expected behavior? Or can we align it with Darwin platform behavior and fix it on swift-corelibs-foundation repo?

GitHub issue constructing an object of class type X with a metatype value must use a 'required' initializer · Issue #71874 · apple/swift · GitHub

It's expected. Objective-C doesn't have a notion of a required initializer so for an imported init we just allow the call even if its potentially unsafe.

Changing NSObject to expose a required init() on Linux would be source breaking since every subclass would have to override this initializer, even if it didn't make sense semantically.

Instead, consider introducing a new protocol with an init() requirement, or changing your superclass bound to something more specific.

Got it.

Foundation on Darwin is implemented in ObjectiveC.

@interface NSObject <NSObject> {
- (instancetype)init
}

So the imported Swift interface for NSObject.init is public required init()

I know we can't change the behavior here on Darwin platform.

Then why not fix the behavior on Linux so that we have consistent behavior.

Considering this is a source breaking change, can we consider accepting it in Swift 6 mode?

(Actually considering the lower usage of this behavior we can also merge it in normal releases. I remember there are few breaking changes already landing to Linux platform from time to time.)

IMO most Linux Swift user rarely used this base class unless they are sharing code from existing iOS/macOS Swift code. So it is import that we makes them compatible.

Patched a PR to verify the fix. Add required to NSObject by Kyle-Ye · Pull Request #4884 · apple/swift-corelibs-foundation · GitHub

1 Like

To get consistent behavior we would need a @_requiredButNotRequired attribute which allows this kind of unsafe pattern, and I don’t think such a pitch would advance very far.

  1. Could you elaborate on the idea of "@_requiredButNotRequired"? I'm not very familiar with this concept.

  2. Does it mean changing NSObject from public init() {} to public required init() {} on Linux can not solve the problem(getting consistent behavior for both platform)?

Suppose I define a class:

class C: NSObject {
  var x: Int

  init(x: Int) { self.x = x }
}

The above compiles on Linux and Darwin today.

Now if init() becomes required on Linux, the above code will work on Darwin but not Linux, and furthermore, I have no way to implement it at all (what do I set x to? 0? 420? 31337?). So we have no way to get consistent behavior on the two platforms given existing language features.

However, declaring a required init does two things, it allows a polymorphic call and it enforces that every subclass overrides the required init so that the polymorphic call can succeed. So if I understand correctly you’re proposing to effectively introduce a new keyword, attribute or some other toggle to allow declaring an init that has the first behavior, but not the second. We can’t do that today, and it’s not clear that we should, given that it’s not actually type safe.

2 Likes

Got it. I'll try to read the relevant compiler code and revisit it later.

I thought we ready implemented such underscore attribute here :joy:.

I went looking for the code and it looks like the relevant logic is encoded in canInheritDesignatedInits() which calls hasUserDefinedDesignatedInit() and areAllStoredPropertiesDefaultInitializable(). Both return early if the class is imported from Objective-C.

1 Like

In summary if the class does not implement init(), we'll actually hit a runtime crash on Darwin platform instead of a compiler crash on Linux platform.

class A: NSObject {
    var a: Int
    init(a: Int) { self.a = a }
}

func make<T: NSObject>() -> T {
    T() // -> Darwin: Thread 1: EXC_BREAKPOINT (code=1, subcode=0x100002078)
}

let a: A = make()
main.swift:10: Fatal error: Use of unimplemented initializer 'init()' for class 'AA.A'

Options:

  1. Fix the behavior on Darwin platform as a breaking change Swift 6 mode. (Leave this to Apple)
  2. Implement @_requiredButNotRequired and add it to workaround the compiler limitation on swift-corelibs-foundation's NSObject init implementation.
  3. Leave it as it is due to platform difference.

Option1 can only be done for Apple.
Option2 need a pitch and review from upstream.
I'll accept option3 and and use other trick to fix it. (Add a protocol with init requirement)

protocol P { init() }
class Box<Delegate: NSObject & P> {
    var delegate: NSObject {
        return Delegate()
    }
}
1 Like

I'd rather see option 2 but that will be confronted with the most resistance…

I have give option2 a try. It required more engineering time than I expected.

Also considering the upstream may not accept it, I just stop my time on implementing it and focus on solving another more serious Linux issue.

I've been thinking of forking swift-coreutils-foundation and accepting a bunch of pull requests which solve some of my needs…

1 Like

Perhaps I'm oversimplifying things, but why not simply replace the NSObject constraint from your Box class with a protocol constraint containing the specific requirements you have on that type?

protocol BoxDelegate {
  init()
}

class Box<Delegate: BoxDelegate> {
  var delegate: Delegate {
    return Delegate()
  }
}

Then any class that wants to be a Delegate must conform to BoxDelegate and implement init().

If you like, you can also get that conformance "for free" with NSObject on Darwin:

#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS)
extension NSObject: BoxDelegate {}
#endif
2 Likes