diff --git a/Package.swift b/Package.swift index b12c0f83578..a48a1b34274 100644 --- a/Package.swift +++ b/Package.swift @@ -23,7 +23,13 @@ let swiftSystem: PackageDescription.Target.Dependency = .product(name: "SystemPa // compatibility with previous NIO versions. let historicalNIOPosixDependencyRequired: [Platform] = [.macOS, .iOS, .tvOS, .watchOS, .linux, .android] -let swiftSettings: [SwiftSetting] = [] +let swiftSettings: [SwiftSetting] = [ + // The Language Steering Group has promised that they won't break the APIs that currently exist under + // this "experimental" feature flag without two subsequent releases. We assume they will respect that + // promise, so we enable this here. For more, see: + // https://forums.swift.org/t/experimental-support-for-lifetime-dependencies-in-swift-6-2-and-beyond/78638 + .enableExperimentalFeature("Lifetimes") +] // This doesn't work when cross-compiling: the privacy manifest will be included in the Bundle and // Foundation will be linked. This is, however, strictly better than unconditionally adding the diff --git a/Sources/NIOCore/ByteBuffer-aux.swift b/Sources/NIOCore/ByteBuffer-aux.swift index 47ef32c1a88..6c17f22c7b1 100644 --- a/Sources/NIOCore/ByteBuffer-aux.swift +++ b/Sources/NIOCore/ByteBuffer-aux.swift @@ -832,6 +832,36 @@ extension ByteBuffer { self = ByteBufferAllocator().buffer(dispatchData: dispatchData) } #endif + + #if compiler(>=6.2) + /// Create a fresh ``ByteBuffer`` with a minimum size, and initializing it safely via an `OutputSpan`. + /// + /// This will allocate a new ``ByteBuffer`` with at least `capacity` bytes of storage, and then calls + /// `initializer` with an `OutputSpan` over the entire allocated storage. This is a convenient method + /// to initialize a buffer directly and safely in a single allocation, including from C code. + /// + /// Once this call returns, the buffer will have its ``ByteBuffer/writerIndex`` appropriately advanced to encompass + /// any memory initialized by the `initializer`. Uninitialized memory will be after the ``ByteBuffer/writerIndex``, + /// available for subsequent use. + /// + /// - info: If you have access to a `Channel`, `ChannelHandlerContext`, or `ByteBufferAllocator` we + /// recommend using `channel.allocator.buffer(capacity:initializingWith:)`. Or if you want to write multiple items into + /// the buffer use `channel.allocator.buffer(capacity: ...)` to allocate a `ByteBuffer` of the right + /// size followed by a `write(minimumWritableBytes:initializingWith:)` instead of using this method. This allows SwiftNIO to do + /// accounting and optimisations of resources acquired for operations on a given `Channel` in the future. + /// + /// - parameters: + /// - capacity: The minimum initial space to allocate for the buffer. + /// - initializer: The initializer that will be invoked to initialize the allocated memory. + @inlinable + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + public init( + initialCapacity capacity: Int, + initializingWith initializer: (_ span: inout OutputRawSpan) throws(ErrorType) -> Void + ) throws(ErrorType) { + self = try ByteBufferAllocator().buffer(capacity: capacity, initializingWith: initializer) + } + #endif } extension ByteBuffer: Codable { @@ -971,6 +1001,32 @@ extension ByteBufferAllocator { return buffer } #endif + + #if compiler(>=6.2) + /// Create a fresh ``ByteBuffer`` with a minimum size, and initializing it safely via an `OutputSpan`. + /// + /// This will allocate a new ``ByteBuffer`` with at least `capacity` bytes of storage, and then calls + /// `initializer` with an `OutputSpan` over the entire allocated storage. This is a convenient method + /// to initialize a buffer directly and safely in a single allocation, including from C code. + /// + /// Once this call returns, the buffer will have its ``ByteBuffer/writerIndex`` appropriately advanced to encompass + /// any memory initialized by the `initializer`. Uninitialized memory will be after the ``ByteBuffer/writerIndex``, + /// available for subsequent use. + /// + /// - parameters: + /// - capacity: The minimum initial space to allocate for the buffer. + /// - initializer: The initializer that will be invoked to initialize the allocated memory. + @inlinable + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + public func buffer( + capacity: Int, + initializingWith initializer: (_ span: inout OutputRawSpan) throws(ErrorType) -> Void + ) throws(ErrorType) -> ByteBuffer { + var buffer = self.buffer(capacity: capacity) + try buffer.writeWithOutputRawSpan(minimumWritableBytes: capacity, initializingWith: initializer) + return buffer + } + #endif } extension Optional where Wrapped == ByteBuffer { diff --git a/Sources/NIOCore/ByteBuffer-core.swift b/Sources/NIOCore/ByteBuffer-core.swift index ce434877e7d..0bb0500fb4e 100644 --- a/Sources/NIOCore/ByteBuffer-core.swift +++ b/Sources/NIOCore/ByteBuffer-core.swift @@ -691,6 +691,52 @@ public struct ByteBuffer { return try body(.init(rebasing: self._slicedStorageBuffer[range])) } + #if compiler(>=6.2) + /// Provides safe high-performance read-only access to the readable bytes of this buffer. + @inlinable + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + public var readableBytesSpan: RawSpan { + @_lifetime(borrow self) + borrowing get { + let range = Range(uncheckedBounds: (lower: self.readerIndex, upper: self.writerIndex)) + return _overrideLifetime(RawSpan(_unsafeBytes: self._slicedStorageBuffer[range]), borrowing: self) + } + } + + /// Provides mutable access to the readable bytes of this buffer. + @inlinable + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + public var mutableReadableBytesSpan: MutableRawSpan { + @_lifetime(&self) + mutating get { + self._copyStorageAndRebaseIfNeeded() + let range = Range(uncheckedBounds: (lower: self.readerIndex, upper: self.writerIndex)) + return _overrideLifetime(MutableRawSpan(_unsafeBytes: self._slicedStorageBuffer[range]), mutating: &self) + } + } + + /// Enables high-performance low-level appending into the writable section of this buffer. + /// + /// The writer index will be advanced by the number of bytes written into the + /// `OutputRawSpan`. + /// + /// - parameters: + /// - minimumWritableBytes: The minimum initial space to allocate for the buffer. + /// - initializer: The initializer that will be invoked to initialize the allocated memory. + @inlinable + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + public mutating func writeWithOutputRawSpan( + minimumWritableBytes: Int, + initializingWith initializer: (_ span: inout OutputRawSpan) throws(ErrorType) -> Void + ) throws(ErrorType) { + try self.writeWithUnsafeMutableBytes(minimumWritableBytes: minimumWritableBytes) { ptr throws(ErrorType) in + var span = OutputRawSpan(buffer: ptr, initializedCount: 0) + try initializer(&span) + return span.byteCount + } + } + #endif + /// Yields the bytes currently writable (`bytesWritable` = `capacity` - `writerIndex`). Before reading those bytes you must first /// write to them otherwise you will trigger undefined behaviour. The writer index will remain unchanged. /// diff --git a/Tests/NIOCoreTests/ByteBufferSpanTests.swift b/Tests/NIOCoreTests/ByteBufferSpanTests.swift new file mode 100644 index 00000000000..cf915838eee --- /dev/null +++ b/Tests/NIOCoreTests/ByteBufferSpanTests.swift @@ -0,0 +1,293 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import Testing + +#if compiler(>=6.2) +@Suite +struct ByteBufferSpanTests { + @Test + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + func testReadableBytesSpanOfEmptyByteBuffer() { + let bb = ByteBuffer() + #expect(bb.readableBytesSpan.byteCount == 0) + } + + @Test + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + func testReadableBytesSpanOfSimpleBuffer() { + let bb = ByteBuffer(string: "Hello, world!") + #expect(bb.readableBytesSpan.byteCount == 13) + let bytesEqual = bb.readableBytesSpan.elementsEqual("Hello, world!".utf8) + #expect(bytesEqual) + } + + @Test + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + func testReadableBytesSpanNotAtTheStart() { + var bb = ByteBuffer(string: "Hello, world!") + bb.moveReaderIndex(forwardBy: 5) + #expect(bb.readableBytesSpan.byteCount == 8) + let bytesEqual = bb.readableBytesSpan.elementsEqual(", world!".utf8) + #expect(bytesEqual) + } + + @Test + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + func testReadableBytesSpanOfSlice() { + let first = ByteBuffer(string: "Hello, world!") + let bb = first.getSlice(at: 5, length: 5)! + #expect(bb.readableBytesSpan.byteCount == 5) + let bytesEqual = bb.readableBytesSpan.elementsEqual(", wor".utf8) + #expect(bytesEqual) + } + + @Test + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + func testMutableReadableBytesSpanOfEmptyByteBuffer() { + var bb = ByteBuffer() + #expect(bb.mutableReadableBytesSpan.byteCount == 0) + } + + @Test + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + func testMutableReadableBytesSpanOfSimpleBuffer() { + var bb = ByteBuffer(string: "Hello, world!") + #expect(bb.mutableReadableBytesSpan.byteCount == 13) + let bytesEqual = bb.mutableReadableBytesSpan.elementsEqual("Hello, world!".utf8) + #expect(bytesEqual) + + var readableBytes = bb.mutableReadableBytesSpan + readableBytes.storeBytes(of: UInt8(ascii: "o"), toByteOffset: 5, as: UInt8.self) + + #expect(String(buffer: bb) == "Helloo world!") + } + + @Test + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + func testMutableReadableBytesSpanNotAtTheStart() { + var bb = ByteBuffer(string: "Hello, world!") + bb.moveReaderIndex(forwardBy: 5) + #expect(bb.mutableReadableBytesSpan.byteCount == 8) + let bytesEqual = bb.mutableReadableBytesSpan.elementsEqual(", world!".utf8) + #expect(bytesEqual) + + var readableBytes = bb.mutableReadableBytesSpan + readableBytes.storeBytes(of: UInt8(ascii: "o"), toByteOffset: 5, as: UInt8.self) + + #expect(String(buffer: bb) == ", worod!") + } + + @Test + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + func testMutableReadableBytesSpanOfSlice() { + let first = ByteBuffer(string: "Hello, world!") + var bb = first.getSlice(at: 5, length: 5)! + #expect(bb.mutableReadableBytesSpan.byteCount == 5) + let bytesEqual = bb.mutableReadableBytesSpan.elementsEqual(", wor".utf8) + #expect(bytesEqual) + + var readableBytes = bb.mutableReadableBytesSpan + readableBytes.storeBytes(of: UInt8(ascii: "o"), toByteOffset: 4, as: UInt8.self) + + #expect(String(buffer: bb) == ", woo") + #expect(String(buffer: first) == "Hello, world!") + } + + @Test + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + func testEvenCreatingMutableSpanTriggersCoW() { + let first = ByteBuffer(string: "Hello, world!") + var second = first + + let firstBackingPtr = first.withVeryUnsafeBytes { $0 }.baseAddress + let secondBackingPtr = second.withVeryUnsafeBytes { $0 }.baseAddress + #expect(firstBackingPtr == secondBackingPtr) + + let readableBytes = second.mutableReadableBytesSpan + _ = consume readableBytes + let firstNewBackingPtr = first.withVeryUnsafeBytes { $0 }.baseAddress + let secondNewBackingPtr = second.withVeryUnsafeBytes { $0 }.baseAddress + #expect(firstNewBackingPtr != secondNewBackingPtr) + #expect(firstBackingPtr == firstNewBackingPtr) + } + + @Test + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + func testAppendingToEmptyBufferViaOutputSpan() { + var bb = ByteBuffer() + bb.writeWithOutputRawSpan(minimumWritableBytes: 15) { span in + #expect(span.byteCount == 0) + #expect(span.capacity >= 15) + #expect(span.freeCapacity >= 15) + var bytesEqual = span.initializedElementsEqual([]) + #expect(bytesEqual) + + span.append(contentsOf: "Hello, world!".utf8) + + #expect(span.byteCount == 13) + #expect(span.capacity >= 2) + #expect(span.freeCapacity >= 2) + + bytesEqual = span.initializedElementsEqual("Hello, world!".utf8) + #expect(bytesEqual) + } + #expect(bb.readableBytes == 13) + #expect(String(buffer: bb) == "Hello, world!") + } + + @Test + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + func testAppendingToNonEmptyBufferViaOutputSpanDoesNotExposeInitialBytes() { + var bb = ByteBuffer() + bb.writeString("Hello") + bb.writeWithOutputRawSpan(minimumWritableBytes: 8) { span in + #expect(span.byteCount == 0) + #expect(span.capacity >= 8) + #expect(span.freeCapacity >= 8) + var bytesEqual = span.initializedElementsEqual([]) + #expect(bytesEqual) + + span.append(contentsOf: ", world!".utf8) + + #expect(span.byteCount == 8) + #expect(span.capacity >= 0) + #expect(span.freeCapacity >= 0) + bytesEqual = span.initializedElementsEqual(", world!".utf8) + #expect(bytesEqual) + } + #expect(bb.readableBytes == 13) + #expect(String(buffer: bb) == "Hello, world!") + } + + @Test + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + func testAppendingToASliceViaOutputSpan() { + let first = ByteBuffer(string: "Hello, world!") + var bb = first.getSlice(at: 5, length: 5)! + #expect(bb.mutableReadableBytesSpan.byteCount == 5) + let bytesEqual = bb.mutableReadableBytesSpan.elementsEqual(", wor".utf8) + #expect(bytesEqual) + + bb.writeWithOutputRawSpan(minimumWritableBytes: 5) { span in + span.append(contentsOf: "olleh".utf8) + } + + #expect(String(buffer: bb) == ", worolleh") + #expect(String(buffer: first) == "Hello, world!") + } + + @Test + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + func testEvenCreatingAnOutputSpanTriggersCoW() { + let first = ByteBuffer(string: "Hello, world!") + var second = first + + let firstBackingPtr = first.withVeryUnsafeBytes { $0 }.baseAddress + let secondBackingPtr = second.withVeryUnsafeBytes { $0 }.baseAddress + #expect(firstBackingPtr == secondBackingPtr) + + second.writeWithOutputRawSpan(minimumWritableBytes: 5) { _ in } + let firstNewBackingPtr = first.withVeryUnsafeBytes { $0 }.baseAddress + let secondNewBackingPtr = second.withVeryUnsafeBytes { $0 }.baseAddress + #expect(firstNewBackingPtr != secondNewBackingPtr) + #expect(firstBackingPtr == firstNewBackingPtr) + } + + @Test + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + func testCanCreateEmptyBufferDirectly() { + let bb = ByteBuffer(initialCapacity: 15) { span in + #expect(span.byteCount == 0) + #expect(span.capacity >= 15) + #expect(span.freeCapacity >= 15) + var bytesEqual = span.initializedElementsEqual([]) + #expect(bytesEqual) + + span.append(contentsOf: "Hello, world!".utf8) + + #expect(span.byteCount == 13) + #expect(span.capacity >= 2) + #expect(span.freeCapacity >= 2) + bytesEqual = span.initializedElementsEqual("Hello, world!".utf8) + #expect(bytesEqual) + } + #expect(bb.readableBytes == 13) + #expect(String(buffer: bb) == "Hello, world!") + } + + @Test + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + func testCanCreateEmptyBufferDirectlyFromAllocator() { + let bb = ByteBufferAllocator().buffer(capacity: 15) { span in + #expect(span.byteCount == 0) + #expect(span.capacity >= 15) + #expect(span.freeCapacity >= 15) + var bytesEqual = span.initializedElementsEqual([]) + #expect(bytesEqual) + + span.append(contentsOf: "Hello, world!".utf8) + + #expect(span.byteCount == 13) + #expect(span.capacity >= 2) + #expect(span.freeCapacity >= 2) + bytesEqual = span.initializedElementsEqual("Hello, world!".utf8) + #expect(bytesEqual) + } + #expect(bb.readableBytes == 13) + #expect(String(buffer: bb) == "Hello, world!") + } +} + +@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) +extension RawSpan { + func elementsEqual(_ other: Other) -> Bool where Other.Element == UInt8 { + guard other.count == self.byteCount else { return false } + + var index = other.startIndex + var offset = 0 + while index < other.endIndex { + guard other[index] == self.unsafeLoadUnaligned(fromByteOffset: offset, as: UInt8.self) else { + return false + } + other.formIndex(after: &index) + offset &+= 1 + } + + return true + } +} + +@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) +extension MutableRawSpan { + func elementsEqual(_ other: Other) -> Bool where Other.Element == UInt8 { + self.bytes.elementsEqual(other) + } +} + +@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) +extension OutputRawSpan { + func initializedElementsEqual(_ other: Other) -> Bool where Other.Element == UInt8 { + self.bytes.elementsEqual(other) + } + + @_lifetime(self: copy self) + mutating func append(contentsOf other: Other) where Other.Element == UInt8 { + for element in other { + self.append(element) + } + } +} +#endif