swift-bridge

swift-bridge generates bindings for calling Rust from Swift and vice versa.

The swift-bridge book is a work-in-progress with many chapter either sparse or empty.

If you have a question that isn't answered by the book yet, please open an issue.

Building

Building an application that uses Swift and Rust means compiling Swift source code and Rust source code into a final binary.

If there was a super-compiler that knew about both Swift and Rust code you could tell that compiler to compile source files from both languages into a final binary.

This doesn't exist, so instead you need to use both a Swift compiler and a Rust compiler in a two-stepped approach.

First you compile one of the languages into a native library.

Then you use the second compiler to compile the other language into your final binary, along the way linking in the native library that you created.

Here's how this process would look if you were linking in a Rust native library when compiling a Swift executable.

┌──────────────────────────────────┐           ┌───────────────────┐       
│// Rust code                      │           │// Swift Code      │       
│                                  │           │                   │       
│pub extern "C" fn rust_hello() {  │           │rust_hello()       │       
│    println!("Hi, I'm Rust!")     │           │                   │       
│}                                 │           │                   │       
└──────────────────────────────────┘           └───────────────────┘       
                 │                                       │                 
    Compile Rust │                                       │ Compile Swift to
   to native lib │                                       │ executable      
                 │                                       │                 
                 ▼                      Link in Rust     │                 
┌────────────────────────────────┐      native lib       │                 
│       libmy_rust_crate.a       │───────────────────────┤                 
└────────────────────────────────┘                       │                 
                                                         │                 
                                                         ▼                 
                                       ┌──────────────────────────────────┐
                                       │     Final Executable Binary      │
                                       │                                  │
                                       └──────────────────────────────────┘

In a similar fashion, you could also compile Swift code into a native library and then link it in when compiling your Rust code.

Which direction to choose largely comes down to whichever is easiest based on the build tools that you already use or plan to use.

This chapter walks you through a few different ways to build Swift and Rust code.

Xcode + Cargo

To demonstrate how to set up an Xcode + Cargo project, we'll create a new iOS application that makes use of swift-bridge in order use rust-analyzer to perform syntax highlighting of Rust code.

When we're done we'll have a simple application where we can type Rust code into a text area and see the syntax highlighted version below it.

Simulator rust analyzer app

Project Setup

Create a new project.

cargo new --lib ios-rust-analyzer
cd ios-rust-analyzer

Install cargo-lipo.

cargo install -f cargo-lipo

Create a new Xcode project within the ios-rust-analyzer directory.

Xxode > File > New Project > iOS > App

We'll name it IosRustAnalyzer.

New iOS App

Naming the iOS App

Your directory should now look something like:

$ tree -L 2
.
├── Cargo.toml
├── IosRustAnalyzer
│   ├── IosRustAnalyzer
│   └── IosRustAnalyzer.xcodeproj
└── src
    └── lib.rs

Create a bash script that we can use to build the application

touch IosRustAnalyzer/build-rust.sh
chmod +x IosRustAnalyzer/build-rust.sh
#!/bin/bash

##################################################
# We call this from an Xcode run script.
##################################################

set -e

if [[ -z "$PROJECT_DIR" ]]; then
    echo "Must provide PROJECT_DIR environment variable set to the Xcode project directory." 1>&2
    exit 1
fi

cd $PROJECT_DIR

export PATH="$HOME/.cargo/bin:$PATH"

# Without this we can't compile on MacOS Big Sur
# https://github.com/TimNN/cargo-lipo/issues/41#issuecomment-774793892
if [[ -n "${DEVELOPER_SDK_DIR:-}" ]]; then
  export LIBRARY_PATH="${DEVELOPER_SDK_DIR}/MacOSX.sdk/usr/lib:${LIBRARY_PATH:-}"
fi

TARGETS=""
if [[ "$PLATFORM_NAME" = "iphonesimulator" ]]; then
    TARGETS="aarch64-apple-ios-sim,x86_64-apple-ios"
else
    TARGETS="aarch64-apple-ios,x86_64-apple-ios"
fi

# if [ $ENABLE_PREVIEWS == "NO" ]; then

  if [[ $CONFIGURATION == "Release" ]]; then
      echo "BUIlDING FOR RELEASE ($TARGETS)"

      cargo lipo --release --manifest-path ../Cargo.toml  --targets $TARGETS
  else
      echo "BUIlDING FOR DEBUG ($TARGETS)"

      cargo lipo --manifest-path ../Cargo.toml  --targets $TARGETS
  fi

# else
#   echo "Skipping the script because of preview mode"
# fi

Create a new build phase that calls ./build-rust.sh — the bash script that we created.

Be sure to drag it before the Compile Sources step.

Xcode build phase - create run script

Xcode build phase - collapsed

Xcode build phase - expanded


Create a directory Generated where our generated Swift and C code will go.

mkdir IosRustAnalyzer/Generated
touch IosRustAnalyzer/Generated/.gitignore

Give IosRustAnalyzer/Generated/.gitignore the following contents:

# IosRustAnalyzer/Generated/.gitignore
*
!.gitignore

Create a new bridging header and name it BridgingHeader.h.

Create bridging header

Give it these contents:

#ifndef BridgingHeader_h
#define BridgingHeader_h

#include "Generated/SwiftBridgeCore.h"
#include "Generated/ios-rust-analyzer/ios-rust-analyzer.h"

#endif

Set the bridging header to $(PROJECT_DIR)/BridgingHeader.h

Bridging Header


In the Cargo.toml, set the crate-type and build script.

[package]
name = "ios-rust-analyzer"
version = "0.1.0"
edition = "2021"

build = "build.rs"

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

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

[dependencies]
swift-bridge = "0.1"
ide = {git = "https://github.com/rust-analyzer/rust-analyzer"}

Create our build script.

touch build.rs
// In build.rs

fn main() {
  // TODO...
}

Build the Cargo project once so that we can generate the files that we'll be linking to.

PROJECT_DIR="${PWD}/IosRustAnalyzer" ./IosRustAnalyzer/build-rust.sh

Set the Debug library search path to $(PROJECT_DIR)/../target/universal/debug, and the Release library search path to $(PROJECT_DIR)/../target/universal/release

Xcode set library search paths


Go to IosRustAnalyzer > General > Frameworks, Libraries, and Embedded Content > + to add and click Add Other > Add Files.

Select the target/universal/debug/libios_rust_analyzer.a file.

Frameworks and libraries

Libraries add other


Select the same target/universal/debug/libios_rust_analyzer.a in the link binaries with libraries build phase.

Link binary build phase


Add the following to the build.rs file that we created earlier.

const XCODE_CONFIGURATION_ENV: &'static str = "CONFIGURATION";

fn main() {
    let out_dir = "IosRustAnalyzer/Generated";

    let bridges = vec!["src/lib.rs"];
    for path in &bridges {
        println!("cargo:rerun-if-changed={}", path);
    }
    println!("cargo:rerun-if-env-changed={}", XCODE_CONFIGURATION_ENV);

    swift_bridge_build::parse_bridges(bridges)
        .write_all_concatenated(out_dir, env!("CARGO_PKG_NAME"));
}

Build again so that we can genrate the files that we're including from BridgingHeader.h.

PROJECT_DIR="${PWD}/IosRustAnalyzer" ./IosRustAnalyzer/build-rust.sh

Right click on IosRustAnalyzer and click Add Files To IosRustAnalyzer. Add the entire Generated directory.

Add files to IosRustAnalyzer

Add generated files


Pressing the Run button should now open up the iOS simulator with Xcode's default "Hello World" iOS app.

iOS simulator hello world

Rust

Now that we've set up our project, it's time to write some code!

Add the following to src/lib.rs.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type RustApp;

        #[swift_bridge(init)]
        fn new() -> RustApp;

        fn generate_html(&self, rust_code: &str) -> String;
    }
}

pub struct RustApp {}

impl RustApp {
    fn new() -> Self {
        RustApp {}
    }

    fn generate_html(&self, rust_code: &str) -> String {
        let (analysis, file_id) = ide::Analysis::from_single_file(rust_code.to_string());

        analysis
            .highlight_as_html(file_id, true)
            .unwrap_or("Error".to_string())
    }
}
}

Swift

Add the following to IosRustAnalyzerApp.swift

import SwiftUI

@main
struct IosRustAnalyzerApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(RustAppWrapper(rust: RustApp()))
        }
    }
}

class RustAppWrapper: ObservableObject {
    var rust: RustApp

    init (rust: RustApp) {
        self.rust = rust
    }
}

Add the following to ContentView.swift

import SwiftUI
import WebKit
import Combine

struct ContentView: View {
    @EnvironmentObject var rustApp: RustAppWrapper

    @State private var rustSource = initialSource
    @State private var rustHtml = ""

    var body: some View {
        VStack {
            TextEditor(text: $rustSource)
                .font(.caption)
                .onReceive(Just(rustSource), perform: {sourceCode in
                    let html = rustApp.rust.generate_html(sourceCode).toString()
                    rustHtml = html
                })

            WebView(text: $rustHtml)
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)

        }
    }
}

struct WebView: UIViewRepresentable {
    @Binding var text: String

    func makeUIView(context: Context) -> WKWebView {
        return WKWebView()
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        uiView.loadHTMLString(text, baseURL: nil)
    }
}

let initialSource = """

fn main () {
    let stack: Stack<u8> = Stack::default();

    for val in 0..100 {
        stack.push(val);
    }
}

#[derive(Default)]
struct Stack<T>(Vec<T>);

impl<T> Stack<T> {
    fn push(&mut self, val: T) {
        self.0.push(val);
    }

    fn pop(&mut self) -> Option<T> {
        self.0.pop()
    }
}

"""


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(RustAppWrapper(rust: RustApp()))
    }
}

Make sure that you see 4 files in your Compile Sources build phase.

Xcode compile sources build phase


Runing the application in the iPhone simulator using Cmd + R should show a working demo!

Simulator rust analyzer app

swiftc + Cargo

One approach to building Rust and Swift is to use Cargo to compile your Rust code and swiftc to compile your Swift code.

In this case, either Cargo needs to link to a compiled Swift native library, or swiftc needs to link to a compiled Rust native library.

We'll use an example project to explore both of these approaches.

Project Setup

cargo new --lib swift-and-rust
cd swift-and-rust
# Cargo.toml

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

[dependencies]
swift-bridge = "0.1"
// src/main.rs
fn main() {
    swift_and_rust::print_hello_swift();
}

#![allow(unused)]
fn main() {
// src/lib.rs

pub use ffi::print_hello_swift;

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        fn print_hello_rust();
    }

    extern "Swift" {
        fn print_hello_swift();
    }
}

fn print_hello_rust() {
    println!("Hello from Rust")
}
}
// main.swift
run()
// lib.swift
func print_hello_swift() {
    print("Hello from Swift")
}

func run() {
    print_hello_rust()
}
# bridging-header.h

#ifndef BridgingHeader_h
#define BridgingHeader_h

#include "./generated/SwiftBridgeCore.h"
#include "./generated/swift-and-rust/swift-and-rust.h"

#endif /* BridgingHeader_h */
mkdir generated

Add the following to your Cargo.toml

# Cargo.toml

[lib]
crate-type = ["staticlib"]
// build.rs

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"));
}
# build-swiftc-links-rust.sh

#!/bin/bash
set -e

cargo build --target x86_64-apple-darwin
swiftc -L target/x86_64-apple-darwin/debug/ -lswift_and_rust -import-objc-header bridging-header.h \
  main.swift lib.swift ./generated/swift-and-rust/swift-and-rust.swift ./generated/SwiftBridgeCore.swift
chmod +x build-swiftc-links-rust.sh
./build-swiftc-links-rust.sh
./main
# The output should be "Hello from Rust"

Unlike when we had swiftc linking in the Rust code, you do not need to set the crate-type when you have Cargo linking in the Swift code.

// build.rs
fn main() {
    println!("cargo:rustc-link-lib=static=my_swift");
    println!("cargo:rustc-link-search=./");
}
# build-swift-static-lib.sh

#!/bin/bash
set -e

# Generate the bridging headers.
# Multiple files can be supplied with "-f" flag, e.g. "-f file1 -f file2".
# Substitute <package name> for the crate name in your Cargo.toml file.
swift-bridge-cli parse-bridges --crate-name <package name> -f src/lib.rs -o generated

swiftc -emit-library -static -module-name my_swift -import-objc-header bridging-header.h \
  lib.swift ./generated/swift-and-rust/swift-and-rust.swift
chmod +x build-swift-static-lib.sh
./build-swift-static-lib.sh

cargo build
./target/debug/swift_and_rust
# The output should be "Hello from Swift"

Creating Swift Packages

In this chapter we'll walk through bundling your Rust library into a Swift Package.

Swift Packages that contain binary dependencies are only available on Apple platforms.

You cannot bundle your Rust code into a Swift Package if you plan to target Linux, Windows or any other non-Apple target.

Instead, use a building approach from one of the other building chapters.

Project setup

mkdir my-rust-lib && cd my-rust-lib
# Cargo.toml

[package]
name = "my-rust-lib"

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

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

[dependencies]
swift-bridge = "0.1"

In src/lib.rs, add the following:


#![allow(unused)]
fn main() {
// src/lib.rs

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        fn hello_rust() -> String;
    }
}

fn hello_rust() -> String {
    String::from("Hello from Rust!")
}
}

Create a new build.rs file with the following contents:

touch build.rs
// build.rs

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"));
}

Create a new bash script for building our Rust native libraries along with a folder that we'll write our parsed bridges too.

touch build-rust.sh
chmod +x build-rust.sh
mkdir generated
# build-rust.sh

#!/bin/bash

set -e

THISDIR=$(dirname $0)
cd $THISDIR

# Build the project for the desired platforms:
cargo build --target x86_64-apple-darwin
cargo build --target aarch64-apple-darwin
mkdir -p ./target/universal-macos/debug

lipo \
    ./target/aarch64-apple-darwin/debug/libmy_rust_lib.a \
    ./target/x86_64-apple-darwin/debug/libmy_rust_lib.a -create -output \
    ./target/universal-macos/debug/libmy_rust_lib.a

cargo build --target aarch64-apple-ios

cargo build --target x86_64-apple-ios
cargo build --target aarch64-apple-ios-sim
mkdir -p ./target/universal-ios/debug

lipo \
    ./target/aarch64-apple-ios-sim/debug/libmy_rust_lib.a \
    ./target/x86_64-apple-ios/debug/libmy_rust_lib.a -create -output \
    ./target/universal-ios/debug/libmy_rust_lib.a

Install Rust toolchains for the desired platforms:

rustup target add x86_64-apple-darwin aarch64-apple-darwin aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim

Run the script to build our Rust libraries:

./build-rust.sh

We can now use the API or the CLI to package the generated bridging code and the Rust libraries into a Swift Package.

API

Here's an example of using the API to package up our generated bridging code and our Rust libraries into a Swift Package.

use std::path::PathBuf;
use std::collections::HashMap;
use swift_bridge_build::{CreatePackageConfig, ApplePlatform};
fn main() {
    swift_bridge_build::create_package(CreatePackageConfig {
        bridge_dir: PathBuf::from("./generated"),
        paths: HashMap::from([
            (ApplePlatform::IOS, "target/aarch64-apple-ios/debug/libmy_rust_lib.a".into()),
            (ApplePlatform::Simulator, "target/universal-ios/debug/libmy_rust_lib.a".into()),
            (ApplePlatform::MacOS, "target/universal-macos/debug/libmy_rust_lib.a".into()),
        ]),
        out_dir: PathBuf::from("MySwiftPackage"),
        package_name: PathBuf::from("MySwiftPackage")
    });
}

CLI

You can use the swift-bridge CLI's create-package command in order to create a Swift Package.

First, install the CLI.

cargo install -f swift-bridge-cli
swift-bridge-cli --help

Then, run the following to package up your generated bridges and your Rust libraries into a Swift Package.

swift-bridge-cli create-package \
  --bridges-dir ./generated \
  --out-dir MySwiftPackage \
  --ios target/aarch64-apple-ios/debug/libmy_rust_lib.a \
  --simulator target/universal-ios/debug/libmy_rust_lib.a \
  --macos target/universal-macos/debug/libmy_rust_lib.a \
  --name MySwiftPackage

Using the Swift Package

We now have a Swift Package (in the MySwiftPackage directory) which we can include in other projects using the Swift Package Manager.

Using the package in an Xcode project

To add the package to an iOS app in Xcode, first open your project's .xcworkspace file.

Next, go to the package dependencies panel, and click on + -> Add Local -> and select the MySwiftPackage directory.

Next, go to the target's general panel and click the + button in the Frameworks, Libraries, and Embedded Content section.

Select Workspace -> MySwiftPackage -> MySwiftPackage.

Import and use it in the same way as the executable.

Using the package in an executable Swift project

Here is an example of an executable Swift project that depends on our newly created MySwiftPackage.

mkdir SwiftProject
touch SwiftProject/Package.swift
mkdir -p SwiftProject/Sources/SwiftProject
touch SwiftProject/Sources/SwiftProject/main.swift

Add these contents to SwiftProject/Package.swift.

// SwiftProject/Package.swift

// swift-tools-version:5.5.0
import PackageDescription
let package = Package(
    name: "SwiftProject",
    dependencies: [
        .package(path: "../MySwiftPackage")
    ],
    targets: [
        .executableTarget(
            name: "SwiftProject",
            dependencies: [
                .product(name: "MySwiftPackage", package: "MySwiftPackage")
            ])
    ]
)

And then add this to our SwiftProject/Sources/SwiftProject/main.swift file.

// SwiftProject/Sources/SwiftProject/main.swift

import MySwiftPackage

print(hello_rust().toString())

And now you can run your Swift project that depends on your Rust based Swift Package:

cd SwiftProject
swift run
# You should see "Hello from Rust!" in your terminal.

The Bridge Module

With swift-bridge you use a "bridge module" in order to declare your FFI interface.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    // Export Rust types and functions for Swift to use.
    extern "Rust" {
        type SomeRustType;
        fn some_type_method(&mut self) -> String;
    }

    // Import Swift types and functions for Swift to use.
    extern "Swift" {
        type SomeSwiftClass;

        #[swift_bridge(swift_name = "someClassMethod")]
        fn some_class_method(&self, arg: u8);
    }
}
}

Your bridge module can contain any number of extern "Rust" and extern "Swift" blocks, each declaring types and functions to expose to and import from Swift, respectively.

How it Works

After you declare your bridge module, you use two code generators at build time to make the FFI layer that you described work.

One code generator generates the Rust side of the FFI layer, and the other code generator produces the Swift side.

Rust code generation

The #[swift_bridge::bridge] procedural macro parses your bridge module at compile time and then generates the Rust side of your FFI layer.

Swift code generation

At build time you run swift-bridge-build (or swift-bridge-cli for non-Cargo based setups) on files that contain bridge modules in order to generate the Swift and C code necessary to make your bridge work.

Let's Begin

This section's sub chapters will go into detail about the different ways that you can use bridge modules to connect Rust and Swift.

In the meantime, here's a quick peak of a simple bridge module:


#![allow(unused)]
fn main() {
// We use the `swift_bridge::bridge` macro to declare a bridge module.
// Then at build time the `swift-bridge-build` crate is used to generate
// the corresponding Swift and C FFI glue code.
#[swift_bridge::bridge]
mod ffi {
    // Create "transparent" structs where both Rust and Swift can directly access the fields.
    struct AppConfig {
        file_manager: CustomFileManager,
    }

    // Transparent enums are also supported.
    enum UserLookup {
        ById(UserId),
        ByName(String),
    }

    // Export opaque Rust types, functions and methods for Swift to use.
    extern "Rust" {
        type RustApp;

        #[swift_bridge(init)]
        fn new(config: AppConfig) -> RustApp;
        
        fn get_user(&self, lookup: UserLookup) -> Option<&User>;
    }

    extern "Rust" {
        type User;
        type MessageBoard;

        #[swift_bridge(get(&nickname))]
        fn informal_name(self: &User) -> &str;
    }

    // Import opaque Swift classes and functions for Rust to use.
    extern "Swift" {
        type CustomFileManager;
        fn save_file(&self, name: &str, contents: &[u8]);
    }
}

struct User {
    nickname: String
}
}

Functions

swift-bridge allows you to export Rust functions for Swift to use and import Swift functions for Rust to use.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {    
    extern "Rust" {
        #[swift_name = "printGreeting"]
        fn print_greeting(name: &str);
    }

    extern "Swift" {
        fn add(lhs: usize, rhs: usize) -> usize;
    }
}

fn print_greeting(name: &str) {
    let sum = ffi::add(1, 2);
    println!("Hello {name}. 1 + 2 = {sum}!")
}
}
// Swift

printGreeting("Tolu")

func add(lhs: UInt, rhs: UInt) -> UInt {
    lhs + rhs
}

Async Rust Functions

swift-bridge supports async/await between Swift and Rust.

Calling an async Rust function from Swift is supported. Calling an async Swift function from Rust is not yet supported.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {    
    extern "Rust" {
        type User;
        type ApiError;

        async fn user_count() -> u32;
        async fn load_user(url: &str) -> Result<User, ApiError>;
    }
}
}
// Swift

let totalUsers = await user_count()

do {
    let user = try await load_user("https://example.com/users/5")
} catch let error as ApiError {
    // ... error handling ...
}

Function Attributes

#[swift_bridge(init)]

Used to generate a Swift initializer for Opaque Types.


#![allow(unused)]
fn main() {
// Rust

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type RegularInitializer;

        #[swift_bridge(init)]
        fn new() -> RegularInitializer;
    }

    extern "Rust" {
        type FailableInitializer;

        #[swift_bridge(init)]
        fn new() -> Option<FailableInitializer>;
    }

    enum SomeError {
        case1,
        case2
    }

    extern "Rust" {
        type ThrowingInitializer;

        #[swift_bridge(init)]
        fn new() -> Result<FailableInitializer, SomeError>;
    }
}
}
// Swift

let regularInitializer = RegularInitializer()

if let failableInitializer = FailableInitializer() {
    // ...
}

do {
    let throwingInitializer = try ThrowingInitializer()
} catch let error {
    // ...
}

#[swift_bridge(Identifiable)]

Used to generate a Swift Identifiable protocol implementation.


#![allow(unused)]
fn main() {
// Rust

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type SomeType;

        #[swift_bridge(Identifiable, swift_name = "someFunction")]
        fn some_function(&self) -> i16;
    }
}
}
// Generated Swift
// (rough example, the real generated code looks a little different)

class SomeType {
    // ...
}
extension SomeType: Identifiable {
    var id: UInt16 {
        return self.someFunction()
    }
}

#[swift_bridge(args_into = (arg_name, another_arg_name))]

Used to name the arguments that should have .into() called on them when passing them to their handler function.

One use case is for exposing a third-party type as a shared struct.


#![allow(unused)]
fn main() {
mod pretend_this_is_some_third_party_crate {
    // We want to expose this third-party struct as a shared struct.
    pub struct UniqueId {
        id: u64
    }
}
use pretend_this_is_some_third_party_crate::UniqueId;

fn a_function (_some_arg: UniqueId, _an_arg: UniqueId, _cool_arg: u8) {
    // ...
}

mod ffi {
    struct FfiUniqueId(u64);

    extern "Rust" {
        // super::a_function does not take a `u64` or an `FfiUniqueId`,
        // but this still works since they both `impl Into<UniqueId>`.
        #[swift_bridge(args_into = (some_arg, an_arg))]
        fn a_function(some_arg: u64, an_arg: FfiUniqueId, cool_arg: u8);
    }
}

impl From<u64> for UniqueId {
    fn from(id: u64) -> UniqueId {
        UniqueId {
            id
        }
    }
}

impl Into<UniqueId> for ffi::FfiUniqueId {
    fn into(self) -> UniqueId {
        UniqueId(self.0)
    }
}
}

#[swift_bridge(associated_to = SomeType)]

Indicates that we are exposing an associated function for a type.


#![allow(unused)]
fn main() {
// Rust

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type Message;

        // Exposes Message::parse to Swift as Message.parse
        #[swift_bridge(associated_to = Message)]
        fn parse(text: &str) -> Option<Message>;
    }
}

struct LongMessage(String);

impl LongMessage {
    fn parse(text: impl ToString) -> Option<Self> {
        let text = text.to_string();

        if text.len() > 10_000 {
            Some(LongMessage(text))
        } else {
            None
        }
    }
}
}
// Swift

func maybeSendLongMessage(text: String) {
    let maybeMessage = Message.parse(text)
    
    if let message = maybeMessage {
        // ... send the message
    }
}

#[swift_bridge(get(field_name))]

Allows you to return the value of an opaque Rust struct's field.

You can prefix the field name with & or &mut in order to return a reference or mutable reference to the field.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type SomeType;

        // Returns self.my_u8
        #[swift_bridge(get(my_u8))]
        fn my_u8(&self) -> u8;

        // Returns &self.my_string
        #[swift_bridge(get(&my_string))]
        fn my_string_reference(&self) -> &str;
    }
}

pub struct SomeType {
    my_u8: u8,
    my_string: String,
}
}

#[swift_bridge(get_with(field_name = path::to::function))]

Allows you to pass an opaque Rust struct's field into a function and then return the value that that function returned.

You can prefix the field name with & or &mut in order to pass the field to the function by reference or mutable reference respectively.


#![allow(unused)]
fn main() {
use Clone;

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type SomeType;

        // Returns ui_to_i16(self.my_u8)
        #[swift_bridge(get_with(my_u8 = u8_to_i16))]
        fn my_u8_converted(&self) -> u16;

        // Returns Clone::clone(&self.my_string)
        #[swift_bridge(get_with(&my_string = Clone::clone))]
        fn my_string_cloned(&self) -> String;

        // Returns string_to_u32(&self.my_string)
        #[swift_bridge(get_with(&my_string = string_to_u32))]
        fn my_string_parsed(&self) -> u32;
    }
}

pub struct SomeType {
    my_u8: u8,
    my_string: String,
}

fn u8_to_i16 (num: u8) -> i16 {
    num as i16
}

fn string_to_u32(string: &str) -> u32 {
    string.parse().unwrap()
}
}

#[swift_bridge(label = "argName")]

Used to set the Swift argument label.


#![allow(unused)]
fn main() {
// Rust
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        fn add(
            #[swift_bridge(label = "leftHand")] 
            left_hand: i32,
            right_hand: i32,
        ) -> i32;
    }
}

fn add(left_hand: i32, right_hand: i32) -> i32 {
    left_hand + right_hand
}
}
// Swift

let sum = add(leftHand: 10, 20)

#[swift_bridge(return_into)]

Allows a swift-bridge definition of fn foo() -> T to work for any fn foo() -> impl Into<T>.


#![allow(unused)]
fn main() {
use some_other_crate::Uuid;

#[swift_bridge::bridge]
mod ffi {
	struct FfiUuid {
	    uuid: [u8; 16]
	}

    extern "Rust" {
        #[swift_bridge(return_into)]
        fn make_uuid() -> FfiUuid;
    }
}

impl From<Uuid> for ffi::FFiUuid {
	fn from(uuid: Uuid) -> ffi::FfiUuid {
	    unsafe { std::mem::transmute(uuid) }
	}
}

use self::some_other_crate::make_uuid;
mod some_other_crate {
	pub struct Uuid {
	    uuid: [u8; 16]
	}

    // Here we can return a Uuid, even though swift-bridge is expecting an FfiUuid.
    pub fn make_uuid() -> Uuid {
        Uuid::new_v4()
    }
}
}

#[swift_bridge(return_with = path::to::some_function)]

Allows a swift-bridge definition of fn foo() -> T to work for a fn foo() -> U by passing T to a fn(T) -> U.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        #[swift_bridge(return_with = some_module::convert_str_to_u32)]
        fn get_str_value_return_with() -> u32;
    }
}

fn get_str_value_return_with() -> &'static str {
    "123"
}

mod some_module {
    pub fn convert_str_to_u32(val: &str) -> u32 {
        val.parse().unwrap()
    }
}
}

#[swift_bridge(rust_name = "function_name")]

Use the given rust_name to find the function's implementation.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        #[swift_bridge(rust_name = "another_function")]
        fn some_function();
    }
}

fn another_function() {
}
}

#[swift_bridge(swift_name = "functionName")]

Sets the function name that is used on the Swift side.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        // Exports `some_function` as `someFunction`.
        #[swift_bridge(swift_name = "someFunction")]
        fn some_function();
    }

    extern "Swift" {
        // Imports `anotherFunction` as `another_function`.
        #[swift_bridge(swift_name = "anotherFunction")]
        fn another_function();
    }
}
}

Opaque Types

... TODO OVERVIEW ...

Exposing Opaque Rust Types

extern "Rust sections are used to expose Rust types and their associated methods and functions so that they can be used from Swift code.


#![allow(unused)]
fn main() {
mod science;
use science::{ScienceLab, Hydrogen, Oxygen, make_water};

#[swift_bridge::bridge]
mod ffi {
	extern "Rust" {
	    type Water;

        #[swift_bridge(associated_to = Water)]
	    fn new() -> Water;

	    fn is_wet(&self) -> bool;
	}

	extern "Rust" {
	    type ScienceLab;
	    type Hydrogen;
	    type Oxygen;

	    fn make_water(
	        lab: &ScienceLab,
	        hydrogen: Hydrogen,
	        oxygen: Oxygen
	    ) -> Water;
	}
}

pub struct Water;

impl Water {
	fn new () -> Self {
	    Water
	}

	fn is_wet(&self) -> bool {
	    unreachable!("Seriously...?")
	}
}
}

Owned, Ref and RefMut

When you define a type in an extern "Rust" block, three corresponding Swift classes get generated.

// Equivalent to `SomeType` in Rust
class SomeType: SomeTypeRefMut {
    // ...
}

// Equivalent to `&mut SomeType` in Rust
class SomeTypeRefMut: SomeTypeRef {
    // ... 
}

// Equivalent to `&SomeType` in Rust
class SomeTypeRef {
    // ... 
}

Here's an example of how &Type and &mut Type are enforced:


#![allow(unused)]
fn main() {
// Rust

extern "Rust" {
    type SomeType;
    
    #[swift_bridge(init)]
    fn new() -> SomeType;
    
    // Callable by SomeType, SomeTypeRef and SomeTypeRefMut.
    fn (&self) everyone();
    
    // Callable by SomeType, and SomeTypeRefMut.
    fn (&mut self) only_owned_and_ref_mut();
    
    // Only callable by SomeType.
    fn (self) only_owned();
}

extern "Rust" {    
    fn make_ref() -> &'static SomeType;
    
    fn make_ref_mut() -> &'static mut SomeType;
}
}
// Swift

func methods() {
    let someType: SomeType = SomeType()
    let someTypeRef: SomeTypeRef = make_ref()
    let someTypeRefMut: SomeTypeRefMut = make_ref_mut()
    
    someType.everyone()
    someType.only_owned_and_ref_mut()
    someType.only_owned()
    
    someTypeRefMut.everyone()
    someTypeRefMut.only_owned_and_ref_mut()
    
    someTypeRef.everyone()
}

func functions() {
    let someType: SomeType = SomeType()
    let someTypeRef: SomeTypeRef = make_ref()
    let someTypeRefMut: SomeTypeRefMut = make_ref_mut()

    takeReference(someType)
    takeReference(someTypeRef)
    takeReference(someTypeRefMut)
}

// Can be called with SomeType, SomeTypeRef and SomeTypeRefMut
func useSomeType(someType: SomeTypeRef) {
    // ...
}

Opaque Type Attributes

#[swift_bridge(already_declared)]

The already_declared attribute allows you to use the same type in multiple bridge modules.


#![allow(unused)]
fn main() {
use some_crate::App;

mod ffi {
	extern "Rust" {
	    type App;

        #[swift_bridge(init)]
	    fn new() -> App;
	}
}

#[swift_bridge::bridge]
#[cfg(feature = "dev-utils")]
mod ffi_dev_utils {
	extern "Rust" {
        // We won't emit Swift and C type definitions for this type
        // since we've already declared it elsewhere.
	    #[swift_bridge(already_declared)]
        type App;

        fn create_logged_in_user(&mut self, user_id: u8);
	}
}
}

#[swift_bridge(Copy($SIZE))]

If you have an opaque Rust type that implements Copy, you will typically want to be able to pass it between Swift and Rust by copying the bytes instead of allocating.

For example, let's say you have some new type wrappers for different kinds of IDs within your system.

use uuid:Uuid;

#[derive(Copy)]
struct UserId(Uuid);

#[derive(Copy)]
struct OrganizationId(Uuid);

You can expose them using:


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        #[swift_bridge(Copy(16))]
        type UserId;

        #[swift_bridge(Copy(16))]
        type OrganizationId;
    }
}
}

The 16 indicates that a UserId has 16 bytes.

swift-bridge will add a compile time assertion that confirms that the given size is correct.

#[swift_bridge(Equatable)]

The Equatable attribute allows you to expose a Rust PartialEq implementation via Swift's Equatable protocol.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        #[swift_bridge(Equatable)]
        type RustPartialEqType;
    }
}

#[derive(PartialEq)]
struct RustPartialEqType(u32);

}
// In Swift

let val1 = RustPartialEqType(5)
let val2 = RustPartialEqType(10)

if val1 == val2 {
    print("Equal")
} else {
    print("Not equal")
}

#[swift_bridge(Hashable)]

The Hashable attribute allows you to expose a Rust Hash trait implementation via Swift's Hashable protocol.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        #[swift_bridge(Hashable)]
        type RustHashType;
    }
}

#[derive(Hash, PartialEq)]
struct RustHashType(u32);
}
// In Swift

let val = RustHashType(10);

let table: [RustHashType: String] = [:]

table[val] = "hello"
table[val] = "world"

//Should print "world"
print(table[val])

Transparent Types

swift-bridge supports defining structs and enums who's fields can be seen by both Swift and Rust.

Transparent Structs

You can define structs whos fields can be accessed by both Rust and Swift.


#![allow(unused)]
fn main() {
// Rust

#[swift_bridge::bridge]
mod ffi {
    #[swift_bridge(swift_repr = "struct")]
    struct SomeSharedStruct {
        some_field: u8,
        another_field: Option<u64>
    }

    extern "Rust" {
        fn some_function(val: SomeSharedStruct);
    }

    extern "Swift" {
        fn another_function() -> SomeSharedStruct;
    }
}

fn some_function (val: ffi::SomeSharedStruct) {
    // ...
}
}
// Swift 

func another_function() -> SomeSharedStruct {
    return SomeSharedStruct(some_field: 123, another_field: nil)
}

Struct Attributes

#[swift_bridge(already_declared)]


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi_1 {
    #[swift_bridge(swift_repr = "struct")]
    struct SomeSharedStruct {
        some_field: u8
    }
}

use ffi_1::SomeSharedStruct;

#[swift_bridge::bridge]
mod ffi_2 {
    // The `already_declared` indicates that instead of creating a new Struct
    // we should use super::SomeSharedStruct;
    #[swift_bridge(already_declared, swift_repr = "struct")]
    struct SomeSharedStruct;

    extern "Rust" {
        fn some_function() -> SomeSharedStruct;
    }
}
}

#[swift_bridge(swift_repr = "...")]

Valid values are "struct" or "class".

How the struct should appear on the Swift side.

Swift structs are copy-on-write, so we do not allow you to mutate the fields of a swift_repr = "struct" since you wouldn't be changing the original struct.

The swift_repr ="class" representation allows you to pass mutable references to shared structs between Rust and Swift.


#![allow(unused)]
fn main() {
// Rust

#[swift_bridge::bridge]
mod ffi {
    #[swift_bridge(struct_repr = "struct")]
    struct SomeStructReprStruct {
        field: UInt8,
    }

    #[swift_bridge(struct_repr = "class")]
    struct SomeStructReprClass {
        field: UInt8,
    }

    // NOTE: This is aspirational. Exposing methods on shared structs
    //  doesn't actually work yet.
    extern "Rust" {
        // All structs can expose `&self` methods.
        fn repr_struct_ref(self: &SomeStructReprStruct);
        fn repr_class_ref(self: &SomeStructReprStruct);

        // Only structs with `swift_repr = "class"` can expose `&self` methods.
        fn repr_class_ref_mut(self: &mut SomeStructReprStruct);
    }
}

impl ffi::SomeStructReprStruct {
    fn repr_struct_ref(&self) {
        // ...
    }

    // swift-bridge cannot expose this method since mutable methods
    // on `swift_repr = "struct"` structs are not supported.
    fn repr_struct_ref_mut(&mut self) {
        // ...
    }
}

impl ffi::SomeStructReprClass {
    fn repr_class_ref(&self) {
        // ...
    }

    fn repr_class_ref_mut(&mut self) {
        // ...
    }
}
}
// Generated Swift

struct SomeStructReprStruct {
    var field: UInt8
    
    func repr_struct_ref() {
      // ... call Rust's SomeStructReprStruct.repr_struct_ref() ...
    }
}

class SomeStructReprClass: SomeStructReprClassRefMut {
    // ...
}
class SomeStructReprClassRefMut: SomeStructReprClassRef {
    // ...
}
class SomeStructReprClassRef {
    // ...
}

Transparent Enums

You can define enums that can be created by both Rust and Swift.


#![allow(unused)]
fn main() {
// Rust

#[swift_bridge::bridge]
mod ffi {
    enum BarCode {
        Upc(i32, i32, i32, i32),
        QrCode {
            code: String
        }
    }

    extern "Rust" {
        fn get_link(code: BarCode) -> String;
    }

    extern "Swift" {
        fn create_bar_code(upc: bool) -> BarCode;
    }
}

fn get_link (code: BarCode) -> String {
    // ...
}
}
// Swift 

func create_bar_code(upc: Bool) -> BarCode {
    if upc {
        return BarCode.Upc(8, 85909, 51226, 3)
    } else {
        return BarCode.QrCode(code: "ABCDEFG")
    }
}

Enum Attributes

#[swift_bridge(already_declared)]


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi_1 {
    enum SomeTransparentEnum {
        Variant
    }
}

use ffi_1::SomeTransparentEnum;

#[swift_bridge::bridge]
mod ffi_2 {
    // The `already_declared` indicates that instead of creating a new enum
    // we should use super::SomeTransparentEnum;
    #[swift_bridge(already_declared)]
    enum SomeTransparentEnum {}

    extern "Rust" {
        fn some_function() -> SomeTransparentEnum;
    }
}
}

#[swift_bridge(swift_name = "...")]

Set the name that is used when generating the enum on the Swift side.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    #[swift_bridge(swift_name = "RenamedEnum")]
    enum SomeTransparentEnum {
        Variant
    }
}
}

Generics


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        // Declare the generic type once.
        #[swift_bridge(declare_generic)]
        type MyType<A, B>;
    }

    extern "Rust" {
        // Bridge as many monomorphized types as you like.
        type MyType<u32, String>;
        fn some_function(arg: MyType<u32, String>) -> &str;

        type MyType<i8, Vec<u8>>;
    }
}

pub struct MyType<T, U> {
    my_field1: T,
    mu_field2: U
}
fn some_function(arg: MyType<u32, String>) -> &str {
    unimplemented!()
}
}

Generic Copy

You do not need to use the declare_geneic attribute for generic opaque Rust types that use their Copy implementation across FFI bounds.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        #[swift_bridge(Copy(6))]
        type MyType<u32, u16>;
        fn some_function(arg: MyType<u32, u16>) -> &str;
    }
}

#[derive(Copy, Clone)]
pub struct MyType<T, U> {
    my_field1: T,
    mu_field2: U
}
fn some_function(arg: MyType<u32, u16>) -> &str {
    unimplemented!()
}
}

Conditional Compilation

You can use the cfg attribute in order to conditionally generate bindings.

Here's an example of only generating some bindings when a "dev-utils" feature is enabled for the Rust crate.


#![allow(unused)]
fn main() {
struct App {
    users: HashMap<u8, Userg>
}

struct User {
    is_logged_in: bool
}

impl App {
	fn new() -> Self {
		App {
		    users: HashMap::new()
		}
	}

	#[cfg(feature = "dev-utils")]
	fn create_logged_in_user(&mut self, user_id: u8);
}

mod ffi {
	extern "Rust" {
	    type App;

        #[swift_bridge(init)]
	    fn new() -> App;

		fn create_logged_in_user(&mut self, user_id: u8)  {
			let user = User {
			    is_logged_in: true
			};
		    self.users.insert(user_id, user)
		}
	}
}

// This example module contains methods that are useful
// during testing and in SwiftUI previews.
// It is only available when the Rust crate is compiled with the "dev-utils" feature.
#[swift_bridge::bridge]
#[cfg(feature = "dev-utils")]
mod ffi_dev_utils {
	extern "Rust" {
	    #[swift_bridge(already_declared)]
        type App;

        fn create_logged_in_user(&mut self, user_id: u8);
	}
}
}

Supported Conditions

Here are the conditions that are currently supported.

If you need a condition that isn't yet supported, please open an issue.

#[cfg(feature = "some-feature")]


#![allow(unused)]
fn main() {
#[swift_bridge]
mod ffi {
    // The code generator will only generate the corresponding
    // Swift and C code if the "extra-utils" feature is enabled.
    #[cfg(feature = "extra-utils")]
	extern "Rust" {
        // ....
    }
}
}

Locations

Here are the different things that you can conditionally compile.

Bridge module

The bridge module can use the cfg attribute.

At build time the swift_bridge_build library determines whether or not a module will be compiled.

If not, we won't generate any of the corresponding C or Swift code for that module.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
// This module's bindings will only be available when the Rust crate is compiled with
// the `ffi-extras` feature
#[cfg(feature = "ffi-extras")]
mod ffi_extras {
  // ...
}
}

extern "Rust" blocks

...This hasn't been implemented yet but should be easy...

Functions can methods can use the #[cfg] attribute.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    #[cfg(all(unix, target_pointer_width = "32"))]
	extern "Rust" {
        // ...
    }
}
}

Rust functions amd methods

...This hasn't been implemented yet but should be easy...

Functions and methods can use the #[cfg] attribute.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
	extern "Rust" {
	    // This function's will only be available when
        // the Rust crate is compiled targetting Windows.
        #[cfg(target_os = "windows")]
	    fn play_solitaire();
    }
}
}

Why a Bridge Module?

The swift-bridge project provides direct support for expressing the Rust+Swift FFI boundary using one or more bridge modules such as:


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        fn generate_random_number() -> u32;
    }
}

fn generate_random_number() -> u32 {
    rand::random()
}
}

swift-bridge's original maintainer wrote swift-bridge for use in a cross platform application where he preferred to keep his FFI code separate from his application code. He believed that this separation would reduce the likelihood of him biasing his core application's design towards types that were easier to bridge to Swift.

While in the future swift-bridge may decide to directly support other approaches to defining FFI boundaries, at present only the bridge module approach is directly supported.

Users with other needs can write wrappers around swift-bridge to expose alternative frontends.

The examples/without-a-bridge-macro example demonstrates how to reuse swift-bridge's code generation facilities without using a bridge module.

Inline Annotations

The main alternative to the bridge module design would be to support inline annotations where one could describe their FFI boundary by annotating their Rust types.

For instance a user might wish to expose their Rust banking code to Swift using an approach such as:


#![allow(unused)]
fn main() {
// IMAGINARY CODE. WE DO NOT PROVIDE A WAY TO DO THIS.

#[derive(Swift)]
pub struct BankAccount {
    balance: u32
}

#[swift_bridge::bridge]
pub fn create_bank_account() -> BankAccount {
    BankAccount {
        balance: 0
    }
}
}

swift-bridge aims to be a low-level library that generates far more efficient FFI code than a human would write and maintain themselves.

The more information that swift-bridge has at compile time, the more efficient code it can generate.

Let's explore an example of bridging a UserId type, along with a function that returns the latest UserId in the system.


#![allow(unused)]
fn main() {
type Uuid = [u8; 16];

#[derive(Copy)]
struct UserId(Uuid);

pub fn get_latest_user() -> Result<UserId, ()> {
    Ok(UserId([123; 16]))
}
}

In our example, the UserId is a wrapper around a 16 byte UUID.

Exposing this as a bridge module might look like:


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        #[swift_bridge(Copy(16))]
        type UserId;

        fn get_latest_user() -> UserId;
    }
}
}

Exposing the UserId using inlined annotation might look something like:


#![allow(unused)]
fn main() {
// WE DO NOT SUPPORT THIS

type Uuid = [u8; 16];

#[derive(Copy, ExposeToSwift)]
struct UserId(Uuid);

#[swift_bridge::bridge]
pub fn get_latest_user() -> Result<UserId, ()> {
    UserId([123; 16])
}
}

In the bridge module example, swift-bridge knows at compile time that the UserId implements Copy and has a size of 16 bytes.

In the inlined annotation example, however, swift-bridge does not know the UserId implements Copy.

While it would be possible to inline this information, it would mean that users would need to remember to inline this information on every function that used the UserId.


#![allow(unused)]
fn main() {
// WE DO NOT SUPPORT THIS

#[swift_bridge::bridge]
#[swift_bridge(UserId impl Copy(16))]
pub fn get_latest_user() -> Result<UserId, ()> {
    UserId([123; 16])
}
}

We expect that users would find it difficult to remember to repeat such annotations, meaning users would tend to expose less efficient bridges than they otherwise could have.

If swift-bridge does not know that the UserId implements Copy, it will need to generate code like:


#![allow(unused)]
fn main() {
pub extern "C" fn __swift_bridge__get_latest_user() -> *mut UserId {
    let user = get_latest_user();
    match user {
        Ok(user) => Box::new(Box::into_raw(user)),
        Err(()) => std::ptr::null_mut() as *mut UserId,
    }
}
}

Whereas if swift-bridge knows that the UserId implements Copy, it might be able to avoid an allocation by generating code such as:


#![allow(unused)]
fn main() {
/// `swift-bridge` could conceivably generate code like this to bridge
/// a `Result<UserId, ()>`.
/// Here we use a 17 byte array where the first byte indicates `Ok` or `Err`
/// and, then `Ok`, the last 16 bytes hold the `UserId`.
/// We expect this to be more performant than the boxing in the previous
/// example codegen.
pub extern "C" fn __swift_bridge__get_latest_user() -> [u8; 17] {
    let mut bytes: [u8; 17] = [0; 17];

    let user = get_latest_user();

    match user {
        Ok(user) => {
            let user_bytes: [u8; 16] = unsafe { std::mem::transmute(user) };
            (&mut bytes[1..]).copy_from_slice(&user_bytes);

            bytes[0] = 255;
            bytes
        }
        Err(()) => {
            bytes
        }
    }
}
}

More generally, the more information that swift-bridge has about the FFI interface, the more optimized code it can generate. The bridge module design steers users towards providing more information to swift-bridge, which we expect to lead to more efficient applications.

Users that do not need such efficiency can explore reusing swift-bridge in alternative projects that better meet their needs.

Built In Types

In addition to allowing you to share your own custom types between Rust and Swift, swift-bridge comes with support for a number of Rust and Swift standard library types.

name in Rustname in Swiftnotes
u8, i8, u16, i16... etcUInt8, Int8, UInt16, Int16 ... etc
boolBool
String, &String, &mut StringRustString, RustStringRef, RustStringRefMut
&strRustStr
Vec<T>RustVec<T>
SwiftArray<T>Array<T>Not yet implemented
&[T]Not yet implemented
&mut [T]Not yet implemented
Box<T>Not yet implemented
Box<dyn FnOnce(A,B,C) -> D>(A, B, C) -> DPassing from Rust to Swift is supported, but Swift to Rust is not yet implemented.
Box<dyn Fn(A,B,C) -> D>(A, B, C) -> DNot yet implemented
Arc<T>Not yet implemented
[T; N]Not yet implemented
*const TUnsafePointer<T>
*mut TUnsafeMutablePointer<T>
Option<T>Optional<T>
fn x() -> Result<T, E>func x() throws -> T
fn x(arg: Result<T, E>)func x(arg: RustResult<T, E>)
(A, B, C, ...)(A, B, C, ...)
Have a Rust standard library type in mind?
Open an issue!
Have a Swift standard library type in mind?
Open an issue!

String <---> String

Rust's std::string::String can be passed to Swift as an owned String, a referenced &String or a mutably referenced &mut String.


#![allow(unused)]
fn main() {
// Rust

#[swift_bridge::bridge]
mod ffi {
	extern "Rust" {
	    type SomeRustType;

	    // Becomes a `RustString` when passed to Swift.
	    fn make_string() -> String;

	    // Becomes a `RustStringRef` when passed to Swift.
	    fn make_ref_string(&self) -> &String;

	    // Becomes a `RustStringRefMut` when passed to Swift.
	    fn make_ref_mut_string(&mut self) -> &mut String;

        // Swift calls this with a `RustString` and
        // Rust receives a `std::string::String`.
	    fn take_string(string: String);
	}

	extern "Swift" {
	    type SomeSwiftType;

	    fn make_rust_string() -> String;
	    fn make_swift_string() -> String;
	}
}
}
// Swift

func make_rust_string() -> RustString {
    RustString("Good Day")
}

// Swift can return anything that implements the `IntoRustString` protocol.
// `swift-brdige` automatically implements this for Swift's `String` type.
func make_swift_string() -> String {
    "Hello World"
}

RustString

Since both Rust and Swift have their own String type, it is technically possible for us to automatically convert owned Rust std::string::Strings into Swift Strings.

However, we do not do this since Swift does not currently have a way to construct a Swift String without copying.

This means that if we were to automatically convert from a Rust std::string::String to a Swift String we would have to create a new Swift allocation and copy all of the Rust std::string::String bytes to that allocation.

swift-bridge seeks to avoid unnecessary allocations, so instead of performing this implicit allocation we pass a RustString type from Rust to Swift.

The RustString's .toString() method can then be called on the Swift side to get a Swift String.

&str <---> RustStr

Rust's std::str can be passed to Swift as a RustStr.


#![allow(unused)]
fn main() {
// Rust

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
	    type SomeRustType;

	    // Becomes a `RustStr` when passed to Swift.
	    fn make_str() -> &'static str;

	    fn get_str(self: &SomeRustType) -> &str;
	}

	extern "Swift" {
	    type SomeSwiftType;

        // Swift returns a `RustStr` and
        // Rust receives a `&str`.
	    fn make_string() -> &str;
	}
}
}

Vec <--> RustVec

Rust's std::vec::Vec is seen on the Swift side as a RustVec.

RustVec implements Swift's IteratorProtocol, allowing you do do things like:

let vec: RustVec = get_rust_vec_somehow()
for value in vec {
    print(value)
}

Example


#![allow(unused)]
fn main() {
// Rust

#[swift_bridge::bridge]
mod ffi {
	extern "Rust" {
	    fn make_rust_vec() -> Vec<u32>;

	    fn make_rust_vec_with_initial_contents(initial: &[i16]) -> Vec<i16>;
	}
}

fn make_rust_vec() -> Vec<u32> {
    vec![5, 8, 11]
}

fn make_rust_vec_with_initial_contents(initial: &[u16]) -> Vec<u16> {
    initial.to_vec()
}
}
// In Swift

func testMakeAVec () {
    let vec: RustVec = get_vec_from_rust()

    XCTAssertEqual(vec.pop(), 5)
    XCTAssertEqual(vec.pop(), 8)
    XCTAssertEqual(vec.pop(), 11)
    XCTAssertEqual(vec.pop(), nil)

    vec.push(50)
    vec.push(75)
    XCTAssertEqual(vec.get(1), 75)
}

func testMakeAnotherVec () {
    let initial: [Int16] = [3, 5, 7]
    initial.withUnsafeBufferPointer({ initalPtr in
        let vec: RustVec = make_rust_vec_with_initial_contents(initialPtr)

        XCTAssertEqual(vec.len(), 3);

	    for (index, value) in vec.enumerate() {
	        XCTAssertEqual(value, initial[index])
	    }
    });
}

Option <---> Optional

Rust's Option is seen on the Swift side as a Swift Optional.

Example


#![allow(unused)]
fn main() {
// Rust

#[swift_bridge::bridge]
mod ffi {
	extern "Rust" {
	    fn make_rust_option() -> Option<u8>;
	}

	extern "Swift" {
	    fn make_swift_optional() -> Option<bool>;
	}
}

fn make_rust_option() -> Option<u8> {
	if ffi::make_swift_optional == Some(true) {
	    Some(111)
	} else {
	    None
	}
}
}
// Swift

func call_rust_and_divide_by_2() -> Optional<UInt8> {
	if case let val? = make_rust_option() {
	    return val / 2
	} else {
	    nil
	}
}

func make_swift_optional() -> Bool? {
    true
}

Result

Returning Result from Rust -> Swift


#![allow(unused)]
fn main() {
// Rust

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type SomeRustType;

        fn run() -> Result<SomeRustType, String>;
    }
}
}
// Swift

func run() throws -> SomeRustType {
    // ...
}

Swift function that takes a callback


#![allow(unused)]
fn main() {
// Rust

#[swift_bridge::bridge]
mod ffi {
    extern "Swift" {
        fn run(
            arg: Box<dyn FnOnce(Result<SomeRustType, String>)>
        );
    }

    extern "Rust" {
        type SomeRustType;
    }
}
}
// Swift

func run(arg: (RustResult<SomeRustType, String>) -> ()) {
    arg(.Err("Something went wrong"))
}

Boxed Functions

Box<dyn FnOnce(A, B) -> C>

swift-bridge supports bridging boxed FnOnce functions with any number of arguments.

There is a panic if you attempt to call a bridged FnOnce function more than once.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
	extern "Swift" {
	    type CreditCardReader;
	    type Card;
	    type CardError;

        fn processCard(
            self: &CreditCardReader,
            callback: Box<dyn FnOnce(Result<Card, CardError>) -> ()>
        );
	}
}
}

(A, B, C, ...) <---> (A, B, C, ...)

Rust's (A, B, C, ...) is seen on the Swift side as a (A, B, C, ...).

Returning Tuple from Rust -> Swift


#![allow(unused)]
fn main() {
// Rust

mod ffi {
    extern "Rust" {
        fn get_midpoint(
            point1: (f32, f32, f32),
            point2: (f32, f32, f32),
        ) -> (f32, f32, f32);
    }

    extern "Swift" {
        fn make_point() -> (f32, f32, f32);
    }
}

fn get_midpoint(
    point1: (f32, f32, f32),
    point2: (f32, f32, f32)
) -> (f32, f32, f32) {
    // ...
}
}
// Swift

func make_point() -> (Float, Float, Float) {
    (1.0, 2.0, 3.0)
}

let midpoint = get_midpoint(
    make_point(1.0, 2.0, 3.0),
    make_point(4.0, 5.0, 6.0)
)

Taking Tuple from Swift -> Rust


#![allow(unused)]
fn main() {
// Rust

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type SomeRustType;
    }
    extern "Rust" {
        fn run(
            arg: (SomeRustType, i32)
        );
    }
}
}
// Swift

let someType = SomeType()
run((someType, 123))

Safety

swift-bridge is fully type safe and mostly memory safe.

Type Safety

All of the Rust and Swift FFI code that swift-bridge generates for you is type safe.

Memory Safety

You can ensure the memory safety of your Rust and Swift projects by following these rules:

  • In Swift, never use a reference to a Rust type after its lifetime.

  • In Swift, pass a mutable reference to Rust that will live alongside another active reference to that type.

  • In Swift, never use a type after passing ownership to Rust.

Let's look at some examples of code that violates these rules:

Never use a reference after its lifetime

It is possible to pass a reference from Rust -> Swift and then have Swift make use of that reference after it is no longer safe to do so.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type SomeType;

        #[swift_bridge(init)]
        fn new();

        fn name(&str) -> &str;
        fn drop(self);
    }
}
}
// Swift

let someType = SomeType()

let name: RustStr = someType.name()
someType.drop()

// Undefined behavior since `SomeType` was dropped.
name.toString()

It isn't possible for swift-bridge to mitigate this, so be mindful when handling references.

Never pass a mutable reference to Rust that will live alongside another active reference

Rust expects that if there is mutable reference to a value, no other references to that value are held.

This rule is not enforced on the Swift side, making it possible to pass aliasing pointers to Rust and trigger undefined behavior.


#![allow(unused)]
fn main() {
// Rust

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type MyList;

        #[swift_bridge(init)]
        fn new() -> MyList;

        fn extend(a: &mut self, b: &MyList);
    }
}
}
// Swift

let myList = MyList()

// This can cause undefined behavior!
myList.extend(myList)


#![allow(unused)]
fn main() {
// Rust
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type MyList;

        fn new() -> MyList;

        fn allegedly_immutable(&self, callback: Box<dyn Fn()>);
        fn mutate(&mut self);
    }
}
}
// Swift

let myList = MyList()

myList.allegedly_immutable({
    // If the `allegedly_immutable` method calls this
    // callback we will take a mutable reference to `MyList`
    // while there is an active immutable reference.
    // This violates Rust's borrowing rules.
    myList.mutate()
})

To stay safe, when passing a mutable reference to a Rust value from Swift to Rust do not pass any other references to that same value.

Never use a value after it is dropped

Today, it is possible to pass ownership of a value from Swift to Rust and then unsafely access the value from Swift.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type MyOwnedType;

        fn drop(ty: MyOwnedType);
    }
}
}
let myOwnedType = MyOwnedType()

drop(myOwnedType)

// Undefined behavior since we no longer own this value.
drop(myOwnedType)

After Swift introduces the consume operator we will be able to prevent this issue by enforcing ownership at compile time.

Contributing to swift-bridge

Design of swift-bridge

This chapter explores how swift-bridge works internally.

Code Generation

  • Talk about Rust token stream generation

  • Talk about Swift codegen

  • Talk about C header codegen

  • Talk about how we test codegen

Adding support for a signature

Bridge modules expose Rust and Swift functions by declaring their function signatures.

For example, in the following bridge module we declare one Swift and one Rust function signature.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        async fn add(a: u8, b: u16) -> u32;
    }

    extern "Swift" {
        type Counter;
        fn increment(&mut self);
    }
}
}

Not all signatures are supported. For example, the following would not compile:


#![allow(unused)]
fn main() {
// This does not compile

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        async fn print_cow(cow: Cow<'static, str>);
    }
}
}

swift-bridge does not currently support the Cow<'static, str>, so the print_cow function signature is unsupported.

This chapter shows how to add support for an unsupported signature.

Implementing Support for a Signature

To support a new signature, we first write automated tests for the signature and then implement just enough code to get those tests passing.

Add the time of writing, the Swift programming language does not have support for 128 bit integers.

Let's pretend that Swift gained support for them and we were tasked with supporting u128 argument and return types in swift-bridge function signatures.


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        fn reflect_u128(num: u128);
    }
}

fn reflect_u128(num: u128) -> num {
    num
}
}

Integration Tests

Our first step would be to add an integration test where we declared this signature in a bridge module and called the function from Rust.

We would first find a good place in crates/swift-integration-tests to declare the signature.

crates/swift-integration-tests/src/primitive.rs would be a good choice.

Before adding our u128 support, the file looks like this:


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        fn test_rust_calls_swift_primitives();

        fn rust_double_u8(arg: u8) -> u8;
        fn rust_double_i8(arg: i8) -> i8;
        fn rust_double_u16(arg: u16) -> u16;
        fn rust_double_i16(arg: i16) -> i16;
        fn rust_double_u32(arg: u32) -> u32;
// ... snip ...
}

Next we would add our reflect_u128 function to the bridge module.

We would then modify the SwiftRustIntegrationTestRunner to call our function.

In this case we would want to modify SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/PrimitiveTests.swift, which before our updates looks something like:

import XCTest
@testable import SwiftRustIntegrationTestRunner

/// Tests for generic types such as `type SomeType<u32>`
class PrimitiveTests: XCTestCase {
    /// Run tests where Rust calls Swift functions that take primitive args.
    func testRustCallsSwiftPrimitives() throws {
        test_rust_calls_swift_primitives()
    }
    
    /// Run tests where Swift calls Rust functions that take primitive args.
    func testSwiftCallsRustPrimitives() throws {
        XCTAssertEqual(rust_double_u8(10), 20);
        XCTAssertEqual(rust_double_i8(10), 20);
        XCTAssertEqual(rust_double_u16(10), 20);
        XCTAssertEqual(rust_double_i16(10), 20);
        XCTAssertEqual(rust_double_u32(10), 20);
        XCTAssertEqual(rust_double_i32(10), 20);
        XCTAssertEqual(rust_double_u64(10), 20);
        XCTAssertEqual(rust_double_i64(10), 20);
        XCTAssertEqual(rust_double_f32(10.0), 20.0);
        XCTAssertEqual(rust_double_f64(10.0), 20.0);
        XCTAssertEqual(rust_negate_bool(true), false);
        XCTAssertEqual(rust_negate_bool(false), true);
    }
}

Codegen Tests

After adding one or more integration tests, we would then add one or more codegen tests. Codegen tests live in crates/swift-bridge-ir/src/codegen/codegen_tests.

In codegen tests we write out the exact code that we expect swift-bridge to generate.

For example, here is the codegen test for supporting Option<u8> in Rust function arguments.


#![allow(unused)]
fn main() {
// Copied from: crates/swift-bridge-ir/src/codegen/codegen_tests/option_codegen_tests.rs

/// Test code generation for Rust function that accepts and returns an Option<T> where T is a
/// primitive.
mod extern_rust_fn_option_primitive {
    use super::*;

    fn bridge_module_tokens() -> TokenStream {
        quote! {
            mod ffi {
                extern "Rust" {
                    fn some_function (arg: Option<u8>) -> Option<f32>;
                }
            }
        }
    }

    fn expected_rust_tokens() -> ExpectedRustTokens {
        ExpectedRustTokens::Contains(quote! {
            #[export_name = "__swift_bridge__$some_function"]
            pub extern "C" fn __swift_bridge__some_function(
                arg: swift_bridge::option::OptionU8
            ) -> swift_bridge::option::OptionF32 {
                if let Some(val) = super::some_function(
                    if arg.is_some {
                        Some(arg.val)
                    } else {
                        None
                    }
                ) {
                    swift_bridge::option::OptionF32 { val, is_some: true}
                } else {
                    swift_bridge::option::OptionF32 { val: 123.4, is_some: false}
                }
            }
        })
    }

    fn expected_swift_code() -> ExpectedSwiftCode {
        ExpectedSwiftCode::ContainsAfterTrim(
            r#"
func some_function(_ arg: Optional<UInt8>) -> Optional<Float> {
    { let val = __swift_bridge__$some_function({ let val = arg; return __private__OptionU8(val: val ?? 123, is_some: val != nil); }()); if val.is_some { return val.val } else { return nil } }()
}
"#,
        )
    }

    const EXPECTED_C_HEADER: ExpectedCHeader = ExpectedCHeader::ExactAfterTrim(
        r#"
struct __private__OptionF32 __swift_bridge__$some_function(struct __private__OptionU8 arg);
    "#,
    );

    #[test]
    fn extern_rust_fn_option_primitive() {
        CodegenTest {
            bridge_module: bridge_module_tokens().into(),
            expected_rust_tokens: expected_rust_tokens(),
            expected_swift_code: expected_swift_code(),
            expected_c_header: EXPECTED_C_HEADER,
        }
            .test();
    }
}
}

Passing Tests

After writing our integration and codegen tests we would add just enough code to make them pass.

This would involve modifying crates/swift-bridge-ir/src/bridged_type.rs until all of our tests passed.

Adding compile time errors

When users write bridge modules that will not compile we want to emit compile time errors that will guide them towards the right fix.

For example, if a user wrote the following bridge module:


#![allow(unused)]
fn main() {
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        #[swift_bridge(InvalidAttribute)]
        type SomeType;
    }
}

pub struct SomeType;
}

We would want to emit a compile time error along the lines of:

error: Unrecognized attribute "InvalidAttribute".
 --> tests/ui/unrecognized-opaque-type-attribute.rs:8:24
  |
8 |         #[swift_bridge(InvalidAttribute)]
  |                        ^^^^^^^^^^^^^^^^

This chapter shows how to add support for compile time errors.

Implementing Support for a Compile Time Error

To support a new compile time error we first write an automated UI test for the error case.

These tests live in crates/swift-bridge-macro/tests/ui and are powered by the trybuild crate.

After adding our UI test, we create a new ParseError variant that can be used to describe the error.

Here are a few example parse errors:


#![allow(unused)]
fn main() {
// via: crates/swift-bridge-ir/src/errors/parse_error.rs

pub(crate) enum ParseError {
    ArgsIntoArgNotFound {
        func: ForeignItemFn,
        missing_arg: Ident,
    },
    /// `extern {}`
    AbiNameMissing {
        /// `extern {}`
        ///  ------
        extern_token: Token![extern],
    },
    /// `extern "Foo" {}`
    AbiNameInvalid {
        /// `extern "Foo" {}`
        ///         -----
        abi_name: LitStr,
    },
    /// `fn foo (&self)`
    ///           ----
    AmbiguousSelf { self_: Receiver },
    /// fn foo (bar: &Bar);
    /// If Bar wasn't declared using a `type Bar` declaration.
    UndeclaredType { ty: Type },

    // ... snip ...
}
}

After adding a parse error variant, we write the code to generate an error message for the new variant. Here are a few examples:


#![allow(unused)]
fn main() {
// via: crates/swift-bridge-ir/src/errors/parse_error.rs

impl Into<syn::Error> for ParseError {
    fn into(self) -> Error {
        match self {
            ParseError::AbiNameMissing {
                extern_token: extern_ident,
            } => Error::new_spanned(
                extern_ident,
                format!(
                    r#"extern modules must have their abi set to "Rust" or "Swift".
```
extern "Rust" {{ ... }}
extern "Swift" {{ ... }}
``` 
                "#
                ),
            ),
            ParseError::UndeclaredType { ty } => {
                let ty_name = ty.to_token_stream().to_string();
                let ty_name = ty_name.split_whitespace().last().unwrap();

                let message = format!(
                    r#"Type must be declared with `type {}`.
"#,
                    ty_name
                );
                Error::new_spanned(ty, message)
            }
            ParseError::DeclaredBuiltInType { ty } => {
                let message = format!(
                    r#"Type {} is already supported
"#,
                    ty.to_token_stream().to_string()
                );
                Error::new_spanned(ty, message)
            }

            // ... snip ...
        }
    }   
}
}

After adding our ParseError we can implement just enough code to make it pass. This typically happens in crates/swift-bridge-ir/src/parse.rs, or one of its descendant modules.

For example, for the given UI test:

// via: crates/swift-bridge-macro/tests/ui/invalid-module-item.rs

#[swift_bridge::bridge]
mod ffi {
    use std;
    fn foo() {}
}

fn main() {}
# via: crates/swift-bridge-macro/tests/ui/invalid-module-item.stderr

error: Only `extern` blocks, structs and enums are supported.
 --> tests/ui/invalid-module-item.rs:6:5
  |
6 |     use std;
  |     ^^^^^^^^

error: Only `extern` blocks, structs and enums are supported.
 --> tests/ui/invalid-module-item.rs:7:5
  |
7 |     fn foo() {}
  |     ^^^^^^^^^^^

We push the ParseError error using:


#![allow(unused)]
fn main() {
// via: crates/swift-bridge-ir/src/parse.rs

for outer_mod_item in item_mod.content.unwrap().1 {
    match outer_mod_item {
        Item::ForeignMod(foreign_mod) => {
            // ...
        }
        Item::Struct(item_struct) => {
            // ...
        }
        Item::Enum(item_enum) => {
            // ...
        }
        invalid_item => {
            let error = ParseError::InvalidModuleItem { item: invalid_item };
            errors.push(error);
        }
    };
}
}