Search code examples
swiftanyvalue-type

How to get the actual type of a Swift struct in the disguise of a `__SwiftValue`?


I'm using YapDatabase to encode/decode my Swift value types. After decoding, the type information seems to be lost, that is type(of:element) returns __SwiftValue instead of, e.g., Reservation.

If I call po element in the debugger though, it seems that the type information is still retained:

(lldb) po element
SecureTruckParking.Reservation(reservationId: 12625, accessInformations: [SecureTruckParking.AccessInformation(accessInformationId: 12706, accessTypeId: 1, accessTypeKey: Optional("1"), accessTypeTenantKey: Optional("ROOT"), encodedValue: "XXX", displayedValue: "XXX"), SecureTruckParking.AccessInformation(accessInformationId: 12707, accessTypeId: 51, accessTypeKey: Optional("51"), accessTypeTenantKey: Optional("ROOT"), encodedValue: "918296", displayedValue: "918296")], customerId: 3156, areaId: 552, productId: 1004, state: "PENDING", startTime: 2020-09-10 08:23:00 +0000, endTime: 2020-09-11 08:23:00 +0000, earliestEntryTime: 2020-09-10 08:23:00 +0000, latestExitTime: 2020-09-11 08:23:00 +0000, totalAmount: 2750.0, currency: "€", netPrice: 2311.0, taxPrice: 439.0, invoiceItems: [SecureTruckParking.InvoiceItem(amount: 1, itemText: "Parkplatzreservierung, 10.09.2020 10:23 - 11.09.2020 10:23 \nREWE Logistikzentrum Neu-Isenburg -> REWE Logistikzentrum Neu-Isenburg", netPrice: 2311.0, taxPrice: 439.0, taxRate: 19.0)], productAttributes: [SecureTruckParking.Attribute(key: "early_bird_count", value: Optional("1"), definitionId: Optional(SecureTruckParking.DefinitionId(key: "early_bird_count", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "DETAILS", value: nil, definitionId: nil), SecureTruckParking.Attribute(key: "INFO_DETAILS", value: nil, definitionId: nil), SecureTruckParking.Attribute(key: "early_bird", value: Optional("false"), definitionId: Optional(SecureTruckParking.DefinitionId(key: "early_bird", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "manualBackofficeCancellationConfirmation", value: Optional("false"), definitionId: Optional(SecureTruckParking.DefinitionId(key: "manualBackofficeCancellationConfirmation", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "manualBackofficeCancellationConfirmationThreshold", value: Optional("1"), definitionId: Optional(SecureTruckParking.DefinitionId(key: "manualBackofficeCancellationConfirmationThreshold", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "product_icon", value: nil, definitionId: Optional(SecureTruckParking.DefinitionId(key: "product_icon", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "product_image", value: nil, definitionId: Optional(SecureTruckParking.DefinitionId(key: "product_image", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "product_tariff_group", value: nil, definitionId: Optional(SecureTruckParking.DefinitionId(key: "product_tariff_group", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "upgrading", value: Optional("false"), definitionId: Optional(SecureTruckParking.DefinitionId(key: "upgrading", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "upgrading_dates_fixed", value: Optional("false"), definitionId: Optional(SecureTruckParking.DefinitionId(key: "upgrading_dates_fixed", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "upgrading_immediate", value: Optional("false"), definitionId: Optional(SecureTruckParking.DefinitionId(key: "upgrading_immediate", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "upgrading_mail", value: Optional("false"), definitionId: Optional(SecureTruckParking.DefinitionId(key: "upgrading_mail", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "upgrading_price_surcharge", value: Optional("false"), definitionId: Optional(SecureTruckParking.DefinitionId(key: "upgrading_price_surcharge", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "early_bird_duration", value: Optional("1440"), definitionId: Optional(SecureTruckParking.DefinitionId(key: "early_bird_duration", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "recurring_max_trips", value: Optional("1"), definitionId: Optional(SecureTruckParking.DefinitionId(key: "recurring_max_trips", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "upgrading_mail_max_lead_time", value: nil, definitionId: Optional(SecureTruckParking.DefinitionId(key: "upgrading_mail_max_lead_time", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "upgrading_mail_min_lead_time", value: nil, definitionId: Optional(SecureTruckParking.DefinitionId(key: "upgrading_mail_min_lead_time", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "bstp_product_type", value: Optional("Reservation"), definitionId: Optional(SecureTruckParking.DefinitionId(key: "bstp_product_type", tenant: "ROOT")))], areaAttributes: [SecureTruckParking.Attribute(key: "product_icon", value: nil, definitionId: Optional(SecureTruckParking.DefinitionId(key: "product_icon", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "product_image", value: nil, definitionId: Optional(SecureTruckParking.DefinitionId(key: "product_image", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "Parking_Area_UST", value: Optional("22222222222222"), definitionId: Optional(SecureTruckParking.DefinitionId(key: "Parking_Area_UST", tenant: "MAN_BOSCH"))), SecureTruckParking.Attribute(key: "ipaw_id", value: Optional("4651"), definitionId: Optional(SecureTruckParking.DefinitionId(key: "ipaw_id", tenant: "ROOT"))), SecureTruckParking.Attribute(key: "bstp_area_type", value: Optional("PROFESSIONAL"), definitionId: Optional(SecureTruckParking.DefinitionId(key: "bstp_area_type", tenant: "ROOT")))])

What is this __SwiftValue and is there a way to get the actual type (besides any horrible approaches of parsing the String(describing: element)?


Solution

  • First of all you're seeing the __SwiftValue because the struct has been wrapped with as AnyObject cast. You can easily verify this yourself:

    struct TestStruct {}
    let ourStruct = TestStruct()
    let structSwiftValue = ourStruct as AnyObject
    print(type(of: ourStruct))
    print(type(of: structSwiftValue))
    

    outputs:

    TestStruct
    __SwiftValue
    

    Good discussion on the topic: https://forums.swift.org/t/anyobject/35659/9

    Now to the essence of your question, unfortunately I believe there's no convenient public API for the time being that would allow you to extract original metadata Type from __SwiftValue.

    Still the topic is too interesting not to venture into private API territory for educational purposes.

    Let's begin with __SwiftValue itself https://github.com/apple/swift/blob/master/stdlib/public/runtime/SwiftValue.mm

    There is promising API available through obj-c:

    // Private methods for debugging purposes.
    
    - (const Metadata *)_swiftTypeMetadata {
      return getSwiftValueTypeMetadata(self);
    }
    - (id /* NSString */)_swiftTypeName {
      TypeNamePair typeName
        = swift_getTypeName(getSwiftValueTypeMetadata(self), true);
      id str = swift_stdlib_NSStringFromUTF8(typeName.data, typeName.length);
      return [str autorelease];
    }
    - (const OpaqueValue *)_swiftValue {
      return getValueFromSwiftValue(self).second;
    }
    

    Let's start with something fairly simple:

    print(structSwiftValue.value(forKey: "_swiftTypeName")!)
    

    outputs:

    ModuleName.TestStruct
    

    Just a String type, but not bad for starters. The next promising candidate to explore seems:

    - (const Metadata *)_swiftTypeMetadata()
    

    We can call it like this (in depth details on this particular way of invoking selectors in Swift can be found here)

    let selector: Selector = NSSelectorFromString("_swiftTypeMetadata")
    let methodIMP: IMP! = structSwiftValue.method(for: selector)
    let metadataPtr = unsafeBitCast(methodIMP,to:(@convention(c)(Any?,Selector)->OpaquePointer).self)(structSwiftValue,selector)
    

    Now the challenge is to somehow make use of Metadata * (i.e. the OpaquePointer after bridging to Swift) in a type(of:) manner.

    I found a way to do it, albeit a very hacky one.
    We begin with a metadata Type variable of some dummy object or struct (doesn't really matter). The general idea is eventually we'd like to replace the metadata pointer with one we got from our previous stage. I noticed the result of type(of:) yields a pointer pointing at the metadata, so there's extra level of indirection, we will have to accommodate for that:

    var placeholderTypeVar: Any.Type = type(of: NSObject())
    print(placeholderTypeVar)
    withUnsafeMutablePointer(to: &placeholderTypeVar) {
        let unsafePtr = UnsafeMutablePointer<OpaquePointer>.allocate(capacity: 1)
        unsafePtr.pointee = metadataPtr
        $0.assign(from: UnsafePointer<Any.Type>.init(OpaquePointer(unsafePtr)), count: 1)
    }
    print(placeholderTypeVar)
    print(type(of: structSwiftValue))
    

    outputs:

    NSObject
    TestStruct
    __SwiftValue
    

    The middle one is the answer. And moreover it's identical to result of type(of: ourStruct) down to memory level (the Metadata * equivalent yielded internally is the very same address!).

    More read about metadata which is a very interesting topic in Swift:
    https://medium.com/ios-os-x-development/types-and-meta-types-in-swift-9cd59ba92295 https://kateinoigakukun.hatenablog.com/entry/2019/03/22/184356 https://medium.com/@weswickwire/creating-a-swift-runtime-library-3cc92fc486cc