In one of my projects, I am using a dependent dictionary type (dictionary whose keys are metatypes and whose values have types dependent on the associated types of the keys). In order to save/resume the state of my program, I am attempting to make my type conform to Codable
. To make the keys be codable, I have devised a solution which I believe is novel.
The basic idea is as follows: NSClassFromString
and NSStringFromClass
allow converting class types to and from String
. A generic class can statically reference an arbitrary (i.e. non-class) type through its parameter. This wrapped type can be exposed through a protocol with a static metatype requirement. This wrapper can be formed from a metatype by using Self
in the body of a protocol extension to Codable
(or Decodable where Self: Encodable
due to language restrictions) or really any other protocol. These features can be combined into some very hacky code:
import Foundation
protocol CodableMetatypeWrapperProtocol: AnyObject {
static var wrappedType: Codable.Type { get }
}
// If I make this private, NSClassFromString(NSStringFromClass(CodableMetatypeWrapper<TestType>.self)) returns nil.
class CodableMetatypeWrapper<T: Codable>: CodableMetatypeWrapperProtocol {
static var wrappedType: Codable.Type { T.self }
}
extension Decodable where Self: Encodable {
static var typeWrapper: CodableMetatypeWrapperProtocol.Type { CodableMetatypeWrapper<Self>.self }
}
struct CodableMetatype: Codable, Hashable {
let type: Codable.Type
init(_ type: Codable.Type) {
self.type = type
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let name: String = NSStringFromClass(type.typeWrapper)
try container.encode(name)
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let name = try container.decode(String.self)
guard let wrapperClass = NSClassFromString(name) as? CodableMetatypeWrapperProtocol.Type else {
throw DecodingError.typeMismatch(CodableMetatype.self, .init(codingPath: decoder.codingPath, debugDescription: "Result of NSClassFromString was nil or was not a CodableMetatypeWrapperProtocol.", underlyingError: nil))
}
self.type = wrapperClass.wrappedType
}
static func == (lhs: CodableMetatype, rhs: CodableMetatype) -> Bool {
ObjectIdentifier(lhs.type) == ObjectIdentifier(rhs.type)
}
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(type))
}
}
The data returned from CodableMetatype.encode
seems to be stable across program execution from my limited testing, and it also seems like the CodableMetatypeWrapper
does not need to be explicitly loaded into the runtime before decoding. The following example code demonstrates how this can work.
struct TestType: Codable {}
/// Obtained from a previous execution.
/// I ran this from a SwiftPM project called `Scratch`.
/// You might need a different value.
let typeString: String? = "\"_TtGC7Scratch22CodableMetatypeWrapperVS_8TestType_\""
//let typeString: String? = nil
let type: CodableMetatype
if let typeString = typeString {
type = try! JSONDecoder().decode(CodableMetatype.self, from: typeString.data(using: .utf8)!)
assert(type == CodableMetatype(TestType.self))
} else {
type = CodableMetatype(TestType.self)
}
print(type)
let newTypeString = String(data: try! JSONEncoder().encode(type), encoding: .utf8)!
if let oldTypeString = typeString {
assert(oldTypeString == newTypeString)
}
print(newTypeString)
let newType = try! JSONDecoder().decode(CodableMetatype.self, from: newTypeString.data(using: .utf8)!)
assert(newType == type)
Although everything appears to work fine right now, I do not really trust this solution. Will I get into trouble by using this code? I am especially worried because I will be using dlopen
midway through the program, loading additional classes into the runtime.
This is my first post here, so feel free to leave any sort of feedback or let me know if this isn't the right place for this sort of question. Thanks!