One of the big benefits of using a lower level language like Rust is that you can compile it into a C library. This allows you to call your code in almost every other language. Swift is one of those languages that can take advantage of this capability. Using the swift-bridge crate, we can have everything that we need generated for us.

Final directory structure

I find it very useful for me personally when people that write tutorials show you what the final result should be and so with this being my first blog post, here is the final directory structure of what the project will look like when we are done.

rust-swift ➜ tree -L 3
.
├── bridging-header.h
├── build.sh
├── generated
│   ├── SwiftBridgeCore.h
│   ├── SwiftBridgeCore.swift
│   └── rust
│       ├── rust.h
│       └── rust.swift
├── main
├── rust
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── build.rs
│   ├── src
│   │   └── lib.rs
│   └── target
│       ├── CACHEDIR.TAG
│       ├── aarch64-apple-darwin
│       ├── debug
│       └── x86_64-apple-darwin
└── swift
    ├── Package.swift
    └── Sources
        ├── lib.swift
        └── main.swift

11 directories, 15 files

Project Setup

Let’s start by creating a parent directory for both of our projects, rust and swift. Then we can cd into that directory.

mkdir rust-swift
cd rust-swift

Creating our Swift Project

Let’s create a new swift project.

mkdir swift
cd swift
swift package init --type executable

Now we need to go ahead and edit a few swift files so that we can call the Rust code that we will be creating in the next section.

vim Sources/main.swift

Here you can either delete all of the contents or you can go ahead and add the following to anywhere in the file:

run()

Now lets create a lib.swift file so that we can store our library calls in a separate file from our main.swiftcode.

cat > Sources/lib.swift << EOF
func run() {
    print_hello_rust()
}
EOF

Here we are just creating the wrapper in Swift that will call the Rust function print_hello_rust.

Creating our Rust Project

Let’s initialize a new Rust project. So first, let’s navigate back up to the parent directory for these 2 projects cd ...

cargo new rust --lib
cd rust

Next, we need to make the Rust code compile into a static library and add the swift-bridge dependencies. We need to edit the Cargo.toml and remove the existing dependencies line.

vim rust/Cargo.toml
[lib]
crate-type = ["staticlib"]

[build-dependencies]
swift-bridge-build = "0.1"

[dependencies]
swift-bridge = "0.1"

The final Cargo.toml file should look like the following:

[package]
name = "rust"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["staticlib"]

[build-dependencies]
swift-bridge-build = "0.1"

[dependencies]
swift-bridge = "0.1"

Create the library

Here we finally add some Rust code to actually do something. Let’s edit rust/src/lib.rs with the following contents.

vim rust/src/lib.rs
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        fn print_hello_rust();
    } 
}

fn print_hello_rust() {
    println!("Hello from Rust!");
}

Here we are simply defining the print_hello_rust function to be exposed to Swift using the swift_bridge macro.

Creating the Swift glue

Here is the where the magic happens on the Swift part of the project. Here we will create the header file that bridges the 2 languages together. In the parent directory, we will create a new file called bridging-header.h that will include the generated files that swift-bridge will create for us.

cat > bridging-header.h << EOF
#ifndef BridgingHeader_h
#define BridgingHeader_h

#include "./generated/SwiftBridgeCore.h"
#include "./generated/rust/rust.h"

#endif /* BridgingHeader_h */
EOF

Ensure that you create the generated directory in the parent directory.

mkdir generated

Building our work

We’re finally almost at the end, the last thing standing between us and calling Rust code in Swift is just compiling everything. Navigate to the parent directory, rust-swift, let’s create a new file within the rust project called build.rs with the following contents:

cat > rust/build.rs << EOF
use std::path::PathBuf;

fn main() {
    let out_dir = PathBuf::from("../generated");
    let bridges = vec!["src/lib.rs"];
    for path in &bridges {
        println!("cargo:rerun-if-changed={}", path);
    }
    
    swift_bridge_build::parse_bridges(bridges)
        .write_all_concatenated(out_dir, env!("CARGO_PKG_NAME"));
}
EOF

Now we need to make a build script so that both projects are built at the same time.

cat > build.sh << EOF

#!/bin/bash
set -e

export SWIFT_BRIDGE_OUT_DIR="$(pwd)/generated"

cargo build --manifest-path rust/Cargo.toml --target aarch64-apple-darwin

swiftc -L rust/target/aarch64-apple-darwin/debug -lrust -import-objc-header bridging-header.h \
    `find swift/Sources/swift -name *.swift` \
    ./generated/rust/rust.swift
EOF
chmod +x build.sh

Notice the target argument. If you are using an Apple Silicon Mac, you need to need to make sure that you are using the same architecture to build otherwise you will need to install any foreign architectures that you want to target. For the Apple silicon Macs, you will use aarch64-apple-darwin and for x86 Macs, you will use x86_64-apple-darwin.

All the hard work is now done, let’s go ahead and run the build!

./build.sh

If everything goes well, we should have a main file in the parent directory that we can execute to see our Rust code running from within Swift.

./main

You should see the output:

Hello from Rust!

Congrats, now you can use this knowledge and use your Rust libraries within SwiftUI to have a native MacOS GUI while having your backend be Rust. Do note that you could use something like Cacao to use Rust for the GUI as well. It uses the “deprecated” Appkit so it might not always be the best option.

References

https://chinedufn.github.io/swift-bridge/building/swiftc-and-cargo/index.html https://gist.github.com/Jomy10/a4873dd43942ed1bf54d387dbc888795