From e0309fc6647db6a0e4e9e373afa32212da927eef Mon Sep 17 00:00:00 2001 From: Natik Gadzhi Date: Sun, 26 Jan 2025 12:31:19 -0800 Subject: [PATCH 1/3] Make `NIOLoopBoundBox` off-EL with sending Motivation: This closes #2754. It's a follow-up for #2753 that adds a way to `makeLoopBoundBox` by sendin a value, leaning into Swift 6 concurrency! Modifications: - Adds `makeBoxSendingValue`. Can't take the credit, @weissi wrote it in the issue. - Adds a test for it. Result: Nice NIOLoopBoundBox API for Swift 6. --- Sources/NIOCore/NIOLoopBound.swift | 27 +++++++++++++-- Tests/NIOPosixTests/NIOLoopBoundTests.swift | 37 +++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) 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..b7c900f960 100644 --- a/Tests/NIOPosixTests/NIOLoopBoundTests.swift +++ b/Tests/NIOPosixTests/NIOLoopBoundTests.swift @@ -75,6 +75,43 @@ final class NIOLoopBoundTests: XCTestCase { ) } + #if compiler(>=6.0) + 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()) From 02c660f994ebc205945d4e09b2a410253de70bdc Mon Sep 17 00:00:00 2001 From: Natik Gadzhi Date: Sun, 26 Jan 2025 12:38:11 -0800 Subject: [PATCH 2/3] Update Tests/NIOPosixTests/NIOLoopBoundTests.swift --- Tests/NIOPosixTests/NIOLoopBoundTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/NIOPosixTests/NIOLoopBoundTests.swift b/Tests/NIOPosixTests/NIOLoopBoundTests.swift index b7c900f960..735a0d7d12 100644 --- a/Tests/NIOPosixTests/NIOLoopBoundTests.swift +++ b/Tests/NIOPosixTests/NIOLoopBoundTests.swift @@ -90,7 +90,6 @@ final class NIOLoopBoundTests: XCTestCase { } let loop = group.any() - let nonSendableThingy = NonSendableThingy(value: 15) let sendableBox = NIOLoopBoundBox.makeBoxSendingValue( From e4b702dab5fbf9855a8ffb949f85abfa6fbeb747 Mon Sep 17 00:00:00 2001 From: Natik Gadzhi Date: Sun, 26 Jan 2025 12:38:17 -0800 Subject: [PATCH 3/3] Update Tests/NIOPosixTests/NIOLoopBoundTests.swift --- Tests/NIOPosixTests/NIOLoopBoundTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/NIOPosixTests/NIOLoopBoundTests.swift b/Tests/NIOPosixTests/NIOLoopBoundTests.swift index 735a0d7d12..6936290e2f 100644 --- a/Tests/NIOPosixTests/NIOLoopBoundTests.swift +++ b/Tests/NIOPosixTests/NIOLoopBoundTests.swift @@ -76,7 +76,7 @@ final class NIOLoopBoundTests: XCTestCase { } #if compiler(>=6.0) - class NonSendableThingy { + fileprivate class NonSendableThingy { var value: Int = 0 init(value: Int) { self.value = value