Skip to content

Commit 88dc990

Browse files
authored
Add some spans to ByteBuffer (#3371)
Motivation: Users would like to be able to access the underlying memory of a ByteBuffer, as evidenced by the plethora of `withUnsafe*` methods that ByteBuffer has. As Swift 6.2 has introduced some initial APIs for safe memory access to underlying storage, we should offer similar APIs on ByteBuffer to enable users to get safer access to that storage. For now, the obvious APIs to be able to supplement are: - withUnsafeReadableBytes - withUnsafeMutableReadableBytes - writeWithUnsafeMutableWritableBytes We can also offer some new APIs to allow initializing a buffer directly from an OutputSpan. Note that we can only do this because the Language Steering Group has pinky promised that they will not break the "Lifetimes" experimental feature: see https://forums.swift.org/t/experimental-support-for-lifetime-dependencies-in-swift-6-2-and-beyond/78638 for more details. We are taking them at their word, and so we are enabling that feature. Modifications: Many new methods and tests. Result: Safer access.
1 parent 74e77da commit 88dc990

File tree

4 files changed

+402
-1
lines changed

4 files changed

+402
-1
lines changed

Package.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ let swiftSystem: PackageDescription.Target.Dependency = .product(name: "SystemPa
2323
// compatibility with previous NIO versions.
2424
let historicalNIOPosixDependencyRequired: [Platform] = [.macOS, .iOS, .tvOS, .watchOS, .linux, .android]
2525

26-
let swiftSettings: [SwiftSetting] = []
26+
let swiftSettings: [SwiftSetting] = [
27+
// The Language Steering Group has promised that they won't break the APIs that currently exist under
28+
// this "experimental" feature flag without two subsequent releases. We assume they will respect that
29+
// promise, so we enable this here. For more, see:
30+
// https://forums.swift.org/t/experimental-support-for-lifetime-dependencies-in-swift-6-2-and-beyond/78638
31+
.enableExperimentalFeature("Lifetimes")
32+
]
2733

2834
// This doesn't work when cross-compiling: the privacy manifest will be included in the Bundle and
2935
// Foundation will be linked. This is, however, strictly better than unconditionally adding the

Sources/NIOCore/ByteBuffer-aux.swift

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,36 @@ extension ByteBuffer {
832832
self = ByteBufferAllocator().buffer(dispatchData: dispatchData)
833833
}
834834
#endif
835+
836+
#if compiler(>=6.2)
837+
/// Create a fresh ``ByteBuffer`` with a minimum size, and initializing it safely via an `OutputSpan`.
838+
///
839+
/// This will allocate a new ``ByteBuffer`` with at least `capacity` bytes of storage, and then calls
840+
/// `initializer` with an `OutputSpan` over the entire allocated storage. This is a convenient method
841+
/// to initialize a buffer directly and safely in a single allocation, including from C code.
842+
///
843+
/// Once this call returns, the buffer will have its ``ByteBuffer/writerIndex`` appropriately advanced to encompass
844+
/// any memory initialized by the `initializer`. Uninitialized memory will be after the ``ByteBuffer/writerIndex``,
845+
/// available for subsequent use.
846+
///
847+
/// - info: If you have access to a `Channel`, `ChannelHandlerContext`, or `ByteBufferAllocator` we
848+
/// recommend using `channel.allocator.buffer(capacity:initializingWith:)`. Or if you want to write multiple items into
849+
/// the buffer use `channel.allocator.buffer(capacity: ...)` to allocate a `ByteBuffer` of the right
850+
/// size followed by a `write(minimumWritableBytes:initializingWith:)` instead of using this method. This allows SwiftNIO to do
851+
/// accounting and optimisations of resources acquired for operations on a given `Channel` in the future.
852+
///
853+
/// - parameters:
854+
/// - capacity: The minimum initial space to allocate for the buffer.
855+
/// - initializer: The initializer that will be invoked to initialize the allocated memory.
856+
@inlinable
857+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *)
858+
public init<ErrorType: Error>(
859+
initialCapacity capacity: Int,
860+
initializingWith initializer: (_ span: inout OutputRawSpan) throws(ErrorType) -> Void
861+
) throws(ErrorType) {
862+
self = try ByteBufferAllocator().buffer(capacity: capacity, initializingWith: initializer)
863+
}
864+
#endif
835865
}
836866

837867
extension ByteBuffer: Codable {
@@ -971,6 +1001,32 @@ extension ByteBufferAllocator {
9711001
return buffer
9721002
}
9731003
#endif
1004+
1005+
#if compiler(>=6.2)
1006+
/// Create a fresh ``ByteBuffer`` with a minimum size, and initializing it safely via an `OutputSpan`.
1007+
///
1008+
/// This will allocate a new ``ByteBuffer`` with at least `capacity` bytes of storage, and then calls
1009+
/// `initializer` with an `OutputSpan` over the entire allocated storage. This is a convenient method
1010+
/// to initialize a buffer directly and safely in a single allocation, including from C code.
1011+
///
1012+
/// Once this call returns, the buffer will have its ``ByteBuffer/writerIndex`` appropriately advanced to encompass
1013+
/// any memory initialized by the `initializer`. Uninitialized memory will be after the ``ByteBuffer/writerIndex``,
1014+
/// available for subsequent use.
1015+
///
1016+
/// - parameters:
1017+
/// - capacity: The minimum initial space to allocate for the buffer.
1018+
/// - initializer: The initializer that will be invoked to initialize the allocated memory.
1019+
@inlinable
1020+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *)
1021+
public func buffer<ErrorType: Error>(
1022+
capacity: Int,
1023+
initializingWith initializer: (_ span: inout OutputRawSpan) throws(ErrorType) -> Void
1024+
) throws(ErrorType) -> ByteBuffer {
1025+
var buffer = self.buffer(capacity: capacity)
1026+
try buffer.writeWithOutputRawSpan(minimumWritableBytes: capacity, initializingWith: initializer)
1027+
return buffer
1028+
}
1029+
#endif
9741030
}
9751031

9761032
extension Optional where Wrapped == ByteBuffer {

Sources/NIOCore/ByteBuffer-core.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,52 @@ public struct ByteBuffer {
691691
return try body(.init(rebasing: self._slicedStorageBuffer[range]))
692692
}
693693

694+
#if compiler(>=6.2)
695+
/// Provides safe high-performance read-only access to the readable bytes of this buffer.
696+
@inlinable
697+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *)
698+
public var readableBytesSpan: RawSpan {
699+
@_lifetime(borrow self)
700+
borrowing get {
701+
let range = Range<Int>(uncheckedBounds: (lower: self.readerIndex, upper: self.writerIndex))
702+
return _overrideLifetime(RawSpan(_unsafeBytes: self._slicedStorageBuffer[range]), borrowing: self)
703+
}
704+
}
705+
706+
/// Provides mutable access to the readable bytes of this buffer.
707+
@inlinable
708+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *)
709+
public var mutableReadableBytesSpan: MutableRawSpan {
710+
@_lifetime(&self)
711+
mutating get {
712+
self._copyStorageAndRebaseIfNeeded()
713+
let range = Range<Int>(uncheckedBounds: (lower: self.readerIndex, upper: self.writerIndex))
714+
return _overrideLifetime(MutableRawSpan(_unsafeBytes: self._slicedStorageBuffer[range]), mutating: &self)
715+
}
716+
}
717+
718+
/// Enables high-performance low-level appending into the writable section of this buffer.
719+
///
720+
/// The writer index will be advanced by the number of bytes written into the
721+
/// `OutputRawSpan`.
722+
///
723+
/// - parameters:
724+
/// - minimumWritableBytes: The minimum initial space to allocate for the buffer.
725+
/// - initializer: The initializer that will be invoked to initialize the allocated memory.
726+
@inlinable
727+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *)
728+
public mutating func writeWithOutputRawSpan<ErrorType: Error>(
729+
minimumWritableBytes: Int,
730+
initializingWith initializer: (_ span: inout OutputRawSpan) throws(ErrorType) -> Void
731+
) throws(ErrorType) {
732+
try self.writeWithUnsafeMutableBytes(minimumWritableBytes: minimumWritableBytes) { ptr throws(ErrorType) in
733+
var span = OutputRawSpan(buffer: ptr, initializedCount: 0)
734+
try initializer(&span)
735+
return span.byteCount
736+
}
737+
}
738+
#endif
739+
694740
/// Yields the bytes currently writable (`bytesWritable` = `capacity` - `writerIndex`). Before reading those bytes you must first
695741
/// write to them otherwise you will trigger undefined behaviour. The writer index will remain unchanged.
696742
///

0 commit comments

Comments
 (0)