Short answer :
The C-Style cast basically means the Swift compiler will just force your closure to be called as if it takes a (Int, Float)
tuple as parameter whereas the as / as? / as! cast will first do some sanity checks on your cast to ensure that the types are compatible and so on.
Since the compiler believes (in certains versions, as seen on the comments on the other answer) that (Int, Float) -> ()
and ((Int, Float)) -> ()
are too far apart to be compatible, the sanity check will just return nil, therefore blocking your call.
What makes it work is that a function / closure taking a (Int, Float)
tuple behaves exactly the same (in the current version of Swift) as a function / closure taking an Int
and a Float
parameter.
Long answer :
I compiled a snippet of code into assembly which I will be referencing from now on. That snippet can be found here : https://swift.godbolt.org/z/CaOb0s
For readability purposes, I used functions instead of actual closures here.
I've created two functions corresponding to the two cases we have :
func twoParamFunc(a: Int, b: Float)-> Void {
print(a, b)
}
func singleParamFunc(tuple: (a: Int, b: Float))-> Void {
print(tuple.a, tuple.b)
}
I then tried to cast those using your two different methods :
let cCastFunction = ((((Int, Float)) -> Void)?(twoParamFunc))!
let asCastFunction = (twoParamFunc as? (((Int, Float)) -> Void))!
And when looking at the assembly code compiled by swift, we can see a lot of differences between the two.
When looking at the C-style cast, we can see that most of the code is basically just calling alloc/retain/release and moving pointers and values around. The only call to external code is through a failure case (the !
dereferencing a null reference), calling $ss18_fatalErrorMessage__4file4line5flagss5NeverOs12StaticStringV_A2HSus6UInt32VtF
Whereas in the swift-style cast, there are a lot of additional calls (the sanity checks I was talking about earlier).
We have for exemple
call (type metadata accessor for (Swift.Int, Swift.Float) -> ())
...
call (type metadata accessor for ((Swift.Int, Swift.Float)) -> ())
...
call swift_dynamicCast@PLT
which clearly shows that the Swift compiler is doing some checks to the compatibility of the types being cast, and are nowhere to be found in the c-style cast.
So now that the C-style cast / Swift-style cast difference has been found, we can try to understand why the call to the C-style casted function works.
When looking at the assembly code generated by the two simple calls to the functions I made in the sample :
twoParamFunc(a: a.0,b: a.1)
singleParamFunc(tuple: a)
We can see that those functions are actually compiled to be called identically :
singleParamFunc
:
mov rdi, qword ptr [rip + (output.a : (Swift.Int, Swift.Float))]
movss xmm0, dword ptr [rip + (output.a : (Swift.Int, Swift.Float))+8]
call (output.singleParamFunc(tuple: (a: Swift.Int, b: Swift.Float)) -> ())
Here we see that the value corresponding to the first value of the tuple is put into register rdi
, and the second one is put into xmm0
, and then the function is called
twoParamFunc
:
mov rax, qword ptr [rip + (output.a : (Swift.Int, Swift.Float))]
movss xmm0, dword ptr [rip + (output.a : (Swift.Int, Swift.Float))+8]
...
mov rdi, rax
...
call (output.twoParamFunc(a: Swift.Int, b: Swift.Float) -> ())
In this function, it is not as straightforward, but now value 1 goes in rax
register which itself is copied into rdi
register, and value 2 still goes in xmm0
, and the function is called.
But in this sample since we are doing other things, the assembly code is a bit messier, I've made another sample to test this cleanly : https://swift.godbolt.org/z/vDCZZV
In this sample (on which I've added another test with a struct) we can see that the assembly code created to called the 3 functions are exactly the same :
mov rdi, qword ptr [rip + (output.structValue : output.struct_test)]
movss xmm0, dword ptr [rip + (output.structValue : output.struct_test)+8]
call (output.test(value: output.struct_test) -> ())
mov rdi, qword ptr [rip + (output.tupleValue : (Swift.Int, Swift.Float))]
movss xmm0, dword ptr [rip + (output.tupleValue : (Swift.Int, Swift.Float))+8]
call (output.test2(tuple: (Swift.Int, Swift.Float)) -> ())
mov ecx, 1
mov edi, ecx
movss xmm0, dword ptr [rip + .LCPI0_0]
call (output.test3(a: Swift.Int, b: Swift.Float) -> ())
To resume, in the current version of swift, any of these three functions could be c-casted into any other and still work.
This ended up being a lot longer than initially planned, but I thought this problem deserved it.