How Does AnyObject Conform to NSObjectProtocol?
Asked Answered
A

4

10

This question was inspired by mz2's answer on the question Check for object type fails with "is not a type" error.

Consider an empty Swift class:

class MyClass { }

Attempting to call any NSObjectProtocol methods on an instance of this class will result in a compile-time error:

let obj = MyClass()
obj.isKindOfClass(MyClass.self) // Error: Value of type 'MyClass' has no member 'isKindOfClass'

However, if I cast the instance as AnyObject, my object now conforms to NSObjectProtocol and I can call the instance methods defined by the protocol:

let obj: AnyObject = MyClass()
obj.isKindOfClass(MyClass.self) // true
obj.conformsToProtocol(NSObjectProtocol) // true
obj.isKindOfClass(NSObject.self) // false

My object doesn't inherit from NSObject, but still conforms to NSObjectProtocol. How does AnyObject conform to NSObjectProtocol?

Arella answered 25/4, 2016 at 22:0 Comment(0)
J
6

In the Cocoa / Objective-C world, AnyObject is id. Having cast this object to AnyObject, you can send any known Objective-C message to it, such as isKindOfClass or conformsToProtocol. Now, when you say isKindOfClass or conformsToProtocol, you're not in the Swift world any more; you're talking to Cocoa with Objective-C. So think about how Objective-C sees this object. All classes in the Objective-C world descend from some base class; a baseless class like MyClass is impossible. And every base class in the Objective-C world conforms to the NSObject protocol (which Swift calls NSObjectProtocol); that's what it is to be (or descend from) a base class! Therefore, to get it into the Objective-C world, Swift presents MyClass as descending from a special bridging base class SwiftObject which does indeed conform to NSObjectProtocol (as you can see here: https://github.com/apple/swift/blob/master/stdlib/public/runtime/SwiftObject.mm).

Johnsonian answered 25/4, 2016 at 22:47 Comment(6)
But AnyObject is defined as @objc public protocol AnyObject { } Does that mean all @objc protocols conform to NSObjectProtocol? What about Any?Arella
It doesn't mean that at all. I'm saying what I'm saying, not some other thing.Johnsonian
You're saying "a baseless class like MyClass is impossible." So are you saying MyClass inherits from SwiftObject? Why does only casting to AnyObject expose the conformance to NSObjectProtocol?Arella
Because only casting to AnyObject allows you to even say conformsToProtocol.Johnsonian
Ok, I think I'm slowly starting to get what you're saying. "Having cast this object to AnyObject, you can send any known Objective-C message to it, such as isKindOfClass or conformsToProtocol." So how is this done? Where is the protocol conformance specified? Somewhere in Foundation is there an extension on AnyObject which conforms it to NSObjectProtocol? Or is this all done through SwiftObject.Arella
Madness. Is isKindOfClass in this case actually sent as a dynamically dispatched message, even though the class itself is not an Objective-C visible type and does not use messaging based dispatch for its own methods?Baguette
B
6

If I'm understanding this correctly based on matt's answer, this works when Swift / Objective-C interop is available because in fact Swift class types ultimately inherit from SwiftObject which when Objective-C interop is compiled in, actually involves an Objective-C class (SwiftObject is in implemented in SwiftObject.mm which is compiled as Objective-C++ when Objective-C interop is used). So, casting a Swift class typed object as AnyObject kind of "leaks" that information.

Peeking at some relevant bits in the implementation from the Swift source code, file swift/stdlib/public/runtime/SwiftObject.mm:

#if SWIFT_OBJC_INTEROP

// …

@interface SwiftObject<NSObject> {
   SwiftObject_s header;
}

// …

@implementation SwiftObject

// …

- (BOOL)isKindOfClass:(Class)someClass {
  for (auto isa = _swift_getClassOfAllocated(self); isa != nullptr;
       isa = _swift_getSuperclass(isa))
    if (isa == (const ClassMetadata*) someClass)
      return YES;

  return NO;
}

// …

// #endif

As predicted by this, with Swift 3 in Linux (where there's no Objective-C runtime available as part of the Swift runtime & Foundation implementation as far as I understand?) the example code from this question and the earlier question & answer that inspired this question fails with the following error compiler error:

ERROR […] value of type 'AnyObject' has no member 'isKindOfClass'
Baguette answered 25/4, 2016 at 23:29 Comment(0)
A
4

Adding some additional information to the already great answers.

I created three programs and looked at the generated assembly from each:

obj1.swift

import Foundation
class MyClass { }
let obj = MyClass()

obj2.swift

import Foundation
class MyClass { }
let obj: AnyObject = MyClass()

obj3.swift

import Foundation
class MyClass { }
let obj: AnyObject = MyClass()
obj.isKindOfClass(MyClass.self)

The differences between obj1 and obj2 are trivial. Any instructions that involve the type of the object have different values:

movq    %rax, __Tv3obj3objCS_7MyClass(%rip)

# ...

globl   __Tv3obj3objCS_7MyClass         .globl  __Tv3obj3objPs9AnyObject_
.zerofill __DATA,__common,__Tv3obj3objCS_7MyClass,8,3

# ...

.no_dead_strip  __Tv3obj3objCS_7MyClass

vs

movq    %rax, __Tv3obj3objPs9AnyObject_(%rip)

# ...

.globl  __Tv3obj3objPs9AnyObject_
.zerofill __DATA,__common,__Tv3obj3objPs9AnyObject_,8,3

# ...

.no_dead_strip  __Tv3obj3objPs9AnyObject_

Full diff here.

This was interesting to me. If the only differences between the two files are the names of the object type, why can the object declared as AnyObject perform the Objective-C selector?

obj3 shows how the isKindOfClass: selector is fired:

LBB0_2:
    # ...
    movq    __Tv3obj3objPs9AnyObject_(%rip), %rax
    movq    %rax, -32(%rbp)
    callq   _swift_getObjectType
    movq    %rax, -8(%rbp)
    movq    -32(%rbp), %rdi
    callq   _swift_unknownRetain
    movq    -24(%rbp), %rax
    cmpq    $14, (%rax)
    movq    %rax, -40(%rbp)
    jne LBB0_4
    movq    -24(%rbp), %rax
    movq    8(%rax), %rcx
    movq    %rcx, -40(%rbp)
LBB0_4:
    movq    -40(%rbp), %rax
    movq    "L_selector(isKindOfClass:)"(%rip), %rsi
    movq    -32(%rbp), %rcx
    movq    %rcx, %rdi
    movq    %rax, %rdx
    callq   _objc_msgSend
    movzbl  %al, %edi
    callq   __TF10ObjectiveC22_convertObjCBoolToBoolFVS_8ObjCBoolSb
    movq    -32(%rbp), %rdi
    movb    %al, -41(%rbp)
    callq   _swift_unknownRelease
    xorl    %eax, %eax
    addq    $48, %rsp

# ...

LBB6_3:
    .section    __TEXT,__objc_methname,cstring_literals
"L_selector_data(isKindOfClass:)":
    .asciz  "isKindOfClass:"

    .section    __DATA,__objc_selrefs,literal_pointers,no_dead_strip
    .align  3
"L_selector(isKindOfClass:)":
    .quad   "L_selector_data(isKindOfClass:)"

Diff between obj2 and obj3 here.

isKindOfClass is sent as a dynamically dispatched method as seen with _objc_msgSend. Both objects are exposed to Objective-C as SwiftObject (.quad _OBJC_METACLASS_$_SwiftObject), declaring the type of the object as AnyObject completes the bridge to NSObjectProtocol.

Arella answered 26/4, 2016 at 18:0 Comment(1)
Something I'm beginning to wonder now as an entirely separate question also based on this is how is it that import Foundation has an effect: casting a Swift class typed object to AnyObject throws a compiler error if you don't import Foundation. Is the use of Objective-C interop in all this modulated based on whether Foundation is used?Baguette
N
1

In addition to matt's answer, which I think is correct:

Is isKindOfClass in this case actually sent as a dynamically dispatched message, even though the class itself is not an Objective-C visible type and does not use messaging based dispatch for its own methods?

No, isKindOfClass is sent as a dynamically dispatched method because the class itself is an Objective-C visible type and does use messaging based dispatch for it's own methods.

It does this because of the @objc in @objc public protocol AnyObject {}

If you cmd-click on AnyObject in XCode you'll see this in the generated headers

/// When used as a concrete type, all known `@objc` methods and 
/// properties are available, as implicitly-unwrapped-optional methods 
/// and properties respectively, on each instance of `AnyObject`.

And in the docs at https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html

To be accessible and usable in Objective-C, a Swift class must be a descendant of an Objective-C class or it must be marked @objc.

(my emphasis)

Adopting a protocol tagged with @objc means your class is an @objc class and is ObjC bridged via the interop mechanism pointed out by mz2 in the answer above.

Nepean answered 26/4, 2016 at 11:44 Comment(4)
Right, but the surprising thing given all of that documented information still is that this works for Swift classes that are not marked @objc and which do not inherit from (in a documented way) from an Objective-C class. And indeed they do not always (AnyObject exists in the language also without Objective-C interop).Baguette
Any time you use AnyObject you're invoking ObjC interop, it says so right there in docs as quoted above.Nepean
Sure, but AnyObject is there also when there is no ObjC interop even compiled into your copy of Swift. For instance class A {}; print(A() as AnyObject) works just fine in Swift 3 on Linux. I suppose we're just getting at the same point though: it does so on ObjC interoperable platforms because the undocumented base class is with ObjC interop an NSObjectProtocol compliant Objective-C class, and the effect (that the methods & properties are available as implicitly unwrapped optionals) is documented, as you point out.Baguette
Thats a great point, I have overlooked the case of running on Linux where there is no ObjC runtime dependence (currently Swift 2.2 on Apple platforms has it as a dependency).Nepean

© 2022 - 2024 — McMap. All rights reserved.