Skip to content

Commit 3299959

Browse files
fixup: Refactor how conformance works and add support for pipe channels
1 parent 3bfafdb commit 3299959

File tree

4 files changed

+96
-21
lines changed

4 files changed

+96
-21
lines changed

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 {
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/BaseSocketChannel.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1479,16 +1479,6 @@ extension BaseSocketChannel {
14791479
}
14801480
}
14811481

1482-
extension BaseSocketChannel: NIOTransportAccessibleChannelCore where SocketType: BaseSocket {
1483-
/// The underlying transport which is a BSD socket file handle.
1484-
typealias Transport = NIOBSDSocket.Handle
1485-
1486-
/// Provides scoped access to the BSD socket file handle.
1487-
func withUnsafeTransport<Result>(_ body: (_ transport: NIOBSDSocket.Handle) throws -> Result) throws -> Result {
1488-
try self.socket.withUnsafeHandle(body)
1489-
}
1490-
}
1491-
14921482
/// Execute the given function and synchronously complete the given `EventLoopPromise` (if not `nil`).
14931483
func executeAndComplete<Value: Sendable>(_ promise: EventLoopPromise<Value>?, _ body: () throws -> Value) {
14941484
do {

Tests/NIOPosixTests/NIOTransportAccessibleChannelCoreTests.swift

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import NIOCore // NOTE: Not @testable import here -- testing public API surface.
16+
import NIOEmbedded
1617
import NIOPosix // NOTE: Not @testable import here -- testing public API surface.
1718
import Testing
1819

@@ -63,21 +64,50 @@ import Testing
6364
@Test func testUnderlyingTransportForUnsupportedChannels() throws {
6465
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
6566
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()
67+
let channel = EmbeddedChannel()
6968
defer { #expect(throws: Never.self) { try channel.close().wait() } }
7069

7170
#expect(channel is any NIOTransportAccessibleChannelCore == false)
7271

73-
try channel.eventLoop.submit {
74-
let syncOps = channel.pipeline.syncOperations
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+
}
7579

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()
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)
82112
}
83113
}

0 commit comments

Comments
 (0)