Diego Lavalle – Swift and Apple Platforms Development

Nov 11, 2021 • Server-Side Swift, Swift Concurrency

URLSession Concurrency on Linux

DL

Being in the situation of having to write Swift code that runs on both Mac and Linux makes you realize some of the subtle differences between these two.

For instance you can use URLSession on the open source toolchain but you need to import FoundationNetworking instead of just Foundation for it to work.

import Foundation

#if canImport(FoundationNetworking)
    import FoundationNetworking
#endif

// Do our thing with URLSession and friends…

Now when if comes to language features both implementations of the tool chain should be at par. This includes the new concurrency constructs introduced in Swift 5.5, meaning we can use async, await and the rest of them.

I decided to test this assertion by writing a tool that fetches comments from the GitHub discussions API which will eventually be rendered under blog posts like this one.

func fetchComments() async -> [Discussion] {
    // Some actual implementation of this function…
}

Effectively the function from the snippet above successfully compiles on both systems without warnings.

Unfortunately this does not extend to the framework's additions which take advantage of the new concurrency features. Take for instance the asynchronous function URLSession.data which on macOS just works.

guard let (data, _) = try? await URLSession.shared.data(for: request) else {
    // Some network error here
}
…

The same call on Linux will give us a compiler error since that exact version of the function does not exist yet.

While I couldn't find any DocC-style documentation for the open source version of Foundation, I was able to verify the status of the API on this status page where it states that URLSession if mostly complete but that getting tasks and other functions remain unimplemented. This can ultimately be verified in the source code.

So all of this means we have an opportunity to try a different concurrency feature called continuations and combine it with the soon-to-be legacy data task function.

import FoundationNetworking

func fetchComments() async -> [Discussion] {
    …
    let data: Data = await withCheckedContinuation { continuation in
        URLSession.shared.dataTask(with: request) { data, _, _ in
            guard let data = data else {
                fatalError()
            }
            continuation.resume(returning: data)
        }.resume()
    }
    …
}

We can even keep both approaches and apply them conditionally based on our target.

import Foundation

#if canImport(FoundationNetworking)
    import FoundationNetworking
#endif

func fetchComments() async -> [Discussion] {
    …
    #if canImport(FoundationNetworking)
        let data: Data = await withCheckedContinuation { … }
    #else
        guard let response = try? await URLSession.shared.data(for: request) else {… }
        …
    #endif
    …
}

The full implementation of the getComments function is part of my Website Data project and can be found here.