@convention(c) functions have some pretty strict limitations, amongst others:
they can't capture context and
they can't capture generic context or parameters (I read that as "be specialized")
That second limitation is why this doesn't work:
struct MyType<T> {
static func doSomething() {
let f: @convention(c) () -> Void = { print(T.self) }
// ^ error: A C function pointer cannot be formed from a closure that captures generic parameters
}
}
That's why I was a little surprised to see this does work:
It even works when called from C code. This feels like it flies in the face of the second limitation I mentioned before, but presumably there's some sort of type resolution path the compiler takes here that makes this valid, even though it does end up generating what I would consider a sort of specialized C function.
I suppose my main question here is:
is this reliable behavior, or a fluke? and
if this is reliable... why isn't it extended to other generic contexts?
Generic parameters become ordinary parameters in the calling convention; specialization is an optimization that is performed, but not required by the implementation model.
That makes a certain sense but... how would you say that differs from:
func run2<T>(_ type: T.Type) {
@RunnerBuilder<T> func runner() { }
let cfunc: @convention(c) () -> Void = runner
// ^ A C function pointer cannot be formed from a local function that captures generic parameters
}
I won't lie, that feels sort of arbitrary. It feels convenient that T is turned into Int inside the function's parameters but not it's body At the end of the day I'm glad it works and I can use it, but it feels like the same rule should apply to both.
Are there any other contexts or examples like this — generics resolved outside of a scope(?) — besides functions (and maybe typealiases?)
Also, how does what you shared about scope apply in an instance like this?
If you approach type parameters as just another kind of input parameter to a function, then if you think about it, it’s actually the same rule as with captures of values.
Consider this example:
func f() {
let fn: @convention(c) () -> () = {
let n = 3
print(n)
}
}
This is clearly fine, because n is local to the closure. There’s no capture.
Now this is not allowed; the closure captures a value from the outer scope:
The closure expression here, { } is written at the top level of the source file outside of any generic declaration. It will have a non-trivial body after the result builder transform runs, but it will not have any captures (it’s at the top level).
As before, the result builder doesn’t fundamentally change anything, other than by adding some stuff to the closure’s body. Capture computation and @convention(c) checking happens after the result builder transform so you can reason about each feature in isolation.