August Feng

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 section

Compiling

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 out

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

Usage

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 files

Note: 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.dylib

Disclaimer: 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.

top