diff --git a/.licenseignore b/.licenseignore index d2535f5..9bafa9f 100644 --- a/.licenseignore +++ b/.licenseignore @@ -17,7 +17,7 @@ *.json Package.swift **/Package.swift -Package@-*.swift +Package@swift*.swift **/Package@-*.swift Package.resolved **/Package.resolved diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift new file mode 100644 index 0000000..d03fdaf --- /dev/null +++ b/Package@swift-6.0.swift @@ -0,0 +1,52 @@ +// swift-tools-version:6.0 +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftPrometheus open source project +// +// Copyright (c) 2018-2025 SwiftPrometheus project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftPrometheus project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import PackageDescription + +let package = Package( + name: "swift-prometheus", + platforms: [.macOS(.v13), .iOS(.v16), .watchOS(.v9), .tvOS(.v16)], + products: [ + .library( + name: "Prometheus", + targets: ["Prometheus"] + ) + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"), + .package(url: "https://github.com/apple/swift-metrics.git", from: "2.4.1"), + ], + targets: [ + .target( + name: "Prometheus", + dependencies: [ + .product(name: "Atomics", package: "swift-atomics"), + .product(name: "CoreMetrics", package: "swift-metrics"), + ] + ), + .testTarget( + name: "PrometheusTests", + dependencies: [ + "Prometheus" + ] + ), + ] +) + +for target in package.targets { + var settings = target.swiftSettings ?? [] + settings.append(.enableExperimentalFeature("StrictConcurrency=complete")) + target.swiftSettings = settings +} diff --git a/Sources/Prometheus/NIOLock.swift b/Sources/Prometheus/NIOLock.swift index 3a80b6c..69bb58f 100644 --- a/Sources/Prometheus/NIOLock.swift +++ b/Sources/Prometheus/NIOLock.swift @@ -31,9 +31,16 @@ import Darwin import ucrt import WinSDK #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import 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 @@ -47,7 +54,7 @@ typealias LockPrimitive = pthread_mutex_t #endif @usableFromInline -enum LockOperations {} +enum LockOperations: Sendable {} extension LockOperations { @inlinable @@ -56,12 +63,15 @@ extension LockOperations { #if os(Windows) InitializeSRWLock(mutex) - #else + #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)) - } + assert( + { + pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) + return true + }() + ) let err = pthread_mutex_init(mutex, &attr) precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") @@ -74,7 +84,7 @@ extension LockOperations { #if os(Windows) // SRWLOCK does not need to be free'd - #else + #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 @@ -86,7 +96,7 @@ extension LockOperations { #if os(Windows) AcquireSRWLockExclusive(mutex) - #else + #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 @@ -98,7 +108,7 @@ extension LockOperations { #if os(Windows) ReleaseSRWLockExclusive(mutex) - #else + #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 @@ -139,9 +149,11 @@ final class LockStorage: ManagedBuffer { @inlinable static func create(value: Value) -> Self { let buffer = Self.create(minimumCapacity: 1) { _ in - return value + value } - // Avoid 'unsafeDowncast' as there is a miscompilation on 5.10. + // 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 @@ -175,7 +187,7 @@ final class LockStorage: ManagedBuffer { @inlinable func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { try self.withUnsafeMutablePointerToElements { lockPtr in - return try body(lockPtr) + try body(lockPtr) } } @@ -189,11 +201,16 @@ final class LockStorage: ManagedBuffer { } } -extension LockStorage: @unchecked Sendable {} +// This compiler guard is here becaue `ManagedBuffer` is already declaring +// Sendable unavailability after 6.1, which `LockStorage` inherits. +#if compiler(<6.2) +@available(*, unavailable) +extension LockStorage: Sendable {} +#endif /// A threading lock based on `libpthread` instead of `libdispatch`. /// -/// - note: ``NIOLock`` has reference semantics. +/// - 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 @@ -229,7 +246,7 @@ struct NIOLock { @inlinable internal func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { - return try self._storage.withLockPrimitive(body) + try self._storage.withLockPrimitive(body) } } @@ -257,7 +274,7 @@ extension NIOLock { } } -extension NIOLock: Sendable {} +extension NIOLock: @unchecked Sendable {} extension UnsafeMutablePointer { @inlinable @@ -265,18 +282,3 @@ extension UnsafeMutablePointer { 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 - }() - ) -} diff --git a/Sources/Prometheus/NIOLockedValueBox.swift b/Sources/Prometheus/NIOLockedValueBox.swift index 9dcef47..129f963 100644 --- a/Sources/Prometheus/NIOLockedValueBox.swift +++ b/Sources/Prometheus/NIOLockedValueBox.swift @@ -27,10 +27,10 @@ /// Provides locked access to `Value`. /// -/// - note: ``NIOLockedValueBox`` has reference semantics and holds the `Value` +/// - Note: ``NIOLockedValueBox`` has reference semantics and holds the `Value` /// alongside a lock behind a reference. /// -/// This is no different than creating a ``Lock`` and protecting all +/// This is no different than creating a `Lock` and protecting all /// accesses to a value using the lock. But it's easy to forget to actually /// acquire/release the lock in the correct place. ``NIOLockedValueBox`` makes /// that much easier. @@ -49,8 +49,50 @@ struct NIOLockedValueBox { /// Access the `Value`, allowing mutation of it. @inlinable func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { - return try self._storage.withLockedValue(mutate) + try self._storage.withLockedValue(mutate) + } + + /// Provides an unsafe view over the lock and its value. + /// + /// This can be beneficial when you require fine grained control over the lock in some + /// situations but don't want lose the benefits of ``withLockedValue(_:)`` in others by + /// switching to ``NIOLock``. + var unsafe: Unsafe { + Unsafe(_storage: self._storage) + } + + /// Provides an unsafe view over the lock and its value. + struct Unsafe { + @usableFromInline + let _storage: LockStorage + + /// Manually acquire the lock. + @inlinable + func lock() { + self._storage.lock() + } + + /// Manually release the lock. + @inlinable + func unlock() { + self._storage.unlock() + } + + /// Mutate the value, assuming the lock has been acquired manually. + /// + /// - Parameter mutate: A closure with scoped access to the value. + /// - Returns: The result of the `mutate` closure. + @inlinable + func withValueAssumingLockIsAcquired( + _ mutate: (_ value: inout Value) throws -> Result + ) rethrows -> Result { + try self._storage.withUnsafeMutablePointerToHeader { value in + try mutate(&value.pointee) + } + } } } -extension NIOLockedValueBox: Sendable where Value: Sendable {} +extension NIOLockedValueBox: @unchecked Sendable where Value: Sendable {} + +extension NIOLockedValueBox.Unsafe: @unchecked Sendable where Value: Sendable {}