Skip to content

Commit c1a1628

Browse files
authored
Merge branch 'main' into add-overwriting-to-niofs
2 parents 7040dd8 + b0e0247 commit c1a1628

10 files changed

+319
-9
lines changed

Benchmarks/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ let package = Package(
99
],
1010
dependencies: [
1111
.package(path: "../"),
12-
.package(url: "https://github.com/ordo-one/package-benchmark.git", from: "1.22.0"),
12+
.package(url: "https://github.com/ordo-one/package-benchmark.git", from: "1.29.11"),
1313
],
1414
targets: [
1515
.executableTarget(

Sources/NIOCore/BSDSocketAPI.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ public enum NIOBSDSocket: Sendable {
129129
#else
130130
public typealias Handle = CInt
131131
#endif
132+
public struct PipeHandle {
133+
public var input: Handle
134+
public var output: Handle
135+
136+
@inlinable
137+
public init(input: Handle, output: Handle) {
138+
self.input = input
139+
self.output = output
140+
}
141+
}
132142
}
133143

134144
extension NIOBSDSocket {

Sources/NIOCore/ChannelPipeline.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ public final class ChannelPipeline: ChannelInvoker {
154154
private var _channel: Optional<Channel>
155155

156156
/// The `Channel` that this `ChannelPipeline` belongs to.
157+
@usableFromInline
157158
internal var channel: Channel {
158159
self.eventLoop.assertInEventLoop()
159160
assert(self._channel != nil || self.destroyed)
@@ -1605,6 +1606,72 @@ extension ChannelPipeline {
16051606
self.eventLoop.assertInEventLoop()
16061607
self._pipeline.triggerUserOutboundEvent0(event, promise: promise)
16071608
}
1609+
1610+
/// Provides scoped access to the underlying transport, if the channel supports it.
1611+
///
1612+
/// This is an advanced API for reading or manipulating the underlying transport that backs a channel. Users
1613+
/// must not close the transport or invalidate any invariants that the channel relies upon for its operation.
1614+
///
1615+
/// Not all channels support access to the underlying channel. If the channel does not support this API, the
1616+
/// closure is not called and this function immediately returns `nil`.
1617+
///
1618+
/// Note that you must call this API with an appropriate closure, or otherwise explicitly specify the correct
1619+
/// transport type prarameter, in order for the closure to be run. Calling this function such that the compiler
1620+
/// infers a type for the transport closure parameter that differs from the channel implementation will result
1621+
/// in the closure not being run and this function will return `nil`.
1622+
///
1623+
/// For example, for socket-based channels, that expose the underlying socket handle:
1624+
///
1625+
/// ```swift
1626+
/// try channel.pipeline.syncOperations.withUnsafeTransportIfAvailable { transport in
1627+
/// // This closure is called.
1628+
/// transport == NIOBSDSocketHandle.invalid
1629+
/// }
1630+
///
1631+
/// try channel.pipeline.syncOperations.withUnsafeTransportIfAvailable { (_: NIOBSDSocket.Handle) in
1632+
/// // This closure is called.
1633+
/// return
1634+
/// }
1635+
///
1636+
/// try channel.pipeline.syncOperations.withUnsafeTransportIfAvailable(of: NIOBSDSocket.Handle.self) { _ in
1637+
/// // This closure is called.
1638+
/// return
1639+
/// }
1640+
///
1641+
/// try channel.pipeline.syncOperations.withUnsafeTransportIfAvailable {
1642+
/// // This closure is NOT called.
1643+
/// return
1644+
/// }
1645+
///
1646+
/// try channel.pipeline.syncOperations.withUnsafeTransportIfAvailable { (_: Any.self) in
1647+
/// // This closure is NOT called.
1648+
/// return
1649+
/// }
1650+
///
1651+
/// try channel.pipeline.syncOperations.withUnsafeTransportIfAvailable(of: Any.self) { _ in
1652+
/// // This closure is NOT called.
1653+
/// return
1654+
/// }
1655+
/// ```
1656+
///
1657+
/// - Parameters:
1658+
/// - type: The expected transport type the channel makes available.
1659+
/// - body: /// A closure that takes the underlying transport, if the channel supports this operation.
1660+
/// - Returns: The value returned by the closure, or `nil` if the channel does not expose its transport.
1661+
/// - Throws: If there was an error accessing the underlying transport, or an error was thrown by the closure.
1662+
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
1663+
@inlinable
1664+
public func withUnsafeTransportIfAvailable<Transport, Result>(
1665+
of type: Transport.Type = Transport.self,
1666+
_ body: (_ transport: Transport) throws -> Result
1667+
) throws -> Result? {
1668+
self.eventLoop.assertInEventLoop()
1669+
guard let core = self._pipeline.channel._channelCore as? any NIOTransportAccessibleChannelCore<Transport>
1670+
else {
1671+
return nil
1672+
}
1673+
return try core.withUnsafeTransport(body)
1674+
}
16081675
}
16091676

16101677
/// Returns a view of operations which can be performed synchronously on this pipeline. All
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
/// A ``ChannelCore`` that provides access to its underlying transport.
16+
///
17+
/// This API is only used for ``Channel`` implementations: if you are not implementing a ``Channel``, do not use this
18+
/// protocol directly. Instead use ``ChannelPipeline/SynchronousOperations/withUnsafeTransportIfAvailable(of:_:)``.
19+
///
20+
/// Not all channels are expected to conform to ``NIOTransportAccessibleChannelCore``, but this is determined at runtime, by
21+
/// ``ChannelPipeline/SynchronousOperations/withUnsafeTransportIfAvailable(of:_:)``.
22+
public protocol NIOTransportAccessibleChannelCore<Transport>: ChannelCore {
23+
/// The type of the underlying transport.
24+
associatedtype Transport
25+
26+
/// Provides scoped access to the underlying transport.
27+
///
28+
/// This is an advanced API for reading or manipulating the underlying transport that backs a channel. Users must
29+
/// not close the transport or invalidate any invariants that NIO relies upon for the channel operation.
30+
///
31+
/// Users should not attempt to use this API direcly, but should instead use
32+
/// ``ChannelPipeline/SynchronousOperations/withUnsafeTransportIfAvailable(of:_:)``.
33+
///
34+
/// Not all channels are expected to conform to ``NIOTransportAccessibleChannelCore``. If your channel implementation
35+
/// does not support this protocol, do not provide a throwing implementation to indicate this. Instead, simply do
36+
/// not conform your channel core to this protocol. Availablity of this functionality is communicated to users by /// ///
37+
/// ``ChannelPipeline/SynchronousOperations/withUnsafeTransportIfAvailable(of:_:)``.
38+
///
39+
/// - Parameter body: A closure that takes the underlying transport.
40+
/// - Returns: The value returned by the closure.
41+
/// - Throws: If the underlying transport is unavailable, or rethrows any error thrown by the closure.
42+
func withUnsafeTransport<Result>(_ body: (_ transport: Transport) throws -> Result) throws -> Result
43+
}

Sources/NIOPosix/BSDSocketAPICommon.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ internal enum Shutdown: _SocketShutdownProtocol, Sendable {
4444

4545
extension NIOBSDSocket {
4646
#if os(Windows)
47-
internal static let invalidHandle: Handle = INVALID_SOCKET
47+
public static let invalidHandle: Handle = INVALID_SOCKET
4848
#else
49-
internal static let invalidHandle: Handle = -1
49+
public static let invalidHandle: Handle = -1
5050
#endif
5151
}
5252

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
17+
extension BaseSocketChannel where SocketType: BaseSocket {
18+
/// The underlying transport type backing this channel.
19+
typealias Transport = NIOBSDSocket.Handle
20+
21+
/// Provides scoped access to the socket file handle.
22+
func withUnsafeTransport<Result>(_ body: (_ transport: NIOBSDSocket.Handle) throws -> Result) throws -> Result {
23+
try self.socket.withUnsafeHandle(body)
24+
}
25+
}
26+
27+
extension BaseSocketChannel where SocketType: PipePair {
28+
/// The underlying transport type backing this channel.
29+
typealias Transport = NIOBSDSocket.PipeHandle
30+
31+
/// Provides scoped access to the pipe file descriptors.
32+
func withUnsafeTransport<Result>(_ body: (_ transport: NIOBSDSocket.PipeHandle) throws -> Result) throws -> Result {
33+
try body(
34+
NIOBSDSocket.PipeHandle(
35+
input: self.socket.input?.fileDescriptor ?? NIOBSDSocket.invalidHandle,
36+
output: self.socket.output?.fileDescriptor ?? NIOBSDSocket.invalidHandle
37+
)
38+
)
39+
}
40+
}
41+
42+
extension SocketChannel: NIOTransportAccessibleChannelCore {}
43+
extension ServerSocketChannel: NIOTransportAccessibleChannelCore {}
44+
extension DatagramChannel: NIOTransportAccessibleChannelCore {}
45+
extension PipeChannel: NIOTransportAccessibleChannelCore {}

Sources/NIOPosix/SelectableEventLoop.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ internal final class SelectableEventLoop: EventLoop, @unchecked Sendable {
137137
// This may only be read/written while holding the _tasksLock.
138138
internal var _pendingTaskPop = false
139139
@usableFromInline
140-
internal var scheduledTaskCounter = ManagedAtomic<UInt64>(0)
140+
internal let scheduledTaskCounter = ManagedAtomic<UInt64>(0)
141141
@usableFromInline
142142
internal var _scheduledTasks = PriorityQueue<ScheduledTask>()
143143
@usableFromInline
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore // NOTE: Not @testable import here -- testing public API surface.
16+
import NIOEmbedded
17+
import NIOPosix // NOTE: Not @testable import here -- testing public API surface.
18+
import Testing
19+
20+
@Suite struct NIOTransportAccessibleChannelCoreTests {
21+
@Test func testUnderlyingSocketAccessForSocketBasedChannel() throws {
22+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
23+
defer { #expect(throws: Never.self) { try group.syncShutdownGracefully() } }
24+
let channel = try DatagramBootstrap(group: group).bind(host: "127.0.0.1", port: 0).wait()
25+
defer { #expect(throws: Never.self) { try channel.close().wait() } }
26+
27+
// We don't expect users to do this runtime check, but test the channel we got back from bootstrap conforms.
28+
#expect(channel is any NIOTransportAccessibleChannelCore)
29+
#expect(channel is any NIOTransportAccessibleChannelCore<NIOBSDSocket.Handle>)
30+
#expect(channel is any NIOTransportAccessibleChannelCore<Any> == false)
31+
32+
// Here we try the public API use, in various flavours.
33+
try channel.eventLoop.submit {
34+
let syncOps = channel.pipeline.syncOperations
35+
36+
// Calling without explicit transport type runs closure if body inefers correct transport type.
37+
try #expect(syncOps.withUnsafeTransportIfAvailable { fd in fd != NIOBSDSocket.invalidHandle } == true)
38+
try #expect(syncOps.withUnsafeTransportIfAvailable { $0 != NIOBSDSocket.invalidHandle } == true)
39+
40+
// Calling with explicit correct transport type runs closure.
41+
try #expect(syncOps.withUnsafeTransportIfAvailable(of: NIOBSDSocket.Handle.self) { _ in 42 } == 42)
42+
try #expect(syncOps.withUnsafeTransportIfAvailable { (_: NIOBSDSocket.Handle) in 42 } == 42)
43+
44+
// Calling with explicit incorrect transport type does not run closure.
45+
try #expect(syncOps.withUnsafeTransportIfAvailable(of: String.self) { _ in 42 } == nil)
46+
try #expect(syncOps.withUnsafeTransportIfAvailable { (_: String) in 42 } == nil)
47+
48+
// Calling with explicit Any transport type does not run closure.
49+
try #expect(syncOps.withUnsafeTransportIfAvailable(of: Any.self) { _ in 42 } == nil)
50+
try #expect(syncOps.withUnsafeTransportIfAvailable { (_: Any) in 42 } == nil)
51+
52+
// Calling without explicit transport type does not run closure, even if body doesn't use transport.
53+
try #expect(syncOps.withUnsafeTransportIfAvailable { 42 } == nil)
54+
55+
// Fun aside: What is the resolved type of the above function and why does it allow ignoring closure param?
56+
try #expect(syncOps.withUnsafeTransportIfAvailable { $0.self } == nil)
57+
// Answer: `$0: any (~Copyable & ~Escapable).Type`
58+
59+
// Calling without explicit transport type does not run closure, even if body uses compatible literal value.
60+
try #expect(syncOps.withUnsafeTransportIfAvailable { transport in transport != -1 } == nil)
61+
}.wait()
62+
}
63+
64+
@Test func testUnderlyingTransportForUnsupportedChannels() throws {
65+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
66+
defer { #expect(throws: Never.self) { try group.syncShutdownGracefully() } }
67+
let channel = EmbeddedChannel()
68+
defer { #expect(throws: Never.self) { try channel.close().wait() } }
69+
70+
#expect(channel is any NIOTransportAccessibleChannelCore == false)
71+
72+
// Calling the public API will never run the closure -- we cannot specify a type to pass the runtime check.
73+
let syncOps = channel.pipeline.syncOperations
74+
try #expect(syncOps.withUnsafeTransportIfAvailable { 42 } == nil)
75+
try #expect(syncOps.withUnsafeTransportIfAvailable(of: Any.self) { _ in 42 } == nil)
76+
try #expect(syncOps.withUnsafeTransportIfAvailable(of: CInt.self) { _ in 42 } == nil)
77+
try #expect(syncOps.withUnsafeTransportIfAvailable(of: type(of: STDOUT_FILENO).self) { _ in 42 } == nil)
78+
}
79+
80+
@Test func testUnderlyingTransportConformanceForExpectedChannels() throws {
81+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
82+
defer { #expect(throws: Never.self) { try group.syncShutdownGracefully() } }
83+
84+
// SeverSocketChannel -- yep.
85+
let serverChannel = try ServerBootstrap(group: group).bind(host: "127.0.0.1", port: 0).wait()
86+
defer { #expect(throws: Never.self) { try serverChannel.close().wait() } }
87+
#expect(serverChannel is any NIOTransportAccessibleChannelCore)
88+
#expect(serverChannel is any NIOTransportAccessibleChannelCore<NIOBSDSocket.Handle>)
89+
90+
// SocketChannel -- yep.
91+
let clientChannel = try ClientBootstrap(group: group).connect(to: serverChannel.localAddress!).wait()
92+
defer { #expect(throws: Never.self) { try clientChannel.close().wait() } }
93+
#expect(clientChannel is any NIOTransportAccessibleChannelCore)
94+
#expect(clientChannel is any NIOTransportAccessibleChannelCore<NIOBSDSocket.Handle>)
95+
96+
// DatagramChannel -- yep.
97+
let datagramChannel = try DatagramBootstrap(group: group).bind(host: "127.0.0.1", port: 0).wait()
98+
defer { #expect(throws: Never.self) { try datagramChannel.close().wait() } }
99+
#expect(datagramChannel is any NIOTransportAccessibleChannelCore)
100+
#expect(datagramChannel is any NIOTransportAccessibleChannelCore<NIOBSDSocket.Handle>)
101+
102+
// PipeChannel -- yep.
103+
let pipeChannel = try NIOPipeBootstrap(group: group).takingOwnershipOfDescriptor(output: STDOUT_FILENO).wait()
104+
defer { #expect(throws: Never.self) { try pipeChannel.close().wait() } }
105+
#expect(pipeChannel is any NIOTransportAccessibleChannelCore)
106+
#expect(pipeChannel is any NIOTransportAccessibleChannelCore<NIOBSDSocket.PipeHandle>)
107+
108+
// EmbeddedChannel -- nope.
109+
let embeddedChannel = EmbeddedChannel()
110+
defer { #expect(throws: Never.self) { try embeddedChannel.close().wait() } }
111+
#expect(embeddedChannel is any NIOTransportAccessibleChannelCore == false)
112+
}
113+
}

Tests/NIOPosixTests/SocketChannelTest.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,6 +1126,23 @@ final class SocketChannelTest: XCTestCase {
11261126
#endif
11271127
}
11281128

1129+
func testBaseSocketChannelWithUnderlyingTransport() throws {
1130+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
1131+
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
1132+
let socket = try Socket.SocketType(protocolFamily: .inet, type: .datagram)
1133+
let channel = try BaseSocketChannel(
1134+
socket: socket,
1135+
parent: nil,
1136+
eventLoop: group.next() as! SelectableEventLoop,
1137+
recvAllocator: FixedSizeRecvByteBufferAllocator(capacity: 1024),
1138+
supportReconnect: false
1139+
)
1140+
1141+
try channel.socket.withUnsafeHandle { fd in
1142+
XCTAssertNotEqual(fd, NIOBSDSocket.invalidHandle)
1143+
try channel.withUnsafeTransport { XCTAssertEqual($0, fd) }
1144+
}
1145+
}
11291146
}
11301147

11311148
final class DropAllReadsOnTheFloorHandler: ChannelDuplexHandler, Sendable {

scripts/check_benchmark_thresholds.sh

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,27 @@ swift package --package-path "$benchmark_package_path" "${swift_package_argument
3535
rc="$?"
3636

3737
# Benchmarks are unchanged, nothing to recalculate
38-
if [[ "$rc" == 0 ]]; then
38+
if [[ "$rc" == 0 ]]; then
3939
exit 0
40+
# Benchmark thresholds changed – 2 is defined in
41+
# package-benchmark/BenchmarkShared/Command+Helpers.swift/ExitCode.thresholdRegression
42+
elif [[ "$rc" == 2 ]]; then
43+
log "Recalculating thresholds..."
44+
45+
swift package --package-path "$benchmark_package_path" "${swift_package_arguments[@]}" benchmark thresholds update --format metricP90AbsoluteThresholds --path "${benchmark_package_path}/Thresholds/${swift_version}/"
46+
update_rc="$?"
47+
48+
if [[ "$update_rc" != 0 ]]; then
49+
error "Benchmark failed to run due to build error."
50+
exit $update_rc
51+
fi
52+
53+
echo "=== BEGIN DIFF ===" # use echo, not log for clean output to be scraped
54+
git diff --exit-code HEAD
55+
# all other errors
56+
else
57+
error "Benchmark failed to run due to build error."
58+
exit $rc
4059
fi
4160

42-
log "Recalculating thresholds..."
4361

44-
swift package --package-path "$benchmark_package_path" "${swift_package_arguments[@]}" benchmark thresholds update --format metricP90AbsoluteThresholds --path "${benchmark_package_path}/Thresholds/${swift_version}/"
45-
echo "=== BEGIN DIFF ===" # use echo, not log for clean output to be scraped
46-
git diff --exit-code HEAD

0 commit comments

Comments
 (0)