diff --git a/CMakeLists.txt b/CMakeLists.txt index 31638c287..b9bf520ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,6 +94,7 @@ list(APPEND CMAKE_MODULE_PATH ${SwiftFoundation_SOURCE_DIR}/cmake/modules) # Availability Macros (only applies to FoundationEssentials and FoundationInternationalization) set(_SwiftFoundation_BaseAvailability "macOS 15, iOS 18, tvOS 18, watchOS 11") +set(_SwiftFoundation_InlineArrayAvailability "macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26") set(_SwiftFoundation_FutureAvailability "macOS 10000, iOS 10000, tvOS 10000, watchOS 10000") # All versions to define for each availability name @@ -106,11 +107,13 @@ list(APPEND _SwiftFoundation_versions # Each availability name to define list(APPEND _SwiftFoundation_availability_names - "FoundationPreview") + "FoundationPreview" + "FoundationInlineArray") # The aligned availability for each name (in the same order) list(APPEND _SwiftFoundation_availability_releases - ${_SwiftFoundation_BaseAvailability}) + ${_SwiftFoundation_BaseAvailability} + ${_SwiftFoundation_InlineArrayAvailability}) foreach(version ${_SwiftFoundation_versions}) foreach(name release IN ZIP_LISTS _SwiftFoundation_availability_names _SwiftFoundation_availability_releases) diff --git a/Package.swift b/Package.swift index a2b1ccf4b..fffeba972 100644 --- a/Package.swift +++ b/Package.swift @@ -8,6 +8,7 @@ import CompilerPluginSupport let availabilityTags: [_Availability] = [ _Availability("FoundationPreview"), // Default FoundationPreview availability + _Availability("FoundationInlineArray", availability: .macOS26) // Availability of InlineArray ] let versionNumbers = ["6.0.2", "6.1", "6.2", "6.3"] diff --git a/Sources/FoundationEssentials/Data/ContiguousBytes.swift b/Sources/FoundationEssentials/Data/ContiguousBytes.swift index c7569c6d7..47907414f 100644 --- a/Sources/FoundationEssentials/Data/ContiguousBytes.swift +++ b/Sources/FoundationEssentials/Data/ContiguousBytes.swift @@ -13,9 +13,10 @@ //===--- ContiguousBytes --------------------------------------------------===// /// Indicates that the conforming type is a contiguous collection of raw bytes -/// whose underlying storage is directly accessible by withUnsafeBytes. +/// whose underlying storage is directly accessible by withBytes. @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) -public protocol ContiguousBytes { +public protocol ContiguousBytes: ~Escapable, ~Copyable { +#if !hasFeature(Embedded) /// Calls the given closure with the contents of underlying storage. /// /// - note: Calling `withUnsafeBytes` multiple times does not guarantee that @@ -23,6 +24,46 @@ public protocol ContiguousBytes { /// - warning: The buffer argument to the body should not be stored or used /// outside of the lifetime of the call to the closure. func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R +#else + /// Calls the given closure with the contents of underlying storage. + /// + /// - note: Calling `withUnsafeBytes` multiple times does not guarantee that + /// the same buffer pointer will be passed in every time. + /// - warning: The buffer argument to the body should not be stored or used + /// outside of the lifetime of the call to the closure. + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R +#endif + + /// Calls the given closure with the contents of underlying storage. + /// + /// - note: Calling `withBytes` multiple times does not guarantee that + /// the same span will be passed in every time. + @available(FoundationPreview 6.3, *) + func withBytes(_ body: (RawSpan) throws(E) -> R) throws(E) -> R +} + +extension ContiguousBytes where Self: ~Escapable, Self: ~Copyable { + /// Calls the given closure with the contents of underlying storage. + /// + /// - note: Calling `withBytes` multiple times does not guarantee that + /// the same span will be passed in every time. + @_alwaysEmitIntoClient + public func withBytes(_ body: (RawSpan) throws(E) -> R) throws(E) -> R { +#if !hasFeature(Embedded) + do { + return try withUnsafeBytes { (buffer) in + try body(buffer.bytes) + } + } catch let error { + // Note: withUnsafeBytes is rethrowing, so we have an "any Error" here that needs casting. + throw error as! E + } +#else + return try withUnsafeBytes { (buffer) throws(E) in + try body(buffer.bytes) + } +#endif + } } //===--- Collection Conformances ------------------------------------------===// @@ -43,43 +84,105 @@ extension ContiguousArray : ContiguousBytes where Element == UInt8 { } @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) extension UnsafeRawBufferPointer : ContiguousBytes { - @inlinable - public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + #if !hasFeature(Embedded) + // Historical ABI + @usableFromInline + @abi(func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R) + func __abi__withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + return try body(self) + } + #endif + + @_alwaysEmitIntoClient + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { return try body(self) } + + @_alwaysEmitIntoClient + public func withBytes(_ body: (RawSpan) throws(E) -> R) throws(E) -> R { + return try body(bytes) + } } @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) extension UnsafeMutableRawBufferPointer : ContiguousBytes { - @inlinable - public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { +#if !hasFeature(Embedded) + // Historical ABI + @usableFromInline + @abi(func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R) + func __abi__withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + return try body(UnsafeRawBufferPointer(self)) + } +#endif + + @_alwaysEmitIntoClient + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { return try body(UnsafeRawBufferPointer(self)) } + + @_alwaysEmitIntoClient + public func withBytes(_ body: (RawSpan) throws(E) -> R) throws(E) -> R { + return try body(bytes) + } } // FIXME: When possible, expand conformance to `where Element : Trivial`. @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) extension UnsafeBufferPointer : ContiguousBytes where Element == UInt8 { - @inlinable - public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { +#if !hasFeature(Embedded) + @usableFromInline + @abi(func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R) + func __abi__withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + return try body(UnsafeRawBufferPointer(self)) + } +#endif + + @_alwaysEmitIntoClient + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { return try body(UnsafeRawBufferPointer(self)) } + + @_alwaysEmitIntoClient + public func withBytes(_ body: (RawSpan) throws(E) -> R) throws(E) -> R { + return try body(span.bytes) + } } // FIXME: When possible, expand conformance to `where Element : Trivial`. @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) extension UnsafeMutableBufferPointer : ContiguousBytes where Element == UInt8 { - @inlinable - public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { +#if !hasFeature(Embedded) + @usableFromInline + @abi(func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R) + func __abi__withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + return try body(UnsafeRawBufferPointer(self)) + } +#endif + + @_alwaysEmitIntoClient + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { return try body(UnsafeRawBufferPointer(self)) } + + @_alwaysEmitIntoClient + public func withBytes(_ body: (RawSpan) throws(E) -> R) throws(E) -> R { + return try body(span.bytes) + } } // FIXME: When possible, expand conformance to `where Element : Trivial`. @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) extension EmptyCollection : ContiguousBytes where Element == UInt8 { - @inlinable - public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { +#if !hasFeature(Embedded) + @usableFromInline + @abi(func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R) + func __abi__withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + return try body(UnsafeRawBufferPointer(start: nil, count: 0)) + } +#endif + + @_alwaysEmitIntoClient + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { return try body(UnsafeRawBufferPointer(start: nil, count: 0)) } } @@ -87,20 +190,34 @@ extension EmptyCollection : ContiguousBytes where Element == UInt8 { // FIXME: When possible, expand conformance to `where Element : Trivial`. @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) extension CollectionOfOne : ContiguousBytes where Element == UInt8 { - @inlinable - public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { +#if !hasFeature(Embedded) + @usableFromInline + @abi(func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R) + func __abi__withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { let element = self.first! return try Swift.withUnsafeBytes(of: element) { return try body($0) } } +#endif + + @_alwaysEmitIntoClient + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { + let element = self.first! + return try Swift.withUnsafeBytes(of: element) { (buffer) throws(E) in + return try body(buffer) + } + } } //===--- Conditional Conformances -----------------------------------------===// @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) extension Slice : ContiguousBytes where Base : ContiguousBytes { - public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> ResultType) rethrows -> ResultType { +#if !hasFeature(Embedded) + @usableFromInline + @abi(func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R) + func __abi__withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> ResultType) rethrows -> ResultType { let offset = base.distance(from: base.startIndex, to: self.startIndex) return try base.withUnsafeBytes { ptr in let slicePtr = ptr.baseAddress?.advanced(by: offset) @@ -108,4 +225,133 @@ extension Slice : ContiguousBytes where Base : ContiguousBytes { return try body(sliceBuffer) } } +#endif + + @_alwaysEmitIntoClient + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws(ErrorType) -> ResultType) throws(ErrorType) -> ResultType { + let offset = base.distance(from: base.startIndex, to: self.startIndex) + +#if !hasFeature(Embedded) + do { + return try base.withUnsafeBytes { (ptr) in + let slicePtr = ptr.baseAddress?.advanced(by: offset) + let sliceBuffer = UnsafeRawBufferPointer(start: slicePtr, count: self.count) + return try body(sliceBuffer) + } + } catch let error { + // Note: withUnsafeBytes is rethrowing, so we have an "any Error" here that needs casting. + throw error as! ErrorType + } +#else + return try base.withUnsafeBytes { (ptr) throws(ErrorType) in + let slicePtr = ptr.baseAddress?.advanced(by: offset) + let sliceBuffer = UnsafeRawBufferPointer(start: slicePtr, count: self.count) + return try body(sliceBuffer) + } +#endif + } +} + +//===--- Span Conformances -----------------------------------------===// + +@available(FoundationPreview 6.3, *) +extension RawSpan: ContiguousBytes { } + +extension RawSpan { + @_alwaysEmitIntoClient + public func withBytes(_ body: (RawSpan) throws(E) -> R) throws(E) -> R { + return try body(self) + } +} + +@available(FoundationPreview 6.3, *) +extension MutableRawSpan: ContiguousBytes { } + +extension MutableRawSpan { + @_alwaysEmitIntoClient + public func withBytes(_ body: (RawSpan) throws(E) -> R) throws(E) -> R { + return try body(bytes) + } +} + +@available(FoundationPreview 6.3, *) +extension OutputRawSpan: ContiguousBytes { } + +extension OutputRawSpan { + @_alwaysEmitIntoClient + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { + try bytes.withUnsafeBytes(body) + } + + @_alwaysEmitIntoClient + public func withBytes(_ body: (RawSpan) throws(E) -> R) throws(E) -> R { + try body(bytes) + } +} + +@available(FoundationInlineArray 6.3, *) +extension UTF8Span: ContiguousBytes { } + +@available(FoundationInlineArray 6.2, *) +extension UTF8Span { + @_alwaysEmitIntoClient + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { + try span.withUnsafeBytes(body) + } + + @_alwaysEmitIntoClient + public func withBytes(_ body: (RawSpan) throws(E) -> R) throws(E) -> R { + return try body(span.bytes) + } +} + +@available(FoundationPreview 6.3, *) +extension Span: ContiguousBytes where Element == UInt8 { } + +extension Span where Element == UInt8 { + @_alwaysEmitIntoClient + public func withBytes(_ body: (RawSpan) throws(E) -> R) throws(E) -> R { + try body(bytes) + } +} + +@available(FoundationPreview 6.3, *) +extension MutableSpan: ContiguousBytes where Element == UInt8 { } + +extension MutableSpan where Element == UInt8 { + @_alwaysEmitIntoClient + public func withBytes(_ body: (RawSpan) throws(E) -> R) throws(E) -> R { + try body(bytes) + } +} + +@available(FoundationPreview 6.3, *) +extension OutputSpan: ContiguousBytes where Element == UInt8 { } + +extension OutputSpan where Element == UInt8 { + @_alwaysEmitIntoClient + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { + try span.withUnsafeBytes(body) + } + + @_alwaysEmitIntoClient + public func withBytes(_ body: (RawSpan) throws(E) -> R) throws(E) -> R { + try body(span.bytes) + } +} + +@available(FoundationInlineArray 6.3, *) +extension InlineArray: ContiguousBytes where Element == UInt8 { } + +@available(FoundationInlineArray 6.2, *) +extension InlineArray where Element == UInt8 { + @_alwaysEmitIntoClient + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { + return try span.withUnsafeBytes(body) + } + + @_alwaysEmitIntoClient + public func withBytes(_ body: (RawSpan) throws(E) -> R) throws(E) -> R { + try body(span.bytes) + } } diff --git a/Tests/FoundationEssentialsTests/ContiguousBytesTests.swift b/Tests/FoundationEssentialsTests/ContiguousBytesTests.swift new file mode 100644 index 000000000..5e0ed5b1f --- /dev/null +++ b/Tests/FoundationEssentialsTests/ContiguousBytesTests.swift @@ -0,0 +1,66 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Testing + +#if canImport(FoundationEssentials) +@testable import FoundationEssentials +#else +@testable import Foundation +#endif + +enum HomeworkError: Error { + case dogAteIt +} + +@available(FoundationPreview 6.3, *) +@discardableResult +func acceptContiguousBytes(_ bytes: borrowing T) -> Int { + do { + // Ensure that we can use withBytes with typed throws. + return try bytes.withBytes { (buffer) throws(HomeworkError) in + if buffer.isEmpty { + throw .dogAteIt + } + + return buffer.byteCount + } + } catch let error { + precondition(error == .dogAteIt) + return -1 + } +} + +struct NC: ~Copyable { } + +@Suite("ContiguousBytesTests") +private struct ContiguousBytesTests { + @Test func span() throws { + if #available(FoundationPreview 6.3, *) { + var bytes: [UInt8] = [1, 2, 3] + bytes.withUnsafeMutableBufferPointer { unsafeBytes in + acceptContiguousBytes(unsafeBytes.span) + acceptContiguousBytes(unsafeBytes.mutableSpan) + acceptContiguousBytes(unsafeBytes.span.bytes) + + var ms = unsafeBytes.mutableSpan + acceptContiguousBytes(ms.bytes) + acceptContiguousBytes(ms.mutableBytes) + + // Noncopyable result type + _ = unsafeBytes.span.withBytes { (buffer) in + return NC() + } + } + } + } +}