Using Rust Crates in Swift Packages

Published on May 30, 2026

Using Rust Crates in Swift Packages

Rust is a good fit for performance-sensitive, security-sensitive, or already well-tested core logic. Swift is a good fit for Apple platform apps and frameworks. The friction is that Swift Package Manager cannot compile a Cargo crate directly.

The practical way to use Rust from a Swift package is to add one explicit bridge layer:

  1. Put the Rust code behind a small C-compatible API.
  2. Build that Rust bridge crate as a static library.
  3. Package the static libraries and C headers as an XCFramework.
  4. Import the XCFramework into SwiftPM as a binary target.
  5. Wrap the C API in a normal Swift target.

This post uses an adblock-rust wrapper as the running example, but the same shape works for any Rust crate: parsers, crypto helpers, sync engines, compression libraries, document processors, rules engines, or your own Rust business logic.

The Shape

A Swift package that wraps Rust usually looks like this:

Package.swift
Sources/MyRustWrapper/MyRustWrapper.swift
Native/my-rust-ffi/Cargo.toml
Native/my-rust-ffi/src/lib.rs
include/MyRustFFI.h
include/module.modulemap
Scripts/build-xcframework.sh
Scripts/package-release.sh
Examples/MyExampleApp/

The names are not important. The roles are:

  • Native/my-rust-ffi is a Rust crate that depends on the Rust crate you actually want to use.
  • include/MyRustFFI.h is the public C header that describes the exported Rust functions.
  • include/module.modulemap gives the C header a module name Swift can import.
  • Scripts/build-xcframework.sh cross-compiles the Rust static library for Apple platforms and creates an XCFramework.
  • Package.swift exposes that XCFramework as a SwiftPM binary target.
  • Sources/MyRustWrapper contains the Swift API your app developers should actually use.
  • Examples/ is optional, but it is useful for proving the package works in a real app.

In the running adblock example, these names are concrete:

Native/adblock-rust-ffi/
include/AdblockRustFFI.h
include/module.modulemap
Artifacts/CAdblockRust.xcframework
Sources/AdblockRust/AdblockEngine.swift
Examples/AdblockWebViewApp/

You can replace adblock-rust with your own crate and keep the architecture.

Prerequisites

For Apple platforms, you need:

  • macOS with Xcode or the Xcode command line tools.
  • SwiftPM, which ships with Swift and Xcode.
  • Rust installed through rustup.
  • A Rust toolchain new enough for your crate and dependencies.

The build script for the running example uses lipo and xcrun xcodebuild, so it needs Apple's command line tools. Its Rust FFI crate declares rust-version = "1.86", so stable must be at least that new.

You should also decide which Apple platforms your Swift package supports. For example:

platforms: [
  .iOS(.v13),
  .macOS(.v11),
]

Those platform declarations live in Package.swift and should match the slices you build into the XCFramework.

Step 1: Create a Rust FFI Crate

Do not expose a large Rust API directly to Swift. Instead, create a small Rust crate whose job is to translate between Rust types and a C ABI.

Its Cargo.toml should build a static library:

[package]
name = "my-rust-ffi"
version = "0.1.0"
edition = "2021"
publish = false
 
[lib]
name = "my_rust_ffi"
crate-type = ["staticlib"]

Then depend on the Rust crate you want to use:

[dependencies]
some_rust_crate = "1.0"

In the ad blocking example, the bridge crate depends on Brave's adblock-rust:

[dependencies]
adblock = { git = "https://github.com/brave/adblock-rust", default-features = true, features = ["content-blocking", "css-validation", "full-regex-handling"] }
serde_json = "1.0"

For distributable binaries, tune the release profile:

[profile.release]
lto = true
codegen-units = 1
strip = true

That trades build time for a smaller optimized static library, which matters when the result is zipped and downloaded by SwiftPM.

Step 2: Export a C ABI from Rust

Swift can import C APIs. Rust can export C-callable functions. The bridge crate connects those two facts.

A common pattern is to keep Rust-owned state behind an opaque pointer:

pub struct MyEngine {
    inner: some_rust_crate::Engine,
}

Then export constructors and destructors:

#[no_mangle]
pub extern "C" fn my_engine_new() -> *mut MyEngine {
    Box::into_raw(Box::new(MyEngine {
        inner: some_rust_crate::Engine::new(),
    }))
}
 
#[no_mangle]
pub unsafe extern "C" fn my_engine_destroy(engine: *mut MyEngine) {
    if !engine.is_null() {
        drop(Box::from_raw(engine));
    }
}

The important parts are:

  • extern "C" gives the function a C calling convention.
  • #[no_mangle] keeps the exported symbol name predictable.
  • Box::into_raw passes ownership to the caller.
  • Box::from_raw takes ownership back so Rust can free the value.

For inputs and outputs, keep the boundary simple. Prefer:

  • bool, fixed-width integers, and size_t
  • raw pointers plus explicit lengths
  • plain #[repr(C)] structs
  • explicit free functions for Rust-allocated memory

For example, the adblock wrapper accepts UTF-8 bytes and returns an opaque engine:

#[no_mangle]
pub unsafe extern "C" fn abr_engine_from_rules(
    rules: *const u8,
    rules_len: usize,
    error_message: *mut *mut c_char,
) -> *mut AbrEngine {
    // Validate pointers, decode UTF-8, build the Rust engine, return a pointer.
}

When Rust returns strings or buffers, Rust should also expose the cleanup function:

#[no_mangle]
pub unsafe extern "C" fn my_free_string(value: *mut c_char) {
    if !value.is_null() {
        drop(CString::from_raw(value));
    }
}

Do not make Swift guess how Rust memory should be released. If Rust allocates it, Rust should free it.

Step 3: Write the C Header

Swift imports the Rust library through a C header, so the header must match the exported Rust ABI.

For the generic engine above, the header would look like this:

#ifndef MY_RUST_FFI_H
#define MY_RUST_FFI_H
 
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
 
#ifdef __cplusplus
extern "C" {
#endif
 
typedef struct MyEngine MyEngine;
 
MyEngine* my_engine_new(void);
void my_engine_destroy(MyEngine* engine);
 
#ifdef __cplusplus
}
#endif
 
#endif

The adblock example adds functions for creating an engine from rules, matching requests, serializing the engine, and converting filter lists into WebKit content blocker JSON:

typedef struct AbrEngine AbrEngine;
 
AbrEngine* abr_engine_from_rules(const uint8_t* rules,
                                 size_t rules_len,
                                 char** error_message);
void abr_engine_destroy(AbrEngine* engine);

The header should stay boring. Avoid Rust-specific types, Swift-specific types, generics, callbacks, and exceptions at the boundary.

Step 4: Add a Module Map

SwiftPM imports C code through Clang modules. Add a module.modulemap beside the header:

module CMyRustLibrary {
  header "MyRustFFI.h"
  export *
}

The module name is what Swift imports:

import CMyRustLibrary

In the adblock example, the C module is named CAdblockRust:

module CAdblockRust {
  header "AdblockRustFFI.h"
  export *
}

The C prefix is a useful convention. It separates the low-level C module from the higher-level Swift module, such as AdblockRust.

Step 5: Build Rust for Apple Targets

An XCFramework can contain multiple platform and architecture slices. For iOS, iOS Simulator, and macOS, a common set of Rust targets is:

aarch64-apple-ios
aarch64-apple-ios-sim
x86_64-apple-ios
aarch64-apple-darwin
x86_64-apple-darwin

Install them with rustup:

rustup toolchain install stable \
  --target aarch64-apple-ios \
  --target aarch64-apple-ios-sim \
  --target x86_64-apple-ios \
  --target aarch64-apple-darwin \
  --target x86_64-apple-darwin

Then build the Rust crate for each target:

cargo +stable build \
  --manifest-path Native/my-rust-ffi/Cargo.toml \
  --release \
  --target aarch64-apple-ios

Repeat that for each platform target you plan to support.

For simulator and macOS, you may need to combine Apple Silicon and Intel libraries with lipo:

lipo -create \
  Native/my-rust-ffi/target/aarch64-apple-ios-sim/release/libmy_rust_ffi.a \
  Native/my-rust-ffi/target/x86_64-apple-ios/release/libmy_rust_ffi.a \
  -output .build/libmy_rust_ffi_simulator.a

Then create the XCFramework:

xcrun xcodebuild -create-xcframework \
  -library Native/my-rust-ffi/target/aarch64-apple-ios/release/libmy_rust_ffi.a -headers include \
  -library .build/libmy_rust_ffi_simulator.a -headers include \
  -library .build/libmy_rust_ffi_macos.a -headers include \
  -output Artifacts/CMyRustLibrary.xcframework

In a real project, put those commands in a script. The running example does that in Scripts/build-xcframework.sh, which creates:

Artifacts/CAdblockRust.xcframework

That XCFramework is the bridge between Cargo and SwiftPM.

Step 6: Add the Binary Target to SwiftPM

Your Package.swift needs two layers:

  1. A binary target for the XCFramework.
  2. A Swift target that wraps the binary target.

For local development, SwiftPM can point directly at a local XCFramework:

let cRustTarget: Target
if let localPath = ProcessInfo.processInfo.environment["MY_RUST_XCFRAMEWORK_PATH"] {
  cRustTarget = .binaryTarget(
    name: "CMyRustLibrary",
    path: localPath
  )
} else {
  cRustTarget = .binaryTarget(
    name: "CMyRustLibrary",
    url: "https://github.com/example/my-rust-swift/releases/download/0.1.0/CMyRustLibrary.xcframework.zip",
    checksum: "..."
  )
}

Then expose a normal Swift library product:

let package = Package(
  name: "MyRustLibrary",
  platforms: [
    .iOS(.v13),
    .macOS(.v11),
  ],
  products: [
    .library(name: "MyRustLibrary", targets: ["MyRustLibrary"])
  ],
  targets: [
    cRustTarget,
    .target(
      name: "MyRustLibrary",
      dependencies: ["CMyRustLibrary"]
    )
  ]
)

The adblock example uses the same split:

  • CAdblockRust: the binary XCFramework target.
  • AdblockRust: the Swift wrapper target.

For local development, build the XCFramework and point SwiftPM at it:

./Scripts/build-xcframework.sh
MY_RUST_XCFRAMEWORK_PATH=Artifacts/CMyRustLibrary.xcframework swift build

If you use a relative path, make sure it is valid from the package being built. Absolute paths are less surprising when building from nested examples or app projects.

Step 7: Wrap the C API in Swift

The Swift wrapper should be the only part of your app-facing API that imports the C module:

import CMyRustLibrary

Keep raw pointers private:

public final class MyEngine {
  private let raw: OpaquePointer
 
  public init() {
    raw = my_engine_new()
  }
 
  deinit {
    my_engine_destroy(raw)
  }
}

Then translate C-style inputs and outputs into Swift types: String, Data, URL, Error, enums, structs, and async APIs where appropriate.

The adblock example wraps raw C functions in an AdblockEngine class:

let rules = "||example.com^"
let engine = try AdblockEngine(rules: rules)
 
let blocked = engine.shouldBlock(
  requestURL: URL(string: "https://example.com/ad.js")!,
  sourceURL: URL(string: "https://site.test")!,
  resourceType: .script
)

Internally, the Swift wrapper converts strings to UTF-8 bytes, calls the C API, turns returned pointers into Swift values, and releases Rust-owned memory with the cleanup functions exported by Rust.

For APIs that can return partial results, surface that explicitly. In the adblock example, the content blocker conversion returns both JSON and a truncated flag because WebKit content blocker rules are capped in the Rust bridge:

let converted = try AdblockEngine.contentBlockingRules(fromFilterSet: rules)
print(converted.json)
print(converted.truncated)

Step 8: Test It in a Real App

A minimal example app is worth the effort because it tests the full path:

  • SwiftPM resolves the package.
  • The binary target loads.
  • The module map is valid.
  • The Swift wrapper links.
  • The Rust code runs on an actual Apple platform.

The running example includes a macOS WebKit app. It depends on the package by path:

dependencies: [
  .package(path: "../..")
]

Then its executable target depends on the Swift product:

.product(name: "AdblockRust", package: "adblock-rust-for-swift")

For your own project, the example app might be a tiny command-line executable, a macOS app, or an iOS sample. The important part is that it imports the Swift wrapper, not the raw C module.

Step 9: Publish the XCFramework

SwiftPM binary targets use zipped XCFrameworks with checksums.

Create the zip:

ditto -c -k --sequesterRsrc --keepParent \
  Artifacts/CMyRustLibrary.xcframework \
  Release/CMyRustLibrary.xcframework.zip

Compute the checksum:

swift package compute-checksum Release/CMyRustLibrary.xcframework.zip

Upload the zip to a release, then update Package.swift:

.binaryTarget(
  name: "CMyRustLibrary",
  url: "https://github.com/example/my-rust-swift/releases/download/0.1.0/CMyRustLibrary.xcframework.zip",
  checksum: "..."
)

The running example has a Scripts/package-release.sh helper that performs the zip and checksum steps for CAdblockRust.xcframework. If you write a similar script, remember that the version or tag argument should match the release URL exactly. If your GitHub tag is v0.1.0, use v0.1.0; if it is 0.1.0, use 0.1.0.

Do not replace the zip behind an existing release URL unless you also update the checksum and retag appropriately. SwiftPM treats the checksum as part of the binary artifact contract.

What to Keep Stable

The most important design choice is to keep the boundary narrow:

  • Rust owns the Rust dependency and complex internal behavior.
  • C owns the stable ABI.
  • The XCFramework owns Apple architecture packaging.
  • SwiftPM owns package distribution.
  • Swift owns the ergonomic app-facing API.

For your own Rust crate, the checklist is:

  1. Add a small Rust FFI crate with crate-type = ["staticlib"].
  2. Export extern "C" functions with #[no_mangle].
  3. Use opaque pointers for Rust-owned state.
  4. Use pointer-plus-length inputs for strings and buffers.
  5. Add explicit Rust cleanup functions for every Rust allocation passed to Swift.
  6. Write a C header matching the exported ABI.
  7. Add a module.modulemap.
  8. Cross-compile static libraries for the Apple targets you support.
  9. Create an XCFramework with headers.
  10. Add a SwiftPM .binaryTarget.
  11. Wrap the C module in a Swift target.
  12. Publish a zipped XCFramework and update the checksum.

Once that structure is in place, your Swift users can work with a normal Swift API while the implementation remains powered by Rust.