CLI tool
chinedufn/swift-bridge avatar
chinedufn/swift-bridge

swift-bridge generates the Swift and C glue from one annotated Rust module

swift-bridge facilitates Rust and Swift interop.

1,147 stars78 forksRustApache-2.0

At a glance

What is it?
swift-bridge turns a Rust module marked with a bridge macro into the Swift and C sides of an FFI layer, handling types like String, Option, Result, structs and classes, plus async functions. The generated code is what makes it safe to use, and the parts of the type table marked as not yet implemented are what bound it.
Who is it for?
Adopt swift-bridge when your Swift application needs to call into Rust and you want the FFI layer generated rather than hand written, especially for iOS and macOS targets where a bridge module and a build step fit the toolchain. Do not adopt it for a project that needs the unimplemented parts of the type table, such as borrowed slices, Box, Arc or Swift to Rust closures, because those are documented as not yet implemented.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 48 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One annotated Rust module becomes the whole FFI layer

The unit of work in swift-bridge is a bridge module. You declare the types and functions you want to import and export in a Rust module, and annotate it with the swift_bridge::bridge macro. At build time, the swift-bridge-build API or the swift-bridge-cli parses those annotated modules and generates the Swift and C sides of the FFI layer.

That division of labour is the whole design. You write one description of the boundary, and two generated artefacts come out of it: a Swift file and a C header. Neither the Swift nor the C code is something you maintain, which is the point. The project describes the result as safer, more performant and more ergonomic than managing Rust and Swift FFI by hand, and the safety claim comes from the fact that the marshalling rules are decided by the generator rather than by whoever wrote the binding that day.

The macro also carries a little of the semantics. Attributes such as swift_bridge(init) mark a function as the initialiser for an opaque type, and swift_bridge(get(&nickname)) marks a getter. These are not Rust attributes, they are instructions to the generator about what shape the Swift side should take.

The documentation lives in a book, The swift-bridge Book, hosted at chinedufn.github.io/swift-bridge, with the crate documentation on docs.rs.

Transparent structs and opaque types are two different kinds of sharing

The bridge module example shows both kinds of sharing in one file, and the difference is worth internalising before writing any code.

A transparent struct is one where both Rust and Swift can directly access the fields. In the example, AppConfig holds a file_manager field of type CustomFileManager, and the struct is declared inside the bridge module without an extern block, which is what marks it as transparent. Transparent enums work the same way, and the example has UserLookup with a ById and a ByName variant.

An opaque type is one where the other side holds a handle rather than the data. The Rust types RustApp, User and MessageBoard are declared inside an extern "Rust" block, and the comment says it plainly: export opaque Rust types, functions and methods for Swift to use. The Swift side sees a handle, and the fields stay on the Rust side.

The extern "Swift" block is the mirror image. CustomFileManager and CustomIoError are declared there, with the comment that they import opaque Swift classes and functions for Rust to use.

So the direction of the extern block decides who owns the data, and transparent or opaque decides how much of it the other side can see. Getting this backwards is the mistake that turns a clean boundary into a mess of handle types.

The code generation is part of your Cargo build

Adding swift-bridge to a project means two dependencies, one for the build and one for the runtime.

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

[dependencies]
swift-bridge = "0.1"

The build dependency is what parses your bridge modules during the Cargo build and writes the generated Swift and C files. The regular dependency is what the macro expands to inside your Rust code, and it pulls in swift-bridge-macro, the crate that does the expansion.

The alternative to the build dependency is the CLI, swift-bridge-cli, which does the same parsing and generation outside the Cargo build. That matters when the generated code has to land somewhere the Rust build cannot reach, for example in an Xcode project that is not part of a Cargo workspace.

Both versions are 0.1, and the workspace pins every internal crate to the same version, so swift-bridge, swift-bridge-build, swift-bridge-macro, swift-bridge-ir and swift-bridge-cli are all released together. The Cargo manifest also lists the swift-bridge-build crate with a path dependency, which means the build crate is developed inside the same repository rather than fetched from a registry.

The type table tells you exactly where the edges are

The built-in types table is the most useful page in the documentation, because it maps Rust types to Swift types and marks what is missing.

The primitive integers line up as you would expect, u8 and i8 become UInt8 and Int8, and bool becomes Bool. The interesting entries are the owned and borrowed string types. A Rust String does not become Swift String, it becomes RustString, and &str becomes RustStr, with RustStringRef and RustStringRefMut for the borrowed forms. The naming keeps ownership visible on the Swift side, which is how the generator avoids pretending that a Swift String and a Rust String have the same lifetime rules.

The rest of the table is a list of gaps. Vec<T> is supported and becomes RustVec<T>, but SwiftArray<T> is marked not yet implemented, as are the borrowed slice forms &[T] and &mut [T], the boxed forms Box<T> and Box<dyn Fn(A,B,C) -> D>, and Arc<T>. The closure case is the interesting one: Box<dyn FnOnce(A,B,C) -> D> does map to (A, B, C) -> D, and the note says passing from Rust to Swift is supported, but Swift to Rust is not yet implemented.

If your design needs to hand a borrowed slice or an Arc across the boundary, this table is where you find out before you have written the code.

Async support is opt-in and it pulls in a runtime

Bridging async functions is supported, and the example includes one on the Swift side, an async fn save_file that takes a reference to the Swift type, a name, a byte slice and returns a Result with a Swift error type.

The cost is visible in the Cargo manifest. The async feature is not on by default, default is an empty feature list, and enabling it turns on tokio and once_cell. The tokio dependency is optional and pulls the rt-multi-thread and sync features, and once_cell is optional from version 1.9. In other words, a project that does not need async functions does not pay for a Rust async runtime, and a project that does needs one inherits the runtime choice that the library made for it.

The dependency on tokio being optional is worth reading twice, because it is a design decision about who owns the executor. A Swift side that drives the future, or a Rust side that does, changes how the generated code behaves, and the library has taken a position by making the runtime a feature rather than a hard dependency.

The test dependencies show the same split, with tokio pulled in for the dev tests with the rt-multi-thread, macros and sync features.

Trying it with the codegen visualizer example

The repository contains example applications that exist to be run quickly, and the codegen-visualizer is the one the README walks through.

bash
git clone https://github.com/chinedufn/swift-bridge
cd swift-bridge/examples/codegen-visualizer

open CodegenVisualizer/CodegenVisualizer.xcodeproj

The last command opens the Xcode project, and the instruction that follows is to click the Run button in Xcode. In other words, the example is meant to be run on a Mac with Xcode rather than from the command line, which is consistent with the target audience of iOS and macOS development.

The other examples in the directory cover the situations you are likely to hit. There is an async-functions example, a multiple-bridge-modules example for when one project needs more than one boundary, a rust-binary-calls-swift-package example, and a without-a-bridge-module example that presumably shows the project with nothing to bridge. The examples README is the index.

The integration testing setup at the top level is worth noticing too, with test-swift-packages.sh and test-swift-rust-integration.sh, plus a SwiftRustIntegrationTestRunner directory that is a workspace member. A library that generates code for two languages can only be trusted if the generated code is actually compiled by both toolchains in CI, and that is what these scripts are for.

A 0.1 line with a dual licence and a documentation book

The repository is not archived, and the last push was on 2026-09-05. The version line is still 0.1, with 0.1.59 released on 2026-01-06, 0.1.58 on 2025-12-06 and 0.1.57 on 2024-08-15.

The release history shows two things. The patch numbers are high, which means the project has been iterating on a lot of small things inside a minor version, and the gap between 0.1.57 and 0.1.58 is long. A 0.1 line also means the macros and the generated API can still change shape between patch versions, so pinning an exact version in a Cargo.lock is the sensible habit.

The licence is dual, Apache-2.0/MIT, with both LICENSE-APACHE and LICENSE-MIT files at the top of the tree. The package metadata lists the licence as Apache-2.0/MIT, so a downstream project can pick either. The crate description on crates.io is a useful summary of the goal: generate FFI bindings for safe interop between Rust and Swift.

The keywords in the manifest are swift, ffi, bindings, ios and mac, which is the clearest statement of the intended use: this is for Apple platform projects, where a Rust core and a Swift interface meet.

Editorial conclusion

Adopt swift-bridge when your Swift application needs to call into Rust and you want the FFI layer generated rather than hand written, especially for iOS and macOS targets where a bridge module and a build step fit the toolchain. Do not adopt it for a project that needs the unimplemented parts of the type table, such as borrowed slices, Box, Arc or Swift to Rust closures, because those are documented as not yet implemented. Verify first that your toolchain can run the code generation during the Cargo build, that the async feature is only enabled when you need it since it pulls in tokio, and that the example projects in the examples directory compile before you commit to a design.

Frequently asked questions

What does swift-bridge generate?

Given a Rust module annotated with the swift_bridge::bridge macro, the swift-bridge-build API or the swift-bridge-cli parses it at build time and generates the Swift and C sides of the FFI layer, so neither the Swift bindings nor the C header are maintained by hand.

How do I add swift-bridge to a Cargo project?

Add swift-bridge-build to build-dependencies and swift-bridge to dependencies, both at version 0.1. The build dependency runs the code generation during the Cargo build, while the CLI variant does the same parsing outside the build.

What is the difference between a transparent struct and an opaque type in swift-bridge?

A transparent struct, declared directly in the bridge module, lets both Rust and Swift access the fields, and transparent enums behave the same way. A type declared inside an extern "Rust" or extern "Swift" block is opaque, so the other side only holds a handle and the data stays where it was declared.

Does swift-bridge support async functions?

Yes. The example bridge module includes an async fn on the Swift side. The support is behind the async feature, which is off by default and enables tokio and once_cell when turned on.

Which Rust types are not supported yet by swift-bridge?

The built-in types table marks SwiftArray<T>, the borrowed slice forms &[T] and &mut [T], Box<T>, the boxed closure forms and Arc<T> as not yet implemented. The Box<dyn FnOnce> case supports passing from Rust to Swift, but not from Swift to Rust.

How do I try swift-bridge locally?

The repository has example applications, and the README walks through the codegen-visualizer: clone the repository, change into examples/codegen-visualizer and open CodegenVisualizer/CodegenVisualizer.xcodeproj in Xcode, then run it. Other examples cover async functions, multiple bridge modules and a Rust binary calling a Swift package.

Official sources

  1. chinedufn/swift-bridge on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/chinedufn-swift-bridge.svg)](https://hysenlabs.com/projects/chinedufn-swift-bridge)

Was this useful?