google / promises

Promises is a modern framework that provides a synchronization construct for Swift and Objective-C.
Apache License 2.0
3.8k stars 293 forks source link

Could not cast value of type #MyClass to NSObject #67

Closed ingun37 closed 6 years ago

ingun37 commented 6 years ago

I ran to this runtime error. Does it mean my class has to be inherited from NSObject to be used as Promise type?

It stops at this library code

  static func asValue(_ value: AnyObject?) -> Value? {
    // Swift nil becomes NSNull during bridging.
    return value as? Value ?? NSNull() as AnyObject as? Value
  }

callstack : ...

4 0x000000010f00ccfb in swift::swift_dynamicCastFailure(void const, char const, void const, char const, char const*) ()

5 0x000000010f00cd60 in swift::swift_dynamicCastFailure(swift::TargetMetadata const, swift::TargetMetadata const, char const*) ()

6 0x000000010f049d8e in swift_dynamicCastObjCClassUnconditional ()

7 0x000000010f00f03d in swift_dynamicCast ()

8 0x000000010f4eaf1a in swift_rt_swift_dynamicCast ()

9 0x000000010f546f64 in specialized setDownCastConditional<A, B>(:) ()

10 0x000000010f5478c2 in specialized static Set.conditionallyBridgeFromObjectiveC(:result:) ()

11 0x000000010f4a792f in static Set.conditionallyBridgeFromObjectiveC(:result:) ()

12 0x000000010f4a79a4 in protocol witness for static _ObjectiveCBridgeable.conditionallyBridgeFromObjectiveC(:result:) in conformance Set ()

13 0x000000010f0100cd in _dynamicCastClassToValueViaObjCBridgeable(swift::OpaqueValue, swift::OpaqueValue, swift::TargetMetadata const, swift::TargetMetadata const, (anonymous namespace)::_ObjectiveCBridgeableWitnessTable const*, swift::DynamicCastFlags) ()

14 0x000000010946f02a in swift_rt_swift_dynamicCast ()

15 0x000000010947eddd in static Promise.asValue(_:) at $projectpath/Pods/PromisesSwift/Sources/Promises/Promise.swift:104

16 0x00000001094794d3 in closure #1 in Promise.then(on:_:) at $projectpath/Pods/PromisesSwift/Sources/Promises/Promise+Then.swift:90

17 0x0000000109479a6d in partial apply for closure #1 in Promise.then(on:_:) ()

18 0x000000010947823c in thunk for @escaping @callee_guaranteed (@owned Swift.AnyObject?) -> (@out Any?) ()

19 0x000000010880567e in __56-[FBLPromise chainOnQueue:chainedFulfill:chainedReject:]_block_invoke.88 at $projectpath/Pods/PromisesObjC/Sources/FBLPromises/FBLPromise.m:271

20 0x00000001088048f5 in __44-[FBLPromise observeOnQueue:fulfill:reject:]_block_invoke_2 at $projectpath/Pods/PromisesObjC/Sources/FBLPromises/FBLPromise.m:224

Thank you.

ingun37 commented 6 years ago

I tested inheriting NSObject and it worked fine. I hope theres other solution because I wouldn't like the idea of inheriting unnecessary class. Thank you

shoumikhin commented 6 years ago

Hi @ingun37, could you provide more context, please? A small code snippet which reproduces the issue for you would be much appreciated! Also, Xcode and Swift version may be useful. Thanks!

ingun37 commented 6 years ago

I found a solution I changed Promise<Set<MyClass>> to Promise<[MyClass]> and it works fine now Im satisfied with the solution but I hv made this reproducing unit test code anyway because I would like to contribute as much as I love this library.

Xcode version: 9.4.1 Swift version: 4.1

class MyClass: Hashable, Decodable {
    let s:String
    init(_ s:String) {
        self.s = s
    }
    var hashValue: Int {return s.hashValue}
    static func == (lhs: apiTests.MyClass, rhs: apiTests.MyClass) -> Bool {
        return lhs.s == rhs.s
    }
}

func testFail() {
    let ex = XCTestExpectation(description: "test")
    Promise { Set([MyClass("a"), MyClass("b")]) }.then { (a)-> Promise<Set<MyClass>> in
        Promise {a}
    }.then {
        print($0)
        ex.fulfill()
    }
    wait(for: [ex], timeout: 10.0)
}
func testSuccess() {
    let ex = XCTestExpectation(description: "test")
    Promise { Set([MyClass("a"), MyClass("b")]) }.then {
        print($0)
        ex.fulfill()
    }
    wait(for: [ex], timeout: 10.0)
}

It doesnt break at the exactly same point but I think its the same bug Thank you

shoumikhin commented 6 years ago

Thank you for reporting that @ingun37 !

The root of the issue goes deeply in Swfit-ObjC interoperability, which has special handling for hashable containers (Set and Dictionary). Feel free to play with the attached example: SwiftObjCCasting.zip

Briefly, if we have the following code in ObjC:

@implementation ObjC
- (nullable id)cast:(nullable id)object {
  return object;
}
@end

When we pass a Swift object in such a method and try to restore the type for the returned value, the Swift dynamic cast will crash in case the object is a hashable container with object of custom Hashable (non-builtin and non-NSObject) types in it:

class TestClass: Hashable {
  init(_ string: String) {
    self.string = string
  }
  var hashValue: Int { return string.hashValue }
  static func == (lhs: TestClass, rhs: TestClass) -> Bool {
    return lhs.string == rhs.string
  }
  private let string: String
}

let input = Set([TestClass("a")])
let output = ObjC().cast(input)
let value = output as? Set<TestClass>  // Run-time crash!

Or the same in case of a Dictionary:

let input = [TestClass("a"): "a"]
let output = ObjC().cast(input)
let value = output as? Dictionary<TestClass, String>  // Run-time crash!

Since Promises for Swift rely on ObjC implementation for compatibility reasons, any value a promise gets resolved with goes through conversion to nullable id and then back to its original Swift type. That's what you see in your original crash stack trace: some promise got resolved with Swift value of hashable container type and tries to notify subscribers about that. The control leaves ObjC core logic and enters Swift wrapper, where the original Swift value is initially passed as Any?, which we try to cast back to the original type in asValue helper func.

Generally, the issue may be perceived as a flaw in Swift-ObjC interoperability and unfortunately, we don't currently have a proper fix or a good workaround for it, other than not using Promises with hashable containers holding custom types (any NSObjects are fine, though), which is obviously less than ideal.

A direction to explore would be something like:

let output = ObjC().cast(input) as AnyObject
if let keys = output.allKeys as? [AnyHashable], let values = output.allObjects {
  let value =  Dictionary(uniqueKeysWithValues: zip(keys, values)) as? Type
} else if let values = output.allObjects as? [AnyHashable] {
  let value = Set(values) as? Type
} else {
  let value = output as? Type
}

Where Type is the original type of input. But that approach is quite suboptimal due to the copy overhead.

Anyhow, I hope the above sheds some light on the root of the problem. We're open for any suggestions and continue searching for a real fix w/o modifying the compiler, of course.