Veloce — A high-performance regex library for Swift, powered by Rust's regex crate via swift-bridge.
Veloce (Italian: "fast") - Because your regex should never be the bottleneck.
Experimental: This project is a playground for exploring Swift-Rust interoperability via FFI. While functional and well-tested, it's primarily a learning exercise in bridging these two languages. Use in production at your own discretion.
| Feature | Veloce | NSRegularExpression | Swift Regex (iOS 16+) |
|---|---|---|---|
| Performance | Fastest | Slowest | Fast |
| ReDoS Protection | Linear time guarantee | Vulnerable | Some protection |
| iOS Version | 13+ | All | 16+ |
| SIMD Optimized | Yes | No | Partial |
| Unicode | Full | Full | Full |
- Up to 15x faster than
NSRegularExpressionfor complex patterns - 1000x+ faster on ReDoS-vulnerable patterns (linear time guarantee)
- SIMD-accelerated literal matching via Rust's
regexcrate - Optimized FFI through
swift-bridgewith minimal overhead
Add Veloce to your Package.swift:
dependencies: [
.package(url: "https://github.com/batuhansk/veloce.git", from: "0.1.0")
]Then add "Veloce" to your target's dependencies:
.target(
name: "YourApp",
dependencies: ["Veloce"]
)Or in Xcode: File → Add Packages → Enter the repository URL.
Note: The package downloads a pre-built XCFramework (~2MB) from GitHub Releases automatically.
import Veloce
// Create a regex
let regex = try Regex(#"\b\w+@\w+\.\w+\b"#)
// Check for matches
if regex.isMatch("Contact: user@example.com") {
print("Found an email!")
}
// Find matches
if let match = regex.firstMatch(in: "Email: test@example.com") {
print("Found: \(match.value)") // "test@example.com"
}
// Find all matches
let emails = regex.allMatches(in: "a@b.c and x@y.z")
print(emails) // ["a@b.c", "x@y.z"]
// Replace
let censored = regex.replacingAll(
"Contact user@example.com or admin@site.org",
with: "[REDACTED]"
)
// "Contact [REDACTED] or [REDACTED]"
// Split
let csv = try Regex(#",\s*"#)
let values = csv.split("a, b, c,d") // ["a", "b", "c", "d"]
// Capture groups
let parser = try Regex(#"(\w+)=(\w+)"#)
let captures = parser.captures(in: "name=John")
if !captures.isEmpty {
print(captures[0]) // "name=John" (full match)
print(captures[1]) // "name"
print(captures[2]) // "John"
}
// Count matches efficiently
let count = regex.matchCount(in: "a@b.c and x@y.z") // 2
// Multi-pattern matching with RegexSet
let keywords = try RegexSet(["error", "warning", "critical"])
if keywords.isMatch("[ERROR] Something went wrong") {
let matched = keywords.matchingPatterns("[ERROR] Something went wrong")
print(matched) // [0] - "error" pattern matched
}public struct Regex: Sendable {
/// Creates a compiled regex from a pattern string
public init(_ pattern: String) throws
/// Returns true if the pattern matches anywhere in the string
public func isMatch(_ string: String) -> Bool
/// Finds the first match
public func firstMatch(in string: String) -> Match?
/// Finds all non-overlapping matches (returns matched strings)
public func allMatches(in string: String) -> [String]
/// Counts matches without extracting them (faster than allMatches().count)
public func matchCount(in string: String) -> Int
/// Replaces the first occurrence
public func replacing(_ string: String, with replacement: String) -> String
/// Replaces all occurrences
public func replacingAll(_ string: String, with replacement: String) -> String
/// Splits the string by the pattern
public func split(_ string: String) -> [String]
/// Extracts all capture groups from the first match
public func captures(in string: String) -> [String]
/// Gets a specific capture group (0 = full match, 1 = first group, etc.)
public func captureGroup(in string: String, group: Int) -> String?
/// Number of capture groups (including full match)
public var captureCount: Int
}public struct RegexSet: Sendable {
/// Creates a compiled regex set from multiple patterns
public init(_ patterns: [String]) throws
/// Returns true if any pattern matches the string
public func isMatch(_ string: String) -> Bool
/// Returns indices of all patterns that matched
public func matchingPatterns(_ string: String) -> [Int]
/// Number of patterns in the set
public var patternCount: Int
}public struct Match: Sendable, Equatable {
/// The matched text
public let value: String
/// Byte range in the original string
public let byteRange: Range<Int>
/// Get the range as String.Index values
public func range(in string: String) -> Range<String.Index>?
}public enum RegexError: Error {
case invalidPattern(String)
}Veloce uses Rust's regex syntax, which is similar to Perl/PCRE but with some differences:
// Basics
try Regex(#"hello"#) // Literal match
try Regex(#"hel+o"#) // One or more 'l'
try Regex(#"hel*o"#) // Zero or more 'l'
try Regex(#"hel?o"#) // Optional 'l'
try Regex(#"[a-z]+"#) // Character class
try Regex(#"\d+"#) // Digit shorthand
try Regex(#"\w+"#) // Word character
try Regex(#"\s+"#) // Whitespace
// Anchors
try Regex(#"^start"#) // Start of string
try Regex(#"end$"#) // End of string
try Regex(#"\bword\b"#) // Word boundary
// Groups
try Regex(#"(a|b)"#) // Alternation
try Regex(#"(\w+)"#) // Capture group
try Regex(#"(?:\w+)"#) // Non-capturing group
try Regex(#"(?<name>\w+)"#) // Named capture group
// Flags (inline)
try Regex(#"(?i)hello"#) // Case insensitive
try Regex(#"(?m)^line"#) // Multiline mode
try Regex(#"(?s)a.b"#) // Dot matches newlineNote: Lookahead/lookbehind assertions (
(?=...),(?!...),(?<=...),(?<!...)) are not supported by the Rust regex crate for performance reasons (they would break the linear time guarantee).
For full syntax documentation, see the Rust regex crate docs.
Unlike most regex engines, Veloce is immune to ReDoS attacks thanks to Rust's regex crate which guarantees linear time matching:
// This pattern causes exponential backtracking in many engines
let evil = try Regex(#"(a+)+b"#)
// With 30 'a's, this would hang NSRegularExpression for hours
// Veloce completes in microseconds
let input = String(repeating: "a", count: 30)
_ = evil.isMatch(input) // Fast!This makes Veloce safe to use with untrusted patterns or inputs.
- Xcode 15+ with Swift 5.9+
- Rust toolchain (install from rustup.rs)
- iOS/macOS SDK
# Clone the repository
git clone https://github.com/batuhansk/veloce.git
cd veloce
# Build the XCFramework (includes generating Swift bindings)
./scripts/build-xcframework.sh
# For local development, update Package.swift to use local binary:
# Comment out the URL-based binaryTarget and uncomment the path-based one
# Run tests
swift test
# Run benchmarks (use release mode for accurate results)
swift test -c release --filter Benchmark- Rust compilation:
cargo buildcompiles the Rust library for each Apple platform - swift-bridge generation: During
cargo build, thebuild.rsscript invokes swift-bridge to:- Parse
#[swift_bridge::bridge]macros inrust/src/lib.rs - Generate Swift bindings in
rust/generated/ - Generate C headers for the FFI layer
- Parse
- Copy bindings: The build script copies generated
.swiftfiles toSources/Veloce/Generated/ - XCFramework creation:
xcodebuild -create-xcframeworkpackages the libraries
If you only need to regenerate the Swift bindings without a full rebuild:
./scripts/generate-bindings.shFor maintainers preparing a new release:
# 1. Prepare the release (builds XCFramework, creates zip, computes checksum)
./scripts/prepare-release.sh 0.1.0
# 2. Update Package.swift with the checksum from the script output
# 3. Commit, tag, and push
git add -A
git commit -m "Release 0.1.0"
git tag 0.1.0
git push origin main --tags
# 4. Create GitHub Release and upload VeloceCore.xcframework.zip- Device: MacBook Pro
- Chip: Apple M2 Max (12 cores: 8 performance + 4 efficiency)
- Memory: 32 GB
- OS: macOS Tahoe 26.0
| Benchmark | Veloce | NSRegularExpression | Speedup |
|---|---|---|---|
| Simple literal (1K matches) | 0.49ms | 0.69ms | 1.4x |
| Email validation (10K emails) | 1.4ms | 7.0ms | 5.0x |
| Find all in 60KB text | 2.7ms | 27.5ms | 10.2x |
| Log parsing with captures | 0.31ms | 4.6ms | 14.8x |
| Replace all digits | 1.8ms | 1.8ms | ~1.0x |
| Split CSV (1K values) | 5.6ms | 21.2ms | 3.8x |
| ReDoS pattern (30 'a's) | 0.17ms | 191ms* | 1124x |
| Batch vs Individual captures | 0.68ms | 3.6ms | 5.3x |
*NSRegularExpression tested with only 15 'a's (would hang with 30)
- Log parsing shows the biggest improvement (14.8x) due to Rust's optimized capture group handling
- Find all is 10x faster thanks to byte-range FFI optimization (avoids string allocations)
- ReDoS resistance is the standout feature (1124x faster) - Veloce handles pathological patterns in microseconds while NSRegularExpression exhibits exponential backtracking
- Split is 3.8x faster thanks to optimized byte-range FFI (was 0.8x slower before optimization)
- Batch captures are 5.3x faster than extracting groups individually
# Run in release mode for accurate results
swift test -c release --filter BenchmarkNote: Results vary by hardware. Run on your own device for accurate comparisons.
// Before (NSRegularExpression)
let pattern = #"\d+"#
let regex = try NSRegularExpression(pattern: pattern)
let range = NSRange(text.startIndex..., in: text)
if regex.firstMatch(in: text, range: range) != nil {
print("Found!")
}
// After (Veloce)
let regex = try Regex(#"\d+"#)
if regex.isMatch(text) {
print("Found!")
}
// Bonus: Multi-pattern matching (no NSRegularExpression equivalent)
let keywords = try RegexSet(["error", "warning", "critical"])
if keywords.isMatch(logLine) {
// Check which patterns matched
let indices = keywords.matchingPatterns(logLine)
}Regex and RegexSet are both Sendable and can be safely shared across threads. Pattern compilation is expensive, so compile once and reuse:
// Good: Compile once
let emailRegex = try Regex(#"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"#)
await withTaskGroup(of: Bool.self) { group in
for email in emails {
group.addTask {
emailRegex.isMatch(email) // Safe concurrent access
}
}
}MIT License - see LICENSE for details.
- regex - The Rust regex crate
- swift-bridge - Zero-overhead Rust/Swift FFI