Types are erased on the Swift/Obj-C boundary
- Dominant language
- Objective-C
- Stars
- 3.8k
- Forks
- 316
- PR merge metrics
- No merged PRs in 30d
Description
Overview
=======
No type information is persisted when wrapping an Objective-C `FBLPromise` value in a `Promise`, nor when deriving an Objective-C value through `Promise.asObjCPromise()`. The `Value` generic type on both `init` and `asObjCPromise` is not the same `Value` type that is on the class, and as such they can be freely parameterized to any combination of distinct values.
Symptoms
========
Erasure when wrapping
-----------------------
The following compiles and runs without issue, demonstrating the erasure when wrapping Objective-C:
```swift
let objCPromise = FBLPromise.__pending()
let swiftPromise = Promise(objCPromise)
objCPromise.__onQueue(.main, then: { value in
print(value)
return value
})
swiftPromise.fulfill(2)
```
Of course, doing anything with `value` that would not be understood by a Swift `Int` causes a runtime failure.
Fulfilling in the other direction with incorrect types also compiles and runs:
```swift
let objCPromise = FBLPromise.__pending()
let swiftPromise = Promise(objCPromise)
swiftPromise.then(on: .main) { _ in }
objCPromise.__fulfill("A value")
```
This snippet fails at runtime regardless of the contents of the `then` block, as the coercion back to `Int` fails.
Erasure when unwrapping
--------------------------
The following also compiles and runs without issue, demonstrating the erasure when moving from Swift to Objective-C:
```swift
let swiftPromise = Promise.pending()
let objCStringPromise: FBLPromise = swiftPromise.asObjCPromise()
let objCPromisePromise: FBLPromise> = swiftPromise.asObjCPromise()
let objCDateFormatterPromise: FBLPromise = swiftPromise.asObjCPromise()
objCStringPromise.__onQueue(.main, then: { value in
print(value)
return value
})
swiftPromise.fulfill(2)
```
All derived Objective-C Promises can be interacted with as if they are the declared types, and, again, fail at runtime when interacted with in a way that the real underlying type doesn't understand.
As in the previous section, fulfilling in the other direction with incorrect types also compiles and runs:
```swift
let swiftPromise = Promise.pending()
let objCStringPromise: FBLPromise = swiftPromise.asObjCPromise()
swiftPromise.then(on: .main) { _ in }
objCPromise.__fulfill("A value")
```
Again, this snippet fails at runtime regardless of the contents of the `then` block, as the coercion back to `Int` fails.
Root Cause
=========
The concrete type used to populate a generic type parameter in a function is resolved at the call site, and function-level generic type parameters are unrelated to the type parameters of the enclosing class.
Within a given `Promise` instance, there is a `Value` symbol `Promise` that is usually what is being interacted with. In the two critical methods, `init` and `asObjCPromise`, they are being shadowed. Concretely, those methods look like `Promise.init` and `Promise.asObjCPromise`, which, to stress the difference in parameters, are entirely equivalent to `Promise.init` and `Promise.asObjCPromise` (`U` and `V` intentionally varied from each other).
As a result, the `Value` on the Objective-C `FBLPromise` is entirely erased from the perspective of the Swift `Promise` when wrapped in `init`. Likewise, the `Value` on the Swift `Promise` is entirely erased from the perspective of the Objective-C `FBLPromise` when unwrapping through `asObjCPromise`.
Possible Solution
============
The two `Value` parameters can be unified by way of exposing them in an extension that applies the necessary `Value: AnyObject` constraint on the Swift `Promise`'s `Value`:
```swift
extension Promise where Value: AnyObject {
public convenience init(_ objCPromise: FBLPromise) {
/* ... */
}
public func asObjCPromise() -> FBLPromise {
/* ... */
}
}
```
This has a few immediate drawbacks:
* The now-convenience `init` is today the primary designated initializer. This would require some reconfiguration.
* It's no longer simple to turn a `Promise` into a `FBLPromise` - the types must match exactly, so the Swift entity would also need to be `Promise`. It's possible that this can be addressed with some fancier generic constraints.
The latter issue may break a number of existing clients in cases that were actually fine.
Contributor guide
Assessment
This issue has not been assessed yet.