Swift + C: Callback Interoperability

Toby O'Connell
CodeX
Published in
3 min readOct 22, 2021

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:

Most of the variables passed to sqlite3_exec have been left as nil for brevity.

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:

A C function pointer cannot be formed from a closure that captures context

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:

--

--

Toby O'Connell
CodeX
Writer for

Swift / iOS developer - I write about things that I find interesting or innovative.