Rust FFI with Swift
About
I've been exploring how to build a frontend for my Rust program.
Initially, I considered a client/server architecture, but that design feels a little odd for a desktop application.
Instead, I entertained the idea of turning my Rust program a library. This way I could continue maintaining a CLI while having also providing a GUI.
In many ways, this feels reminiscent of a client/server model, but with the "server" invoked on demand as a plain function call.
While searching for FFI libraries, I settled on the uniffi-rs project because it was backed by Mozilla.
The Journey
The exported rust function
The uniffi-rs project has tutorial section where it uses an add function for
demonstration purposes.
I was ambitious and wanted try some functions with side effects (but not ambitious enough to handle errors.. yet).
An ls implementation would prove that the library can make system calls and
also return heap-like objects that Swift can use:
uniffi::setup_scaffolding!();
use std::{fs::DirEntry, path::PathBuf};
#[uniffi::export]
pub fn ls(path: String) -> Vec<String> {
let to_path = |de: DirEntry| de.path();
let to_string_try = |pb: PathBuf| pb.into_os_string().into_string();
match std::fs::read_dir(path) {
Ok(dir) => dir
.into_iter()
.filter_map(Result::ok)
.map(to_path)
.map(to_string_try)
.filter_map(Result::ok)
.collect(),
Err(_) => vec!["🙃".to_string()],
}
}The package configuration
The cargo package should be of library type, and I'd like the package name to reflect the compiled library. Consequently, I'm initialize the package like this:
cargo init --lib --name foobar
The library should be also compile dynamically so we'll configure the lib
target in Cargo.toml:
[lib]
crate-type = ["cdylib"]
name = "foobar"We'll also need the uniffi package as dependencies:
cargo add uniffi --features cli # XXX: the cli feature is used by 'uniffi-bindgen.rs' in the next sectionCompiling
When we compile the library, we'll also want to compile the bindings used for FFI.
The uniffi project implements a cli program for this; we just need to compile and run it. Create this target in the Cargo.toml file:
[[bin]]
name = "uniffi-bindgen"
path = "uniffi-bindgen.rs"
Create a uniffi-bindgen.rs file with this content at the root of the project,
and we'll be able to run the program in one step with cargo.
fn main() {
uniffi::uniffi_bindgen_main()
}Finally, let's compile our library and the bindings:
# compile the library
cargo build --release
# compile the bindings
cargo run --bin uniffi-bindgen generate --library target/release/libfoobar.dylib --language swift --out-dir outCompiled bindings
In the out directory, I'll have three files now:
out
├── foobar.swift
├── foobarFFI.h
└── foobarFFI.modulemap
1 directory, 3 files
The foobar.swift file will contain our ls function amongst some
infrastructure for FFI; it's the bindings.
The foobarFFI.h header file contains the memory layout of the data structures
and functions in foobar.swift.
The modulemap file is used by Clang/LLVM and allows us to use the import
foobar statement.
Compiling a swift module
Disclaimer: From here on, I'll be honest and admit I don't know the Swift ecosystem too well so forgive me if I sound not very helpful.
You'll need to compile the foobar.swift file and modulemap file into a swift
module:
swiftc \
-module-name foobar \
-emit-library -o libfoobar.dylib \
-emit-module -emit-module-path ./ \
-parse-as-library \
-L ./target/release/ \
-lfoobar \
-Xcc -fmodule-map-file=out/foobarFFI.modulemap \
out/foobar.swiftUsage
In the previous step, the swiftc command will generate a foobar.swiftmodule
file and a libfoobar.dylib file (amongst some other irrelevant files).
REPL
With those two files, we can spin up a REPL with Swift and run our Rust code!
swift repl -I . -L . -l foobar -Xcc -fmodule-map-file=out/foobarFFI.modulemap
# import foobar
# ls(path: ".")Xcode
I don't know what the typical structure pattern is for FFI projects in Swift, so I'll just be dragging/copying files into XCode into some verbose folder names for illustration purposes:
tree foobar-*
foobar-lib
└── libfoobar.dylib
foobar-module
└── foobar.swiftmodule
foobar-modulemap
├── foobarFFI.h
└── foobarFFI.modulemap
3 directories, 4 filesNote: When we copy these files into Xcode, Xcode will automatically configure some project settings. It got me almost to the finish line; I still had to configure the Import Path as seen in the next section though.
After copying the libfoobar.dylib file, we must modify it otherwise the
application won't find the library at runtime:
install_name_tool -id @rpath/libfoobar.dylib libfoobar.dylibDisclaimer: I don't completely understand the nuances of this, and I might revisit this article once I understand it more deeply.
Import Path
When we ran the REPL, we provided two search path flags: -I and -L flags.
The -I flag provided the search path to find the foobar.swiftmodule file.
It's described as:
Add directory to the import search path
In XCode, this flag is configured in the Build Settings > Swift Compiler - Search Paths > Import Paths configuration.
Library Import Path
The -L flag provided the search path to find libfoobar.dylib. It's described
as:
Add directory to library link search path
In XCode, this is automatically configured when we drag and drop the file into the UI.
I think it's the Build Settings > Search Paths > Library Import Paths configuration.
Module Map
The -fmodule-map-file=out/foobarFFI.modulemap doesn't map to directly to a
configuration in Xcode because Xcc means to forward the flag to the Clang
compiler.
Instead, we'll use the Build Settings > Swifth Compiler - Custom Flags > Other Swift Flags configuration and provide two elements:
-Xcc-fmodule-map-file=$(PROJECT_DIR)/swift.19/foobar-modulemap/foobarFFI.modulemap
Also note that the foobarFFI.h must be found alongside the
foobarFFI.modulemap file.
Application
I won't go into the details of initializing an XCode project, but I'll share the program code:
ContentView.swift
import SwiftUI
import foobar
struct ContentView: View {
@State var files = [String]()
@State var path = "."
func ls() {
if path.isEmpty {
path = ".'"
}
files = foobar.ls(path: path)
}
var body: some View {
VStack {
HStack {
Button("ls", action: ls)
TextField("Path", text: $path)
.onSubmit(ls)
}
List(0..<files.count, id: \.self) { index in
let name = files[index]
Text(name)
}
}
.frame(width: 250)
.frame(minHeight: 300)
.navigationTitle("ls")
.padding()
}
}App.swift
This file is typically prefixed with the name of the project, and not actually just App.swift. Nevertheless, here is the content.
import SwiftUI
@main
struct Application: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.windowResizability(.contentSize)
}
}Conclusion
Success! I've created my first Swift application with a Rust backend using FFI.