Swift + C: Callback Interoperability
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
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.
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,
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.
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: