diff --git a/Sources/NIOCore/NIOLoopBound.swift b/Sources/NIOCore/NIOLoopBound.swift index 2037177d6e..56841e57d4 100644 --- a/Sources/NIOCore/NIOLoopBound.swift +++ b/Sources/NIOCore/NIOLoopBound.swift @@ -17,7 +17,7 @@ /// /// ``NIOLoopBound`` is useful to transport a value of a non-`Sendable` type that needs to go from one place in /// your code to another where you (but not the compiler) know is on one and the same ``EventLoop``. Usually this -/// involves `@Sendable` closures. This type is safe because it verifies (using ``EventLoop/preconditionInEventLoop(file:line:)-2fxvb``) +/// involves `@Sendable` or `sending` closures. This type is safe because it verifies (using ``EventLoop/preconditionInEventLoop(file:line:)-2fxvb``) /// that this is actually true. /// /// A ``NIOLoopBound`` can only be constructed, read from or written to when you are provably @@ -59,7 +59,7 @@ public struct NIOLoopBound: @unchecked Sendable { /// /// ``NIOLoopBoundBox`` is useful to transport a value of a non-`Sendable` type that needs to go from one place in /// your code to another where you (but not the compiler) know is on one and the same ``EventLoop``. Usually this -/// involves `@Sendable` closures. This type is safe because it verifies (using ``EventLoop/preconditionInEventLoop(file:line:)-7ukrq``) +/// involves `@Sendable` or `sending` closures. This type is safe because it verifies (using ``EventLoop/preconditionInEventLoop(file:line:)-7ukrq``) /// that this is actually true. /// /// A ``NIOLoopBoundBox`` can only be read from or written to when you are provably @@ -142,3 +142,26 @@ public final class NIOLoopBoundBox: @unchecked Sendable { } } } + +#if compiler(>=6.0) // `sending` is >= 6.0 +extension NIOLoopBoundBox { + /// Initialise a ``NIOLoopBoundBox`` by `sending` (i.e. transferring) a value, validly callable off `eventLoop`. + /// + /// Contrary to ``init(_:eventLoop:)``, this method can be called off `eventLoop` because we are `sending` the value across the isolation domain. + /// Because we're `sending` `value`, we just need to protect it against mutations (because nothing else can have access to it anymore). + public static func makeBoxSendingValue( + _ value: sending Value, + as: Value.Type = Value.self, + eventLoop: EventLoop + ) -> NIOLoopBoundBox { + // Here, we -- possibly surprisingly -- do not precondition being on the EventLoop. This is okay for a few + // reasons: + // - This function only works by `sending` the value, so we don't need to worry about somebody + // still holding a reference to this — Swift 6 will ensure the value is not modified erroneously. + // - Because of Swift's Definitive Initialisation (DI), we know that we did write `self._value` before `init` + // returns. + // - The only way to ever write (or read indeed) `self._value` is by proving to be inside the `EventLoop`. + .init(_value: value, uncheckedEventLoop: eventLoop) + } +} +#endif diff --git a/Tests/NIOPosixTests/NIOLoopBoundTests.swift b/Tests/NIOPosixTests/NIOLoopBoundTests.swift index 5bc106f3a5..6936290e2f 100644 --- a/Tests/NIOPosixTests/NIOLoopBoundTests.swift +++ b/Tests/NIOPosixTests/NIOLoopBoundTests.swift @@ -75,6 +75,42 @@ final class NIOLoopBoundTests: XCTestCase { ) } + #if compiler(>=6.0) + fileprivate class NonSendableThingy { + var value: Int = 0 + init(value: Int) { + self.value = value + } + } + + func testLoopBoundBoxCanBeInitialisedWithSendingValueOffLoopAndLaterSetToValue() { + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + XCTAssertNoThrow(try group.syncShutdownGracefully()) + } + + let loop = group.any() + let nonSendableThingy = NonSendableThingy(value: 15) + + let sendableBox = NIOLoopBoundBox.makeBoxSendingValue( + nonSendableThingy, + as: NonSendableThingy.self, + eventLoop: loop + ) + for _ in 0..<(100 - 15) { + loop.execute { + sendableBox.value.value += 1 + } + } + XCTAssertEqual( + 100, + try loop.submit { + sendableBox.value.value + }.wait() + ) + } + #endif + func testInPlaceMutation() { var loopBound = NIOLoopBound(CoWValue(), eventLoop: loop) XCTAssertTrue(loopBound.value.mutateInPlace())