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

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

Closed
ingun37 opened this issue Aug 28, 2018 · 4 comments
Closed

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

ingun37 opened this issue Aug 28, 2018 · 4 comments

Comments

@ingun37
Copy link

ingun37 commented Aug 28, 2018

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::TargetMetadataswift::InProcess const*, swift::TargetMetadataswift::InProcess 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::TargetMetadataswift::InProcess const*, swift::TargetMetadataswift::InProcess 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
Copy link
Author

ingun37 commented Aug 28, 2018

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
Copy link
Contributor

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
Copy link
Author

ingun37 commented Aug 28, 2018

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
Copy link
Contributor

shoumikhin commented Sep 1, 2018

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants