From d1b4b39e86d290073087b83f6e3760fd4b3b87b4 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 9 Oct 2022 21:53:58 -0400 Subject: [PATCH] Fix MultiThreadDictionary crash on iOS 15 due to using existential types See #178 --- Tusker/MultiThreadDictionary.swift | 81 ++++++++++++++---------------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/Tusker/MultiThreadDictionary.swift b/Tusker/MultiThreadDictionary.swift index 8b5c316b..fd8cd206 100644 --- a/Tusker/MultiThreadDictionary.swift +++ b/Tusker/MultiThreadDictionary.swift @@ -13,24 +13,26 @@ import os // to make the lock semantics more clear @available(iOS, obsoleted: 16.0) class MultiThreadDictionary { - private let lock: any Lock<[Key: Value]> + private let lock: LockHolder<[AnyHashable: Any]> init() { if #available(iOS 16.0, *) { - self.lock = OSAllocatedUnfairLock(initialState: [:]) + let lock = OSAllocatedUnfairLock(initialState: [:]) + self.lock = LockHolder(withLock: lock.withLock(_:)) } else { - self.lock = MutexLock(initialState: [:]) + let lock = UnfairLock(initialState: [:]) + self.lock = LockHolder(withLock: lock.withLock(_:)) } } subscript(key: Key) -> Value? { get { - return lock.withLock { dict in + return try! lock.withLock { dict in dict[key] - } + } as! Value? } set(value) { - lock.withLock { dict in + _ = try! lock.withLock { dict in dict[key] = value } } @@ -38,22 +40,34 @@ class MultiThreadDictionary { /// If the result of this function is unused, it is preferable to use `removeValueWithoutReturning` as it executes asynchronously and doesn't block the calling thread. func removeValue(forKey key: Key) -> Value? { - return lock.withLock { dict in + return try! lock.withLock { dict in dict.removeValue(forKey: key) - } + } as! Value? } func contains(key: Key) -> Bool { - return lock.withLock { dict in + return try! lock.withLock { dict in dict.keys.contains(key) - } + } as! Bool } - func withLock(_ body: @Sendable (inout [Key: Value]) throws -> R) rethrows -> R where R: Sendable { - return try lock.withLock(body) + // TODO: this should really be throws/rethrows but the stupid type-erased lock makes that not possible + func withLock(_ body: @Sendable (inout [Key: Value]) -> R) -> R where R: Sendable { + return try! lock.withLock { dict in + var downcasted = dict as! [Key: Value] + defer { dict = downcasted } + return body(&downcasted) + } as! R } } +// this type erased struct is necessary due to a compiler bug with stored constrained existential types +// see https://github.com/apple/swift/issues/61403 +// see #178 +fileprivate struct LockHolder { + let withLock: (_ body: @Sendable (inout State) throws -> any Sendable) throws -> any Sendable +} + // TODO: replace this only with OSAllocatedUnfairLock @available(iOS, obsoleted: 16.0) fileprivate protocol Lock { @@ -65,41 +79,22 @@ fileprivate protocol Lock { extension OSAllocatedUnfairLock: Lock { } -// something is wrong with the UnfairLock impl and it results in segv_accerrs -fileprivate class MutexLock: Lock { +// from http://www.russbishop.net/the-law +fileprivate class UnfairLock: Lock { + private var lock: UnsafeMutablePointer private var state: State - private var lock = NSLock() - init(initialState: State) { self.state = initialState + self.lock = .allocate(capacity: 1) + self.lock.initialize(to: os_unfair_lock()) } - - func withLock(_ body: @Sendable (inout State) throws -> R) rethrows -> R where R : Sendable { - if !lock.lock(before: Date(timeIntervalSinceNow: 1)) { - // if we can't acquire the lock after 1 second, something has gone catastrophically wrong - fatalError() - } - defer { lock.unlock() } + deinit { + self.lock.deinitialize(count: 1) + self.lock.deallocate() + } + func withLock(_ body: (inout State) throws -> R) rethrows -> R where R: Sendable { + os_unfair_lock_lock(lock) + defer { os_unfair_lock_unlock(lock) } return try body(&state) } } - -//// from http://www.russbishop.net/the-law -//fileprivate class UnfairLock: Lock { -// private var lock: UnsafeMutablePointer -// private var state: State -// init(initialState: State) { -// self.state = initialState -// self.lock = .allocate(capacity: 1) -// self.lock.initialize(to: os_unfair_lock()) -// } -// deinit { -// self.lock.deinitialize(count: 1) -// self.lock.deallocate() -// } -// func withLock(_ body: (inout State) throws -> R) rethrows -> R where R: Sendable { -// os_unfair_lock_lock(lock) -// defer { os_unfair_lock_unlock(lock) } -// return try body(&state) -// } -//}