Search code examples
swiftmultithreadingcore-foundation

CFAttributedString thread safety


Is this code showing that CFAttributedString isn't thread safe? Or am I doing something wrong in the setup?

I thought that CFAttributedString was safe to read from multiple threads, but I see crashes in this code every few runs.

@IBAction func testIt(_ sender: Any?) {
    let testString = "Hello world! Lets make this a bit longerrrrrrrrrrrrrrrr." as CFString
    let testStringLength = CFStringGetLength(testString)

    let testAttributedString = CFAttributedStringCreateMutable(kCFAllocatorDefault, testStringLength)
    CFAttributedStringReplaceString(testAttributedString, CFRange(location: 0, length: 0), testString)
    CFAttributedStringEndEditing(testAttributedString)
    for i in 0..<testStringLength {
        let range = CFRange(location: i, length: 1)
        let keyAndValue = "\(i)" as CFString
        CFAttributedStringSetAttribute(testAttributedString, range, keyAndValue, keyAndValue)
    }

    let immutableTestAttributedString = CFAttributedStringCreateCopy(kCFAllocatorDefault, testAttributedString)
    DispatchQueue.concurrentPerform(iterations: 100) { _ in
        var index: CFIndex = 0
        var effectiveRange: CFRange = CFRange(location: 0, length: 0)
        while index < testStringLength {
            // Crash happens here EXC_BAD_ACCESS (code=1, address=0x24)
            let _ = CFAttributedStringGetAttributes(immutableTestAttributedString, index, &effectiveRange)
            index = effectiveRange.location + effectiveRange.length
        }
    }
}

Solution

  • let testString = "Hello world! Lets make this a bit longerrrrrrrrrrrrrrrr." as CFString
    

    This is a Swift string masquerading as a CFString, which is about two layers of indirection and invokes a very different code path down in the guts (whether that should still work or not is between you and a radar).

    Try creating a proper CFString and see if it works the way you expect.

    var bytes = Array("Hello world! Lets make this a bit longerrrrrrrrrrrrrrrr.".utf16)
    let testString = CFStringCreateWithCharacters(nil, &bytes, bytes.count)
    

    (Of course I highly recommend doing all this work in NSAttributedString rather than CFAttributedString. The Swift->Foundation bridging is much simpler and is used constantly compared to the Swift->Foundation->CoreFoundation bridging. This may just be a bug in that bridging, but your world is still going to be a lot easier if you avoid it.)


    While I haven't been able to reproduce the problem w/ a pure CFString, this is definitely not thread safe. CoreFoundation is open source (sort-of…, but enough for this purpose), so you can look at the code yourself. Eventually CFAttributedStringGetAttributes calls blockForLocation, which updates an internal cache and does not having locking around that. I haven't seen anything in the docs that promises this is thread safe.