2021-04-28 12:00:00
Llama Preview: Swift Closures and Delegates
A new preview release of SourceGear.Llama.Swift.Sdk is now available on NuGet. It's an MSBuild project SDK for .NET 5 that allows compiling Swift, with support for calling .NET class libraries. See my previous blog entry for more background.
This release:
is updated for Swift 5.4, which was released a couple days ago
contains a bunch of bug fixes
adds support for interop between Swift closures and .NET delegates
Reminder: Llama is at the "proof of concept" stage, and is not production ready.
More on Closures and Delegates
Consider the generic delegate type System.Func<T,TResult>
.
The Llama binding generator creates a Swift class named System.Func_2<T,TResult>
, as a wrapper for its .NET counterpart.
Right away we
encounter the one thing here that I find a little bit sad:
To match C#, I wish the name Func_2
could be just Func
.
But Swift won't allow us to define multiple types with the same name but varying number of type parameters, like Func<T1>
and Func<T1,T2>
and Func<T1,T2,T3>
and so on.
Actually, under the hood, .NET isn't really doing this either, as the real names for these types have a suffix containing a backtick plus the number of type params, but C# hides the System.Func`2
name from us. Anyway, when this kind of conflicting name situation happens, Llama gives them names like Func_2
.
The Func_2
class contains an initializer that accepts a closure, so we can create one of these delegates like this:
let d = try! System.Func_2({ (x : Int64) in x * 4});
The Swift closure itself is the expression { (x : Int64) in x * 4}
, a simple function that multiples its argument by 4.
The resulting .NET delegate is a wrapper that will call the closure when the delegate is invoked. Under the hood, this is accomplished by converting the closure to a function pointer and using the CIL instruction calli
when Invoke()
happens.
I am rather fond of the fact that Swift can figure out the generic types in this case, but specifying the types explicitly would have been fine too:
let d = try! System.Func_2<Int64,Int64>(callback: { (x : Int64) in x * 4});
So now that we have one of these delegates, we can just call it:
let x = try d(13); try! System.Console.WriteLine(x);
This bit of magic is implemented using the callAsFunction
feature of Swift, which was added in version 5.2 of the language. The Llama binding generator exposes the delegate's Invoke()
method with the name callAsFunction
, and it Just Works.
Which broadly describes why I find Swift to be such a pleasant language -- so many things Just Work.