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:
- Put the Rust code behind a small C-compatible API.
- Build that Rust bridge crate as a static library.
- Package the static libraries and C headers as an XCFramework.
- Import the XCFramework into SwiftPM as a binary target.
- 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-ffiis a Rust crate that depends on the Rust crate you actually want to use.include/MyRustFFI.his the public C header that describes the exported Rust functions.include/module.modulemapgives the C header a module name Swift can import.Scripts/build-xcframework.shcross-compiles the Rust static library for Apple platforms and creates an XCFramework.Package.swiftexposes that XCFramework as a SwiftPM binary target.Sources/MyRustWrappercontains 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 = trueThat 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_rawpasses ownership to the caller.Box::from_rawtakes ownership back so Rust can free the value.
For inputs and outputs, keep the boundary simple. Prefer:
bool, fixed-width integers, andsize_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
#endifThe 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 CMyRustLibraryIn 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-darwinInstall 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-darwinThen build the Rust crate for each target:
cargo +stable build \
--manifest-path Native/my-rust-ffi/Cargo.toml \
--release \
--target aarch64-apple-iosRepeat 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.aThen 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.xcframeworkIn a real project, put those commands in a script. The running example does that
in Scripts/build-xcframework.sh, which creates:
Artifacts/CAdblockRust.xcframeworkThat XCFramework is the bridge between Cargo and SwiftPM.
Step 6: Add the Binary Target to SwiftPM
Your Package.swift needs two layers:
- A binary target for the XCFramework.
- 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 buildIf 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 CMyRustLibraryKeep 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.zipCompute the checksum:
swift package compute-checksum Release/CMyRustLibrary.xcframework.zipUpload 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:
- Add a small Rust FFI crate with
crate-type = ["staticlib"]. - Export
extern "C"functions with#[no_mangle]. - Use opaque pointers for Rust-owned state.
- Use pointer-plus-length inputs for strings and buffers.
- Add explicit Rust cleanup functions for every Rust allocation passed to Swift.
- Write a C header matching the exported ABI.
- Add a
module.modulemap. - Cross-compile static libraries for the Apple targets you support.
- Create an XCFramework with headers.
- Add a SwiftPM
.binaryTarget. - Wrap the C module in a Swift target.
- 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.