Typecasting to a protocol via a generic function

I'm a security researcher and I sometimes use Swift to pull in classes from frameworks on macOS, when I am unable to easily access them. The below example shows how I would get and use the NSString class, just for reference:

let FoundationPtr = dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", RTLD_LAZY)
let NSStringPtr = dlsym(FoundationPtr, "OBJC_CLASS_$_NSString")
@objc protocol NSStringProtocol: NSObjectProtocol {
    init(string: String)
}
let _NSString = unsafeBitCast(NSStringPtr, to: NSStringProtocol.Type.self)
let string = _NSString.init(string: "Hello, world!")
print(string)

This works nicely, but is a bit verbose. I tried to make a generic function that does all of this like so:

func makeClassFromFrameworkSymbol<T: NSObjectProtocol>(
    frameworkName: String, className: String, castTo: T.Type = NSObjectProtocol.self
) -> T.Type? {
    let FrameworkPtr = dlopen(
        "/System/Library/Frameworks/\(frameworkName).framework/\(frameworkName)", RTLD_LAZY)
    let ClassPtr = dlsym(FrameworkPtr, "OBJC_CLASS_$_\(className)")
    return unsafeBitCast(ClassPtr, to: T.Type.self)
}

Unfortunately, the return type of that function, when used as before, is (any NSStringProtocol).Type instead of any NSStringProtocol.Type (notice the parentheses).

Trying to use the function like so:

let __NSString = makeClassFromFrameworkSymbol(
    frameworkName: "Foundation", className: "NSString", castTo: NSStringProtocol.self)
let string2 = __NSString!.init(string: "Hello, world!")

results in compiler errors.

Am I doing something wrong with my generic function?

Swift's type system doesn't really let you be generic over protocols; as you noticed, substituting any Protocol into T.Type gives you (any Protocol).Type, and there isn't a way yet to express that you want any (Protocol.Type) for an arbitrary Protocol. Instead, you could pass NSObjectProtocol.Type as the entire generic parameter, like:

func makeClassFromFrameworkSymbol<T>(
    frameworkName: String, className: String, castTo: T = NSObjectProtocol.Type.self
) -> T? {
    let FrameworkPtr = dlopen(
        "/System/Library/Frameworks/\(frameworkName).framework/\(frameworkName)", RTLD_LAZY)
    let ClassPtr = dlsym(FrameworkPtr, "OBJC_CLASS_$_\(className)")

    // ensure the value size we got actually corresponds to a pointer
    assert(MemoryLayout<T>.size == MemoryLayout<UnsafeRawPointer>.size)
    return unsafeBitCast(ClassPtr, to: T.self)
}
2 Likes

Nice! Yeah, I just figured this out as I was playing around with it more. I wish this was something we could do with <T: NSObjectProtocol> though. Maybe it will be available in the future.

Also, just curious, why did you add an assert? Is that just to ensure that we don't pass in a bad type (e.g. sort of taking the role of what I thought <T: NSObjectProtocol> was gonna do)?

Yeah, that's pretty much it. Since going fully generic gives up static type safety it seemed like a good idea to assert that the type matches at runtime if nothing else.

1 Like

Thanks for the clarification! Do you think what I was trying to do originally (trying to enforce that only Protocol types are passed in) would be something that will be added to Swift in the future? Or are such additions to the type system not really on the roadmap?

(deleted my original reply as I was replying to the wrong thing)

In fact this feature idea has come up a few times in the past, so I'll summarize what I understand. I think what it boils down to is two new requirement kinds:

  • A U: Protocol requirement that says the substituted type for U must be a protocol type.
  • A T: U requirement that's like a conformance requirement, except the right-hand side is another type parameter, which must be subject to the first kind of requirement.

The first requirement kind alone would be easy to implement I think -- it would be a new kind of layout requirement.

The second is trickier. Introducing new requirement kinds involves solving various problems:

  1. Checking substitutions at the call site
  2. Figuring out the runtime metadata representation
  3. Solving the "derived requirements" problem inside the generic declaration itself

For (1), once you have concrete replacement types for T and U, you can check that U is a protocol, and that T conforms to it.

For (2), I have no idea. It seems like we'd need some way to recover a witness table for T: U at runtime once we have a concrete T and U.

For (3), we need to understand what are the "consequences" of T: U inside the generic declaration itself. For example, what can I do with T inside an expression? Well, if I don't know anything about U, it's effectively unconstrained, but imagine you're in a constrained extension that adds the same-type requirement U == any Sequence. Now, T: U and U == any Sequence together imply T: Sequence, which does have real consequences; it generates the member type T.Element, it tells us that T.Iterator: IteratorProtocol, and that T.Element == T.Iterator.Element. To solve this in general case, I think we'd need to reason about these kinds of situations where an "abstract" conformance requirement becomes concrete.

Today, requirements are lowered to a string rewrite system, which is a 'flat' representation in a sense. It would be hard to encode this kind of abstract conformance requirement, or a generic associated type, without fundamentally changing this lowering. However, now that I have something approximating a formal model for "derived requirements" that's independent of the lowering (Slides from a talk about Swift generics), it would be feasible to eventually try to tackle the problem using a more general term rewriting system instead.

This is all a lot of work; it's doable in principle, though.

3 Likes

Thank you so much for this! I'm not a compiler author myself, so a lot of this went over my head, but I am really glad you laid it out well and even linked to further resources. Maybe one day I'll look back on this and finally fully understand it!