Is it possible for a type to have 2 metadata pointers?

I'm writing tests for calling the Metadata Access Function on some types, and surprisingly one of the tests failed because the type metadata had different pointers. Consider the following:

// I just need the dictionary descriptor
let metadata = reflectStruct([String: Int].self)!
let accessor = metadata.descriptor.accessor
let response = accessor(.complete, Int.self, Double.self)
print(response.type) // Swift.Dictionary<Swift.Int, Swift.Double>
XCTAssert(response.type == [Int: Double].self) // Assertion hit!

I was surprised because I thought all metadata was unique and a type only had a single metadata record. I did some more playing around and managed to crash the runtime:

print([Int: Double].self)
let response = accessor(.complete, Int.self, Double.self) // Crash!

Relevant Backtrace:

frame #0: 0x00007fff2cebaad5 libswiftCore.dylib`swift::MetadataCacheKey::compareProtocolConformanceDescriptors(swift::TargetProtocolConformanceDescriptor<swift::InProcess> const*, swift::TargetProtocolConformanceDescriptor<swift::InProcess> const*) + 21
    frame #1: 0x00007fff2ceb0a16 libswiftCore.dylib`_swift_getGenericMetadata(swift::MetadataRequest, void const* const*, swift::TargetTypeContextDescriptor<swift::InProcess> const*) + 502
    frame #2: 0x00007fff2ce8a9c0 libswiftCore.dylib`__swift_instantiateCanonicalPrespecializedGenericMetadata + 32
  * frame #3: 0x0000000102b2988a EchoTests`echo_callAccessor2(ptr=0x00007fff2ce49f70, request=0, arg0=0x00007fff815257e0, arg1=0x00007fff81525440) at CallAccessor.c:47:10

Interestingly though, this only occurs on types who use __swift_instantiateCanonicalPrespecializedGenericMetadata in their metadata accessor (I've only tested dictionary). Other accessors who don't use this prespecialization stub pass the assertion and don't crash if I print them before:

struct Foo<T, U> {}

let metadata = ...
let accessor = ...
print(Foo<Int, Int>.self)
let response = accessor(.complete, Int.self, Int.self) // no crash
XCTAssert(response.type == Foo<Int, Int>.self) // no assertion

Am I doing something wrong, or could there possibly be an issue in the compiler/runtime somewhere?

cc: @John_McCall @nate_chandler (who implemented metadata prespecialization)

Hi Alejandro,

There would be two different metadata pointers for Dictionary<K, V> (for fixed K and V) if there were two different conformances of K to Hashable. In general, there should be exactly one metadata record for any given list of metadata records and witness tables¹ to be passed to the metadata access function. In this case, it looks like the issue is that the witness table being passed to the function is garbage (and maybe null).

Here, it looks like the problem may be that you aren't passing all the arguments that the metadata accessor for Dictionary expects. Its signature looks something like

define swiftcc %swift.metadata_response @"$sSDMa"(i64 %0, %swift.type* %1, %swift.type* %2, i8** %3)

If the value at reflectStruct([String: Int].self).descriptor.accessor is a pointer to $sSDMa, then calling it like

accessor(.complete, Int.self, Double.self)

doesn't pass the final argument expected by $sSDMa (the witness table for the key to Hashable) resulting in a garbage (maybe null) value being seen as the value for that argument by that function. We need to pass a witness table for Int's conformance to Hashable as well.

I was able to see similar behavior by misdeclaring the signature and calling it as you are

@_silgen_name("$sSDMa")
func metadataAccessorForDictionaryBad(state: UInt64, keyType: Any.Type, valueType: Any.Type) -> Any.Type

func accessBad() {
  print(Dictionary<Int, Int>.self)
  print(metadataAccessorForDictionaryBad(state: 0, keyType: Int.self, valueType: Int.self))
}

accessBad()

By declaring the signature with the fourth argument that's required, the witness table for the key's conformance to Hashable, and passing along the appropriate witness table

@_silgen_name("$sSDMa")
func metadataAccessorForDictionary(state: UInt64, keyType: Any.Type, valueType: Any.Type, witnessTable: UInt64) -> Any.Type

func accessGood() {
  print(Dictionary<Int, Int>.self)
  print(metadataAccessorForDictionary(state: 0, keyType: Int.self, valueType: Int.self, witnessTable: witnessTableForIntToHashable))
}

accessGood()

I am able to see the expected behavior (no crash). The constant witnessTableForIntToHashable is defined in a cpp file like this

extern uint64_t $sSiSHsWP;
uint64_t witnessTableForIntToHashable = (uint64_t)&$sSiSHsWP;

As for your Foo<T, U> example, that works because neither generic parameter is constrained to conform to a protocol, so there are no witness tables to pass to the metadata accessor. Once you add a conformance (say of T to Hashable as with Dictionary), you will start to see similar behavior.

¹ Up to equivalence of witness tables. See MetadataCacheKey::compareWitnessTables.

Thanks!
Nate

5 Likes

Oh apologies I had no idea the metadata accessor had a parameter for the witness tables needed (It also looks like if there are more than 3 arguments (key parameters and witness tables combined), then its all placed within the same single argument). This makes total sense now. Thank you so much for the detailed response!

3 Likes
Terms of Service

Privacy Policy

Cookie Policy