Integrating Rust and SwiftUI (Inspired by Mitchell Hashimoto)
Inspired by Mitchell Hashimoto in his flight plan library written in Zig, I created a diving plan library in Rust. And again, following his work in Ghostty and his article about how he created the native Mac App for Ghostty, I decided to explore the same approach in my diving plan library and following his article on how he created a native macOS app, but having the Rust code library as its backend instead of Zig. in this article, I’ll explain how I did it:
Mitchell’s article already explains the approach well, so I recommend reading it first. While his article is about Zig, the approach in this article is the same but for Rust. The high-level idea is the same, but the details differ since the C-ABI and the build system differ.
In this post, I’ll walk you through how I built a native macOS application that leverages the power of Swift for the GUI and Rust for the backend processing. This architecture combines Swift’s excellent UI capabilities with Rust’s performance and safety guarantees.
The project consists of two main components:
- A GUI layer is written in Swift using SwiftUI
- A backend library written in Rust with FFI bindings
While investigating this, most of the articles I found did not use Swift Package Manager but Xcode, which is entirely different, and from my perspective, Xcode could be better. It adds a lot of complexity, it could have a better developer experience (talking for myself, I’m not familiar with the Apple dev ecosystem), and it is hard to track all the configuration through code. So, I did not want to depend on Xcode.
The approach posted in this article is the result of hours of Googling, asking Claude, and trial and error. Before starting, a few notes:
I’m not using the Open Water library here. I’m just testing the connection between Swift and Rust.
* I'm not an expert in Rust, Swift, or the Apple ecosystem; the approach exposed here was the way that I found to have a functional app. Sure, there are better ways to do it; if you have any idea, please let me know or feel free to open a pull request to Open Water.
* I'm using Swift Package Manager. I'm not creating the project using Xcode, which would change the structure a lot.
* I'm exposing a single function to keep it simple for the article. In future articles, I'll add more complex data structures.
* I'm using Brew as its system library. This opens up a lot of questions that I'm still exploring.
* Is the pkg approach the best way to link the Rust library? I don't know; I'm exploring it.
* This is not a copy-pasting approach; I'm explaining the steps I followed for Open Water, but your project might differ. The intention is to give you a starting point.
* In Mitchell's article, he exposes many details that I'm not covering here, nor did I do for Open Water. For example, the usage of Lipo for Universal (Multi-Arch) Library or advanced window management.
* I still need to determine if a static or dynamic library is best.
Ok, let’s work!
Creating the project
swift package init
cargo new app
Project structure
The above commands should create a boilerplate. My project structure in Open Water is the following. Ignore for now the api
and cli
folders, they are part of Open Water but not part of this article.
├── Cargo.toml
├── api
│ └── main.rs
├── cli
│ ├── actions.rs
│ ├── args.rs
│ └── main.rs
├── gui
│ ├── Package.swift
│ ├── Sources
│ │ ├── OpenWaterBridge
│ │ │ └── OpenWater.swift
│ │ ├── OpenWaterCore
│ │ │ ├── module.modulemap
│ │ │ └── openwater.h
│ │ └── OpenWaterGUI
│ │ └── main.swift
│ └── openwater.pc
└── lib
├── errors.rs
├── ffi.rs
├── lib.rs
├── models.rs
└── parser.rs
Cargo.toml
For this project, I’m using a dynamic library. I’m not pasting the entire Cargo file of Open Water because I have different binaries defined there that are not relevant to this article.
Writing the C API
This is our Rust code exposed to Swift. The #[no_mangle]
and extern "C"
attributes make it callable from C (and thus Swift).
Rust has excellent support for creating C-compatible interfaces, which allows Rust code to be called from other languages like C and Swift. This is done through what’s called the “Foreign Function Interface” (FFI). You can learn more in the official documentation here.
use std::ffi::CString;
use std::os::raw::c_char;
#[no_mangle]
pub extern "C" fn openwater_init() -> *const c_char {
let message = "Hello Diego from Rust!";
let c_str = CString::new(message).unwrap();
c_str.into_raw() as *const c_char
}
Writing the header file
This header file declares what Rust functions are available to Swift.
#ifndef openwater_core_h
#define openwater_core_h
#include <stdint.h>
#if defined(WIN32)
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif
EXPORT const char* openwater_init(void);
Module map
A module.modulemap
file says how and what to import and use our C interface.
module OpenWaterCore {
umbrella header "openwater.h"
link "openwater"
export *
}
The bridge
This is our safety layer between Swift and Rust. It converts Rust’s C-style strings into Swift strings and provides a clean API for our UI to use.
import OpenWaterCore
import Foundation
public enum OpenWaterBridge {
public static func openWaterInit() -> String {
guard let cString = openwater_init() else {
return "Failed to init OpenWater"
}
let result = String(cString: cString)
return result
}
}
The SwiftUI app
This is our application’s entry point and UI definition. It imports our bridge (OpenWaterBridge). I’m creating a simple a window with a button that prints the string that I have set in the FFI code. It’s what users will see and interact with. For now, I’m not using the Open Water library here. I’m just testing the connection between Swift and Rust.
import SwiftUI
import OpenWaterBridge
import AppKit
@main
struct OpenWaterApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
.windowResizability(.contentSize)
.defaultSize(width: 800, height: 600)
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
}
}
struct ContentView: View {
@State private var isHovering = false
var body: some View {
VStack {
Text("OpenWater Dive Log")
.font(.title)
.padding()
Button("Parse UDDF") {
print(OpenWaterBridge.openWaterInit())
}
.buttonStyle(.bordered)
.onHover { hovering in
isHovering = hovering
}
}
.frame(minWidth: 400, minHeight: 300)
.padding()
}
}
Package configuration
This is our project’s build configuration file. It defines three essential layers:
- The GUI executable that users will interact with
- A bridge layer to safely communicate with Rust
- A system library configuration to link our Rust code
// swift-tools-version:5.8
import PackageDescription
let package = Package(
name: "OpenWaterGUI",
platforms: [.macOS(.v13)],
products: [
.executable(name: "OpenWaterGUI", targets: ["OpenWaterGUI"])
],
targets: [
.executableTarget(
name: "OpenWaterGUI",
dependencies: ["OpenWaterBridge"],
path: "Sources/OpenWaterGUI"
),
.target(
name: "OpenWaterBridge",
dependencies: ["OpenWaterCore"],
path: "Sources/OpenWaterBridge"
),
.systemLibrary(
name: "OpenWaterCore",
path: "Sources/OpenWaterCore",
pkgConfig: "openwater",
providers: [
.brew(["openwater"])
]
)
]
)
pkg-config file: Linking Everything Together
The pkg-config configuration file plays a crucial role. It tells the build system how to find and link our Rust library. This file works in conjunction with our Package.swift configuration (specifically with the systemLibrary target), specifically in this line.
prefix=/usr/local
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include
Name: openwater
Description: OpenWater library
Version: 0.1.0
Libs: -L${libdir} -lopenwater
Cflags: -I${includedir}
Building the project
This was the part that took me the most time to figure out. I had to create a custom build system in order to be able to run the SwiftUI app.
We have to copy the dynamic library to the system path, the header file to the include path and the pkg-config file to the pkg-config path. I created a Makefile to automate this process:
run-app: ## Compile and run the GUI
@echo "Running GUI..."
cd src && \
cargo build --lib --release && \
cd .. && \
cp target/release/libopenwater.dylib /usr/local/lib/ && \
cd src/gui && \
cp Sources/OpenWaterCore/openwater.h /usr/local/include/ && \
cp openwater.pc /usr/local/lib/pkgconfig/ && \
swift build && \
swift run
How It All Works Together
Package.swift
file tells Swift how to build everythingmodule.modulemap
tells Swift what to import and use our C interfacemain.swift
creates our UI and calls functions through the bridgeOpenWater.swift
safely converts data between Swift and Rustffi.rs
contains our actual Rust functionalityopenwater.h
ensures everything can communicate properly
The result
Finally…
Open Water is not intended to be a full dive log, it’s just my hobby project to learn Rust and systems performance. So, for now, I’m happy with the results of integrating Rust with a native macOS app. Still, I have many questions about the best practices for this approach. In the future, I’d like to explore:
- Expose more complex data structures sending real UDDF logs information from Rust to Swift and render graphics as air consumption and maps.
- Windows isolation using cgroups to learn how to share the CPU and memory between processes.
Thanks for reading. If you have any idea or thoughts about this article, please let me know or feel free to open a pull request to Open Water.
🐋 🌊 🤿