Skip to content

Commit 3bfafdb

Browse files
fixup: Polish up the new approach, based on syncOperations
1 parent ccc1b15 commit 3bfafdb

File tree

6 files changed

+150
-72
lines changed

6 files changed

+150
-72
lines changed

Sources/NIOCore/ChannelPipeline.swift

Lines changed: 51 additions & 7 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)
@@ -1608,25 +1609,68 @@ extension ChannelPipeline {
16081609

16091610
/// Provides scoped access to the underlying transport, if the channel supports it.
16101611
///
1611-
/// This is an advanced API for reading or manipulating the underlying transport that backs a channel. Users must
1612-
/// not close the transport or invalidate any invariants that NIO relies upon for the channel operation.
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.
16131614
///
16141615
/// Not all channels support access to the underlying channel. If the channel does not support this API, the
16151616
/// closure is not called and this function immediately returns `nil`.
16161617
///
1617-
/// - Parameter body: A closure that takes the underlying transport, if the channel supports this operation.
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.
16181660
/// - Returns: The value returned by the closure, or `nil` if the channel does not expose its transport.
1619-
/// - Throws: If the underlying transport is unavailable, or rethrows any error thrown by the closure.
1661+
/// - Throws: If there was an error accessing the underlying transport, or an error was thrown by the closure.
16201662
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
1663+
@inlinable
16211664
public func withUnsafeTransportIfAvailable<Transport, Result>(
1622-
of _: Transport.Type,
1665+
of type: Transport.Type = Transport.self,
16231666
_ body: (_ transport: Transport) throws -> Result
16241667
) throws -> Result? {
16251668
self.eventLoop.assertInEventLoop()
1626-
guard let channel = self._pipeline._channel as? any NIOTransportAccessibleChannel<Transport> else {
1669+
guard let core = self._pipeline.channel._channelCore as? any NIOTransportAccessibleChannelCore<Transport>
1670+
else {
16271671
return nil
16281672
}
1629-
return try channel.withUnsafeTransport(body)
1673+
return try core.withUnsafeTransport(body)
16301674
}
16311675
}
16321676

Sources/NIOCore/NIOTransportAccessibleChannel.swift renamed to Sources/NIOCore/NIOTransportAccessibleChannelCore.swift

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
/// A ``NIOTransportAccessibleChannel`` is a ``ChannelCore`` that provides access to its underlying transport.
16-
public protocol NIOTransportAccessibleChannel<Transport>: ChannelCore {
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 {
1723
/// The type of the underlying transport.
1824
associatedtype Transport
1925

@@ -22,12 +28,14 @@ public protocol NIOTransportAccessibleChannel<Transport>: ChannelCore {
2228
/// This is an advanced API for reading or manipulating the underlying transport that backs a channel. Users must
2329
/// not close the transport or invalidate any invariants that NIO relies upon for the channel operation.
2430
///
25-
/// Not all channels are expected to conform to ``NIOTransportAccessibleChannel``, but this can be determined at
26-
/// runtime.
27-
///
2831
/// Users should not attempt to use this API direcly, but should instead use
2932
/// ``ChannelPipeline/SynchronousOperations/withUnsafeTransportIfAvailable(of:_:)``.
3033
///
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+
///
3139
/// - Parameter body: A closure that takes the underlying transport.
3240
/// - Returns: The value returned by the closure.
3341
/// - Throws: If the underlying transport is unavailable, or rethrows any error thrown by the closure.

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

Sources/NIOPosix/BaseSocketChannel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1479,7 +1479,7 @@ extension BaseSocketChannel {
14791479
}
14801480
}
14811481

1482-
extension BaseSocketChannel: NIOTransportAccessibleChannel where SocketType: BaseSocket {
1482+
extension BaseSocketChannel: NIOTransportAccessibleChannelCore where SocketType: BaseSocket {
14831483
/// The underlying transport which is a BSD socket file handle.
14841484
typealias Transport = NIOBSDSocket.Handle
14851485

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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 NIOPosix // NOTE: Not @testable import here -- testing public API surface.
17+
import Testing
18+
19+
@Suite struct NIOTransportAccessibleChannelCoreTests {
20+
@Test func testUnderlyingSocketAccessForSocketBasedChannel() throws {
21+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
22+
defer { #expect(throws: Never.self) { try group.syncShutdownGracefully() } }
23+
let channel = try DatagramBootstrap(group: group).bind(host: "127.0.0.1", port: 0).wait()
24+
defer { #expect(throws: Never.self) { try channel.close().wait() } }
25+
26+
// We don't expect users to do this runtime check, but test the channel we got back from bootstrap conforms.
27+
#expect(channel is any NIOTransportAccessibleChannelCore)
28+
#expect(channel is any NIOTransportAccessibleChannelCore<NIOBSDSocket.Handle>)
29+
#expect(channel is any NIOTransportAccessibleChannelCore<Any> == false)
30+
31+
// Here we try the public API use, in various flavours.
32+
try channel.eventLoop.submit {
33+
let syncOps = channel.pipeline.syncOperations
34+
35+
// Calling without explicit transport type runs closure if body inefers correct transport type.
36+
try #expect(syncOps.withUnsafeTransportIfAvailable { fd in fd != NIOBSDSocket.invalidHandle } == true)
37+
try #expect(syncOps.withUnsafeTransportIfAvailable { $0 != NIOBSDSocket.invalidHandle } == true)
38+
39+
// Calling with explicit correct transport type runs closure.
40+
try #expect(syncOps.withUnsafeTransportIfAvailable(of: NIOBSDSocket.Handle.self) { _ in 42 } == 42)
41+
try #expect(syncOps.withUnsafeTransportIfAvailable { (_: NIOBSDSocket.Handle) in 42 } == 42)
42+
43+
// Calling with explicit incorrect transport type does not run closure.
44+
try #expect(syncOps.withUnsafeTransportIfAvailable(of: String.self) { _ in 42 } == nil)
45+
try #expect(syncOps.withUnsafeTransportIfAvailable { (_: String) in 42 } == nil)
46+
47+
// Calling with explicit Any transport type does not run closure.
48+
try #expect(syncOps.withUnsafeTransportIfAvailable(of: Any.self) { _ in 42 } == nil)
49+
try #expect(syncOps.withUnsafeTransportIfAvailable { (_: Any) in 42 } == nil)
50+
51+
// Calling without explicit transport type does not run closure, even if body doesn't use transport.
52+
try #expect(syncOps.withUnsafeTransportIfAvailable { 42 } == nil)
53+
54+
// Fun aside: What is the resolved type of the above function and why does it allow ignoring closure param?
55+
try #expect(syncOps.withUnsafeTransportIfAvailable { $0.self } == nil)
56+
// Answer: `$0: any (~Copyable & ~Escapable).Type`
57+
58+
// Calling without explicit transport type does not run closure, even if body uses compatible literal value.
59+
try #expect(syncOps.withUnsafeTransportIfAvailable { transport in transport != -1 } == nil)
60+
}.wait()
61+
}
62+
63+
@Test func testUnderlyingTransportForUnsupportedChannels() throws {
64+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
65+
defer { #expect(throws: Never.self) { try group.syncShutdownGracefully() } }
66+
67+
// Right now pipe channels do not expose their underlying transport, so we'll use this to test API behaviour.
68+
let channel = try NIOPipeBootstrap(group: group).takingOwnershipOfDescriptor(output: STDOUT_FILENO).wait()
69+
defer { #expect(throws: Never.self) { try channel.close().wait() } }
70+
71+
#expect(channel is any NIOTransportAccessibleChannelCore == false)
72+
73+
try channel.eventLoop.submit {
74+
let syncOps = channel.pipeline.syncOperations
75+
76+
// Calling the public API will never run the closure -- we cannot specify a type to pass the runtime check.
77+
try #expect(syncOps.withUnsafeTransportIfAvailable { 42 } == nil)
78+
try #expect(syncOps.withUnsafeTransportIfAvailable(of: Any.self) { _ in 42 } == nil)
79+
try #expect(syncOps.withUnsafeTransportIfAvailable(of: CInt.self) { _ in 42 } == nil)
80+
try #expect(syncOps.withUnsafeTransportIfAvailable(of: type(of: STDOUT_FILENO).self) { _ in 42 } == nil)
81+
}.wait()
82+
}
83+
}

Tests/NIOPosixTests/NIOTransportAccessibleChannelTests.swift

Lines changed: 0 additions & 57 deletions
This file was deleted.

0 commit comments

Comments
 (0)