swiftlang / swift

The Swift Programming Language
https://swift.org
Apache License 2.0
67.54k stars 10.35k forks source link

CMTime as dictionary value causes crash in release mode #65063

Open iby opened 1 year ago

iby commented 1 year ago

Description

Starting with Xcode 14, when SWIFT_OPTIMIZATION_LEVEL is set to anything other than none, using CMTime as dictionary key causes a EXC_BAD_ACCESS crash on macOS 12 and below, but works in macOS 13.

I ran into a similar https://github.com/apple/swift/issues/55054 issue before, but managed to make it working. With Xcode 14 things went broken again. I'm deploying this to macOS 12 and here's the code:

import Foundation
import CoreMedia

@available(macOS, obsoleted: 13)
extension CMTime: Hashable {
    public var hashValue: Int {
        var hasher = Hasher()
        hash(into: &hasher)
        return hasher.finalize()
    }

    public func hash(into hasher: inout Hasher) {
        hasher.combine(self.value)
        hasher.combine(self.timescale)
        hasher.combine(self.epoch)
        hasher.combine(self.flags.rawValue)
    }
}

print("hashValue:", CMTime.zero.hashValue)
print("hashValue:", (CMTime.zero as any Hashable).hashValue)
print("dictionary:", [CMTime.zero: "foo"]) // Crashes here when SWIFT_OPTIMIZATION_LEVEL is set to non-`none` value…

Recently, as of macOS 13, CMTime comes with Hashable conformance, which isn't available in earlier versions. Even though I'm defining extension CMTime: Hashable, and it works as expected when invoked directly, but something goes wrong when used in a dictionary.

There's a similar discussion on StackOverflow – apparently, for someone, this is working when adding @available(iOS, obsoleted: 16) for iOS, but this doesn't help on macOS.

Steps to reproduce

Run the release scheme in the attached CMTime Hashable.zip project.

Alternatively, run the above code in macOS 12 or below with some SWIFT_OPTIMIZATION_LEVEL other than none.

Expected behavior

It should be possible to use CMTime as a hashable value in macOS 12 and below.

Environment

P. S. I also tried @_optimize(none) no both extensions as I'm guessing Swift compiler strips down those symbols as duplicated (purely guessing), but that didn't help…

iby commented 2 months ago

I'm seeing a whole bunch of other issues with sets depending on how they get created… but I'm guessing this is related?

swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4) Target: arm64-apple-macosx14.0 Xcode 15.4 (15F31d)

import CoreMedia
import OrderedCollections

do {
    // 👍 Works…
    let times = (0 ..< 1000).map({ CMTime(seconds: Double($0), preferredTimescale: 600) })
    var remainder = times.reduce(into: Set(), { $0.insert($1) })
    while !remainder.isEmpty {
        let element = remainder.randomElement()!
        if remainder.remove(element) == nil { fatalError("Can't remove \(element), contains: \(remainder.contains(element))") }
    }
}

do {
    // 👍 Works…
    let times = (0 ..< 1000).map({ CMTime(seconds: Double($0), preferredTimescale: 600) })
    var remainder = times.reduce(into: OrderedSet(), { $0.append($1) })
    while !remainder.isEmpty {
        let element = remainder.randomElement()!
        remainder.remove(element)
    }
}

do {
    // 🔃 Fails: Goes into recursion…
    let times = (0 ..< 1000).map({ CMTime(seconds: Double($0), preferredTimescale: 600) })
    var remainder = Set(times)
    while !remainder.isEmpty {
        let element = remainder.randomElement()!
        // remainder.remove(element) // Recursion…
        if remainder.remove(element) == nil { fatalError("Can't remove \(element), contains: \(remainder.contains(element))") } // Remove: no, Contains: yes
    }
}

do {
    // 🔃 Fails: Swift/UnsafeBufferPointer.swift:1400: Fatal error
    let times = (0 ..< 1000).map({ CMTime(seconds: Double($0), preferredTimescale: 600) })
    var remainder = OrderedSet(times)
    while !remainder.isEmpty {
        let element = remainder.randomElement()!
        // remainder.remove(element) // Swift/UnsafeBufferPointer.swift:1400: Fatal error
        if remainder.remove(element) == nil { fatalError("Can't remove \(element), contains: \(remainder.contains(element))") } // Remove: no, Contains: yes
    }
}