Swift and C++ interoperability in practice
Swift’s new C++ interoperability is a game-changer, letting you tap into mature C++ libraries from Swift’s safe, expressive syntax. In this post, I’ll guide you through the language-pair preview introduced in Swift 5.9—showing how to call C++ functions, wrap C++ classes, and shuttle complex data types across the language divide. Along the way, we’ll demystify the build setup—module maps, header exposure, and SwiftPM/Xcode tweaks—and cover best practices for memory management and error handling. By the end, you’ll have a clear, hands-on roadmap for blending Swift’s developer-friendly design with C++’s performance muscle.
Project structure
.
├── ATMProj
│ ├── ATMProj
│ │ ├── Assets.xcassets
│ │ ├── ATMProjApp.swift
│ │ ├── ATMViewModel.swift
│ │ ├── ContentView.swift
│ └── ATMProj.xcodeproj
└── cpp
├── CMakeLists.txt
├── Makefile
├── Package.swift
└── Sources
├── ATMCLI
│ └── main.swift
├── ATMPackage
│ └── ATMWrapper.swift
└── ATMWithdrawCpp
├── include
│ └── ATMWithdrawCpp
│ ├── ATM.h
│ └── ATMWithdrawCpp.h
└── src
└── ATM.cpp
The repo is split into two top-level directories: ATMProj
, which holds the SwiftUI application (your app entry point, view model, UI files and assets), and cpp
, which contains the C++ core along with build scripts and Swift bridging code. The cpp
folder—complete with CMakeLists.txt
, Makefile
, Package.swift
and a Sources
tree for your CLI target, Swift wrapper and native C++ implementation—is crucial both for driving the C++ build via CMake and for producing the module map that lets Swift import and interact with your C++ code. This clear separation keeps your Swift app and cross-language plumbing organized and easy to maintain.
Prerequisites
Ensure you have Xcode installed (with the iOS/macOS toolchains) and CMake (e.g., via Homebrew with brew install cmake
) set up before building and running the project.
C++
First things first, C++!
#pragma once
class ATM {
public:
ATM(int initialBalance);
bool withdraw(int amount);
int getBalance() const;
private:
int balance;
};
ATM.h Defines the ATM
class interface with an initializer, withdraw
method, and getBalance
accessor.
ATM.cpp
#include "ATMWithdrawCpp/ATM.h"
ATM::ATM(int initialBalance) : balance(initialBalance) {}
bool ATM::withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
return true;
}
return false;
}
int ATM::getBalance() const {
return balance;
}
ATM.cpp Implements the ATM
logic by setting the initial balance, enforcing withdrawal checks (only deducting when funds are sufficient), and returning the current balance.
ATMWithdrawCpp.h
#pragma once
#include "ATM.h"
ATMWithdrawCpp.h Provides a thin header that re-exports ATM.h
for module map generation, allowing Swift to import and interact with the C++ class definitions.
CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(ATMWithdrawCpp LANGUAGES CXX)
# Locate the C++ library sources relative to project root
add_library(ATMWithdrawCpp STATIC
${CMAKE_CURRENT_SOURCE_DIR}/Sources/ATMWithdrawCpp/src/ATM.cpp
)
target_include_directories(ATMWithdrawCpp PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/Sources/ATMWithdrawCpp/include
)
set_target_properties(ATMWithdrawCpp PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
)
CMakeLists.txt Configures the CMake project for ATMWithdrawCpp
, specifying the minimum CMake version, project name, and C++ language. It adds ATM.cpp
as a static library target, sets the public include directories for headers, and enforces the use of the C++17 standard.
Makefile
# Variables
BUILD_DIR = build
CMAKE = cmake
CMAKE_BUILD_TYPE = Release
SPM = swift build --configuration release
SPM_RUN = swift run ATMCLI
.PHONY: all build-cpp build-swift run clean
all: build-cpp build-swift run
build-cpp:
@mkdir -p $(BUILD_DIR)
cd $(BUILD_DIR) && $(CMAKE) -DCMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) ..
cd $(BUILD_DIR) && $(CMAKE) --build . --config $(CMAKE_BUILD_TYPE)
build-swift:
$(SPM)
run:
$(SPM_RUN)
clean:
rm -rf $(BUILD_DIR)
swift package clean
Makefile Automates both C++ and Swift workflows with Make targets. It defines variables for the build directory, CMake settings, and SwiftPM commands, then declares phony targets (all
, build-cpp
, build-swift
, run
, and clean
) to orchestrate creating the build directory, configuring and building the C++ static library, invoking Swift package build and run, and cleaning artifacts. The Swift build/run steps are included upfront but will be described in detail later.
Swift
Now we will focus on making Swift part of our small library.
ATMWrapper.swift
import ATMWithdrawCpp
public struct ATMWrapper {
private var underlying: ATM
public init(initialBalance: Int32) {
underlying = ATM(initialBalance)
}
public mutating func withdraw(amount: Int32) -> Bool {
return underlying.withdraw(amount)
}
public func getBalance() -> Int32 {
return underlying.getBalance()
}
}
ATMWrapper.swift Defines the Swift-side ATMWrapper
struct that imports the ATMWithdrawCpp
module. It initializes an underlying C++ ATM
instance, exposes a withdraw(amount:)
method returning a Boolean success flag, and provides a getBalance()
accessor to retrieve the current balance from C++.
main.swift
import ATMPackage
import Foundation
print("🏧 ATM CLI 🏧")
var atm = ATMWrapper(initialBalance: 1000)
print("Current balance: \(atm.getBalance())")
print("Enter amount to withdraw:", terminator: " ")
if let input = readLine(), let amount: Int32 = Int32(input) {
if atm.withdraw(amount: amount) {
print("✅ Withdrawn \(amount). New balance: \(atm.getBalance())")
} else {
print("❌ Insufficient funds.")
}
} else {
print("Invalid input.")
}
main.swift
Implements the command-line interface by importing ATMPackage
and Foundation
, printing an interactive ATM banner, instantiating ATMWrapper
with an initial balance, displaying the current balance, reading user input for withdrawal amount, invoking the withdraw
method, and printing success or error messages accordingly.
Package.swift
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "ATMProject",
platforms: [.iOS(.v17), .macOS(.v14)],
products: [
.library(name: "ATMPackage", targets: ["ATMPackage"]),
.executable(name: "ATMCLI", targets: ["ATMCLI"])
],
targets: [
// C++ library target
.target(
name: "ATMWithdrawCpp",
path: "Sources/ATMWithdrawCpp",
publicHeadersPath: "include",
cxxSettings: [
.headerSearchPath("include"),
.define("SWIFT_PACKAGE")
]
),
// Swift wrapper target
.target(
name: "ATMPackage",
dependencies: ["ATMWithdrawCpp"],
path: "Sources/ATMPackage",
swiftSettings: [
// enable C++ interop
.interoperabilityMode(.Cxx)
]
),
// CLI executable target
.executableTarget(
name: "ATMCLI",
dependencies: ["ATMPackage"],
path: "Sources/ATMCLI",
swiftSettings: [
.interoperabilityMode(.Cxx)
]
)
]
)
Package.swift
Defines the Swift package manifest for ATMProject
, declaring the package name, supported platforms, and products (the ATMPackage
library and ATMCLI
executable). It configures four targets:
ATMWithdrawCpp
: the C++ library target withpublicHeadersPath
andcxxSettings
that specify header search paths and defineSWIFT_PACKAGE
for module map generation.ATMPackage
andATMCLI
: the Swift wrapper and CLI targets, both depending (directly or indirectly) onATMWithdrawCpp
and includingswiftSettings
with.interoperabilityMode(.Cxx)
to enable C++ interoperability and ensure Swift can import the generated C++ module map. This flag must be added to every target that uses C++ code, whether directly or via another target.
Building and Running the CLI
Use the build-swift
make target (or run swift build --configuration release
) to compile the Swift package—which will also build the linked C++ core—and then execute swift run ATMCLI
to launch the interactive ATM command-line interface.
Xcode project
Create a new Xcode project (e.g., App or Command Line Tool template) and add your local Swift package via File > Swift Packages > Add Package Dependency by pointing to the cpp/Package.swift
directory. Link the ATMPackage
(and ATMCLI
if applicable) into your app or tool target. Attempt to build—the compiler will error out because C++ interoperability isn’t enabled by default. To resolve this, open your target’s Build Settings, find Other Swift Flags under Swift Compiler - Custom Flags, and add:-cxx-interoperability-mode=default
After adding this flag, rebuild and Xcode will correctly import and compile against your C++ module.
ATMViewModel.swift
import Foundation
import ATMPackage
class ATMViewModel: ObservableObject {
@Published var balance: Int32
private var atm: ATMWrapper
init(atm: ATMWrapper = ATMWrapper(initialBalance: 1000)) {
self.atm = atm
self.balance = atm.getBalance()
}
func withdraw(amount: String) {
guard let value = Int32(amount), atm.withdraw(amount: value) else { return }
balance = atm.getBalance()
}
}
Defines the SwiftUI ObservableObject
view model that wraps ATMWrapper
to manage account balance state. It initializes with an ATMWrapper
, publishes the current balance, and provides a withdraw(amount:)
method that attempts to withdraw a string-converted amount, updating the published balance on success.
ContentView.swift
import SwiftUI
struct ContentView: View {
@State private var input = ""
@ObservedObject var viewModel: ATMViewModel
var body: some View {
VStack(spacing: 20) {
Text("🏧 ATM Machine 🏧").font(.largeTitle)
Text("Balance: \(viewModel.balance)").font(.title2)
TextField("Amount", text: $input)
.keyboardType(.numberPad)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Button("Withdraw") {
viewModel.withdraw(amount: input)
input = ""
}
.buttonStyle(.borderedProminent)
Spacer()
}
.padding()
}
}
Defines the SwiftUI View
that binds to ATMViewModel
with an @State
input for withdrawal amounts. It displays the ATM title and current balance, provides a number-pad text field for user input, and includes a "Withdraw" button styled prominently. When tapped, the button calls viewModel.withdraw(amount:)
and clears the input field, reflecting updated balance state in the UI.
Conclusions
The C++ interoperability showcased here demonstrates how even a simple CLI can leverage high-performance C++ logic directly from Swift, making it easy to build cross‑platform tools with a familiar Swift interface. Integrating into an iOS SwiftUI app is equally straightforward—once the interoperability flags are set, your C++ core becomes a shared library that seamlessly updates UI state. This approach unlocks the possibility of a truly common library layer: write and test your ATM logic once in C++, then reuse it not only on Apple platforms (iOS, macOS) but also on Linux, Windows, or even in embedded systems. With Swift’s growing C++ interop support and standard tooling like SwiftPM and CMake, you can maintain one robust codebase for diverse targets, reducing duplication and accelerating development across all environments.