Swift + C: Callback Interoperability
Background
A very common pattern across programming languages is the use of callback functions. A callback function is a reference to executable code passed as an argument to other code, allowing it to be executed at a later time.
In swift we encounter this patten often. One particularly common example is URLSession.dataTask(with:completionHandler:)
. After a request has been made, the network may take some time to respond. Rather than wait idly, we can give a completion handler to be executed when the response is received allowing the application to proceed in the meantime.
One place we see this pattern in C is in the SQLite3
framework packaged with Swift. SQLite3
facilitates access and manipulation of Structured Query Language databases and is the underlying technology powering CoreData
.
As databases can take time to search or alter, SQLite3
provides the function sqlite3_exec
with takes a C style callback. When this function is bridged across to Swift the @convention(c)
annotation is applied. This annotation denotes that the callback is going to be used in a C context and must follow certain criteria. One of these criteria is that the function cannot capture any data. This poses a problem as we are likely to want to pass the callback arguments onto some other object that exists outside of the callback - we may want pass database information to a view layer example.
Solution
One way many C APIs work around this limitation is by passing an additional context parameter. This allows a reference to some object to be passed through to the callback in addition to the arguments that are returned later. We can then pass the other arguments to the object so they can be transfered outside of the callback scope.
Assume the following setup:
Here we can see that the Callback
typealias is marked with the aforementioned @convention(c)
annotation. If we modify the code to capture an object within the scope of callback
we see the following error:
The way we can work around this is by passing our object as the context argument like so:
Here we create an Unmanaged
object which we are expected to manage the lifetime of manually. We convert this to an UnsafeMutableRawPointer
so it can be passed into sqlite3_exec
as context data. The context data is then passed into our callback where is can be converted back into our Object
type ready for use.
passRetained
creates a reference keeping the object alive indefinitely until takeRetainedValue
is called. At that point, the retain is consumed and the object can be deallocated. If the object was guaranteed to still be alive at the time the callback is executed, passUnretained
and takeUnretainedValue
could be used, but be aware that if the object goes out of scope, this would result in a crash very similar to how unowned
variables work.
Bonus Material
Ideally, we’d like to extend this pattern so we are not reliant on any one object to handle the response of the C API. One way of achieving this is to pass a Swift closure as the context argument. The following repo has an example of that pattern for those who are interested: