Getting started with Swift-C++ interop
Playing around with Swift toolchain experimental features.
Intro
Swift is a very comfortable language. It has some quirks and a learning curve, but ultimately you can ship production-ready code with it pretty fast. However, sometimes you have performance-critical sections and Swift just doesn't cut it. In such cases, a popular choice is using C++. And the question arises "how do I call this C++ func from Swift"? Usually, you have to write an Objective-C wrapper that will act as a public interface for your C++ code. And Swift toolchain can import Objective-C declarations to Swift. The main limitation is that you cannot use C++ classes in Objective-C, only simple POD structs.
We'll write a Sieve of Eratosthenes algorithm with both C++ and Swift. Then learn how to enable C++ interop, call C++ code from Swift, and compare implementation performance. Keep in mind that the feature is experimental and subject to changes. This publication compiles on Xcode Version 14.2
Algorithm
The Sieve of Eratosthenes finds all prime numbers less than or equal N. Prime number is an integer that's divisible only by itself and 1. The algorithm creates a boolean array to indicate if each number is prime. And progressively iterates over them, marking all multiples as not prime.
Here is the Swift implementation.
// primes.swift
func primes(n: Int) -> [Int] {
var isPrime = [Bool](repeating: true, count: n + 1)
for value in stride(from: 2, to: n + 1, by: 1) where isPrime[value] {
if value * value > n { break }
for multiple in stride(from: value * 2, to: n + 1, by: value) {
isPrime[multiple] = false
}
}
var result = [Int]()
for value in stride(from: 2, to: n + 1, by: 1) where isPrime[value] {
result.append(value)
}
return result
}
For C++ we need a header and a source file. Note, that we typedef
to have a cleaner name for referring to std::vector<long>
.
// primes.hpp
#include <vector>
typedef std::vector<long> VectorLong;
VectorLong primes(const long &n);
// primes.cpp
#include <algorithm>
#include "primes.hpp"
VectorLong primes(const long &n) {
std::vector<char> isPrime(n + 1); // faster than std::vector<bool>
std::fill(isPrime.begin(), isPrime.end(), true);
for (long value = 2; value * value <= n; ++value) {
if (!isPrime[value]) { continue; }
for (long multiple = value * 2; multiple <= n; multiple += value) {
isPrime[multiple] = false;
}
}
VectorLong result;
for (long value = 2; value <= n; ++value) {
if (!isPrime[value]) { continue; }
result.push_back(value);
}
return result;
}
Project structure
We'll do a Swift Package with two separate targets to hold our Swift and C++ code. To import C++ code from Swift, we need a modulemap.
// module.modulemap
module CXX {
header "CXX.hpp"
requires cplusplus
}
// CXX.hpp
#include "primes.hpp"
And do not forget to pass -enable-experimental-cxx-interop
to the Swift target in Package.swift
.
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "SwiftCXXInteropExample",
platforms: [
.macOS(.v12),
],
products: [
.library(name: "CXX", targets: ["CXX"]),
.executable(name: "CLI", targets: ["CLI"])
],
dependencies: [],
targets: [
.target(name: "CXX"),
.executableTarget(
name: "CLI",
dependencies: ["CXX"],
swiftSettings: [.unsafeFlags(["-enable-experimental-cxx-interop"])]
)
]
)
See the doc from Apple for more info on how to enable C++ interop.
Trying out
It's much easier to use our VectorLong
from Swift with conformance to RandomAccessCollection
and, happily, it's really easy to do.
import CXX
extension VectorLong: RandomAccessCollection {
public var startIndex: Int { 0 }
public var endIndex: Int { size() }
}
Now we can call our C++ function from Swift and print the results to the console.
let cxxVector = primes(100)
let swiftArray = [Int](cxxVector)
print(swiftArray)
Let's see if our C++ implementation actually performs faster.
let signposter = OSSignposter()
let count = 100
let n = 10_000_000
for _ in 0..<count {
let state = signposter.beginInterval("C++")
let _ = primes(n)
signposter.endInterval("C++", state)
}
for _ in 0..<count {
let state = signposter.beginInterval("Swift")
let _ = primes(n: n)
signposter.endInterval("Swift", state)
}
Slightly faster with an average duration of 26 ms vs. 28 ms for Swift.
Final thoughts
We were able to directly use std::vector
in Swift. I personally haven't found a convenient way to round-trip between Swift Map
and std::map
, Set
and std::set
. Nevertheless, C++ interop is rapidly developing and the future seems bright.
CppInteroperability folder in the Swift repo contains more information on interop features, limitations and plans.
See the full code at https://github.com/ksemianov/SwiftCXXInteropExample