diff --git a/Sources/SwiftNetwork/Connection/Connection.swift b/Sources/SwiftNetwork/Connection/Connection.swift index 9b711f4b..21865d94 100644 --- a/Sources/SwiftNetwork/Connection/Connection.swift +++ b/Sources/SwiftNetwork/Connection/Connection.swift @@ -1058,8 +1058,8 @@ public struct StreamBridge: StreamProtocol { public struct CustomLink: StreamProtocol { public typealias ContentType = Void - private var tx: ((Span) -> Void)? = nil - private var rx: ((@escaping (Span) -> Void) -> Void)? = nil + private var tx: (@Sendable (_ bytes: Span) -> Void)? = nil + private var rx: (@Sendable (_ inject: @escaping (_ bytes: Span) -> Void) -> Void)? = nil public let belowProtocol: Void /// Configure CustomLink for tx byte handling @@ -1073,7 +1073,7 @@ public struct CustomLink: StreamProtocol { /// /// - Parameter handler: A closure that will be called with a /// span of bytes to be written to the network. - public func tx(_ handler: @escaping (Span) -> Void) -> Self { + public func tx(_ handler: @escaping @Sendable (_ bytes: Span) -> Void) -> Self { var mutableSelf = self mutableSelf.tx = handler return mutableSelf @@ -1095,7 +1095,9 @@ public struct CustomLink: StreamProtocol { /// - Parameter handler: A closure that will be called with an /// escaping closure that should be stored for later use when bytes /// need to be injected into the protocol stack. - public func rx(_ handler: @escaping ((@escaping (Span) -> Void) -> Void)) -> Self { + public func rx( + _ handler: @escaping @Sendable (_ inject: @escaping (_ bytes: Span) -> Void) -> Void + ) -> Self { var mutableSelf = self mutableSelf.rx = handler return mutableSelf diff --git a/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift b/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift index 98119e81..338e15d6 100644 --- a/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift @@ -28,7 +28,7 @@ internal import Synchronization @_spi(Essentials) @available(Network 0.1.0, *) -public struct DatagramDrops: Equatable { +public struct DatagramDrops: Equatable, Sendable { private var dropRanges: [ClosedRange] private var datagramCount = 0 public var blockPacketGeneration = false @@ -66,8 +66,8 @@ public struct DatagramDrops: Equatable { } } -public typealias BridgeObserveFirstByteHandler = ((UInt8) -> Void)? -public typealias BridgeObserveFrameHandler = ((UInt8, Int) -> Void)? +public typealias BridgeObserveFirstByteHandler = (@Sendable (_ firstByte: UInt8) -> Void)? +public typealias BridgeObserveFrameHandler = (@Sendable (_ firstByte: UInt8, _ byteCount: Int) -> Void)? @_spi(Essentials) @available(Network 0.1.0, *) @@ -76,7 +76,7 @@ public struct BridgeDatagramProtocol: NetworkProtocol { public typealias Metadata = BridgeMetadata public typealias Instance = BridgeInstance - public struct BridgeOptions: PerProtocolOptions { + public struct BridgeOptions: PerProtocolOptions, Sendable { public var linkDelay: NetworkDuration = .zero public var observeFirstByteHandler: BridgeObserveFirstByteHandler = nil public var observeFrameHandler: BridgeObserveFrameHandler = nil @@ -395,7 +395,7 @@ public struct BridgeStreamProtocol: NetworkProtocol { public typealias Metadata = BridgeMetadata public typealias Instance = BridgeInstance - public struct BridgeOptions: PerProtocolOptions { + public struct BridgeOptions: PerProtocolOptions, Sendable { init() {} init?(from serializedBytes: [UInt8]) { diff --git a/Sources/SwiftNetwork/Protocols/CustomIPProtocol.swift b/Sources/SwiftNetwork/Protocols/CustomIPProtocol.swift index 988c6e10..32365650 100644 --- a/Sources/SwiftNetwork/Protocols/CustomIPProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/CustomIPProtocol.swift @@ -28,7 +28,7 @@ public struct CustomIPProtocol: NetworkProtocol { public typealias Options = CustomIPOptions public typealias Metadata = CustomIPMetadata - public struct CustomIPOptions: PerProtocolOptions { + public struct CustomIPOptions: PerProtocolOptions, Sendable { var ipProtocolNumber: UInt8 = 0 init() {} diff --git a/Sources/SwiftNetwork/Protocols/CustomLinkProtocol.swift b/Sources/SwiftNetwork/Protocols/CustomLinkProtocol.swift index 359f1771..98680a76 100644 --- a/Sources/SwiftNetwork/Protocols/CustomLinkProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/CustomLinkProtocol.swift @@ -33,9 +33,9 @@ public struct CustomLinkProtocol: NetworkProtocol { public typealias Metadata = CustomLinkMetadata public typealias Instance = CustomLinkInstance - public struct CustomLinkOptions: PerProtocolOptions { - public var tx: ((Span) -> Void)? = nil - public var rx: ((@escaping (Span) -> Void) -> Void)? = nil + public struct CustomLinkOptions: PerProtocolOptions, Sendable { + public var tx: (@Sendable (_ bytes: Span) -> Void)? = nil + public var rx: (@Sendable (_ inject: @escaping (_ bytes: Span) -> Void) -> Void)? = nil init() {} init?(from serializedBytes: [UInt8]) { @@ -87,8 +87,8 @@ public struct CustomLinkProtocol: NetworkProtocol { var log = NetworkLoggerState() public var eventManager = ProtocolEventManager() private var incomingFrames = FrameArray() - public var tx: ((Span) -> Void)? = nil - public var rx: ((@escaping (Span) -> Void) -> Void)? = nil + public var tx: (@Sendable (Span) -> Void)? = nil + public var rx: (@Sendable (@escaping (Span) -> Void) -> Void)? = nil public func setup( remote: Endpoint?, @@ -179,12 +179,12 @@ public struct CustomLinkProtocol: NetworkProtocol { @_spi(Essentials) @available(Network 0.1.0, *) extension ProtocolOptions { - public var tx: ((Span) -> Void)? { + public var tx: (@Sendable (_ bytes: Span) -> Void)? { get { perProtocolOptions!.tx } set { perProtocolOptions!.tx = newValue } } - public var rx: ((@escaping (Span) -> Void) -> Void)? { + public var rx: (@Sendable (_ inject: @escaping (_ bytes: Span) -> Void) -> Void)? { get { perProtocolOptions!.rx } set { perProtocolOptions!.rx = newValue } } diff --git a/Sources/SwiftNetwork/Protocols/DemuxProtocol.swift b/Sources/SwiftNetwork/Protocols/DemuxProtocol.swift index 03e47a92..c3771842 100644 --- a/Sources/SwiftNetwork/Protocols/DemuxProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/DemuxProtocol.swift @@ -120,7 +120,7 @@ public struct DemuxProtocol: NetworkProtocol { public typealias Options = DemuxOptions public typealias Metadata = DemuxMetadata - public struct DemuxOptions: PerProtocolOptions { + public struct DemuxOptions: PerProtocolOptions, Sendable { var demuxPatterns = Deque() init() {} diff --git a/Sources/SwiftNetwork/Protocols/IPProtocol.swift b/Sources/SwiftNetwork/Protocols/IPProtocol.swift index 33499c19..4d33e2b7 100644 --- a/Sources/SwiftNetwork/Protocols/IPProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/IPProtocol.swift @@ -69,7 +69,7 @@ public struct IPProtocol: NetworkProtocol { 1280 } - public enum Version: UInt8 { + public enum Version: UInt8, Sendable { /// Allows any IP version. case any = 0 /// Uses only IP version 4 (IPv4). @@ -78,7 +78,7 @@ public struct IPProtocol: NetworkProtocol { case v6 = 6 } - public enum AddressPreference: UInt8 { + public enum AddressPreference: UInt8, Sendable { case any = 0 case temporary = 1 case stable = 2 @@ -110,7 +110,7 @@ public struct IPProtocol: NetworkProtocol { } } - public struct IPOptions: PerProtocolOptions { + public struct IPOptions: PerProtocolOptions, Sendable { var version: Version = .any var localAddressPreference: AddressPreference = .any diff --git a/Sources/SwiftNetwork/Protocols/SwiftTLSProtocol.swift b/Sources/SwiftNetwork/Protocols/SwiftTLSProtocol.swift index 5eae895b..d52e869f 100644 --- a/Sources/SwiftNetwork/Protocols/SwiftTLSProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/SwiftTLSProtocol.swift @@ -87,7 +87,7 @@ public struct SwiftTLSProtocol: NetworkProtocol { _tlsOptions.externalPSK = .init(externalIdentity: identity, epsk: .init(data: epsk)) } #else - private struct SwiftTLSOptionsStorage { + private struct SwiftTLSOptionsStorage: Sendable { var serverName: String? var quicTransportParameters: [UInt8]? var applicationProtocols: [String]? @@ -1311,6 +1311,13 @@ public struct SwiftTLSProtocol: NetworkProtocol { } +#if !EXPORT_SWIFTTLS +// Under `EXPORT_SWIFTTLS` the options store a `SwiftTLSOptions`, which is not `Sendable`, so the options can only be +// `Sendable` when they hold their own copy of the fields. +@available(Network 0.1.0, *) +extension SwiftTLSProtocol.SwiftTLSProtocolOptions: Sendable {} +#endif + @_spi(ProtocolProvider) @available(Network 0.1.0, *) extension ProtocolOptions { diff --git a/Sources/SwiftNetwork/Protocols/TCPProtocol.swift b/Sources/SwiftNetwork/Protocols/TCPProtocol.swift index c56415d5..c605730a 100644 --- a/Sources/SwiftNetwork/Protocols/TCPProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/TCPProtocol.swift @@ -55,7 +55,7 @@ public struct TCPProtocol: NetworkProtocol { 16 } - public struct TCPOptions: PerProtocolOptions { + public struct TCPOptions: PerProtocolOptions, Sendable { internal var _maximumSegmentSize: UInt32 = 0 public var maximumSegmentSize: UInt32 { diff --git a/Tests/SwiftNetworkTests/SwiftNetworkConnectionTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkConnectionTests.swift index 8cda6475..57db3ef5 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkConnectionTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkConnectionTests.swift @@ -828,7 +828,9 @@ final class SwiftNetworkConnectionTests: NetTestCase { func testNoTransportCustomLink() { let group = DispatchGroup() group.enter() - var injection: ((Span) -> Void)? + // The rx handler writes this during connection setup, before the `.ready` update leaves `group`, and the + // test reads it only after `group.wait`, so the group orders the two accesses. + nonisolated(unsafe) var injection: ((Span) -> Void)? let c1 = NetworkConnection( to: Endpoint(address: IPv4Address.loopback, port: 7778), using: .parameters { diff --git a/Tests/SwiftNetworkTests/SwiftNetworkQUICHarnessTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkQUICHarnessTests.swift index 47aff810..fcf41e3e 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkQUICHarnessTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkQUICHarnessTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import Synchronization import XCTest #if !targetEnvironment(simulator) && (os(iOS) || os(macOS) || os(Linux)) @@ -762,11 +763,11 @@ final class SwiftNetworkQUICHarnessTests: NetTestCase { let clientOptions = QUICProtocol.options() clientOptions.connectionOptions.initialPacketSize = 1400 - var observedInitialPacketSizes: [Int] = [] + let observedInitialPacketSizes = Mutex<[Int]>([]) let observeFrameHandler: BridgeObserveFrameHandler = { firstByte, byteCount in // Verify initial packet guard (firstByte & 0xF0) == 0xC0 else { return } - observedInitialPacketSizes.append(byteCount) + observedInitialPacketSizes.withLock { $0.append(byteCount) } } QUICTestHarness().runQUICTest( @@ -777,8 +778,11 @@ final class SwiftNetworkQUICHarnessTests: NetTestCase { bridgeObserveFrameHandler: observeFrameHandler ) - XCTAssertFalse(observedInitialPacketSizes.isEmpty, "Should have observed at least one Initial packet") - XCTAssertEqual(observedInitialPacketSizes.first, 1400) + XCTAssertFalse( + observedInitialPacketSizes.withLock { $0.isEmpty }, + "Should have observed at least one Initial packet" + ) + XCTAssertEqual(observedInitialPacketSizes.withLock { $0.first }, 1400) } func testQUICDatagramRemoteMaxDatagramFrameSize() { diff --git a/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift index 7b487d2b..6cfcd515 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkQUICSpinBitTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import Synchronization import XCTest #if !targetEnvironment(simulator) && (os(iOS) || os(macOS) || os(Linux)) @@ -64,10 +65,10 @@ final class SwiftNetworkQUICSpinBitTests: NetTestCase { // it from the server. var hasSpinBit = true - var observedSpinBitValues: Set = [] + let observedSpinBitValues = Mutex>([]) let observeFirstByteHandler: BridgeObserveFirstByteHandler = { firstByte in guard (firstByte & 0xC0) == 0x40 else { return } - observedSpinBitValues.insert((firstByte & 0x20) != 0) + observedSpinBitValues.withLock { values -> Void in values.insert((firstByte & 0x20) != 0) } } QUICTestHarness().runQUICTest( dataBlock: Array("Hello World!".utf8), @@ -99,11 +100,11 @@ final class SwiftNetworkQUICSpinBitTests: NetTestCase { harness.context.async { defer { expectation.fulfill() } XCTAssertFalse( - observedSpinBitValues.isEmpty, + observedSpinBitValues.withLock { $0.isEmpty }, "BridgeDatagramProtocol should have observed short-header packets with spin bit values" ) XCTAssertTrue( - observedSpinBitValues.contains(true), + observedSpinBitValues.withLock { $0.contains(true) }, "BridgeDatagramProtocol should have observed at least one packet with spin bit set" ) }