Davide De Rosa

Cross-platform Swift: Core libraries

Cross-platform Swift: Core libraries

part of a series: < prev | next >

Table of contents (ongoing)

  1. Introduction
  2. Combine
  3. Core libraries

When programming Swift in Xcode, we take for granted a few things that are in fact different when you compile on other platforms. Here I show you those I stumbled upon, and how I worked around some limitations of the bare Swift toolchain.

Foundation

Have you ever compiled Swift code without importing Foundation? Do you even think it’s possible? Look at the most basic Swift template in Xcode:

//
//  File.swift
//  Passepartout
//
//  Created by Davide De Rosa on 5/5/25.
//

import Foundation

which in Objective-C would be:

//
//  File.m
//  Passepartout
//
//  Created by Davide De Rosa on 5/5/25.
//

#import <Foundation/Foundation.h>

Let’s cut it short to the bad news: Foundation is not part of the Swift language.

Foundation is, however, available as an import in the prebuilt Swift toolchains for non-Apple platforms. Why is that? Over the years, those pseudo-implicit imports may have convinced most of us that Foundation has always been a standard library. First, as an Objective-C wrapper of CoreFoundation. Later, as an integral part of Swift.

The maintainers of the otherwise modern Apple language are well aware of this heavy legacy, so they created a replacement for Foundation that is not bound to the Apple SDK. Beware that if you build the Swift toolchain by yourself, Foundation is not included by default. This tells me that maybe, maybe, Foundation will also depart from Swift someday. You were warned.

Below are a few Foundation entities that many developers are familiar with:

  • Data for binary data (bridging ObjC NSData)
  • Date for dates and timestamps (briding ObjC NSDate)
  • UUID for unique identifiers
  • JSONEncoder and JSONDecoder (these are pure Swift)
  • URL, URLSession and tasks (bridigng ObjC NSURL and NSURLSession)
  • FileManager
  • A bunch of other NS-prefixed stuff

The Foundation import is not as a comprehensive as on Apple. This new version has opt-in modules due to the dependencies that they imply:

  • FoundationNetworking for URL and URLSession, due to the dependency on libcurl
  • FoundationXML for XMLParser, due to the dependency on libxml2

Can you get rid of all this stuff already? I doubt you can, unless you started a new project knowing these gotchas beforehand. In my case, I need to keep the burden of this non-standard library, at least until Swift will have a richer standard library.

Last but not least, Foundation comes with the Grand Central Dispatch API, but listen to me: it’s time you learn Concurrency.

Swift standard library

You heard that, Swift has a standard library, but few would be able to delineate its boundaries. The Apple documentation certainly doesn’t help when figuring out what comes from the language, and what comes from Foundation.

A partial list:

  • Primitive types: Bool, Int, String, Double, …, but not Data or Date!
  • Protocols: Comparable, Equatable, Hashable, Identifiable, Codable, …
  • Collections: Array, Dictionary, Set, Collection, …
  • Flow constructs: Result, Error
  • String description protocols: CustomStringConvertible and variations

Thank God, Concurrency is also part of the library.

Platform-specific packages

When developing on macOS, you may notice that standard C functions are implicitly bridged to Swift code. This autocompletion is proof:

C autocompletion of memcpy in Xcode

The other Swift environments behave differently, and honestly, I prefer it that way. I’d rather import things explicitly when needed. Therefore, the compiler will stop at C symbols in Swift code out of the box. Windows in particular doesn’t follow the POSIX naming, and some symbols come from very differently named headers. For example, the first symbol that triggered a compiler failure for me was AF_INET, which on Windows is part of WinSock2.h.

While the header names are relevant in C code, Swift remains a bit more agnostic by hiding them behind platform-specific packages:

  • import WinSDK on Windows
  • import Linux on Linux
  • import Android on Android

The imports still require some #if conditionals here and there. At that point, you may run into a few C symbols that are only available on Apple platforms, like any variation of NSEC_PER_* in my library. In that case, you can redefine those symbols yourself, or refactor the code to not use them at all.

Another thing I noticed is name clashes. To name one, Windows seems to have a name clash on UUID (alias for GUID in Win32), which I resolved this way:

#if os(Windows)
import WinSDK
public typealias UUID = Foundation.UUID
#endif

There might be more similar occurrences, but you can get around them with conditionals and forced typealias. If you know better ways, please leave a comment below the post.

Objective-C runtime

The need for the Objective-C runtime in your Swift code may be incredibly subtle. Look at a linker error I faced:

lld-link: error: undefined symbol: objc_autoreleaseReturnValue

I was lucky to catch the issue in a place where the compiler could not provide any hint. The culprit was a class of the Partout API that was using the JavaScriptCore framework, which in turn required Swift closures to be Objective-C blocks to inject custom functions into the JavaScript code. If you’ve never seen that, here’s a bit of the offending code:

inject("getText", object: vm.getText as @convention(block) (String) -> Any?)

Frankly, I wondered why the code compiled at all, so I tried to put @objc on top of a Swift class on Linux:

File.swift:1:2: error: Objective-C interoperability is disabled

Gosh, then why does @convention(block) compile on non-Apple, if the linker is always bound to fail? I have no idea, but look at your code carefully if it compiles fine but the linker complains about some Objective-C runtime or obscure NS* entity.

Fun fact: JavaScriptCore is an Objective-C framework with iOS 16 as the minimum target, which deceived me into seeing it as a “new thing”.

Bottom line

Stick with pure Swift as much as you can, and when you use Foundation, do it with a grain of salt: the Swift standard library is much smaller than you think. Spot any Objective-C sorcery, and kill it for good.

In the next article, I will show you how conditionals and dependency injection allows us to still use frameworks that are not available on all platforms.