Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR-8129] @objc Protocol + Generics == Crashes #50661

Open
CharlesJS opened this issue Jun 27, 2018 · 5 comments
Open

[SR-8129] @objc Protocol + Generics == Crashes #50661

CharlesJS opened this issue Jun 27, 2018 · 5 comments
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself crash Bug: A crash, i.e., an abnormal termination of software

Comments

@CharlesJS
Copy link

Previous ID SR-8129
Radar None
Original Reporter @CharlesJS
Type Bug
Environment

Swift version 4.2-dev (LLVM 9d4565013d, Clang 5f1c2da6c0, Swift 18fc42d)

Additional Detail from JIRA
Votes 0
Component/s Compiler
Labels Bug, CompilerCrash
Assignee None
Priority Medium

md5: 35ee686022c730b43cd8faff5e515f3e

Issue Description:

When working with Objective-C frameworks (including some of the ones included with the OS), it's not uncommon to find APIs that look like this:

- (id)foo:(Protocol *)proto;

which take a protocol, and return a proxy object that conforms to the protocol. In Objective-C, these aren't bad to use, but in Swift the above gets interpreted as:

func foo(_ proto: Protocol) -> Any

which requires us to jump through some type-checking hoops before we'll be able to use the proxy in any meaningful way.

It might be helpful to make a wrapper around such APIs using Generics, which could potentially make these APIs more convenient to use from Swift. Unfortunately, I can't find any way to explicitly mention that a conforming protocol metatype is desired (`<P: Super.Type>` results in 'error: inheritance from non-protocol, non-class type 'Super.Type''; please accept my apologies if there's some brain-dead simple way to specify this that I've overlooked), but we can generically require any type conforming to the protocol, and then simply document that the function should only passed protocol metatypes, like so:

@objc protocol Super {}
@objc protocol Sub: Super {}

// Only pass a protocol to this!
func bar<P: Super>(proto: P.Type) {}

bar(proto: Sub.self)

Interestingly, this will compile if the protocols do not have the `@objc` attribute on them, but will throw `error: in argument type 'Sub.Protocol', 'Sub' does not conform to expected type 'Super'` if they are pure-Swift protocols.

Anyway, we now have a function to which we can pass arbitrary Objective-C protocols (that conform to `Super`). Unfortunately, the Objective-C bridge does not recognize this parameter in the way that it would an explicitly specified protocol metatype:

func bar<P: Super>(proto: P.Type) {
    // compiles, runs, works:
    let proxy1 = foo(Sub.self)

    // error: cannot convert value of type 'P.Type' to expected argument type 'Protocol'
    let proxy2 = foo(proto)
}

Moreover, most of the methods we'd use to manually invoke the Objective-C bridge cause crashes at runtime. For example, the standard cast to `AnyObject` behaves thus:

func bar<P: Super>(proto: P.Type) {
    // <Protocol: 0x103a82100>
    print(Sub.self as AnyObject)
        
    // Assertion failed: (isa<X>(Val) && "cast<Ty>() argument of
    // incompatible type!"), function cast, file
    // /path/to/swift/llvm/include/llvm/Support/Casting.h, line 255
    //
    // (If the compiler's been built in Release mode, you just get
    // 'Terminated due to signal: SEGMENTATION FAULT (11)')
    print(proto as AnyObject)
}

What if we explicitly cast to `Super`, since we know whatever we have should conform to that? Well, this gets a bit weird depending on which cast operator you use:

func bar1<P: Super>(proto: P.Type) {
    let intermediate = proto as Super.Type
    print(intermediate as AnyObject)  // crash!
}

func bar2<P: Super>(proto: P.Type) {
    let intermediate = proto as! Super.Type
    print(intermediate as AnyObject) // crash!
}

func bar3<P: Super>(proto: P.Type) {
    // warning: conditional cast from 'P.Type' to 'Super.Type' always succeeds
    let intermediate = proto as? Super.Type

    print(intermediate as AnyObject) // <Protocol: 0x10500d100>
}

The `bar3` method above actually works as intended ( ! ) , although it causes a warning that can't be eliminated. The warning suggests using `as` instead; of course, if we do that, the cast to AnyObject will crash.

So what's causing this? It turns out that the compiler generates different assembly calls to do the actual bridging depending on the operator used and whether the type is generic or not, which I've illustrated by adding comments in the code below.

func bar<P: Super>(proto: P.Type) {
    // callq  0x100001a60; type metadata accessor for crasher.Sub
    // movq   %rax, -0x18(%rbp)
    // movq   -0x20(%rbp), %rdi
    // movq   %rdx, -0x28(%rbp)
    // callq  0x100001d80; type metadata accessor for crasher.Sub.Protocol
    // leaq   -0x18(%rbp), %rdi
    // movq   %rax, %rsi
    // movq   %rdx, -0x30(%rbp)
    // callq  0x100001df2; symbol stub for: Swift._bridgeAnythingToObjectiveC<A>(A) -> Swift.AnyObject
    // (works)
    let p1 = Sub.self as AnyObject

    // callq  0x100001e28; symbol stub for: swift_getObjCClassFromMetadata  (crashes)
    let p2 = proto as AnyObject

    // callq  0x100001c80; type metadata accessor for Swift.Optional<crasher.Super.Type>
    // leaq   -0x20(%rbp), %r13
    // movq   %rax, %rdi
    // movq   %rdx, -0x28(%rbp)
    // callq  0x100001dd0; symbol stub for: Swift.Optional._bridgeToObjectiveC() -> Swift.AnyObject (works)
    let i3 = proto as? Super.Type
    let p3 = i3 as AnyObject

    // callq  0x100001e28; symbol stub for: swift_getObjCClassFromMetadata (crashes)
    let i4 = proto as Super.Type // or as!; same result
    let p3 = i4 as AnyObject
}

From this, it appears that with the generic type, as well as with the generic type cast using `as` or `as!`, the compiler assumes that the generic type will always be a concrete class metatype. This turns out to be a bad assumption, since the generic parameter may be a protocol metatype.

I hope you've found this report helpful.

@belkadan
Copy link
Contributor

This is correct-ish behavior. SomeProto.Type is closer to Class <SomeProto> than Protocol *. We have a special spelling for the other one, SomeProto.Protocol, but (AFAIK) no way to use it in generic parameters.

@jckarter, is there a way to write this?

@CharlesJS
Copy link
Author

Closer, but not exclusively so. If there's a valid input you can provide to a function that causes the type checker to crash when handling that input, particularly in a way that's difficult to check for ahead of time, I'd argue that that, at the least, is not correct behavior.

Thanks for the mention of `P.Protocol`; I'd tried that, and forgotten to mention it. As you say, it doesn't work as a generic constraint; the compiler emits `error: inheritance from non-protocol, non-class type 'P.Protocol'`. If such a mechanism were added, that would be great, and indeed a better solution than what I've trying to do. In that case, the ability for <T: P.Type> to match a protocol's metatype could simply be removed to get rid of the crasher, although I respectfully ask please not to do this removal without adding the equivalent syntax to match a protocol metatype, as it would break my use case (particularly since I've found a suitable workaround; simply casting to `Any` before casting to `AnyObject` destroys the existing type information, and forces the compiler to emit `_bridgeAnythingToObjectiveC`, avoiding the crash).

@belkadan
Copy link
Contributor

Ah, sorry, I missed the type checker crash. The run-time failures in the casting machinery are expected, but the type checker crash is not!

@CharlesJS
Copy link
Author

I should clarify that the crash occurs at runtime; it occurs while checking the type while performing the cast. Still a bug IMO.

@jckarter
Copy link
Member

`as AnyObject` should never crash either. There isn't way to write a function that's generic over protocols currently, but we should fix the runtime crashes here.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
@AnthonyLatsis AnthonyLatsis added the crash Bug: A crash, i.e., an abnormal termination of software label Dec 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself crash Bug: A crash, i.e., an abnormal termination of software
Projects
None yet
Development

No branches or pull requests

4 participants