Hello, all. This post will discuss the issues of using Swift's generics model with C++ function templates, namely, the best way to support calling C++ function templates with one or more generic arguments.
The current state of the world
Currently, we have very preliminary support for function templates. We can import function templates with a full concrete substitution map where all replacement types can be converted to C++ types. This means we can't invoke a C++ function template with generics or even custom object types.
Here are a few examples:
template<class T>
void myCxxFunctionTemplate(T) { }
func basicCaller() {
myCxxFunctionTemplate(0) // Works!
}
func genericCaller<T>(arg: T) {
myCxxFunctionTemplate(arg)
}
func basicGenericCaller() {
genericCaller(0)
}
func complexGenericCaller(condition: Bool) {
if condition { genericCaller(0 as UInt32) }
else { genericCaller(0 as Int32) }
}
// Note: *public*
public func impossibleGenericCaller<T>(arg: T) {
genericCaller(arg)
}
Currently, basicCaller
works... and that's it. We need to determine how to support or gracefully fail for the rest of the test cases above.
Proposed solution
I propose that we essentially treat Swift generics as C++ templates; that is, we force all generic functions to be fully specialized if they invoke a C++ function template. This means, somewhere in the compiler, we would visit all invocations of any function that calls a C++ function template with a generic argument.
In the basic example, the compiler would find that basicGenericCaller
calls genericCaller
, which it knows calls a C++ function template. So, it would create a specialization of genericCaller
where T = Int
.
In the more complicated example: when the compiler visited complexGenericCaller
, it would see that there are two calls to genericCaller
and would then create two specializations of genericCaller
with T = UInt32
and T = Int32
.
Last, the compiler would see that it cannot make a specialization of genericCaller
based on the function call in impossibleGenericCaller
, so it would create a static error. If this were a private function, then the compiler would simply ignore it.
Likely all specialization would be "transparent" or "always inlinable," so optimization passes could remove them.
Specializing each function vs. "if" statements
Another possible implementation to improve code size would be to modify the body of genericCaller
to call out to specific specializations of myCxxFunctionTemplate
based on T
. This could be achived with an if-statement like so:
func genericCaller<T>(arg: T) {
if T == Int.self {
myCxxFunctionTemplate(arg as Int)
} else if T == Int32.self {
myCxxFunctionTemplate(arg as Int32)
} else if T == UInt32.self {
myCxxFunctionTemplate(arg as UInt32)
} else {
fatalError()
}
}
I propose that we implement this as a possible optimization later on, and right now, we only implement the more straightforward solution outlined above.
Should specialization of generics happen in the type checker or a SIL pass?
I propose the logic for analyzing invocations of C++ functions, and their generic callers is implemented in a mandatory raw SIL pass, similar to SILGenCleanup or MandatoryCombine.
The current specialization logic exists in the type checker; we could conceivably implement this logic in the type checker as well, which would allow us to error earlier.
Why a mandatory SIL pass? There are a few reasons. First, it seems like the logical place to put this type of checking and transformation. In a SIL pass, it will be self-contained and separated from mostly unrelated type checking code. Other specialization and inlining passes have similar logic, so it makes sense to group all these transformations together.
Second, this logic will be fairly expensive, and I'm not sure we want to slow down the type checker by visiting and analyzing every call made in a program. Note: no matter where this logic is implemented, it will likely only be enabled when C++ interoperability is also enabled.
Third, we could potentially accept more code if this pass was run after mandatory inlining and other optimization passes that remove logic that would otherwise create an error. For example, we could potential specialize glob
in the below example, something that could not easily be done in the type checker:
var glob = { myCxxFunctionTemplate($0) }
func caller() { glob(0) }
@_must_specialize
The C++ Interoperability Manifesto suggests the addition of the @_must_specialze
attribute. I do not think this attribute is necessary for implementing generic functions that call C++ templates. This may be a helpful feature to implement down the road, but the proposed pass would be capable of generating errors itself and could easily keep track of what functions must be specialized internally.
References
The C++ Interoperability Manifesto
Initial Support for C++ Function Templates
I look forward to your feedback and guidance.