blob: 784114fd3af056d88af88359cb8e19405fc118ae [file] [edit]
/// Fetches a remote class object from the app process.
///
/// The caller of this method should pass the local class object in its process as @c theClass.
/// eDO will map @c theClass to the appropriate class object in the remote process and return that.
///
/// - Parameters:
/// - aClass: The class object to fetch from the remote process.
/// - hostPort: The server port of the remote process.
///
/// - Returns: A class object, which is the same type as @c theClass in the remote process.
/// Invocations made to the returned Class object will be executed in the remote process.
/// If the remote process doesn't have such class, `nil` will be returned.
///
/// - Attention: Only methods that are marked `@objc dynamic` can be called. Swift compiler won't
/// prevent from calling other types of methods, but it will crash the current process.
public func remoteClassObject<T: NSObject>(of aClass: T.Type, on port: EDOHostPort) -> T.Type? {
let className = NSStringFromClass(aClass)
let classRequest = EDOClassRequest(className: className, hostPort: port)
// The return type is intentionally `Any` instead of `AnyClass`. ARC and Swift treat `Class`
// objects as immortal, which means there is no need for retain/release. However, the actual
// object returned by this method is a proxy object that is not immortal. Therefore, use `Any` as
// the return type to ensure the compiler retains/releases the object as usual. See this thread
// for more details:
// https://forums.swift.org/t/why-does-casting-type-metadata-to-anyobject-later-result-in-destroy-value-being-called-on-the-anyobject/66371/4
let remoteClass = EDOClientService<AnyObject>.responseObject(with: classRequest, on: port)
guard let remoteClass else {
return nil
}
// The following cast is safe because `remoteClass` is `AnyObject`, which will be treated the same
// as `T.Type` when the compiler generates code to call a class method.
//
// `T` is an Objective-C-compatible class, either defined in Swift or in Objective-C:
//
// * For an `@objc` Swift class, `T.Type` is a “class metadata record”. Class metadata records are
// compatible with the Objective-C `Class` type, so the compiler passes a class metadata record
// directly to `objc_msgSend`.
// * For an Objective-C class, `T.Type` is an “Objective-C class wrapper metadata record”. The
// compiler calls `swift_getObjCClassFromMetadata` to get the underlying Objective-C `Class`
// object from the metadata record in order to pass it to `objc_msgSend`.
//
// One thing note is that `AnyObject` is compatible with `T.Type` in terms of calling class
// methods, regardless of how `T` was defined:
//
// * If `T` is an `@objc` Swift class, the compiler passes the `AnyObject` value directly to
// `objc_msgSend`, which is the desired outcome.
// * If `T` is an Objective-C class, the compiler passes the `AnyObject` value to
// `swift_getObjCClassFromMetadata`.
//
// Fortunately, `swift_getObjCClassFromMetadata` can handle all cases: Objective-C class wrapper
// metadata records, class metadata records, and Objective-C `Class` objects. For the latter two,
// the function returns the value as-is. Thus, the `AnyObject` value will be returned and then
// passed to `objc_msgSend` as desired.
return unsafeBitCast(remoteClass as AnyObject, to: T.Type.self)
}