From 430389f34a97ea68f680c8f33e3502f53e02736b Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Fri, 14 Mar 2025 08:35:18 +0100 Subject: [PATCH 01/10] Use Lock from NIO --- Sources/ArgumentParser/Utilities/Mutex.swift | 332 ++++++++++++++++++- 1 file changed, 325 insertions(+), 7 deletions(-) diff --git a/Sources/ArgumentParser/Utilities/Mutex.swift b/Sources/ArgumentParser/Utilities/Mutex.swift index 2f3bd040..6d557d0f 100644 --- a/Sources/ArgumentParser/Utilities/Mutex.swift +++ b/Sources/ArgumentParser/Utilities/Mutex.swift @@ -9,12 +9,6 @@ // //===----------------------------------------------------------------------===// -#if swift(>=6.0) -internal import Foundation -#else -import Foundation -#endif - /// A synchronization primitive that protects shared mutable state via mutual /// exclusion. /// @@ -24,7 +18,7 @@ import Foundation /// `Mutex` allowing for exclusive access. class Mutex: @unchecked Sendable { /// The lock used to synchronize access to the value. - var lock: NSLock + var lock: Lock /// The value protected by the mutex. var value: T @@ -58,3 +52,327 @@ class Mutex: @unchecked Sendable { return try body(&self.value) } } + + +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if canImport(Darwin) +import Darwin +#elseif os(Windows) +import ucrt +import WinSDK +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(Bionic) +import Bionic +#elseif canImport(WASILibc) +import WASILibc +#if canImport(wasi_pthread) +import wasi_pthread +#endif +#else +#error("The concurrency lock module was unable to identify your C library.") +#endif + +/// A threading lock based on `libpthread` instead of `libdispatch`. +/// +/// This object provides a lock on top of a single `pthread_mutex_t`. This kind +/// of lock is safe to use with `libpthread`-based threading models, such as the +/// one used by NIO. On Windows, the lock is based on the substantially similar +/// `SRWLOCK` type. +@available(*, deprecated, renamed: "NIOLock") +public final class Lock { + #if os(Windows) + fileprivate let mutex: UnsafeMutablePointer = + UnsafeMutablePointer.allocate(capacity: 1) + #else + fileprivate let mutex: UnsafeMutablePointer = + UnsafeMutablePointer.allocate(capacity: 1) + #endif + + /// Create a new lock. + public init() { + #if os(Windows) + InitializeSRWLock(self.mutex) + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + var attr = pthread_mutexattr_t() + pthread_mutexattr_init(&attr) + debugOnly { + pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) + } + + let err = pthread_mutex_init(self.mutex, &attr) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } + + deinit { + #if os(Windows) + // SRWLOCK does not need to be free'd + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + let err = pthread_mutex_destroy(self.mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + mutex.deallocate() + } + + /// Acquire the lock. + /// + /// Whenever possible, consider using `withLock` instead of this method and + /// `unlock`, to simplify lock handling. + public func lock() { + #if os(Windows) + AcquireSRWLockExclusive(self.mutex) + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + let err = pthread_mutex_lock(self.mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } + + /// Release the lock. + /// + /// Whenever possible, consider using `withLock` instead of this method and + /// `lock`, to simplify lock handling. + public func unlock() { + #if os(Windows) + ReleaseSRWLockExclusive(self.mutex) + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + let err = pthread_mutex_unlock(self.mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } + + /// Acquire the lock for the duration of the given block. + /// + /// This convenience method should be preferred to `lock` and `unlock` in + /// most situations, as it ensures that the lock will be released regardless + /// of how `body` exits. + /// + /// - Parameter body: The block to execute while holding the lock. + /// - Returns: The value returned by the block. + @inlinable + public func withLock(_ body: () throws -> T) rethrows -> T { + self.lock() + defer { + self.unlock() + } + return try body() + } + + // specialise Void return (for performance) + @inlinable + public func withLockVoid(_ body: () throws -> Void) rethrows { + try self.withLock(body) + } +} + +/// A `Lock` with a built-in state variable. +/// +/// This class provides a convenience addition to `Lock`: it provides the ability to wait +/// until the state variable is set to a specific value to acquire the lock. +public final class ConditionLock { + private var _value: T + private let mutex: NIOLock + #if os(Windows) + private let cond: UnsafeMutablePointer = + UnsafeMutablePointer.allocate(capacity: 1) + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + private let cond: UnsafeMutablePointer = + UnsafeMutablePointer.allocate(capacity: 1) + #endif + + /// Create the lock, and initialize the state variable to `value`. + /// + /// - Parameter value: The initial value to give the state variable. + public init(value: T) { + self._value = value + self.mutex = NIOLock() + #if os(Windows) + InitializeConditionVariable(self.cond) + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + let err = pthread_cond_init(self.cond, nil) + precondition(err == 0, "\(#function) failed in pthread_cond with error \(err)") + #endif + } + + deinit { + #if os(Windows) + // condition variables do not need to be explicitly destroyed + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + let err = pthread_cond_destroy(self.cond) + precondition(err == 0, "\(#function) failed in pthread_cond with error \(err)") + self.cond.deallocate() + #endif + } + + /// Acquire the lock, regardless of the value of the state variable. + public func lock() { + self.mutex.lock() + } + + /// Release the lock, regardless of the value of the state variable. + public func unlock() { + self.mutex.unlock() + } + + /// The value of the state variable. + /// + /// Obtaining the value of the state variable requires acquiring the lock. + /// This means that it is not safe to access this property while holding the + /// lock: it is only safe to use it when not holding it. + public var value: T { + self.lock() + defer { + self.unlock() + } + return self._value + } + + /// Acquire the lock when the state variable is equal to `wantedValue`. + /// + /// - Parameter wantedValue: The value to wait for the state variable + /// to have before acquiring the lock. + public func lock(whenValue wantedValue: T) { + self.lock() + while true { + if self._value == wantedValue { + break + } + self.mutex.withLockPrimitive { mutex in + #if os(Windows) + let result = SleepConditionVariableSRW(self.cond, mutex, INFINITE, 0) + precondition(result, "\(#function) failed in SleepConditionVariableSRW with error \(GetLastError())") + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + let err = pthread_cond_wait(self.cond, mutex) + precondition(err == 0, "\(#function) failed in pthread_cond with error \(err)") + #endif + } + } + } + + /// Acquire the lock when the state variable is equal to `wantedValue`, + /// waiting no more than `timeoutSeconds` seconds. + /// + /// - Parameter wantedValue: The value to wait for the state variable + /// to have before acquiring the lock. + /// - Parameter timeoutSeconds: The number of seconds to wait to acquire + /// the lock before giving up. + /// - Returns: `true` if the lock was acquired, `false` if the wait timed out. + public func lock(whenValue wantedValue: T, timeoutSeconds: Double) -> Bool { + precondition(timeoutSeconds >= 0) + + #if os(Windows) + var dwMilliseconds: DWORD = DWORD(timeoutSeconds * 1000) + + self.lock() + while true { + if self._value == wantedValue { + return true + } + + let dwWaitStart = timeGetTime() + let result = self.mutex.withLockPrimitive { mutex in + SleepConditionVariableSRW(self.cond, mutex, dwMilliseconds, 0) + } + if !result { + let dwError = GetLastError() + if dwError == ERROR_TIMEOUT { + self.unlock() + return false + } + fatalError("SleepConditionVariableSRW: \(dwError)") + } + // NOTE: this may be a spurious wakeup, adjust the timeout accordingly + dwMilliseconds = dwMilliseconds - (timeGetTime() - dwWaitStart) + } + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + let nsecPerSec: Int64 = 1_000_000_000 + self.lock() + // the timeout as a (seconds, nano seconds) pair + let timeoutNS = Int64(timeoutSeconds * Double(nsecPerSec)) + + var curTime = timeval() + gettimeofday(&curTime, nil) + + let allNSecs: Int64 = timeoutNS + Int64(curTime.tv_usec) * 1000 + #if canImport(wasi_pthread) + let tvSec = curTime.tv_sec + (allNSecs / nsecPerSec) + #else + let tvSec = curTime.tv_sec + Int((allNSecs / nsecPerSec)) + #endif + + var timeoutAbs = timespec( + tv_sec: tvSec, + tv_nsec: Int(allNSecs % nsecPerSec) + ) + assert(timeoutAbs.tv_nsec >= 0 && timeoutAbs.tv_nsec < Int(nsecPerSec)) + assert(timeoutAbs.tv_sec >= curTime.tv_sec) + return self.mutex.withLockPrimitive { mutex -> Bool in + while true { + if self._value == wantedValue { + return true + } + switch pthread_cond_timedwait(self.cond, mutex, &timeoutAbs) { + case 0: + continue + case ETIMEDOUT: + self.unlock() + return false + case let e: + fatalError("caught error \(e) when calling pthread_cond_timedwait") + } + } + } + #else + return true + #endif + } + + /// Release the lock, setting the state variable to `newValue`. + /// + /// - Parameter newValue: The value to give to the state variable when we + /// release the lock. + public func unlock(withValue newValue: T) { + self._value = newValue + self.unlock() + #if os(Windows) + WakeAllConditionVariable(self.cond) + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + let err = pthread_cond_broadcast(self.cond) + precondition(err == 0, "\(#function) failed in pthread_cond with error \(err)") + #endif + } +} + +/// A utility function that runs the body code only in debug builds, without +/// emitting compiler warnings. +/// +/// This is currently the only way to do this in Swift: see +/// https://forums.swift.org/t/support-debug-only-code/11037 for a discussion. +@inlinable +internal func debugOnly(_ body: () -> Void) { + assert( + { + body() + return true + }() + ) +} + +@available(*, deprecated) +extension Lock: @unchecked Sendable {} +extension ConditionLock: @unchecked Sendable {} \ No newline at end of file From b5bb88f5db595c6b29d068253454031de19f7d61 Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Fri, 14 Mar 2025 08:38:12 +0100 Subject: [PATCH 02/10] NIOLock --- Sources/ArgumentParser/Utilities/Mutex.swift | 393 ++++++++----------- 1 file changed, 166 insertions(+), 227 deletions(-) diff --git a/Sources/ArgumentParser/Utilities/Mutex.swift b/Sources/ArgumentParser/Utilities/Mutex.swift index 6d557d0f..6ef4cbe2 100644 --- a/Sources/ArgumentParser/Utilities/Mutex.swift +++ b/Sources/ArgumentParser/Utilities/Mutex.swift @@ -18,7 +18,7 @@ /// `Mutex` allowing for exclusive access. class Mutex: @unchecked Sendable { /// The lock used to synchronize access to the value. - var lock: Lock + var lock: NIOLock /// The value protected by the mutex. var value: T @@ -58,7 +58,7 @@ class Mutex: @unchecked Sendable { // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2017-2022 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -85,29 +85,27 @@ import WASILibc import wasi_pthread #endif #else -#error("The concurrency lock module was unable to identify your C library.") +#error("The concurrency NIOLock module was unable to identify your C library.") #endif -/// A threading lock based on `libpthread` instead of `libdispatch`. -/// -/// This object provides a lock on top of a single `pthread_mutex_t`. This kind -/// of lock is safe to use with `libpthread`-based threading models, such as the -/// one used by NIO. On Windows, the lock is based on the substantially similar -/// `SRWLOCK` type. -@available(*, deprecated, renamed: "NIOLock") -public final class Lock { - #if os(Windows) - fileprivate let mutex: UnsafeMutablePointer = - UnsafeMutablePointer.allocate(capacity: 1) - #else - fileprivate let mutex: UnsafeMutablePointer = - UnsafeMutablePointer.allocate(capacity: 1) - #endif +#if os(Windows) +@usableFromInline +typealias LockPrimitive = SRWLOCK +#else +@usableFromInline +typealias LockPrimitive = pthread_mutex_t +#endif + +@usableFromInline +enum LockOperations {} + +extension LockOperations { + @inlinable + static func create(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() - /// Create a new lock. - public init() { #if os(Windows) - InitializeSRWLock(self.mutex) + InitializeSRWLock(mutex) #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) var attr = pthread_mutexattr_t() pthread_mutexattr_init(&attr) @@ -115,264 +113,205 @@ public final class Lock { pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) } - let err = pthread_mutex_init(self.mutex, &attr) + let err = pthread_mutex_init(mutex, &attr) precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") #endif } - deinit { + @inlinable + static func destroy(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + #if os(Windows) // SRWLOCK does not need to be free'd #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_mutex_destroy(self.mutex) + let err = pthread_mutex_destroy(mutex) precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") #endif - mutex.deallocate() } - /// Acquire the lock. - /// - /// Whenever possible, consider using `withLock` instead of this method and - /// `unlock`, to simplify lock handling. - public func lock() { + @inlinable + static func lock(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + #if os(Windows) - AcquireSRWLockExclusive(self.mutex) + AcquireSRWLockExclusive(mutex) #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_mutex_lock(self.mutex) + let err = pthread_mutex_lock(mutex) precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") #endif } - /// Release the lock. - /// - /// Whenever possible, consider using `withLock` instead of this method and - /// `lock`, to simplify lock handling. - public func unlock() { + @inlinable + static func unlock(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + #if os(Windows) - ReleaseSRWLockExclusive(self.mutex) + ReleaseSRWLockExclusive(mutex) #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_mutex_unlock(self.mutex) + let err = pthread_mutex_unlock(mutex) precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") #endif } +} + +// Tail allocate both the mutex and a generic value using ManagedBuffer. +// Both the header pointer and the elements pointer are stable for +// the class's entire lifetime. +// +// However, for safety reasons, we elect to place the lock in the "elements" +// section of the buffer instead of the head. The reasoning here is subtle, +// so buckle in. +// +// _As a practical matter_, the implementation of ManagedBuffer ensures that +// the pointer to the header is stable across the lifetime of the class, and so +// each time you call `withUnsafeMutablePointers` or `withUnsafeMutablePointerToHeader` +// the value of the header pointer will be the same. This is because ManagedBuffer uses +// `Builtin.addressOf` to load the value of the header, and that does ~magic~ to ensure +// that it does not invoke any weird Swift accessors that might copy the value. +// +// _However_, the header is also available via the `.header` field on the ManagedBuffer. +// This presents a problem! The reason there's an issue is that `Builtin.addressOf` and friends +// do not interact with Swift's exclusivity model. That is, the various `with` functions do not +// conceptually trigger a mutating access to `.header`. For elements this isn't a concern because +// there's literally no other way to perform the access, but for `.header` it's entirely possible +// to accidentally recursively read it. +// +// Our implementation is free from these issues, so we don't _really_ need to worry about it. +// However, out of an abundance of caution, we store the Value in the header, and the LockPrimitive +// in the trailing elements. We still don't use `.header`, but it's better to be safe than sorry, +// and future maintainers will be happier that we were cautious. +// +// See also: https://github.com/apple/swift/pull/40000 +@usableFromInline +final class LockStorage: ManagedBuffer { - /// Acquire the lock for the duration of the given block. - /// - /// This convenience method should be preferred to `lock` and `unlock` in - /// most situations, as it ensures that the lock will be released regardless - /// of how `body` exits. - /// - /// - Parameter body: The block to execute while holding the lock. - /// - Returns: The value returned by the block. @inlinable - public func withLock(_ body: () throws -> T) rethrows -> T { - self.lock() - defer { - self.unlock() + static func create(value: Value) -> Self { + let buffer = Self.create(minimumCapacity: 1) { _ in + value } - return try body() + // Intentionally using a force cast here to avoid a miss compiliation in 5.10. + // This is as fast as an unsafeDownCast since ManagedBuffer is inlined and the optimizer + // can eliminate the upcast/downcast pair + let storage = buffer as! Self + + storage.withUnsafeMutablePointers { _, lockPtr in + LockOperations.create(lockPtr) + } + + return storage } - // specialise Void return (for performance) @inlinable - public func withLockVoid(_ body: () throws -> Void) rethrows { - try self.withLock(body) + func lock() { + self.withUnsafeMutablePointerToElements { lockPtr in + LockOperations.lock(lockPtr) + } } -} -/// A `Lock` with a built-in state variable. -/// -/// This class provides a convenience addition to `Lock`: it provides the ability to wait -/// until the state variable is set to a specific value to acquire the lock. -public final class ConditionLock { - private var _value: T - private let mutex: NIOLock - #if os(Windows) - private let cond: UnsafeMutablePointer = - UnsafeMutablePointer.allocate(capacity: 1) - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - private let cond: UnsafeMutablePointer = - UnsafeMutablePointer.allocate(capacity: 1) - #endif - - /// Create the lock, and initialize the state variable to `value`. - /// - /// - Parameter value: The initial value to give the state variable. - public init(value: T) { - self._value = value - self.mutex = NIOLock() - #if os(Windows) - InitializeConditionVariable(self.cond) - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_cond_init(self.cond, nil) - precondition(err == 0, "\(#function) failed in pthread_cond with error \(err)") - #endif + @inlinable + func unlock() { + self.withUnsafeMutablePointerToElements { lockPtr in + LockOperations.unlock(lockPtr) + } } + @inlinable deinit { - #if os(Windows) - // condition variables do not need to be explicitly destroyed - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_cond_destroy(self.cond) - precondition(err == 0, "\(#function) failed in pthread_cond with error \(err)") - self.cond.deallocate() - #endif + self.withUnsafeMutablePointerToElements { lockPtr in + LockOperations.destroy(lockPtr) + } } - /// Acquire the lock, regardless of the value of the state variable. - public func lock() { - self.mutex.lock() + @inlinable + func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { + try self.withUnsafeMutablePointerToElements { lockPtr in + try body(lockPtr) + } } - /// Release the lock, regardless of the value of the state variable. - public func unlock() { - self.mutex.unlock() + @inlinable + func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { + try self.withUnsafeMutablePointers { valuePtr, lockPtr in + LockOperations.lock(lockPtr) + defer { LockOperations.unlock(lockPtr) } + return try mutate(&valuePtr.pointee) + } } +} - /// The value of the state variable. - /// - /// Obtaining the value of the state variable requires acquiring the lock. - /// This means that it is not safe to access this property while holding the - /// lock: it is only safe to use it when not holding it. - public var value: T { - self.lock() - defer { - self.unlock() - } - return self._value +/// A threading lock based on `libpthread` instead of `libdispatch`. +/// +/// - Note: ``NIOLock`` has reference semantics. +/// +/// This object provides a lock on top of a single `pthread_mutex_t`. This kind +/// of lock is safe to use with `libpthread`-based threading models, such as the +/// one used by NIO. On Windows, the lock is based on the substantially similar +/// `SRWLOCK` type. +public struct NIOLock { + @usableFromInline + internal let _storage: LockStorage + + /// Create a new lock. + @inlinable + public init() { + self._storage = .create(value: ()) } - /// Acquire the lock when the state variable is equal to `wantedValue`. + /// Acquire the lock. /// - /// - Parameter wantedValue: The value to wait for the state variable - /// to have before acquiring the lock. - public func lock(whenValue wantedValue: T) { - self.lock() - while true { - if self._value == wantedValue { - break - } - self.mutex.withLockPrimitive { mutex in - #if os(Windows) - let result = SleepConditionVariableSRW(self.cond, mutex, INFINITE, 0) - precondition(result, "\(#function) failed in SleepConditionVariableSRW with error \(GetLastError())") - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_cond_wait(self.cond, mutex) - precondition(err == 0, "\(#function) failed in pthread_cond with error \(err)") - #endif - } - } + /// Whenever possible, consider using `withLock` instead of this method and + /// `unlock`, to simplify lock handling. + @inlinable + public func lock() { + self._storage.lock() } - /// Acquire the lock when the state variable is equal to `wantedValue`, - /// waiting no more than `timeoutSeconds` seconds. + /// Release the lock. /// - /// - Parameter wantedValue: The value to wait for the state variable - /// to have before acquiring the lock. - /// - Parameter timeoutSeconds: The number of seconds to wait to acquire - /// the lock before giving up. - /// - Returns: `true` if the lock was acquired, `false` if the wait timed out. - public func lock(whenValue wantedValue: T, timeoutSeconds: Double) -> Bool { - precondition(timeoutSeconds >= 0) + /// Whenever possible, consider using `withLock` instead of this method and + /// `lock`, to simplify lock handling. + @inlinable + public func unlock() { + self._storage.unlock() + } - #if os(Windows) - var dwMilliseconds: DWORD = DWORD(timeoutSeconds * 1000) + @inlinable + internal func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { + try self._storage.withLockPrimitive(body) + } +} +extension NIOLock { + /// Acquire the lock for the duration of the given block. + /// + /// This convenience method should be preferred to `lock` and `unlock` in + /// most situations, as it ensures that the lock will be released regardless + /// of how `body` exits. + /// + /// - Parameter body: The block to execute while holding the lock. + /// - Returns: The value returned by the block. + @inlinable + public func withLock(_ body: () throws -> T) rethrows -> T { self.lock() - while true { - if self._value == wantedValue { - return true - } - - let dwWaitStart = timeGetTime() - let result = self.mutex.withLockPrimitive { mutex in - SleepConditionVariableSRW(self.cond, mutex, dwMilliseconds, 0) - } - if !result { - let dwError = GetLastError() - if dwError == ERROR_TIMEOUT { - self.unlock() - return false - } - fatalError("SleepConditionVariableSRW: \(dwError)") - } - // NOTE: this may be a spurious wakeup, adjust the timeout accordingly - dwMilliseconds = dwMilliseconds - (timeGetTime() - dwWaitStart) - } - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let nsecPerSec: Int64 = 1_000_000_000 - self.lock() - // the timeout as a (seconds, nano seconds) pair - let timeoutNS = Int64(timeoutSeconds * Double(nsecPerSec)) - - var curTime = timeval() - gettimeofday(&curTime, nil) - - let allNSecs: Int64 = timeoutNS + Int64(curTime.tv_usec) * 1000 - #if canImport(wasi_pthread) - let tvSec = curTime.tv_sec + (allNSecs / nsecPerSec) - #else - let tvSec = curTime.tv_sec + Int((allNSecs / nsecPerSec)) - #endif - - var timeoutAbs = timespec( - tv_sec: tvSec, - tv_nsec: Int(allNSecs % nsecPerSec) - ) - assert(timeoutAbs.tv_nsec >= 0 && timeoutAbs.tv_nsec < Int(nsecPerSec)) - assert(timeoutAbs.tv_sec >= curTime.tv_sec) - return self.mutex.withLockPrimitive { mutex -> Bool in - while true { - if self._value == wantedValue { - return true - } - switch pthread_cond_timedwait(self.cond, mutex, &timeoutAbs) { - case 0: - continue - case ETIMEDOUT: - self.unlock() - return false - case let e: - fatalError("caught error \(e) when calling pthread_cond_timedwait") - } - } + defer { + self.unlock() } - #else - return true - #endif + return try body() } - /// Release the lock, setting the state variable to `newValue`. - /// - /// - Parameter newValue: The value to give to the state variable when we - /// release the lock. - public func unlock(withValue newValue: T) { - self._value = newValue - self.unlock() - #if os(Windows) - WakeAllConditionVariable(self.cond) - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_cond_broadcast(self.cond) - precondition(err == 0, "\(#function) failed in pthread_cond with error \(err)") - #endif + @inlinable + public func withLockVoid(_ body: () throws -> Void) rethrows { + try self.withLock(body) } } -/// A utility function that runs the body code only in debug builds, without -/// emitting compiler warnings. -/// -/// This is currently the only way to do this in Swift: see -/// https://forums.swift.org/t/support-debug-only-code/11037 for a discussion. -@inlinable -internal func debugOnly(_ body: () -> Void) { - assert( - { - body() - return true - }() - ) -} +extension NIOLock: @unchecked Sendable {} -@available(*, deprecated) -extension Lock: @unchecked Sendable {} -extension ConditionLock: @unchecked Sendable {} \ No newline at end of file +extension UnsafeMutablePointer { + @inlinable + func assertValidAlignment() { + assert(UInt(bitPattern: self) % UInt(MemoryLayout.alignment) == 0) + } +} \ No newline at end of file From 683285076b5040f5dd512199bd57073112e9a8de Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Fri, 14 Mar 2025 08:39:53 +0100 Subject: [PATCH 03/10] internal --- Sources/ArgumentParser/Utilities/Mutex.swift | 27 +++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/Sources/ArgumentParser/Utilities/Mutex.swift b/Sources/ArgumentParser/Utilities/Mutex.swift index 6ef4cbe2..d9445892 100644 --- a/Sources/ArgumentParser/Utilities/Mutex.swift +++ b/Sources/ArgumentParser/Utilities/Mutex.swift @@ -249,13 +249,13 @@ final class LockStorage: ManagedBuffer { /// of lock is safe to use with `libpthread`-based threading models, such as the /// one used by NIO. On Windows, the lock is based on the substantially similar /// `SRWLOCK` type. -public struct NIOLock { +internal struct NIOLock { @usableFromInline internal let _storage: LockStorage /// Create a new lock. @inlinable - public init() { + init() { self._storage = .create(value: ()) } @@ -264,7 +264,7 @@ public struct NIOLock { /// Whenever possible, consider using `withLock` instead of this method and /// `unlock`, to simplify lock handling. @inlinable - public func lock() { + func lock() { self._storage.lock() } @@ -273,7 +273,7 @@ public struct NIOLock { /// Whenever possible, consider using `withLock` instead of this method and /// `lock`, to simplify lock handling. @inlinable - public func unlock() { + func unlock() { self._storage.unlock() } @@ -293,7 +293,7 @@ extension NIOLock { /// - Parameter body: The block to execute while holding the lock. /// - Returns: The value returned by the block. @inlinable - public func withLock(_ body: () throws -> T) rethrows -> T { + func withLock(_ body: () throws -> T) rethrows -> T { self.lock() defer { self.unlock() @@ -302,7 +302,7 @@ extension NIOLock { } @inlinable - public func withLockVoid(_ body: () throws -> Void) rethrows { + func withLockVoid(_ body: () throws -> Void) rethrows { try self.withLock(body) } } @@ -314,4 +314,19 @@ extension UnsafeMutablePointer { func assertValidAlignment() { assert(UInt(bitPattern: self) % UInt(MemoryLayout.alignment) == 0) } +} + +/// A utility function that runs the body code only in debug builds, without +/// emitting compiler warnings. +/// +/// This is currently the only way to do this in Swift: see +/// https://forums.swift.org/t/support-debug-only-code/11037 for a discussion. +@inlinable +internal func debugOnly(_ body: () -> Void) { + assert( + { + body() + return true + }() + ) } \ No newline at end of file From ecad0635ed6f90c9f6938a9a25850ece9aca7ec0 Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Fri, 14 Mar 2025 08:50:41 +0100 Subject: [PATCH 04/10] remove more foundation --- Sources/ArgumentParser/Parsing/CommandParser.swift | 4 ++++ Sources/ArgumentParser/Usage/DumpHelpGenerator.swift | 6 +++++- Sources/ArgumentParser/Usage/MessageInfo.swift | 8 ++++++++ Sources/ArgumentParser/Usage/UsageGenerator.swift | 4 ++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index 275dde07..a18f2149 100644 --- a/Sources/ArgumentParser/Parsing/CommandParser.swift +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -10,7 +10,11 @@ //===----------------------------------------------------------------------===// #if swift(>=6.0) +#if canImport(FoundationEssentials) +internal import class FoundationEssentials.ProcessInfo +#else internal import class Foundation.ProcessInfo +#endif #else import class Foundation.ProcessInfo #endif diff --git a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift index dc4b6c00..ecf7063b 100644 --- a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift @@ -11,7 +11,11 @@ #if swift(>=6.0) internal import ArgumentParserToolInfo -internal import class Foundation.JSONEncoder +#if canImport(FoundationEssentials) +internal import class FoundationEssentials.ProcessInfo +#else +internal import class Foundation.ProcessInfo +#endif #else import ArgumentParserToolInfo import class Foundation.JSONEncoder diff --git a/Sources/ArgumentParser/Usage/MessageInfo.swift b/Sources/ArgumentParser/Usage/MessageInfo.swift index 4e42b460..51db7de7 100644 --- a/Sources/ArgumentParser/Usage/MessageInfo.swift +++ b/Sources/ArgumentParser/Usage/MessageInfo.swift @@ -10,8 +10,12 @@ //===----------------------------------------------------------------------===// #if swift(>=6.0) +#if canImport(FoundationEssentials) +internal import protocol FoundationEssentials.LocalizedError +#else internal import protocol Foundation.LocalizedError internal import class Foundation.NSError +#endif #else import protocol Foundation.LocalizedError import class Foundation.NSError @@ -135,11 +139,15 @@ enum MessageInfo { // No way to unwrap bind description in pattern self = .other(message: error.errorDescription!, exitCode: .failure) default: + #if canImport(FoundationEssentials) + self = .other(message: String(describing: error), exitCode: .failure) + #else if Swift.type(of: error) is NSError.Type { self = .other(message: error.localizedDescription, exitCode: .failure) } else { self = .other(message: String(describing: error), exitCode: .failure) } + #endif } } else if let parserError = parserError { let usage: String = { diff --git a/Sources/ArgumentParser/Usage/UsageGenerator.swift b/Sources/ArgumentParser/Usage/UsageGenerator.swift index 9c628c0c..8d509b58 100644 --- a/Sources/ArgumentParser/Usage/UsageGenerator.swift +++ b/Sources/ArgumentParser/Usage/UsageGenerator.swift @@ -10,7 +10,11 @@ //===----------------------------------------------------------------------===// #if swift(>=6.0) +#if canImport(FoundationEssentials) +internal import protocol FoundationEssentials.LocalizedError +#else internal import protocol Foundation.LocalizedError +#endif #else import protocol Foundation.LocalizedError #endif From da0d3b6ab53e491d92bf175e55c8866085f1e843 Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Fri, 14 Mar 2025 09:55:46 +0100 Subject: [PATCH 05/10] remove more foundation --- Package.swift | 3 +- .../Completions/CompletionsGenerator.swift | 4 +- .../FishCompletionsGenerator.swift | 2 +- .../Completions/ZshCompletionsGenerator.swift | 6 +- .../Parsable Types/ParsableArguments.swift | 2 +- .../Parsing/CommandParser.swift | 5 + .../Usage/DumpHelpGenerator.swift | 8 +- .../ArgumentParser/Usage/MessageInfo.swift | 4 + .../ArgumentParser/Usage/UsageGenerator.swift | 4 + .../Utilities/BidirectionalCollection.swift | 129 ++++++++++++++++++ 10 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 Sources/ArgumentParser/Utilities/BidirectionalCollection.swift diff --git a/Package.swift b/Package.swift index a27eaa22..88b6a6c0 100644 --- a/Package.swift +++ b/Package.swift @@ -119,7 +119,8 @@ var package = Package( name: "ArgumentParserUnitTests", dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], exclude: ["CMakeLists.txt", "Snapshots"]), - ] + ], + swiftLanguageModes: [.v6] ) #if os(macOS) diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index d52fca85..89aa88b5 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -206,11 +206,11 @@ extension String { func shellEscapeForSingleQuotedString(iterationCount: UInt64 = 1) -> Self { iterationCount == 0 ? self - : replacingOccurrences(of: "'", with: "'\\''") + : replacing("'", with: "'\\''") .shellEscapeForSingleQuotedString(iterationCount: iterationCount - 1) } func shellEscapeForVariableName() -> Self { - replacingOccurrences(of: "-", with: "_") + replacing("-", with: "_") } } diff --git a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift index 3054187b..f006bbf0 100644 --- a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift @@ -133,7 +133,7 @@ extension Name { extension String { fileprivate func fishEscape() -> String { - replacingOccurrences(of: "'", with: #"\'"#) + replacing("'", with: #"\'"#) } } diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 826af9d2..f20b31f7 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -222,11 +222,7 @@ extension [ParsableCommand.Type] { extension String { fileprivate func zshEscapeForSingleQuotedExplanation() -> String { - replacingOccurrences( - of: #"[\\\[\]]"#, - with: #"\\$0"#, - options: .regularExpression - ) + replacing(#"[\\\[\]]"#, with: #"\\$0"#) .shellEscapeForSingleQuotedString() } } diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift index 7a476999..1bbf9273 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift @@ -35,7 +35,7 @@ struct _WrappedParsableCommand: ParsableCommand { // If the type is named something like "TransformOptions", we only want // to use "transform" as the command name. - if let optionsRange = name.range(of: "_options"), + if let optionsRange = name._range(of: "_options"), optionsRange.upperBound == name.endIndex { return String(name[..=6.0) internal import ArgumentParserToolInfo #if canImport(FoundationEssentials) -internal import class FoundationEssentials.ProcessInfo +internal import class FoundationEssentials.JSONEncoder #else -internal import class Foundation.ProcessInfo +internal import class Foundation.JSONEncoder #endif #else import ArgumentParserToolInfo +#if canImport(FoundationEssentials) +import class FoundationEssentials.JSONEncoder +#else import class Foundation.JSONEncoder #endif +#endif internal struct DumpHelpGenerator { private var toolInfo: ToolInfoV0 diff --git a/Sources/ArgumentParser/Usage/MessageInfo.swift b/Sources/ArgumentParser/Usage/MessageInfo.swift index 51db7de7..ed22cdf5 100644 --- a/Sources/ArgumentParser/Usage/MessageInfo.swift +++ b/Sources/ArgumentParser/Usage/MessageInfo.swift @@ -17,9 +17,13 @@ internal import protocol Foundation.LocalizedError internal import class Foundation.NSError #endif #else +#if canImport(FoundationEssentials) +import protocol FoundationEssentials.LocalizedError +#else import protocol Foundation.LocalizedError import class Foundation.NSError #endif +#endif enum MessageInfo { case help(text: String) diff --git a/Sources/ArgumentParser/Usage/UsageGenerator.swift b/Sources/ArgumentParser/Usage/UsageGenerator.swift index 8d509b58..d1e74580 100644 --- a/Sources/ArgumentParser/Usage/UsageGenerator.swift +++ b/Sources/ArgumentParser/Usage/UsageGenerator.swift @@ -16,8 +16,12 @@ internal import protocol FoundationEssentials.LocalizedError internal import protocol Foundation.LocalizedError #endif #else +#if canImport(FoundationEssentials) +import protocol FoundationEssentials.LocalizedError +#else import protocol Foundation.LocalizedError #endif +#endif struct UsageGenerator { var toolName: String diff --git a/Sources/ArgumentParser/Utilities/BidirectionalCollection.swift b/Sources/ArgumentParser/Utilities/BidirectionalCollection.swift new file mode 100644 index 00000000..2301c12b --- /dev/null +++ b/Sources/ArgumentParser/Utilities/BidirectionalCollection.swift @@ -0,0 +1,129 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +extension BidirectionalCollection where Index == String.Index { + internal func _alignIndex(roundingDown i: Index) -> Index { + index(i, offsetBy: 0) + } + + internal func _alignIndex(roundingUp i: Index) -> Index { + let truncated = _alignIndex(roundingDown: i) + guard i > truncated && truncated < endIndex else { return truncated } + return index(after: truncated) + } + + internal func _boundaryAlignedRange(_ r: some RangeExpression) -> Range { + let range = r.relative(to: self) + return _alignIndex(roundingDown: range.lowerBound)..<_alignIndex(roundingUp: range.upperBound) + } + + internal func _checkRange(_ r: Range) -> Range? { + guard r.lowerBound >= startIndex, r.upperBound <= endIndex else { + return nil + } + return r + } +} + +extension BidirectionalCollection { + func _trimmingCharacters(while predicate: (Element) -> Bool) -> SubSequence { + var idx = startIndex + while idx < endIndex && predicate(self[idx]) { + formIndex(after: &idx) + } + + let startOfNonTrimmedRange = idx // Points at the first char not in the set + guard startOfNonTrimmedRange != endIndex else { + return self[endIndex...] + } + + let beforeEnd = index(before: endIndex) + guard startOfNonTrimmedRange < beforeEnd else { + return self[startOfNonTrimmedRange ..< endIndex] + } + + var backIdx = beforeEnd + // No need to bound-check because we've already trimmed from the beginning, so we'd definitely break off of this loop before `backIdx` rewinds before `startIndex` + while predicate(self[backIdx]) { + formIndex(before: &backIdx) + } + return self[startOfNonTrimmedRange ... backIdx] + } + + // Equal to calling `index(&idx, offsetBy: -other.count)` with just one loop + func _index(_ index: Index, backwardsOffsetByCountOf other: S) -> Index? { + var idx = index + var otherIdx = other.endIndex + while otherIdx > other.startIndex { + guard idx > startIndex else { + // other.count > self.count: bail + return nil + } + other.formIndex(before: &otherIdx) + formIndex(before: &idx) + } + return idx + } + + func _range(of other: S, anchored: Bool = false, backwards: Bool = false) -> Range? where S.Element == Element, Element : Equatable { + var result: Range? = nil + var fromLoc: Index + var toLoc: Index + if backwards { + guard let idx = _index(endIndex, backwardsOffsetByCountOf: other) else { + // other.count > string.count: bail + return nil + } + fromLoc = idx + + toLoc = anchored ? fromLoc : startIndex + } else { + fromLoc = startIndex + if anchored { + toLoc = fromLoc + } else { + guard let idx = _index(endIndex, backwardsOffsetByCountOf: other) else { + return nil + } + toLoc = idx + } + } + + let delta = fromLoc <= toLoc ? 1 : -1 + + while true { + var str1Index = fromLoc + var str2Index = other.startIndex + + while str2Index < other.endIndex && str1Index < endIndex { + if self[str1Index] != other[str2Index] { + break + } + formIndex(after: &str1Index) + other.formIndex(after: &str2Index) + } + + if str2Index == other.endIndex { + result = fromLoc.. Date: Fri, 14 Mar 2025 09:56:54 +0100 Subject: [PATCH 06/10] remove language mode --- Package.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 88b6a6c0..a27eaa22 100644 --- a/Package.swift +++ b/Package.swift @@ -119,8 +119,7 @@ var package = Package( name: "ArgumentParserUnitTests", dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], exclude: ["CMakeLists.txt", "Snapshots"]), - ], - swiftLanguageModes: [.v6] + ] ) #if os(macOS) From f1fbc9f0ae9738e7d55b9dd996c54c194dc10a3d Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Fri, 14 Mar 2025 23:43:22 +0100 Subject: [PATCH 07/10] support macOS and fix regex replacement --- .../Completions/CompletionsGenerator.swift | 16 +++++++++++++++- .../Completions/FishCompletionsGenerator.swift | 6 ++++++ .../Completions/ZshCompletionsGenerator.swift | 12 +++++++++++- .../ArgumentParserUnitTests/ExitCodeTests.swift | 6 +++++- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index 89aa88b5..af1b40df 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -203,14 +203,28 @@ extension Sequence where Element == ParsableCommand.Type { } extension String { + #if canImport(FoundationEssentials) func shellEscapeForSingleQuotedString(iterationCount: UInt64 = 1) -> Self { iterationCount == 0 ? self : replacing("'", with: "'\\''") .shellEscapeForSingleQuotedString(iterationCount: iterationCount - 1) } - + #else + func shellEscapeForSingleQuotedString(iterationCount: UInt64 = 1) -> Self { + iterationCount == 0 + ? self + : replacingOccurrences(of: "'", with: "'\\''") + .shellEscapeForSingleQuotedString(iterationCount: iterationCount - 1) + } + #endif + #if canImport(FoundationEssentials) func shellEscapeForVariableName() -> Self { replacing("-", with: "_") } + #else + func shellEscapeForVariableName() -> Self { + replacingOccurrences(of: "-", with: "_") + } + #endif } diff --git a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift index f006bbf0..db282f4f 100644 --- a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift @@ -132,9 +132,15 @@ extension Name { } extension String { + #if canImport(FoundationEssentials) fileprivate func fishEscape() -> String { replacing("'", with: #"\'"#) } + #else + fileprivate func fishEscape() -> String { + replacingOccurrences(of: "'", with: #"\'"#) + } + #endif } extension FishCompletionsGenerator { diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index f20b31f7..988ade8f 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -221,10 +221,20 @@ extension [ParsableCommand.Type] { } extension String { + #if canImport(FoundationEssentials) fileprivate func zshEscapeForSingleQuotedExplanation() -> String { - replacing(#"[\\\[\]]"#, with: #"\\$0"#) + replacing(#/[\\\[\]]/#, with: { "\\\($0.output)" }) .shellEscapeForSingleQuotedString() } + #else + fileprivate func zshEscapeForSingleQuotedExplanation() -> String { + replacingOccurrences( + of: #"[\\\[\]]"#, + with: #"\\$0"#, + options: .regularExpression) + .shellEscapeForSingleQuotedString() + } + #endif } extension ArgumentDefinition { diff --git a/Tests/ArgumentParserUnitTests/ExitCodeTests.swift b/Tests/ArgumentParserUnitTests/ExitCodeTests.swift index fbe62da7..138db1c9 100644 --- a/Tests/ArgumentParserUnitTests/ExitCodeTests.swift +++ b/Tests/ArgumentParserUnitTests/ExitCodeTests.swift @@ -83,7 +83,10 @@ extension ExitCodeTests { // MARK: - NSError tests extension ExitCodeTests { - func testNSErrorIsHandled() { + func testNSErrorIsHandled() throws{ + #if canImport(FoundationEssentials) + throw XCTSkip("FoundationEssentials doesn't have NSError") + #else struct NSErrorCommand: ParsableCommand { static let fileNotFoundNSError = NSError( domain: "", code: 1, @@ -98,5 +101,6 @@ extension ExitCodeTests { XCTAssertEqual( NSErrorCommand.message(for: NSErrorCommand.fileNotFoundNSError), "The file “foo/bar” couldn’t be opened because there is no such file") + #endif } } From ed38120ce70cf8c077f47df046572abdef9ce3ae Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Sat, 15 Mar 2025 15:51:09 +0100 Subject: [PATCH 08/10] use Synchronization.Mutex if available --- Sources/ArgumentParser/Utilities/Mutex.swift | 304 ++----------------- 1 file changed, 25 insertions(+), 279 deletions(-) diff --git a/Sources/ArgumentParser/Utilities/Mutex.swift b/Sources/ArgumentParser/Utilities/Mutex.swift index d9445892..5c371038 100644 --- a/Sources/ArgumentParser/Utilities/Mutex.swift +++ b/Sources/ArgumentParser/Utilities/Mutex.swift @@ -9,6 +9,29 @@ // //===----------------------------------------------------------------------===// +#if canImport(Synchronization) +import Synchronization + +/// A synchronization primitive that protects shared mutable state via mutual +/// exclusion. +/// +/// The `Mutex` type offers non-recursive exclusive access to the state it is +/// protecting by blocking threads attempting to acquire the lock. Only one +/// execution context at a time has access to the value stored within the +/// `Mutex` allowing for exclusive access. +class Mutex: @unchecked Sendable { + private let mutex: Synchronization.Mutex + + init(_ value: sending T) { + self.mutex = .init(value) + } + + func withLock(_ body: (inout sending T) throws -> sending U) rethrows -> sending U { + try mutex.withLock(body) + } +} +#else +import Foundation /// A synchronization primitive that protects shared mutable state via mutual /// exclusion. /// @@ -18,7 +41,7 @@ /// `Mutex` allowing for exclusive access. class Mutex: @unchecked Sendable { /// The lock used to synchronize access to the value. - var lock: NIOLock + var lock: NSLock /// The value protected by the mutex. var value: T @@ -52,281 +75,4 @@ class Mutex: @unchecked Sendable { return try body(&self.value) } } - - -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftNIO open source project -// -// Copyright (c) 2017-2022 Apple Inc. and the SwiftNIO project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftNIO project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if canImport(Darwin) -import Darwin -#elseif os(Windows) -import ucrt -import WinSDK -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#elseif canImport(Bionic) -import Bionic -#elseif canImport(WASILibc) -import WASILibc -#if canImport(wasi_pthread) -import wasi_pthread -#endif -#else -#error("The concurrency NIOLock module was unable to identify your C library.") -#endif - -#if os(Windows) -@usableFromInline -typealias LockPrimitive = SRWLOCK -#else -@usableFromInline -typealias LockPrimitive = pthread_mutex_t -#endif - -@usableFromInline -enum LockOperations {} - -extension LockOperations { - @inlinable - static func create(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - #if os(Windows) - InitializeSRWLock(mutex) - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - var attr = pthread_mutexattr_t() - pthread_mutexattr_init(&attr) - debugOnly { - pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) - } - - let err = pthread_mutex_init(mutex, &attr) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif - } - - @inlinable - static func destroy(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - #if os(Windows) - // SRWLOCK does not need to be free'd - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_mutex_destroy(mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif - } - - @inlinable - static func lock(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - #if os(Windows) - AcquireSRWLockExclusive(mutex) - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_mutex_lock(mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif - } - - @inlinable - static func unlock(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - #if os(Windows) - ReleaseSRWLockExclusive(mutex) - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_mutex_unlock(mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif - } -} - -// Tail allocate both the mutex and a generic value using ManagedBuffer. -// Both the header pointer and the elements pointer are stable for -// the class's entire lifetime. -// -// However, for safety reasons, we elect to place the lock in the "elements" -// section of the buffer instead of the head. The reasoning here is subtle, -// so buckle in. -// -// _As a practical matter_, the implementation of ManagedBuffer ensures that -// the pointer to the header is stable across the lifetime of the class, and so -// each time you call `withUnsafeMutablePointers` or `withUnsafeMutablePointerToHeader` -// the value of the header pointer will be the same. This is because ManagedBuffer uses -// `Builtin.addressOf` to load the value of the header, and that does ~magic~ to ensure -// that it does not invoke any weird Swift accessors that might copy the value. -// -// _However_, the header is also available via the `.header` field on the ManagedBuffer. -// This presents a problem! The reason there's an issue is that `Builtin.addressOf` and friends -// do not interact with Swift's exclusivity model. That is, the various `with` functions do not -// conceptually trigger a mutating access to `.header`. For elements this isn't a concern because -// there's literally no other way to perform the access, but for `.header` it's entirely possible -// to accidentally recursively read it. -// -// Our implementation is free from these issues, so we don't _really_ need to worry about it. -// However, out of an abundance of caution, we store the Value in the header, and the LockPrimitive -// in the trailing elements. We still don't use `.header`, but it's better to be safe than sorry, -// and future maintainers will be happier that we were cautious. -// -// See also: https://github.com/apple/swift/pull/40000 -@usableFromInline -final class LockStorage: ManagedBuffer { - - @inlinable - static func create(value: Value) -> Self { - let buffer = Self.create(minimumCapacity: 1) { _ in - value - } - // Intentionally using a force cast here to avoid a miss compiliation in 5.10. - // This is as fast as an unsafeDownCast since ManagedBuffer is inlined and the optimizer - // can eliminate the upcast/downcast pair - let storage = buffer as! Self - - storage.withUnsafeMutablePointers { _, lockPtr in - LockOperations.create(lockPtr) - } - - return storage - } - - @inlinable - func lock() { - self.withUnsafeMutablePointerToElements { lockPtr in - LockOperations.lock(lockPtr) - } - } - - @inlinable - func unlock() { - self.withUnsafeMutablePointerToElements { lockPtr in - LockOperations.unlock(lockPtr) - } - } - - @inlinable - deinit { - self.withUnsafeMutablePointerToElements { lockPtr in - LockOperations.destroy(lockPtr) - } - } - - @inlinable - func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { - try self.withUnsafeMutablePointerToElements { lockPtr in - try body(lockPtr) - } - } - - @inlinable - func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { - try self.withUnsafeMutablePointers { valuePtr, lockPtr in - LockOperations.lock(lockPtr) - defer { LockOperations.unlock(lockPtr) } - return try mutate(&valuePtr.pointee) - } - } -} - -/// A threading lock based on `libpthread` instead of `libdispatch`. -/// -/// - Note: ``NIOLock`` has reference semantics. -/// -/// This object provides a lock on top of a single `pthread_mutex_t`. This kind -/// of lock is safe to use with `libpthread`-based threading models, such as the -/// one used by NIO. On Windows, the lock is based on the substantially similar -/// `SRWLOCK` type. -internal struct NIOLock { - @usableFromInline - internal let _storage: LockStorage - - /// Create a new lock. - @inlinable - init() { - self._storage = .create(value: ()) - } - - /// Acquire the lock. - /// - /// Whenever possible, consider using `withLock` instead of this method and - /// `unlock`, to simplify lock handling. - @inlinable - func lock() { - self._storage.lock() - } - - /// Release the lock. - /// - /// Whenever possible, consider using `withLock` instead of this method and - /// `lock`, to simplify lock handling. - @inlinable - func unlock() { - self._storage.unlock() - } - - @inlinable - internal func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { - try self._storage.withLockPrimitive(body) - } -} - -extension NIOLock { - /// Acquire the lock for the duration of the given block. - /// - /// This convenience method should be preferred to `lock` and `unlock` in - /// most situations, as it ensures that the lock will be released regardless - /// of how `body` exits. - /// - /// - Parameter body: The block to execute while holding the lock. - /// - Returns: The value returned by the block. - @inlinable - func withLock(_ body: () throws -> T) rethrows -> T { - self.lock() - defer { - self.unlock() - } - return try body() - } - - @inlinable - func withLockVoid(_ body: () throws -> Void) rethrows { - try self.withLock(body) - } -} - -extension NIOLock: @unchecked Sendable {} - -extension UnsafeMutablePointer { - @inlinable - func assertValidAlignment() { - assert(UInt(bitPattern: self) % UInt(MemoryLayout.alignment) == 0) - } -} - -/// A utility function that runs the body code only in debug builds, without -/// emitting compiler warnings. -/// -/// This is currently the only way to do this in Swift: see -/// https://forums.swift.org/t/support-debug-only-code/11037 for a discussion. -@inlinable -internal func debugOnly(_ body: () -> Void) { - assert( - { - body() - return true - }() - ) -} \ No newline at end of file +#endif \ No newline at end of file From a38abc1aa64fea4fe6d87afebe6df5b3cb6b544f Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Fri, 21 Mar 2025 16:37:36 +0100 Subject: [PATCH 09/10] adds newline --- Sources/ArgumentParser/Completions/CompletionsGenerator.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index af1b40df..b9506661 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -218,6 +218,7 @@ extension String { .shellEscapeForSingleQuotedString(iterationCount: iterationCount - 1) } #endif + #if canImport(FoundationEssentials) func shellEscapeForVariableName() -> Self { replacing("-", with: "_") From 2e8148c9783b2e986b82415f0661ebbade49b8bc Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Tue, 25 Mar 2025 22:16:26 +0100 Subject: [PATCH 10/10] make mutex work on all swift versions 5.7 ... 6.0 --- .../Utilities/BidirectionalCollection.swift | 47 --- Sources/ArgumentParser/Utilities/Mutex.swift | 295 ++++++++++++++++-- 2 files changed, 272 insertions(+), 70 deletions(-) diff --git a/Sources/ArgumentParser/Utilities/BidirectionalCollection.swift b/Sources/ArgumentParser/Utilities/BidirectionalCollection.swift index 2301c12b..32710e2f 100644 --- a/Sources/ArgumentParser/Utilities/BidirectionalCollection.swift +++ b/Sources/ArgumentParser/Utilities/BidirectionalCollection.swift @@ -10,55 +10,8 @@ // //===----------------------------------------------------------------------===// -extension BidirectionalCollection where Index == String.Index { - internal func _alignIndex(roundingDown i: Index) -> Index { - index(i, offsetBy: 0) - } - - internal func _alignIndex(roundingUp i: Index) -> Index { - let truncated = _alignIndex(roundingDown: i) - guard i > truncated && truncated < endIndex else { return truncated } - return index(after: truncated) - } - - internal func _boundaryAlignedRange(_ r: some RangeExpression) -> Range { - let range = r.relative(to: self) - return _alignIndex(roundingDown: range.lowerBound)..<_alignIndex(roundingUp: range.upperBound) - } - - internal func _checkRange(_ r: Range) -> Range? { - guard r.lowerBound >= startIndex, r.upperBound <= endIndex else { - return nil - } - return r - } -} extension BidirectionalCollection { - func _trimmingCharacters(while predicate: (Element) -> Bool) -> SubSequence { - var idx = startIndex - while idx < endIndex && predicate(self[idx]) { - formIndex(after: &idx) - } - - let startOfNonTrimmedRange = idx // Points at the first char not in the set - guard startOfNonTrimmedRange != endIndex else { - return self[endIndex...] - } - - let beforeEnd = index(before: endIndex) - guard startOfNonTrimmedRange < beforeEnd else { - return self[startOfNonTrimmedRange ..< endIndex] - } - - var backIdx = beforeEnd - // No need to bound-check because we've already trimmed from the beginning, so we'd definitely break off of this loop before `backIdx` rewinds before `startIndex` - while predicate(self[backIdx]) { - formIndex(before: &backIdx) - } - return self[startOfNonTrimmedRange ... backIdx] - } - // Equal to calling `index(&idx, offsetBy: -other.count)` with just one loop func _index(_ index: Index, backwardsOffsetByCountOf other: S) -> Index? { var idx = index diff --git a/Sources/ArgumentParser/Utilities/Mutex.swift b/Sources/ArgumentParser/Utilities/Mutex.swift index 5c371038..7f533c37 100644 --- a/Sources/ArgumentParser/Utilities/Mutex.swift +++ b/Sources/ArgumentParser/Utilities/Mutex.swift @@ -9,8 +9,9 @@ // //===----------------------------------------------------------------------===// -#if canImport(Synchronization) -import Synchronization +#if !canImport(FoundationEssentials) +import class Foundation.NSLock +#endif /// A synchronization primitive that protects shared mutable state via mutual /// exclusion. @@ -19,29 +20,13 @@ import Synchronization /// protecting by blocking threads attempting to acquire the lock. Only one /// execution context at a time has access to the value stored within the /// `Mutex` allowing for exclusive access. -class Mutex: @unchecked Sendable { - private let mutex: Synchronization.Mutex - - init(_ value: sending T) { - self.mutex = .init(value) - } - - func withLock(_ body: (inout sending T) throws -> sending U) rethrows -> sending U { - try mutex.withLock(body) - } -} -#else -import Foundation -/// A synchronization primitive that protects shared mutable state via mutual -/// exclusion. -/// -/// The `Mutex` type offers non-recursive exclusive access to the state it is -/// protecting by blocking threads attempting to acquire the lock. Only one -/// execution context at a time has access to the value stored within the -/// `Mutex` allowing for exclusive access. -class Mutex: @unchecked Sendable { +final class Mutex: @unchecked Sendable { /// The lock used to synchronize access to the value. + #if canImport(FoundationEssentials) + var lock: NIOLock + #else var lock: NSLock + #endif /// The value protected by the mutex. var value: T @@ -75,4 +60,268 @@ class Mutex: @unchecked Sendable { return try body(&self.value) } } + +#if canImport(FoundationEssentials) + +#if os(Windows) +import ucrt +import WinSDK +#elseif canImport(Glibc) +@preconcurrency import Glibc +#elseif canImport(Musl) +@preconcurrency import Musl +#elseif canImport(Bionic) +@preconcurrency import Bionic +#elseif canImport(WASILibc) +@preconcurrency import WASILibc +#if canImport(wasi_pthread) +import wasi_pthread +#endif +#else +#error("The concurrency NIOLock module was unable to identify your C library.") +#endif + +#if os(Windows) +@usableFromInline +typealias LockPrimitive = SRWLOCK +#else +@usableFromInline +typealias LockPrimitive = pthread_mutex_t +#endif + +@usableFromInline +enum LockOperations {} + +extension LockOperations { + @inlinable + static func create(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + #if os(Windows) + InitializeSRWLock(mutex) + #elseif !os(WASI) + var attr = pthread_mutexattr_t() + pthread_mutexattr_init(&attr) + debugOnly { + pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) + } + + let err = pthread_mutex_init(mutex, &attr) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } + + @inlinable + static func destroy(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + #if os(Windows) + // SRWLOCK does not need to be free'd + #elseif !os(WASI) + let err = pthread_mutex_destroy(mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } + + @inlinable + static func lock(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + #if os(Windows) + AcquireSRWLockExclusive(mutex) + #elseif !os(WASI) + let err = pthread_mutex_lock(mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } + + @inlinable + static func unlock(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + #if os(Windows) + ReleaseSRWLockExclusive(mutex) + #elseif !os(WASI) + let err = pthread_mutex_unlock(mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } +} + +// Tail allocate both the mutex and a generic value using ManagedBuffer. +// Both the header pointer and the elements pointer are stable for +// the class's entire lifetime. +// +// However, for safety reasons, we elect to place the lock in the "elements" +// section of the buffer instead of the head. The reasoning here is subtle, +// so buckle in. +// +// _As a practical matter_, the implementation of ManagedBuffer ensures that +// the pointer to the header is stable across the lifetime of the class, and so +// each time you call `withUnsafeMutablePointers` or `withUnsafeMutablePointerToHeader` +// the value of the header pointer will be the same. This is because ManagedBuffer uses +// `Builtin.addressOf` to load the value of the header, and that does ~magic~ to ensure +// that it does not invoke any weird Swift accessors that might copy the value. +// +// _However_, the header is also available via the `.header` field on the ManagedBuffer. +// This presents a problem! The reason there's an issue is that `Builtin.addressOf` and friends +// do not interact with Swift's exclusivity model. That is, the various `with` functions do not +// conceptually trigger a mutating access to `.header`. For elements this isn't a concern because +// there's literally no other way to perform the access, but for `.header` it's entirely possible +// to accidentally recursively read it. +// +// Our implementation is free from these issues, so we don't _really_ need to worry about it. +// However, out of an abundance of caution, we store the Value in the header, and the LockPrimitive +// in the trailing elements. We still don't use `.header`, but it's better to be safe than sorry, +// and future maintainers will be happier that we were cautious. +// +// See also: https://github.com/apple/swift/pull/40000 +@usableFromInline +final class LockStorage: ManagedBuffer { + + @inlinable + static func create(value: Value) -> Self { + let buffer = Self.create(minimumCapacity: 1) { _ in + value + } + // Intentionally using a force cast here to avoid a miss compiliation in 5.10. + // This is as fast as an unsafeDownCast since ManagedBuffer is inlined and the optimizer + // can eliminate the upcast/downcast pair + let storage = buffer as! Self + + storage.withUnsafeMutablePointers { _, lockPtr in + LockOperations.create(lockPtr) + } + + return storage + } + + @inlinable + func lock() { + self.withUnsafeMutablePointerToElements { lockPtr in + LockOperations.lock(lockPtr) + } + } + + @inlinable + func unlock() { + self.withUnsafeMutablePointerToElements { lockPtr in + LockOperations.unlock(lockPtr) + } + } + + @inlinable + deinit { + self.withUnsafeMutablePointerToElements { lockPtr in + LockOperations.destroy(lockPtr) + } + } + + @inlinable + func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { + try self.withUnsafeMutablePointerToElements { lockPtr in + try body(lockPtr) + } + } + + @inlinable + func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { + try self.withUnsafeMutablePointers { valuePtr, lockPtr in + LockOperations.lock(lockPtr) + defer { LockOperations.unlock(lockPtr) } + return try mutate(&valuePtr.pointee) + } + } +} + +/// A threading lock based on `libpthread` instead of `libdispatch`. +/// +/// - Note: ``NIOLock`` has reference semantics. +/// +/// This object provides a lock on top of a single `pthread_mutex_t`. This kind +/// of lock is safe to use with `libpthread`-based threading models, such as the +/// one used by NIO. On Windows, the lock is based on the substantially similar +/// `SRWLOCK` type. +public struct NIOLock { + @usableFromInline + internal let _storage: LockStorage + + /// Create a new lock. + @inlinable + public init() { + self._storage = .create(value: ()) + } + + /// Acquire the lock. + /// + /// Whenever possible, consider using `withLock` instead of this method and + /// `unlock`, to simplify lock handling. + @inlinable + public func lock() { + self._storage.lock() + } + + /// Release the lock. + /// + /// Whenever possible, consider using `withLock` instead of this method and + /// `lock`, to simplify lock handling. + @inlinable + public func unlock() { + self._storage.unlock() + } + + @inlinable + internal func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { + try self._storage.withLockPrimitive(body) + } +} + +extension NIOLock { + /// Acquire the lock for the duration of the given block. + /// + /// This convenience method should be preferred to `lock` and `unlock` in + /// most situations, as it ensures that the lock will be released regardless + /// of how `body` exits. + /// + /// - Parameter body: The block to execute while holding the lock. + /// - Returns: The value returned by the block. + @inlinable + public func withLock(_ body: () throws -> T) rethrows -> T { + self.lock() + defer { + self.unlock() + } + return try body() + } + + @inlinable + public func withLockVoid(_ body: () throws -> Void) rethrows { + try self.withLock(body) + } +} + +extension NIOLock: @unchecked Sendable {} + +extension UnsafeMutablePointer { + @inlinable + func assertValidAlignment() { + assert(UInt(bitPattern: self) % UInt(MemoryLayout.alignment) == 0) + } +} + +/// A utility function that runs the body code only in debug builds, without +/// emitting compiler warnings. +/// +/// This is currently the only way to do this in Swift: see +/// https://forums.swift.org/t/support-debug-only-code/11037 for a discussion. +@inlinable +internal func debugOnly(_ body: () -> Void) { + assert( + { + body() + return true + }() + ) +} + #endif \ No newline at end of file