Skip to content

Commit 6553ef5

Browse files
authored
Add Sendability annotations (#308)
Motivation: This becomes a critical feature in swift 6. Modifications: Add Sendability annotations where required. Factor out code protected by locks. Result: Compiles without warnings with Sendability checking
1 parent 11e749e commit 6553ef5

15 files changed

+268
-170
lines changed

Sources/Logging/Locks.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import Musl
4646
/// of lock is safe to use with `libpthread`-based threading models, such as the
4747
/// one used by NIO. On Windows, the lock is based on the substantially similar
4848
/// `SRWLOCK` type.
49-
internal final class Lock {
49+
internal final class Lock: @unchecked Sendable {
5050
#if canImport(WASILibc)
5151
// WASILibc is single threaded, provides no locks
5252
#elseif os(Windows)
@@ -148,7 +148,7 @@ extension Lock {
148148
/// of lock is safe to use with `libpthread`-based threading models, such as the
149149
/// one used by NIO. On Windows, the lock is based on the substantially similar
150150
/// `SRWLOCK` type.
151-
internal final class ReadWriteLock {
151+
internal final class ReadWriteLock: @unchecked Sendable {
152152
#if canImport(WASILibc)
153153
// WASILibc is single threaded, provides no locks
154154
#elseif os(Windows)
@@ -189,7 +189,7 @@ internal final class ReadWriteLock {
189189
///
190190
/// Whenever possible, consider using `withReaderLock` instead of this
191191
/// method and `unlock`, to simplify lock handling.
192-
public func lockRead() {
192+
fileprivate func lockRead() {
193193
#if canImport(WASILibc)
194194
// WASILibc is single threaded, provides no locks
195195
#elseif os(Windows)
@@ -205,7 +205,7 @@ internal final class ReadWriteLock {
205205
///
206206
/// Whenever possible, consider using `withWriterLock` instead of this
207207
/// method and `unlock`, to simplify lock handling.
208-
public func lockWrite() {
208+
fileprivate func lockWrite() {
209209
#if canImport(WASILibc)
210210
// WASILibc is single threaded, provides no locks
211211
#elseif os(Windows)
@@ -222,7 +222,7 @@ internal final class ReadWriteLock {
222222
/// Whenever possible, consider using `withReaderLock` and `withWriterLock`
223223
/// instead of this method and `lockRead` and `lockWrite`, to simplify lock
224224
/// handling.
225-
public func unlock() {
225+
fileprivate func unlock() {
226226
#if canImport(WASILibc)
227227
// WASILibc is single threaded, provides no locks
228228
#elseif os(Windows)

Sources/Logging/Logging.swift

Lines changed: 105 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import Darwin
1717
#elseif os(Windows)
1818
import CRT
1919
#elseif canImport(Glibc)
20+
#if compiler(>=6.0)
21+
@preconcurrency import Glibc
22+
#else
2023
import Glibc
24+
#endif
2125
#elseif canImport(Musl)
2226
import Musl
2327
#elseif canImport(WASILibc)
@@ -498,11 +502,12 @@ extension Logger {
498502
/// configured. `LoggingSystem` is set up just once in a given program to set up the desired logging backend
499503
/// implementation.
500504
public enum LoggingSystem {
501-
private static let _factory = FactoryBox { label, _ in StreamLogHandler.standardError(label: label) }
502-
private static let _metadataProviderFactory = MetadataProviderBox(nil)
505+
private static let _factory = FactoryBox({ label, _ in StreamLogHandler.standardError(label: label) },
506+
violationErrorMesage: "logging system can only be initialized once per process.")
507+
private static let _metadataProviderFactory = MetadataProviderBox(nil, violationErrorMesage: "logging system can only be initialized once per process.")
503508

504509
#if DEBUG
505-
private static var _warnOnceBox: WarnOnceBox = WarnOnceBox()
510+
private static let _warnOnceBox: WarnOnceBox = WarnOnceBox()
506511
#endif
507512

508513
/// `bootstrap` is a one-time configuration function which globally selects the desired logging backend
@@ -511,8 +516,9 @@ public enum LoggingSystem {
511516
///
512517
/// - parameters:
513518
/// - factory: A closure that given a `Logger` identifier, produces an instance of the `LogHandler`.
514-
public static func bootstrap(_ factory: @escaping (String) -> any LogHandler) {
515-
self._factory.replaceFactory({ label, _ in
519+
@preconcurrency
520+
public static func bootstrap(_ factory: @escaping @Sendable(String) -> any LogHandler) {
521+
self._factory.replace({ label, _ in
516522
factory(label)
517523
}, validate: true)
518524
}
@@ -527,25 +533,26 @@ public enum LoggingSystem {
527533
/// - parameters:
528534
/// - metadataProvider: The `MetadataProvider` used to inject runtime-generated metadata from the execution context.
529535
/// - factory: A closure that given a `Logger` identifier, produces an instance of the `LogHandler`.
530-
public static func bootstrap(_ factory: @escaping (String, Logger.MetadataProvider?) -> any LogHandler,
536+
@preconcurrency
537+
public static func bootstrap(_ factory: @escaping @Sendable(String, Logger.MetadataProvider?) -> any LogHandler,
531538
metadataProvider: Logger.MetadataProvider?) {
532-
self._metadataProviderFactory.replaceMetadataProvider(metadataProvider, validate: true)
533-
self._factory.replaceFactory(factory, validate: true)
539+
self._metadataProviderFactory.replace(metadataProvider, validate: true)
540+
self._factory.replace(factory, validate: true)
534541
}
535542

536543
// for our testing we want to allow multiple bootstrapping
537-
internal static func bootstrapInternal(_ factory: @escaping (String) -> any LogHandler) {
538-
self._metadataProviderFactory.replaceMetadataProvider(nil, validate: false)
539-
self._factory.replaceFactory({ label, _ in
544+
internal static func bootstrapInternal(_ factory: @escaping @Sendable(String) -> any LogHandler) {
545+
self._metadataProviderFactory.replace(nil, validate: false)
546+
self._factory.replace({ label, _ in
540547
factory(label)
541548
}, validate: false)
542549
}
543550

544551
// for our testing we want to allow multiple bootstrapping
545-
internal static func bootstrapInternal(_ factory: @escaping (String, Logger.MetadataProvider?) -> any LogHandler,
552+
internal static func bootstrapInternal(_ factory: @escaping @Sendable(String, Logger.MetadataProvider?) -> any LogHandler,
546553
metadataProvider: Logger.MetadataProvider?) {
547-
self._metadataProviderFactory.replaceMetadataProvider(metadataProvider, validate: false)
548-
self._factory.replaceFactory(factory, validate: false)
554+
self._metadataProviderFactory.replace(metadataProvider, validate: false)
555+
self._factory.replace(factory, validate: false)
549556
}
550557

551558
fileprivate static var factory: (String, Logger.MetadataProvider?) -> any LogHandler {
@@ -564,7 +571,7 @@ public enum LoggingSystem {
564571
/// factory to avoid using the bootstrapped metadata provider may sometimes be useful, usually it will lead to
565572
/// un-expected behavior, so make sure to always propagate it to your handlers.
566573
public static var metadataProvider: Logger.MetadataProvider? {
567-
return self._metadataProviderFactory.metadataProvider
574+
return self._metadataProviderFactory.underlying
568575
}
569576

570577
#if DEBUG
@@ -576,54 +583,71 @@ public enum LoggingSystem {
576583
}
577584
#endif
578585

579-
private final class FactoryBox {
586+
/// Protects an object such that it can only be accessed through a Reader-Writer lock.
587+
final class RWLockedValueBox<Value: Sendable>: @unchecked Sendable {
580588
private let lock = ReadWriteLock()
581-
fileprivate var _underlying: (_ label: String, _ provider: Logger.MetadataProvider?) -> any LogHandler
582-
private var initialized = false
589+
private var storage: Value
583590

584-
init(_ underlying: @escaping (String, Logger.MetadataProvider?) -> any LogHandler) {
585-
self._underlying = underlying
591+
init(initialValue: Value) {
592+
self.storage = initialValue
586593
}
587594

588-
func replaceFactory(_ factory: @escaping (String, Logger.MetadataProvider?) -> any LogHandler, validate: Bool) {
589-
self.lock.withWriterLock {
590-
precondition(!validate || !self.initialized, "logging system can only be initialized once per process.")
591-
self._underlying = factory
592-
self.initialized = true
595+
func withReadLock<Result>(_ operation: (Value) -> Result) -> Result {
596+
self.lock.withReaderLock {
597+
operation(self.storage)
593598
}
594599
}
595600

596-
var underlying: (String, Logger.MetadataProvider?) -> any LogHandler {
597-
return self.lock.withReaderLock {
598-
return self._underlying
601+
func withWriteLock<Result>(_ operation: (inout Value) -> Result) -> Result {
602+
self.lock.withWriterLock {
603+
operation(&self.storage)
599604
}
600605
}
601606
}
602607

603-
private final class MetadataProviderBox {
604-
private let lock = ReadWriteLock()
605-
606-
internal var _underlying: Logger.MetadataProvider?
607-
private var initialized = false
608-
609-
init(_ underlying: Logger.MetadataProvider?) {
610-
self._underlying = underlying
611-
}
608+
/// Protects an object applying the constraints that it can only be accessed through a Reader-Writer lock
609+
/// and can ony bre updated once from the initial value given.
610+
private struct ReplaceOnceBox<BoxedType: Sendable> {
611+
private struct ReplaceOnce: Sendable {
612+
private var initialized = false
613+
private var _underlying: BoxedType
614+
private let violationErrorMessage: String
612615

613-
func replaceMetadataProvider(_ metadataProvider: Logger.MetadataProvider?, validate: Bool) {
614-
self.lock.withWriterLock {
615-
precondition(!validate || !self.initialized, "logging system can only be initialized once per process.")
616-
self._underlying = metadataProvider
616+
mutating func replaceUnderlying(_ underlying: BoxedType, validate: Bool) {
617+
precondition(!validate || !self.initialized, self.violationErrorMessage)
618+
self._underlying = underlying
617619
self.initialized = true
618620
}
619-
}
620621

621-
var metadataProvider: Logger.MetadataProvider? {
622-
return self.lock.withReaderLock {
622+
var underlying: BoxedType {
623623
return self._underlying
624624
}
625+
626+
init(underlying: BoxedType, violationErrorMessage: String) {
627+
self._underlying = underlying
628+
self.violationErrorMessage = violationErrorMessage
629+
}
630+
}
631+
632+
private let storage: RWLockedValueBox<ReplaceOnce>
633+
634+
init(_ underlying: BoxedType, violationErrorMesage: String) {
635+
self.storage = .init(initialValue: ReplaceOnce(underlying: underlying,
636+
violationErrorMessage: violationErrorMesage))
637+
}
638+
639+
func replace(_ newUnderlying: BoxedType, validate: Bool) {
640+
self.storage.withWriteLock { $0.replaceUnderlying(newUnderlying, validate: validate) }
641+
}
642+
643+
var underlying: BoxedType {
644+
self.storage.withReadLock { $0.underlying }
625645
}
626646
}
647+
648+
private typealias FactoryBox = ReplaceOnceBox< @Sendable(_ label: String, _ provider: Logger.MetadataProvider?) -> any LogHandler>
649+
650+
private typealias MetadataProviderBox = ReplaceOnceBox<Logger.MetadataProvider?>
627651
}
628652

629653
extension Logger {
@@ -1021,7 +1045,7 @@ internal typealias CFilePointer = UnsafeMutablePointer<FILE>
10211045
/// A wrapper to facilitate `print`-ing to stderr and stdio that
10221046
/// ensures access to the underlying `FILE` is locked to prevent
10231047
/// cross-thread interleaving of output.
1024-
internal struct StdioOutputStream: TextOutputStream {
1048+
internal struct StdioOutputStream: TextOutputStream, @unchecked Sendable {
10251049
internal let file: CFilePointer
10261050
internal let flushMode: FlushMode
10271051

@@ -1062,8 +1086,41 @@ internal struct StdioOutputStream: TextOutputStream {
10621086
return contiguousString.utf8
10631087
}
10641088

1065-
internal static let stderr = StdioOutputStream(file: systemStderr, flushMode: .always)
1066-
internal static let stdout = StdioOutputStream(file: systemStdout, flushMode: .always)
1089+
internal static let stderr = {
1090+
// Prevent name clashes
1091+
#if canImport(Darwin)
1092+
let systemStderr = Darwin.stderr
1093+
#elseif os(Windows)
1094+
let systemStderr = CRT.stderr
1095+
#elseif canImport(Glibc)
1096+
let systemStderr = Glibc.stderr!
1097+
#elseif canImport(Musl)
1098+
let systemStderr = Musl.stderr!
1099+
#elseif canImport(WASILibc)
1100+
let systemStderr = WASILibc.stderr!
1101+
#else
1102+
#error("Unsupported runtime")
1103+
#endif
1104+
return StdioOutputStream(file: systemStderr, flushMode: .always)
1105+
}()
1106+
1107+
internal static let stdout = {
1108+
// Prevent name clashes
1109+
#if canImport(Darwin)
1110+
let systemStdout = Darwin.stdout
1111+
#elseif os(Windows)
1112+
let systemStdout = CRT.stdout
1113+
#elseif canImport(Glibc)
1114+
let systemStdout = Glibc.stdout!
1115+
#elseif canImport(Musl)
1116+
let systemStdout = Musl.stdout!
1117+
#elseif canImport(WASILibc)
1118+
let systemStdout = WASILibc.stdout!
1119+
#else
1120+
#error("Unsupported runtime")
1121+
#endif
1122+
return StdioOutputStream(file: systemStdout, flushMode: .always)
1123+
}()
10671124

10681125
/// Defines the flushing strategy for the underlying stream.
10691126
internal enum FlushMode {
@@ -1072,26 +1129,6 @@ internal struct StdioOutputStream: TextOutputStream {
10721129
}
10731130
}
10741131

1075-
// Prevent name clashes
1076-
#if canImport(Darwin)
1077-
let systemStderr = Darwin.stderr
1078-
let systemStdout = Darwin.stdout
1079-
#elseif os(Windows)
1080-
let systemStderr = CRT.stderr
1081-
let systemStdout = CRT.stdout
1082-
#elseif canImport(Glibc)
1083-
let systemStderr = Glibc.stderr!
1084-
let systemStdout = Glibc.stdout!
1085-
#elseif canImport(Musl)
1086-
let systemStderr = Musl.stderr!
1087-
let systemStdout = Musl.stdout!
1088-
#elseif canImport(WASILibc)
1089-
let systemStderr = WASILibc.stderr!
1090-
let systemStdout = WASILibc.stdout!
1091-
#else
1092-
#error("Unsupported runtime")
1093-
#endif
1094-
10951132
/// `StreamLogHandler` is a simple implementation of `LogHandler` for directing
10961133
/// `Logger` output to either `stderr` or `stdout` via the factory methods.
10971134
///
@@ -1341,7 +1378,7 @@ extension Logger.MetadataValue: ExpressibleByArrayLiteral {
13411378

13421379
#if DEBUG
13431380
/// Contains state to manage all kinds of "warn only once" warnings which the logging system may want to issue.
1344-
private final class WarnOnceBox {
1381+
private final class WarnOnceBox: @unchecked Sendable {
13451382
private let lock: Lock = Lock()
13461383
private var warnOnceLogHandlerNotSupportedMetadataProviderPerType = Set<ObjectIdentifier>()
13471384

Tests/LoggingTests/CompatibilityTest.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ final class CompatibilityTest: XCTestCase {
1919
func testAllLogLevelsWorkWithOldSchoolLogHandlerWorks() {
2020
let testLogging = OldSchoolTestLogging()
2121

22-
var logger = Logger(label: "\(#function)", factory: testLogging.make)
22+
var logger = Logger(label: "\(#function)", factory: { testLogging.make(label: $0) })
2323
logger.logLevel = .trace
2424

2525
logger.trace("yes: trace")

Tests/LoggingTests/GlobalLoggingTest.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@
1313
//===----------------------------------------------------------------------===//
1414
@testable import Logging
1515
import XCTest
16+
#if compiler(>=6.0) || canImport(Darwin)
17+
import Dispatch
18+
#else
19+
@preconcurrency import Dispatch
20+
#endif
1621

1722
class GlobalLoggerTest: XCTestCase {
1823
func test1() throws {
1924
// bootstrap with our test logging impl
2025
let logging = TestLogging()
21-
LoggingSystem.bootstrapInternal(logging.make)
26+
LoggingSystem.bootstrapInternal { logging.make(label: $0) }
2227

2328
// change test logging config to log traces and above
2429
logging.config.set(value: Logger.Level.debug)
@@ -45,7 +50,7 @@ class GlobalLoggerTest: XCTestCase {
4550
func test2() throws {
4651
// bootstrap with our test logging impl
4752
let logging = TestLogging()
48-
LoggingSystem.bootstrapInternal(logging.make)
53+
LoggingSystem.bootstrapInternal { logging.make(label: $0) }
4954

5055
// change test logging config to log errors and above
5156
logging.config.set(value: Logger.Level.error)
@@ -72,7 +77,7 @@ class GlobalLoggerTest: XCTestCase {
7277
func test3() throws {
7378
// bootstrap with our test logging impl
7479
let logging = TestLogging()
75-
LoggingSystem.bootstrapInternal(logging.make)
80+
LoggingSystem.bootstrapInternal { logging.make(label: $0) }
7681

7782
// change test logging config
7883
logging.config.set(value: .warning)

Tests/LoggingTests/LocalLoggingTest.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@
1313
//===----------------------------------------------------------------------===//
1414
@testable import Logging
1515
import XCTest
16+
#if compiler(>=6.0) || canImport(Darwin)
17+
import Dispatch
18+
#else
19+
@preconcurrency import Dispatch
20+
#endif
1621

1722
class LocalLoggerTest: XCTestCase {
1823
func test1() throws {
1924
// bootstrap with our test logging impl
2025
let logging = TestLogging()
21-
LoggingSystem.bootstrapInternal(logging.make)
26+
LoggingSystem.bootstrapInternal { logging.make(label: $0) }
2227

2328
// change test logging config to log traces and above
2429
logging.config.set(value: Logger.Level.debug)
@@ -46,7 +51,7 @@ class LocalLoggerTest: XCTestCase {
4651
func test2() throws {
4752
// bootstrap with our test logging impl
4853
let logging = TestLogging()
49-
LoggingSystem.bootstrapInternal(logging.make)
54+
LoggingSystem.bootstrapInternal { logging.make(label: $0) }
5055

5156
// change test logging config to log errors and above
5257
logging.config.set(value: Logger.Level.error)

0 commit comments

Comments
 (0)