diff --git a/Sources/NIOCore/NIOLoopBound.swift b/Sources/NIOCore/NIOLoopBound.swift index c105f361d1..a3631f5b2f 100644 --- a/Sources/NIOCore/NIOLoopBound.swift +++ b/Sources/NIOCore/NIOLoopBound.swift @@ -107,6 +107,26 @@ public final class NIOLoopBoundBox: @unchecked Sendable { return .init(_value: nil, uncheckedEventLoop: eventLoop) } + /// Initialise a ``NIOLoopBoundBox`` by sending a `Sendable` value, validly callable off `eventLoop`. + /// + /// Contrary to ``init(_:eventLoop:)``, this method can be called off `eventLoop` because we know that `value` is `Sendable`. + /// So we don't need to protect `value` itself, we just need to protect the ``NIOLoopBoundBox`` against mutations which we do because the ``value`` + /// accessors are checking that we're on `eventLoop`. + public static func makeBoxSendingValue( + _ value: Value, + as: Value.Type = Value.self, + eventLoop: EventLoop + ) -> NIOLoopBoundBox where Value: Sendable { + // Here, we -- possibly surprisingly -- do not precondition being on the EventLoop. This is okay for a few + // reasons: + // - This function only works with `Sendable` values, so we don't need to worry about somebody + // still holding a reference to this. + // - 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`. + return .init(_value: value, uncheckedEventLoop: eventLoop) + } + /// Access the `value` with the precondition that the code is running on `eventLoop`. /// /// - note: ``NIOLoopBoundBox`` itself is reference-typed, so any writes will affect anybody sharing this reference. diff --git a/Tests/NIOPosixTests/NIOLoopBoundTests.swift b/Tests/NIOPosixTests/NIOLoopBoundTests.swift index 4785c5d4dd..0c525125ad 100644 --- a/Tests/NIOPosixTests/NIOLoopBoundTests.swift +++ b/Tests/NIOPosixTests/NIOLoopBoundTests.swift @@ -49,6 +49,25 @@ final class NIOLoopBoundTests: XCTestCase { }.wait()) } + func testLoopBoundBoxCanBeInitialisedWithSendableValueOffLoopAndLaterSetToValue() { + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + XCTAssertNoThrow(try group.syncShutdownGracefully()) + } + + let loop = group.any() + + let sendableBox = NIOLoopBoundBox.makeBoxSendingValue(15, as: Int.self, eventLoop: loop) + for _ in 0..<(100 - 15) { + loop.execute { + sendableBox.value += 1 + } + } + XCTAssertEqual(100, try loop.submit { + sendableBox.value + }.wait()) + } + // MARK: - Helpers func sendableBlackhole(_ sendableThing: S) {}