Skip to content

Add opt-in API for channels to expose their underlying transport#3509

Merged
simonjbeaumont merged 8 commits intoapple:mainfrom
simonjbeaumont:sb/channel-underlying-transport
Feb 17, 2026
Merged

Add opt-in API for channels to expose their underlying transport#3509
simonjbeaumont merged 8 commits intoapple:mainfrom
simonjbeaumont:sb/channel-underlying-transport

Conversation

@simonjbeaumont
Copy link
Copy Markdown
Contributor

@simonjbeaumont simonjbeaumont commented Feb 11, 2026

Motivation

NIO channels abstract over an underlying transport mechanisms (sockets, pipes, etc.), which users typically need not interact with. However, there are scenarios where users need direct access to the underlying transport for low-level operations that work outside NIOs abstraction. One example is performing out out-of-band operations on the underlying file descriptor for a socket-based channel.

This PR adds a structured way to access the underlying transport of a channel, for channels that choose to implement it.

Modifications

  • Add a new public protocol NIOTransportAccessibleChannelCore<Transport> which provides a scoped withUnsafeTransport(_:), used for channel implementations to opt-in.
  • Add conformance to NIOTransportAccessibleChannel<NIOBSDSocket.Handle> for BaseSocketChannel, to make this API available for all socket-based channels, including channels returned from the socket-based bootstrap public APIs.
  • Add public API ChannelPipeline.SynchronousOperations.withUnsafeTransportIfAvailable(_:)

Note that not all channels need to or should their transport, which is why this was added as an additional protocol that refines ChannelCore, vs. extending Channel or ChannelCore with a default implementation.

The protocol uses a primary associated type allowing channels to provide typed access to the transport. E.g. this could be used in NIO Transport Services to expose the underlying NWConnection, if desired.

The method itself is spelled with "unsafe", uses scoped access, and has clear documentation that users must not violated any of NIOs assumptions about the state of the underlying transport. It's very much not intended for every day use. It being on ChannelCore should defer users of the low-level API, since Channel._channelCore is marked as for NIO internal use, and the public withUnsafeTransportIfAvailable(_:) will take care of the runtime checks for channels that have opted into this API.

Result

  • New opt-in API for channel implementations to expose their underlying transport
  • All socket-based channels from NIOPosix now expose the underlying socket file descriptor
  • New API ChannelPipeline.SynchronousOperations.withUnsafeTransportIfAvailable(_:) for users

Co-authored-by: Agam Dua <agam_dua@apple.com>
@simonjbeaumont simonjbeaumont added the 🆕 semver/minor Adds new public API. label Feb 11, 2026
@simonjbeaumont simonjbeaumont marked this pull request as ready for review February 11, 2026 15:34
@simonjbeaumont simonjbeaumont force-pushed the sb/channel-underlying-transport branch from 765e0da to ccc1b15 Compare February 13, 2026 09:28
Comment on lines +1614 to +1615
/// Not all channels support access to the underlying channel. If the channel does not support this API, the
/// closure is not called and this function immediately returns `nil`.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This matches the semantics of Collection.withContiguousStorageIfAvailable(_:) w.r.t. to if the type doesn't support the operation, i.e. the closure is not called and the optional generic return type is used to signal that.

@simonjbeaumont simonjbeaumont force-pushed the sb/channel-underlying-transport branch 3 times, most recently from 0037d00 to 0334941 Compare February 13, 2026 13:05
Copy link
Copy Markdown
Contributor Author

@simonjbeaumont simonjbeaumont left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lukasa OK, this one is now ready for a re-review. PTAL 🙏

Comment on lines +58 to +59
// Calling without explicit transport type does not run closure, even if body uses compatible literal value.
try #expect(syncOps.withUnsafeTransportIfAvailable { transport in transport != -1 } == nil)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is probably the sharpest edge of this design.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this edge isn't as sharp as it looks. The problem here is that the literal ends up being inferred to be of type Int, which isn't CInt, because there is no type information to suggest a better choice. If you wrote -1 as CInt or CInt(-1) then you'll find this works correctly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I understood why it didn't work. I just think that this isn't going to be super obvious for adopters, but It think it's the best we can do and this is an off-the-beaten path, advanced API, so I'm OK with it if you are.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's even less surprising than the closure that excludes the argument not being run. But again, I think this is an OK trade.

@simonjbeaumont simonjbeaumont force-pushed the sb/channel-underlying-transport branch from 0334941 to 70a59e4 Compare February 13, 2026 13:13
@simonjbeaumont simonjbeaumont force-pushed the sb/channel-underlying-transport branch from 70a59e4 to 3bfafdb Compare February 13, 2026 13:25
}
}

extension BaseSocketChannel: NIOTransportAccessibleChannelCore where SocketType: BaseSocket {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably do this with `PipeChannel while we're here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, this is the channel I used to test the behaviour when this API isn't implemented 😆.

I'll have to think of something else for that, maybe EmbeddedChannel?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I've pushed a new commit that refactors how we do the conformance so that we can get conformances on other channels, including pipe channel. And I've added tests that all the channels you can get back from a bootstrap conform.

Copy link
Copy Markdown
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one!

@simonjbeaumont simonjbeaumont merged commit b0e0247 into apple:main Feb 17, 2026
55 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🆕 semver/minor Adds new public API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants