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.swift
code.
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