StringZilla for Swift
June 30, 2026 · View on GitHub
StringZilla is a Foundation-free Swift package that runs everywhere a Swift toolchain does, not just on Apple platforms.
Its SIMD-accelerated string kernels are built directly on the C core, so the same search, comparison, hashing, and Unicode-aware methods work identically on Linux servers and embedded targets as they do on macOS, iOS, tvOS, watchOS, and visionOS.
There is no dependency on Foundation, Darwin, or any Apple-only runtime, which keeps binaries small and portable.
The package adds these methods directly to String, String.UTF8View, and Substring.UTF8View, all operating on the underlying UTF-8 bytes without intermediate copies.
All operations are exposed through the StringZillaViewable protocol, which every supported string type conforms to.
Search methods return native Swift String.Index values or a Range<Index>, so results splice cleanly back into your strings.
Installation
Add StringZilla as a Swift Package Manager dependency in your Package.swift.
let package = Package(
name: "MyApp",
dependencies: [
.package(url: "https://github.com/ashvardanian/StringZilla.git", from: "4.0.0")
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "StringZilla", package: "StringZilla")
]
)
]
)
SwiftPM is the only build path on every platform, and it is the same swift build on Linux and embedded targets as on Apple hardware.
Linux needs no special configuration: the package compiles the C kernels directly and links against Glibc instead of Darwin automatically.
Then import the module where you need it.
import StringZilla
The product name is StringZilla; importing it pulls in both the Swift extension and the underlying C kernels.
Searching and Counting
Substring search returns the Index? of the first or last match, or nil when the needle is absent.
let haystack = "Hello, world! Hello, Swift!"
let first = haystack.findFirst(substring: "Hello") // Index of position 0
let last = haystack.findLast(substring: "Hello") // Index of the second "Hello"
assert(first == haystack.startIndex)
assert(haystack.findFirst(substring: "Rust") == nil)
Byte-set search locates the first or last byte that belongs to, or is excluded from, a set of characters. The character set is itself any string-like value, treated as a bag of bytes.
let text = " trim me "
let firstNonSpace = text.findFirst(characterNotFrom: " ") // first non-blank byte
let lastNonSpace = text.findLast(characterNotFrom: " ") // last non-blank byte
let firstVowel = text.findFirst(characterFrom: "aeiou") // first vowel
let lastVowel = text.findLast(characterFrom: "aeiou") // last vowel
All six finders are generic over the needle type, so you can search a String for a String.UTF8View needle and vice versa.
Comparison and Equality
Comparisons are SIMD-accelerated and return a StringZillaOrdering of .ascending, .equal, or .descending, or a Bool.
assert("apple".compare("banana") == .ascending) // byte-order lexicographic
assert("abc".equals("abc")) // byte-level equality
utf8UncasedOrder(_:) performs the same ordering but with full Unicode case folding, so "STRASSE" and "straße" compare as equal.
assert("STRASSE".utf8UncasedOrder("straße") == .equal)
Unicode Case Folding and Normalization
utf8UncasedFind(substring:) performs a case-insensitive search using full Unicode folding and returns a byte-accurate Range<Index>?.
The matched length can differ from the needle length, since folding can change byte counts.
if let range = "Grüße".utf8UncasedFind(substring: "GRÜSSE") {
print("matched", "Grüße"[range])
}
For repeated case-insensitive searches with the same needle, build a Utf8UncasedNeedle once and reuse it.
The needle caches its precomputed metadata, but is not safe for concurrent use.
let needle = Utf8UncasedNeedle("hello")
let r = needle.findFirst(in: "Say HELLO to the world")
assert(r != nil)
utf8UncasedFoldedBytes() returns the fully case-folded UTF-8 bytes, which may be longer than the input since "ß" folds to "ss".
let folded = "Straße".utf8UncasedFoldedBytes() // [UInt8] of "strasse"
assert(folded == Array("strasse".utf8))
Normalization is driven by StringZillaNormalizationForm, one of .nfc, .nfd, .nfkc, or .nfkd.
utf8Normalized(_:) returns the normalized UTF-8 bytes and defaults to .nfc, utf8NormalizationViolation(_:) returns the Index? of the first non-conforming byte, and isUtf8Normalized(_:) is a convenience Bool.
let nfc = "e\u{0301}".utf8Normalized(.nfc) // composed "é" bytes
assert("café".isUtf8Normalized(.nfc)) // already composed
let fi = "\u{FB01}".utf8Normalized(.nfkc) // ligature "fi" → "fi" bytes
assert(fi == Array("fi".utf8))
let bad = "e\u{0301}".utf8NormalizationViolation(.nfc) // Index of the violation
assert(bad != nil)
Splitting and Segmentation
These methods return byte-accurate [Range<Index>] arrays you can subscript back into the source string.
utf8Words() splits into UAX-29 words that tile the input, so every byte belongs to exactly one word.
for range in "Hello, 世界!".utf8Words() { // tiles the Latin run and the CJK run
print("Hello, 世界!"[range])
}
utf8Lines(skipEmpty:) splits on the Unicode line-break characters and CRLF, and utf8Tokens(skipEmpty:) splits on the 25 Unicode White_Space characters.
Both keep empty segments by default under the cross-language KEEP policy; pass skipEmpty: true to drop them.
With N delimiters you get N+1 segments, so "a\n\nb\n".utf8Lines() yields four ranges: "a", "", "b", and "".
let lines = "a\n\nb\n".utf8Lines() // 4 ranges, including empties
assert(lines.count == 4)
let words = " hi there ".utf8Tokens(skipEmpty: true) // ["hi", "there"]
assert(words.count == 2)
Hashing and Checksums
hash(seed:) computes a fast 64-bit UInt64 hash of the content, with an optional seed.
let h = "the quick brown fox".hash()
let seeded = "the quick brown fox".hash(seed: 42)
assert(h != seeded) // a different seed yields a different hash
For data arriving in chunks, StringZillaHasher hashes incrementally.
update(_:) is chainable, finalize() and its alias digest() return the UInt64 without consuming the state, and reset(seed:) restarts it.
let hasher = StringZillaHasher(seed: 0)
hasher.update("the quick ").update("brown fox")
let digest = hasher.finalize()
assert(digest == "the quick brown fox".hash())
sha256() returns the SHA-256 digest of the content as a 32-byte [UInt8].
let sum = "hello".sha256() // [UInt8] of length 32
assert(sum.count == 32)
The streaming StringZillaSha256 mirrors the incremental hasher.
update(_:) accepts either a string view or a [UInt8], finalize() and its alias digest() return the 32-byte digest, hexdigest() returns the 64-character lowercase hex string, and reset() restarts it.
let sha = StringZillaSha256()
sha.update("hello, ").update("world")
let hex = sha.hexdigest() // 64-char hex string
assert(hex.count == 64)