diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 68fec3b13..9776f70d3 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -89,6 +89,7 @@ add_library(Testing Support/Graph.swift Support/JSON.swift Support/Locked.swift + Support/Locked+Platform.swift Support/VersionNumber.swift Support/Versions.swift Discovery+Macro.swift diff --git a/Sources/Testing/ExitTests/WaitFor.swift b/Sources/Testing/ExitTests/WaitFor.swift index f0326ff3c..8c6ad52f3 100644 --- a/Sources/Testing/ExitTests/WaitFor.swift +++ b/Sources/Testing/ExitTests/WaitFor.swift @@ -80,42 +80,7 @@ func wait(for pid: consuming pid_t) async throws -> ExitStatus { } #elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) /// A mapping of awaited child PIDs to their corresponding Swift continuations. -private nonisolated(unsafe) let _childProcessContinuations = { - let result = ManagedBuffer<[pid_t: CheckedContinuation], pthread_mutex_t>.create( - minimumCapacity: 1, - makingHeaderWith: { _ in [:] } - ) - - result.withUnsafeMutablePointers { _, lock in - _ = pthread_mutex_init(lock, nil) - } - - return result -}() - -/// Access the value in `_childProcessContinuations` while guarded by its lock. -/// -/// - Parameters: -/// - body: A closure to invoke while the lock is held. -/// -/// - Returns: Whatever is returned by `body`. -/// -/// - Throws: Whatever is thrown by `body`. -private func _withLockedChildProcessContinuations( - _ body: ( - _ childProcessContinuations: inout [pid_t: CheckedContinuation], - _ lock: UnsafeMutablePointer - ) throws -> R -) rethrows -> R { - try _childProcessContinuations.withUnsafeMutablePointers { childProcessContinuations, lock in - _ = pthread_mutex_lock(lock) - defer { - _ = pthread_mutex_unlock(lock) - } - - return try body(&childProcessContinuations.pointee, lock) - } -} +private let _childProcessContinuations = LockedWith]>() /// A condition variable used to suspend the waiter thread created by /// `_createWaitThread()` when there are no child processes to await. @@ -147,7 +112,7 @@ private let _createWaitThread: Void = { var siginfo = siginfo_t() if 0 == waitid(P_ALL, 0, &siginfo, WEXITED | WNOWAIT) { if case let pid = siginfo.si_pid, pid != 0 { - let continuation = _withLockedChildProcessContinuations { childProcessContinuations, _ in + let continuation = _childProcessContinuations.withLock { childProcessContinuations in childProcessContinuations.removeValue(forKey: pid) } @@ -168,7 +133,7 @@ private let _createWaitThread: Void = { // newly-scheduled waiter process. (If this condition is spuriously // woken, we'll just loop again, which is fine.) Note that we read errno // outside the lock in case acquiring the lock perturbs it. - _withLockedChildProcessContinuations { childProcessContinuations, lock in + _childProcessContinuations.withUnsafeUnderlyingLock { lock, childProcessContinuations in if childProcessContinuations.isEmpty { _ = pthread_cond_wait(_waitThreadNoChildrenCondition, lock) } @@ -240,7 +205,7 @@ func wait(for pid: consuming pid_t) async throws -> ExitStatus { _createWaitThread return try await withCheckedThrowingContinuation { continuation in - _withLockedChildProcessContinuations { childProcessContinuations, _ in + _childProcessContinuations.withLock { childProcessContinuations in // We don't need to worry about a race condition here because waitid() // does not clear the wait/zombie state of the child process. If it sees // the child process has terminated and manages to acquire the lock before diff --git a/Sources/Testing/Support/Locked+Platform.swift b/Sources/Testing/Support/Locked+Platform.swift new file mode 100644 index 000000000..a2ba82ac2 --- /dev/null +++ b/Sources/Testing/Support/Locked+Platform.swift @@ -0,0 +1,97 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023–2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +internal import _TestingInternals + +extension Never: Lockable { + static func initializeLock(at lock: UnsafeMutablePointer) {} + static func deinitializeLock(at lock: UnsafeMutablePointer) {} + static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) {} + static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) {} +} + +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK +extension os_unfair_lock_s: Lockable { + static func initializeLock(at lock: UnsafeMutablePointer) { + lock.initialize(to: .init()) + } + + static func deinitializeLock(at lock: UnsafeMutablePointer) { + // No deinitialization needed. + } + + static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) { + os_unfair_lock_lock(lock) + } + + static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) { + os_unfair_lock_unlock(lock) + } +} +#endif + +#if os(FreeBSD) || os(OpenBSD) +typealias pthread_mutex_t = _TestingInternals.pthread_mutex_t? +typealias pthread_cond_t = _TestingInternals.pthread_cond_t? +#endif + +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || (os(WASI) && _runtime(_multithreaded)) +extension pthread_mutex_t: Lockable { + static func initializeLock(at lock: UnsafeMutablePointer) { + _ = pthread_mutex_init(lock, nil) + } + + static func deinitializeLock(at lock: UnsafeMutablePointer) { + _ = pthread_mutex_destroy(lock) + } + + static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) { + _ = pthread_mutex_lock(lock) + } + + static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) { + _ = pthread_mutex_unlock(lock) + } +} +#endif + +#if os(Windows) +extension SRWLOCK: Lockable { + static func initializeLock(at lock: UnsafeMutablePointer) { + InitializeSRWLock(lock) + } + + static func deinitializeLock(at lock: UnsafeMutablePointer) { + // No deinitialization needed. + } + + static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) { + AcquireSRWLockExclusive(lock) + } + + static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) { + ReleaseSRWLockExclusive(lock) + } +} +#endif + +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK +typealias DefaultLock = os_unfair_lock +#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || (os(WASI) && _runtime(_multithreaded)) +typealias DefaultLock = pthread_mutex_t +#elseif os(Windows) +typealias DefaultLock = SRWLOCK +#elseif os(WASI) +// No locks on WASI without multithreaded runtime. +typealias DefaultLock = Never +#else +#warning("Platform-specific implementation missing: locking unavailable") +typealias DefaultLock = Never +#endif diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index fac062adb..d1db8ef1f 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -9,7 +9,37 @@ // internal import _TestingInternals -private import Synchronization + +/// A protocol defining a type, generally platform-specific, that satisfies the +/// requirements of a lock or mutex. +protocol Lockable { + /// Initialize the lock at the given address. + /// + /// - Parameters: + /// - lock: A pointer to uninitialized memory that should be initialized as + /// an instance of this type. + static func initializeLock(at lock: UnsafeMutablePointer) + + /// Deinitialize the lock at the given address. + /// + /// - Parameters: + /// - lock: A pointer to initialized memory that should be deinitialized. + static func deinitializeLock(at lock: UnsafeMutablePointer) + + /// Acquire the lock at the given address. + /// + /// - Parameters: + /// - lock: The address of the lock to acquire. + static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) + + /// Relinquish the lock at the given address. + /// + /// - Parameters: + /// - lock: The address of the lock to relinquish. + static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) +} + +// MARK: - /// A type that wraps a value requiring access from a synchronous caller during /// concurrent execution. @@ -22,48 +52,30 @@ private import Synchronization /// concurrency tools. /// /// This type is not part of the public interface of the testing library. -struct Locked { - /// A type providing storage for the underlying lock and wrapped value. -#if SWT_TARGET_OS_APPLE && canImport(os) - private typealias _Storage = ManagedBuffer -#else - private final class _Storage { - let mutex: Mutex - - init(_ rawValue: consuming sending T) { - mutex = Mutex(rawValue) +struct LockedWith: RawRepresentable where L: Lockable { + /// A type providing heap-allocated storage for an instance of ``Locked``. + private final class _Storage: ManagedBuffer { + deinit { + withUnsafeMutablePointerToElements { lock in + L.deinitializeLock(at: lock) + } } } -#endif /// Storage for the underlying lock and wrapped value. - private nonisolated(unsafe) var _storage: _Storage -} - -extension Locked: Sendable where T: Sendable {} + private nonisolated(unsafe) var _storage: ManagedBuffer -extension Locked: RawRepresentable { init(rawValue: T) { -#if SWT_TARGET_OS_APPLE && canImport(os) - _storage = .create(minimumCapacity: 1, makingHeaderWith: { _ in rawValue }) + _storage = _Storage.create(minimumCapacity: 1, makingHeaderWith: { _ in rawValue }) _storage.withUnsafeMutablePointerToElements { lock in - lock.initialize(to: .init()) + L.initializeLock(at: lock) } -#else - nonisolated(unsafe) let rawValue = rawValue - _storage = _Storage(rawValue) -#endif } var rawValue: T { - withLock { rawValue in - nonisolated(unsafe) let rawValue = rawValue - return rawValue - } + withLock { $0 } } -} -extension Locked { /// Acquire the lock and invoke a function while it is held. /// /// - Parameters: @@ -76,27 +88,55 @@ extension Locked { /// This function can be used to synchronize access to shared data from a /// synchronous caller. Wherever possible, use actor isolation or other Swift /// concurrency tools. - func withLock(_ body: (inout T) throws -> sending R) rethrows -> sending R where R: ~Copyable { -#if SWT_TARGET_OS_APPLE && canImport(os) - nonisolated(unsafe) let result = try _storage.withUnsafeMutablePointers { rawValue, lock in - os_unfair_lock_lock(lock) + nonmutating func withLock(_ body: (inout T) throws -> R) rethrows -> R where R: ~Copyable { + try _storage.withUnsafeMutablePointers { rawValue, lock in + L.unsafelyAcquireLock(at: lock) defer { - os_unfair_lock_unlock(lock) + L.unsafelyRelinquishLock(at: lock) } return try body(&rawValue.pointee) } - return result -#else - try _storage.mutex.withLock { rawValue in - try body(&rawValue) + } + + /// Acquire the lock and invoke a function while it is held, yielding both the + /// protected value and a reference to the underlying lock guarding it. + /// + /// - Parameters: + /// - body: A closure to invoke while the lock is held. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`. + /// + /// This function is equivalent to ``withLock(_:)`` except that the closure + /// passed to it also takes a reference to the underlying lock guarding this + /// instance's wrapped value. This function can be used when platform-specific + /// functionality such as a `pthread_cond_t` is needed. Because the caller has + /// direct access to the lock and is able to unlock and re-lock it, it is + /// unsafe to modify the protected value. + /// + /// - Warning: Callers that unlock the lock _must_ lock it again before the + /// closure returns. If the lock is not acquired when `body` returns, the + /// effect is undefined. + nonmutating func withUnsafeUnderlyingLock(_ body: (UnsafeMutablePointer, T) throws -> R) rethrows -> R where R: ~Copyable { + try withLock { value in + try _storage.withUnsafeMutablePointerToElements { lock in + try body(lock, value) + } } -#endif } } +extension LockedWith: Sendable where T: Sendable {} + +/// A type that wraps a value requiring access from a synchronous caller during +/// concurrent execution and which uses the default platform-specific lock type +/// for the current platform. +typealias Locked = LockedWith + // MARK: - Additions -extension Locked where T: AdditiveArithmetic & Sendable { +extension LockedWith where T: AdditiveArithmetic { /// Add something to the current wrapped value of this instance. /// /// - Parameters: @@ -112,7 +152,7 @@ extension Locked where T: AdditiveArithmetic & Sendable { } } -extension Locked where T: Numeric & Sendable { +extension LockedWith where T: Numeric { /// Increment the current wrapped value of this instance. /// /// - Returns: The sum of ``rawValue`` and `1`. @@ -132,7 +172,7 @@ extension Locked where T: Numeric & Sendable { } } -extension Locked { +extension LockedWith { /// Initialize an instance of this type with a raw value of `nil`. init() where T == V? { self.init(rawValue: nil) @@ -148,10 +188,3 @@ extension Locked { self.init(rawValue: []) } } - -// MARK: - POSIX conveniences - -#if os(FreeBSD) || os(OpenBSD) -typealias pthread_mutex_t = _TestingInternals.pthread_mutex_t? -typealias pthread_cond_t = _TestingInternals.pthread_cond_t? -#endif diff --git a/Tests/TestingTests/Support/LockTests.swift b/Tests/TestingTests/Support/LockTests.swift index 486143e1e..2a41e4c1d 100644 --- a/Tests/TestingTests/Support/LockTests.swift +++ b/Tests/TestingTests/Support/LockTests.swift @@ -13,9 +13,7 @@ private import _TestingInternals @Suite("Locked Tests") struct LockTests { - @Test("Locking and unlocking") - func locking() { - let lock = Locked(rawValue: 0) + func testLock(_ lock: LockedWith) { #expect(lock.rawValue == 0) lock.withLock { value in value = 1 @@ -23,9 +21,21 @@ struct LockTests { #expect(lock.rawValue == 1) } - @Test("Repeatedly accessing a lock") - func lockRepeatedly() async { - let lock = Locked(rawValue: 0) + @Test("Platform-default lock") + func locking() { + testLock(Locked(rawValue: 0)) + } + +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK + @Test("pthread_mutex_t (Darwin alternate)") + func lockingWith_pthread_mutex_t() { + testLock(LockedWith(rawValue: 0)) + } +#endif + + @Test("No lock") + func noLock() async { + let lock = LockedWith(rawValue: 0) await withTaskGroup { taskGroup in for _ in 0 ..< 100_000 { taskGroup.addTask { @@ -33,6 +43,20 @@ struct LockTests { } } } - #expect(lock.rawValue == 100_000) + #expect(lock.rawValue != 100_000) + } + + @Test("Get the underlying lock") + func underlyingLock() { + let lock = Locked(rawValue: 0) + testLock(lock) + lock.withUnsafeUnderlyingLock { underlyingLock, _ in + DefaultLock.unsafelyRelinquishLock(at: underlyingLock) + lock.withLock { value in + value += 1000 + } + DefaultLock.unsafelyAcquireLock(at: underlyingLock) + } + #expect(lock.rawValue == 1001) } }