Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 45 additions & 45 deletions Sources/SKLogging/NonDarwinLogging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//
//===----------------------------------------------------------------------===//

import Synchronization
public import Synchronization
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions

#if canImport(Darwin)
Expand All @@ -23,54 +23,45 @@ import Foundation
// MARK: - Log settings

@_spi(SourceKitLSP) @frozen public enum LogConfig {
/// The globally set log level
private static let _logLevel = ThreadSafeBox<NonDarwinLogLevel>(
initialValue: {
if let envVar = ProcessInfo.processInfo.environment["SOURCEKIT_LSP_LOG_LEVEL"],
let logLevel = NonDarwinLogLevel(envVar)
{
return logLevel
}
#if DEBUG
return .debug
#else
return .default
#endif
}()
)

@_spi(SourceKitLSP) public static var logLevel: NonDarwinLogLevel {
get {
_logLevel.value
private static var initialLogLevel: NonDarwinLogLevel {
if let envVar = ProcessInfo.processInfo.environment["SOURCEKIT_LSP_LOG_LEVEL"],
let logLevel = NonDarwinLogLevel(envVar)
{
return logLevel
}
set {
_logLevel.value = newValue
#if DEBUG
return .debug
#else
return .default
#endif
}

private static var initialPrivacyLevel: NonDarwinLogPrivacy {
if let envVar = ProcessInfo.processInfo.environment["SOURCEKIT_LSP_LOG_PRIVACY_LEVEL"],
let privacyLevel = NonDarwinLogPrivacy(envVar)
{
return privacyLevel
}
#if DEBUG
return .private
#else
return .public
#endif
}

/// The globally set privacy level
private static let _privacyLevel = ThreadSafeBox<NonDarwinLogPrivacy>(
initialValue: {
if let envVar = ProcessInfo.processInfo.environment["SOURCEKIT_LSP_LOG_PRIVACY_LEVEL"],
let privacyLevel = NonDarwinLogPrivacy(envVar)
{
return privacyLevel
}
#if DEBUG
return .private
#else
return .public
#endif
}()
)
private static let _logLevel = RefBox(Atomic<NonDarwinLogLevel>(initialLogLevel))
private static let _privacyLevel = RefBox(Atomic<NonDarwinLogPrivacy>(initialPrivacyLevel))

/// The globally set log level
@_spi(SourceKitLSP) public static var logLevel: NonDarwinLogLevel {
get { _logLevel.value.load(ordering: .relaxed) }
set { _logLevel.value.store(newValue, ordering: .relaxed) }
}

/// The globally set privacy level
@_spi(SourceKitLSP) public static var privacyLevel: NonDarwinLogPrivacy {
get {
_privacyLevel.value
}
set {
_privacyLevel.value = newValue
}
get { _privacyLevel.value.load(ordering: .relaxed) }
set { _privacyLevel.value.store(newValue, ordering: .relaxed) }
}
}

Expand All @@ -81,13 +72,18 @@ import Foundation
///
/// For documentation of the different log levels see
/// https://developer.apple.com/documentation/os/oslogtype.
@_spi(SourceKitLSP) @frozen public enum NonDarwinLogLevel: Comparable, CustomStringConvertible, Sendable {
@_spi(SourceKitLSP) @frozen
public enum NonDarwinLogLevel: UInt8, Comparable, CustomStringConvertible, AtomicRepresentable, Sendable {
case debug
case info
case `default`
case error
case fault

public static func < (lhs: NonDarwinLogLevel, rhs: NonDarwinLogLevel) -> Bool {
lhs.rawValue < rhs.rawValue
}

@_spi(SourceKitLSP) public init?(_ value: String) {
switch value.lowercased() {
case "debug": self = .debug
Expand Down Expand Up @@ -138,11 +134,15 @@ import Foundation
///
/// For documentation of the different privacy levels see
/// https://developer.apple.com/documentation/os/oslogprivacy.
@_spi(SourceKitLSP) @frozen public enum NonDarwinLogPrivacy: Comparable, Sendable {
@_spi(SourceKitLSP) @frozen public enum NonDarwinLogPrivacy: UInt8, Comparable, Sendable, AtomicRepresentable {
case `public`
case `private`
case sensitive

@_spi(SourceKitLSP) public static func < (lhs: NonDarwinLogPrivacy, rhs: NonDarwinLogPrivacy) -> Bool {
lhs.rawValue < rhs.rawValue
}

@_spi(SourceKitLSP) public init?(_ value: String) {
switch value.lowercased() {
case "sensitive": self = .sensitive
Expand Down
5 changes: 3 additions & 2 deletions Sources/ToolsProtocolsSwiftExtensions/AsyncUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ extension Task where Failure == Never {
operation: {
try Task.checkCancellation()
return try await withCheckedThrowingContinuation { continuation in
handleWrapper.value = operation(continuation)
let handle = operation(continuation)
handleWrapper.withLock { $0 = handle }

// Check if the task was cancelled. This ensures we send a
// CancelNotification even if the task gets cancelled after we register
Expand Down Expand Up @@ -285,7 +286,7 @@ package func withTimeout<T: Sendable>(
let stream = AsyncThrowingStream<T?, Error> { continuation in
Task {
try await Task.sleep(for: timeout)
didHitTimeout.value = true
didHitTimeout.withLock { $0 = true }
continuation.yield(nil)
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/ToolsProtocolsSwiftExtensions/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ set(sources
Collection+Only.swift
Duration+Seconds.swift
FileManagerExtensions.swift
NSLock+WithLock.swift
PipeAsStringHandler.swift
RefBox.swift
Task+WithPriorityChangedHandler.swift
ThreadSafeBox.swift
URLExtensions.swift
Expand Down
21 changes: 0 additions & 21 deletions Sources/ToolsProtocolsSwiftExtensions/NSLock+WithLock.swift

This file was deleted.

24 changes: 24 additions & 0 deletions Sources/ToolsProtocolsSwiftExtensions/RefBox.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2026 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
//
//===----------------------------------------------------------------------===//

/// A reference-typed container for a single value, useful for sharing non-copyable
/// values (such as `Mutex<T>` or `Atomic<T>`) by reference — including capturing them
/// in `@Sendable` closures, which can't otherwise capture `~Copyable` types.
@_spi(SourceKitLSP) public final class RefBox<Value: ~Copyable> {
public let value: Value

@inlinable public init(_ value: consuming Value) {
self.value = value
}
}

extension RefBox: Sendable where Value: ~Copyable & Sendable {}
68 changes: 30 additions & 38 deletions Sources/ToolsProtocolsSwiftExtensions/ThreadSafeBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,52 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
// Copyright (c) 2014 - 2026 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
//
//===----------------------------------------------------------------------===//

import Foundation
public import Synchronization

/// A thread safe container that contains a value of type `T`.
/// A wrapper around a heap-allocated `RefBox<Mutex<Value>>`, providing ergonomic
/// thread-safe access to a mutable shared state.
///
/// - Note: Unchecked sendable conformance because value is guarded by a lock.
package class ThreadSafeBox<T: Sendable>: @unchecked Sendable {
/// Lock guarding `_value`.
private let lock = NSLock()
/// `var value` is read-only. Writes must go through ``withLock(_:)`` so that
/// read-modify-write patterns (e.g. `+=`, `append`) hold the lock for the
/// entire operation rather than acquiring it twice.
@_spi(SourceKitLSP) @frozen public struct ThreadSafeBox<Value: ~Copyable>: Sendable {
@usableFromInline let box: RefBox<Mutex<Value>>

private var _value: T

package var value: T {
get {
return lock.withLock {
return _value
}
}
set {
lock.withLock {
_value = newValue
}
}
_modify {
lock.lock()
defer { lock.unlock() }
yield &_value
}
@inlinable public init(initialValue: consuming sending Value) {
self.box = RefBox(Mutex(initialValue))
}

package init(initialValue: T) {
_value = initialValue
@inlinable public func withLock<Result: ~Copyable, E: Error>(
_ body: (inout sending Value) throws(E) -> sending Result
) throws(E) -> sending Result {
try box.value.withLock(body)
}
}

package func withLock<Result>(_ body: (inout T) throws -> Result) rethrows -> Result {
return try lock.withLock {
return try body(&_value)
}
extension ThreadSafeBox where Value: Sendable {
/// Atomically reads the wrapped value. Writes must go through ``withLock(_:)``.
@inlinable public var value: Value {
withLock { $0 }
}
}

/// If the value in the box is an optional, return it and reset it to `nil`
/// in an atomic operation.
package func takeValue<U>() -> T where U? == T {
lock.withLock {
guard let value = self._value else { return nil }
self._value = nil
return value
extension ThreadSafeBox {
/// Atomically reads the wrapped optional value and resets it to `nil`.
@inlinable public func takeValue<Wrapped>() -> sending Wrapped? where Value == Wrapped? {
withLock { state in
// Don't use `Optional.take()` because the compiler can't see through `take()`
// to prove the returned value is disjoint from `state`.
let result = state
state = nil
return result
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ struct AsyncQueueTests {

let serialRan = ThreadSafeBox<Bool>(initialValue: false)
let serialTask = queue.async(metadata: .serial) {
serialRan.value = true
serialRan.withLock { $0 = true }
}

// Release only the last concurrent task. The serial task must still wait
Expand Down
45 changes: 45 additions & 0 deletions Tests/ToolsProtocolsSwiftExtensionsTests/RefBoxTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2026 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
//
//===----------------------------------------------------------------------===//

import Synchronization
import Testing
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions

struct RefBoxTests {
/// `RefBox<Mutex<T>>` is `Sendable` and can be captured by a detached task.
@Test func mutexBoxIsSendableAndShareable() async {
let box = RefBox(Mutex<Int>(0))
let task = Task { box.value.withLock { $0 += 1 } }
await task.value
#expect(box.value.withLock { $0 } == 1)
}

/// `withLock` returns the body's result and propagates mutations.
@Test func mutexBoxWithLockReturnsAndMutates() {
let box = RefBox(Mutex<[Int]>([]))
let count = box.value.withLock { (arr: inout [Int]) -> Int in
arr.append(1)
arr.append(2)
return arr.count
}
#expect(count == 2)
#expect(box.value.withLock { $0 } == [1, 2])
}

/// `RefBox<Atomic<T>>` is `Sendable` and shareable across tasks.
@Test func atomicBoxIsSendableAndShareable() async {
let flag = RefBox(Atomic<Bool>(false))
let task = Task { flag.value.store(true, ordering: .relaxed) }
await task.value
#expect(flag.value.load(ordering: .relaxed) == true)
}
}
Loading