Method dispatch to struct instance with concrete type known only at runtime?

So I have an UnsafePointer<UInt8> that points to some instance of a generic struct S (let's assume I don't have access to that struct), and a dynamic type of that struct (a variable of type Any.Type).

I also have a protocol P with a single requirement foo() that this struct is known to conform to.

Is there any way to call foo() on the struct instance pointed to by that pointer without creating an existential?

In other words, the problem is how to implement the function callFoo below?

protocol P {
    func foo()
}

struct S<T>: P {
    func foo() { print("foo") }
}

func callFoo(rawBytes: UnsafePointer<UInt8>, dynamicType: Any.Type) {
    // dynamicType will be S<Int>.self or S<String>.self at runtime,
    // so I can't just cast it to S.

    // How to implement this function?
}

To my understanding, we have all the necessary information to do method dispatch to the foo() method, but how to actually do it? How do I trick the type system?
Any solution, even the most magical one (with underscored APIs), will suffice.

cc @Alejandro @Mike_Ash @Joe_Groff

I’m probably not the right person to answer this, but I’ll give it a shot :slight_smile:

I’m not sure if there’s a way to do this without going through an existential. You could create an Any existential for the instance and then cast it to P to call foo. This would require some metadata to make possible.

The tricky part is the dynamicType because we could just load the bytes as some type, but because it’s an Any.Type it prevents us from going the easy route :frowning:

Maybe there’s a better way to do this that I don’t know, but I hope this helped a little bit :smile:

It seems like you could simplify the problem by changing callFoo to be generic:

func callFoo<T>(rawBytes: UnsafeRawPointer, _: T.Type) {
  rawBytes.load(as: S<T>.self).foo()
}

If the type is dynamically unknown at the call site, you can switch out the cases you expect:

let x: Any.Type = ...
if x == S<Int>.self {
  callFoo(rawBytes: p, Int.self)
} else if x == S<String>.self {
  callFoo(rawBytes: p, String.self)
}

It might also be simpler and more efficient to use an enum instead of Any.Type to enumerate the limited set of cases you expect to encounter.

Thank you Joe.

This is the problem though: the set of cases is unlimited, only the nominal type is the same.

Isn't this how unconstrained generic functions work in Swift? If I remember correctly from this awesome talk, they just accept a pointer to the value and a pointer to the type metadata, which is exactly what I have here in callFoo (Any.Type can be unsafeBitCasted to pointer and it will be the pointer to the metadata record). I guess I'd just have to get the calling convention right.

1 Like

Yeah, the call signature of your callFoo is very similar to what the compiler will generate for a generic foo<T>(_: T) function. If the type conforming to P is truly unrestricted, you can avoid forming an existential box by putting your logic inside an extension method on P. Inside extension methods, the Self type is bound to the concrete conforming type, and values of Self will not be boxed. So you could do this:

func callFoo(rawBytes: UnsafeRawPointer, dynamicType: Any.Type) {
  // First, we need to check that the type conforms to P
  guard let pType = dynamicType as? P.Type else {
    fatalError("unexpected type")
  }

  // Call a static extension method on P to process the pointer with the concrete type
  pType.callFooThroughPointer(rawBytes)
}

extension P {
  static func callFooThroughPointer(_ rawBytes: UnsafeRawPointer) {
    rawBytes.load(as: Self.self).foo()
  }
}
7 Likes

Whoah. This is amazing, I didn't think about it. Thank you so much, Joe!

1 Like
Terms of Service

Privacy Policy

Cookie Policy