This guide explains how to work on the Rust FFI code alongside the Swift SDK.
The SDK uses a pre-built XCFramework (libzcashlc) for the Rust FFI layer. For most SDK development, you don't need to rebuild the FFI — SPM automatically downloads the pre-built binary from GitHub Releases.
However, if you need to modify the Rust code in rust/, you'll need to set up local FFI development.
Package.swift automatically detects the presence of LocalPackages/Package.swift (created by the init script). When it exists, the SDK builds against your locally-built FFI instead of downloading the release binary. When it doesn't exist, the release binary is used as usual.
This means switching modes is as simple as:
- Enable local FFI:
./Scripts/init-local-ffi.sh - Disable local FFI:
rm -rf LocalPackages/(or./Scripts/reset-local-ffi.sh)
No manual Package.swift edits are needed.
-
Rust toolchain — Install via rustup:
curl --proto '=https' --tlsv1.3 -sSf https://sh.rustup.rs | sh
-
Apple platform targets — Install the required Rust targets:
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios rustup target add aarch64-apple-darwin x86_64-apple-darwin
You must run init-local-ffi.sh before opening the project in Xcode. Without it, SPM will attempt to download the release binary, which may not exist for development branches.
# Clone the repository
git clone https://github.com/zcash/zcash-swift-wallet-sdk
cd zcash-swift-wallet-sdk
# Initialize local FFI (builds from source)
./Scripts/init-local-ffi.shThe --cached flag downloads a pre-built release instead of building from source. This only works when Package.swift points to a published release:
./Scripts/init-local-ffi.sh --cachedWarning: Only use --cached if there have been no FFI changes on your branch since the last release. Using a stale pre-built binary with modified Swift bindings could cause silent data corruption and loss of funds. Additionally, --cached skips the Rust build entirely, so the first call to rebuild-local-ffi.sh will be a full (non-incremental) build.
You can open the project two ways:
- Workspace (recommended for FFI development) — includes the FFIBuilder target that automatically rebuilds the FFI when you build in Xcode:
open ZcashSDK.xcworkspace
- Package directly — simpler, but you'll need to run
rebuild-local-ffi.shmanually after Rust changes:open Package.swift
If Xcode was already open before you ran init-local-ffi.sh, reset package caches: File > Packages > Reset Package Caches.
# 1. Edit Rust code
vim rust/src/lib.rs
# 2. Fast incremental rebuild (seconds, not minutes!)
./Scripts/rebuild-local-ffi.sh # iOS Simulator (default)
./Scripts/rebuild-local-ffi.sh ios-device # iOS Device
./Scripts/rebuild-local-ffi.sh macos # macOS
# 3. Build/test in Xcode
# Clean build folder if Xcode doesn't pick up changes: Cmd+Shift+K./Scripts/reset-local-ffi.shIf using Xcode, you may also need to reset package caches: File > Packages > Reset Package Caches.
One-time setup that creates the local development environment.
./Scripts/init-local-ffi.sh # Build from source (recommended)
./Scripts/init-local-ffi.sh --cached # Download pre-built releaseThis script:
- Builds the full XCFramework (all 5 architectures) or downloads a pre-built one
- Creates
LocalPackages/with an SPM wrapper package Package.swiftautomatically detectsLocalPackages/and switches to local mode
Fast incremental rebuild for the current development target. Requires init-local-ffi.sh to have been run first.
./Scripts/rebuild-local-ffi.sh [target]Targets:
ios-sim(default) — iOS Simulator, auto-detects arm64 vs x86_64ios-device— iOS Device (arm64)macos— macOS, auto-detects arm64 vs x86_64
Why it's fast: Only builds ONE architecture, and Cargo's incremental compilation means small changes rebuild in seconds.
Note: This creates a single-architecture build. Run init-local-ffi.sh before submitting PRs to verify all architectures compile.
Removes LocalPackages/ and switches back to the release binary.
./Scripts/reset-local-ffi.shThe XCFramework contains three platform slices:
ios-arm64— iOS devicesios-arm64_x86_64-simulator— iOS Simulator (universal)macos-arm64_x86_64— macOS (universal)
| Development Target | Rust Target | XCFramework Slice |
|---|---|---|
| iOS Simulator (Apple Silicon) | aarch64-apple-ios-sim |
ios-arm64_x86_64-simulator |
| iOS Simulator (Intel) | x86_64-apple-ios |
ios-arm64_x86_64-simulator |
| iOS Device | aarch64-apple-ios |
ios-arm64 |
| macOS (Apple Silicon) | aarch64-apple-darwin |
macos-arm64_x86_64 |
| macOS (Intel) | x86_64-apple-darwin |
macos-arm64_x86_64 |
The LocalPackages directory contains a Swift package named libzcashlc with the same product name as the binary target in Package.swift. When Package.swift detects that LocalPackages/Package.swift exists, it adds LocalPackages as a path dependency and uses it instead of the .binaryTarget declaration. This switching is automatic — no manual edits to Package.swift are needed.
The shared ZcashLightClientKit scheme in ZcashSDK.xcworkspace includes FFIBuilder as a build dependency. FFIBuilder runs rebuild-local-ffi.sh with the appropriate platform based on your selected destination, so Rust code is automatically recompiled when you build in Xcode.
Note: The FFIBuilder target requires init-local-ffi.sh to have been run first — it calls rebuild-local-ffi.sh, which expects LocalPackages/ to exist.
| Approach | Best for |
|---|---|
Manual script (rebuild-local-ffi.sh) |
Occasional FFI changes, simple setup |
| FFIBuilder target in workspace | Frequent FFI changes, prefer staying in Xcode |
This means LocalPackages/ doesn't exist and SPM is trying to download the release binary. Run ./Scripts/init-local-ffi.sh to set up local development, then reset package caches in Xcode: File > Packages > Reset Package Caches.
- Clean the build folder: Cmd+Shift+K
- If that doesn't work, reset package caches: File > Packages > Reset Package Caches
- If that doesn't work, close Xcode and delete DerivedData:
rm -rf ~/Library/Developer/Xcode/DerivedData
Ensure all Rust targets are installed:
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
rustup target add aarch64-apple-darwin x86_64-apple-darwinThe headers are regenerated during cargo build. If you see stale headers:
rm -rf target/Headers
./Scripts/rebuild-local-ffi.shAfter running init-local-ffi.sh or reset-local-ffi.sh, Xcode may need to re-resolve packages:
- File > Packages > Reset Package Caches
- If that doesn't help, close and reopen the workspace
When opening ZcashSDK.xcworkspace for the first time after running init-local-ffi.sh, FFIBuilder may fail with "Command PhaseScriptExecution failed with a nonzero exit code". This is a timing issue -- Xcode may attempt to build FFIBuilder before package resolution has completed. Run "Product > Build For > Testing" manually and the build should succeed. Subsequent builds will work normally.
The Makefile (used by init-local-ffi.sh) and rebuild-local-ffi.sh invoke cargo with slightly different environment variables, which can cause Cargo to invalidate its build cache. This means the first rebuild-local-ffi.sh after init-local-ffi.sh (or vice versa) may do a full rebuild. Subsequent incremental rebuilds within the same tool will be fast.
The scripts source ~/.cargo/env to find the Rust toolchain. If you installed Rust via a non-standard method (e.g., Homebrew, Nix), you may need to ensure cargo and rustup are on the default PATH or add the appropriate source/export to ~/.zprofile.
Before submitting a PR that modifies Rust code:
# Full rebuild to verify all architectures compile
./Scripts/init-local-ffi.sh
# Run tests
swift test