DEV Community

Cover image for Getting started with Swift-C++ interop
Konstantin Semianov
Konstantin Semianov

Posted on

Getting started with Swift-C++ interop

Photo by Pablo García Saldaña on Unsplash.

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
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
// 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;
}
Enter fullscreen mode Exit fullscreen mode

Project structure

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"
Enter fullscreen mode Exit fullscreen mode

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"])]
        )
    ]
)
Enter fullscreen mode Exit fullscreen mode

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() }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

Metrics

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

Top comments (0)