Skip to content

Commit aedb869

Browse files
authored
Merge pull request #80116 from glessard/rdar137710901-utf8view-span-notsmol
[SE-0456] Span properties over utf8 views
2 parents daf8d97 + 7539366 commit aedb869

File tree

7 files changed

+384
-3
lines changed

7 files changed

+384
-3
lines changed

stdlib/public/core/StringGuts.swift

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -417,6 +417,93 @@ extension _StringGuts {
417417
}
418418
}
419419

420+
#if _runtime(_ObjC)
421+
extension _StringGuts {
422+
423+
private static var _associationKey: UnsafeRawPointer {
424+
struct AssociationKey {}
425+
// We never dereference this, we only use this address as a unique key
426+
return unsafe unsafeBitCast(
427+
ObjectIdentifier(AssociationKey.self),
428+
to: UnsafeRawPointer.self
429+
)
430+
}
431+
432+
private func _getAssociatedStorage() -> __StringStorage? {
433+
_internalInvariant(_object.hasObjCBridgeableObject)
434+
let getter = unsafe unsafeBitCast(
435+
getGetAssociatedObjectPtr(),
436+
to: (@convention(c)(
437+
AnyObject,
438+
UnsafeRawPointer
439+
) -> UnsafeRawPointer?).self
440+
)
441+
442+
if let assocPtr = unsafe getter(
443+
_object.objCBridgeableObject,
444+
Self._associationKey
445+
) {
446+
let storage: __StringStorage
447+
storage = unsafe Unmanaged.fromOpaque(assocPtr).takeUnretainedValue()
448+
return storage
449+
}
450+
return nil
451+
}
452+
453+
private func _setAssociatedStorage(_ storage: __StringStorage) {
454+
_internalInvariant(_object.hasObjCBridgeableObject)
455+
let setter = unsafe unsafeBitCast(
456+
getSetAssociatedObjectPtr(),
457+
to: (@convention(c)(
458+
AnyObject,
459+
UnsafeRawPointer,
460+
AnyObject?,
461+
UInt
462+
) -> Void).self
463+
)
464+
465+
unsafe setter(
466+
_object.objCBridgeableObject,
467+
Self._associationKey,
468+
storage,
469+
1 //OBJC_ASSOCIATION_RETAIN_NONATOMIC
470+
)
471+
}
472+
473+
internal func _getOrAllocateAssociatedStorage() -> __StringStorage {
474+
_internalInvariant(_object.hasObjCBridgeableObject)
475+
let unwrapped: __StringStorage
476+
// libobjc already provides the necessary memory barriers for
477+
// double checked locking to be safe, per comments on
478+
// https://github.com/swiftlang/swift/pull/75148
479+
if let storage = _getAssociatedStorage() {
480+
unwrapped = storage
481+
} else {
482+
let lock = _object.objCBridgeableObject
483+
objc_sync_enter(lock)
484+
if let storage = _getAssociatedStorage() {
485+
unwrapped = storage
486+
} else {
487+
var contents = String.UnicodeScalarView()
488+
// always reserve a capacity larger than a small string
489+
contents.reserveCapacity(
490+
Swift.max(_SmallString.capacity + 1, count + count >> 1)
491+
)
492+
for c in String.UnicodeScalarView(self) {
493+
contents.append(c)
494+
}
495+
_precondition(contents._guts._object.hasNativeStorage)
496+
unwrapped = (consume contents)._guts._object.nativeStorage
497+
_setAssociatedStorage(unwrapped)
498+
}
499+
defer { _fixLifetime(unwrapped) }
500+
objc_sync_exit(lock)
501+
}
502+
return unwrapped
503+
}
504+
}
505+
#endif
506+
420507
// Old SPI(corelibs-foundation)
421508
extension _StringGuts {
422509
public // SPI(corelibs-foundation)

stdlib/public/core/StringUTF8View.swift

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -317,6 +317,49 @@ extension String.UTF8View {
317317
}
318318
}
319319

320+
extension String.UTF8View {
321+
322+
/// A span over the UTF8 code units that make up this string.
323+
///
324+
/// - Note: In the case of bridged UTF16 String instances (on Apple
325+
/// platforms,) this property transcodes the code units the first time
326+
/// it is called. The transcoded buffer is cached, and subsequent calls
327+
/// to `span` can reuse the buffer.
328+
///
329+
/// Returns: a `Span` over the UTF8 code units of this String.
330+
///
331+
/// Complexity: O(1) for native UTF8 Strings,
332+
/// amortized O(1) for bridged UTF16 Strings.
333+
@available(SwiftStdlib 6.2, *)
334+
public var span: Span<UTF8.CodeUnit> {
335+
@lifetime(borrow self)
336+
borrowing get {
337+
#if _runtime(_ObjC)
338+
// handle non-UTF8 Objective-C bridging cases here
339+
if !_guts.isFastUTF8 && _guts._object.hasObjCBridgeableObject {
340+
let storage = _guts._getOrAllocateAssociatedStorage()
341+
let (start, count) = unsafe (storage.start, storage.count)
342+
let span = unsafe Span(_unsafeStart: start, count: count)
343+
return unsafe _overrideLifetime(span, borrowing: self)
344+
}
345+
#endif
346+
let count = _guts.count
347+
if _guts.isSmall {
348+
let a = Builtin.addressOfBorrow(self)
349+
let address = unsafe UnsafePointer<UTF8.CodeUnit>(a)
350+
let span = unsafe Span(_unsafeStart: address, count: count)
351+
fatalError("Span over the small string form is not supported yet.")
352+
return unsafe _overrideLifetime(span, borrowing: self)
353+
}
354+
_precondition(_guts.isFastUTF8)
355+
let buffer = unsafe _guts._object.fastUTF8
356+
_internalInvariant(count == buffer.count)
357+
let span = unsafe Span(_unsafeElements: buffer)
358+
return unsafe _overrideLifetime(span, borrowing: self)
359+
}
360+
}
361+
}
362+
320363
// Index conversions
321364
extension String.UTF8View.Index {
322365
/// Creates an index in the given UTF-8 view that corresponds exactly to the

stdlib/public/core/Substring.swift

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -749,6 +749,61 @@ extension Substring.UTF8View: BidirectionalCollection {
749749
}
750750
}
751751

752+
extension Substring.UTF8View {
753+
754+
/// A span over the UTF8 code units that make up this substring.
755+
///
756+
/// - Note: In the case of bridged UTF16 String instances (on Apple
757+
/// platforms,) this property needs to transcode the code units every time
758+
/// it is called.
759+
/// For example, if `string` has the bridged UTF16 representation,
760+
/// for word in string.split(separator: " ") {
761+
/// useSpan(word.span)
762+
/// }
763+
/// is accidentally quadratic because of this issue. A workaround is to
764+
/// explicitly convert the string into its native UTF8 representation:
765+
/// var nativeString = consume string
766+
/// nativeString.makeContiguousUTF8()
767+
/// for word in nativeString.split(separator: " ") {
768+
/// useSpan(word.span)
769+
/// }
770+
/// This second option has linear time complexity, as expected.
771+
///
772+
/// Returns: a `Span` over the UTF8 code units of this Substring.
773+
///
774+
/// Complexity: O(1) for native UTF8 Strings, O(n) for bridged UTF16 Strings.
775+
@available(SwiftStdlib 6.2, *)
776+
public var span: Span<UTF8.CodeUnit> {
777+
@lifetime(borrow self)
778+
borrowing get {
779+
#if _runtime(_ObjC)
780+
// handle non-UTF8 Objective-C bridging cases here
781+
if !_wholeGuts.isFastUTF8 && _wholeGuts._object.hasObjCBridgeableObject {
782+
let base: String.UTF8View = self._base
783+
let first = base._foreignDistance(from: base.startIndex, to: startIndex)
784+
let count = base._foreignDistance(from: startIndex, to: endIndex)
785+
let span = unsafe base.span._extracting(first..<(first &+ count))
786+
return unsafe _overrideLifetime(span, borrowing: self)
787+
}
788+
#endif
789+
let first = _slice._startIndex._encodedOffset
790+
let end = _slice._endIndex._encodedOffset
791+
if _wholeGuts.isSmall {
792+
let a = Builtin.addressOfBorrow(self)
793+
let offset = first &+ (2 &* MemoryLayout<String.Index>.stride)
794+
let start = unsafe UnsafePointer<UTF8.CodeUnit>(a).advanced(by: offset)
795+
let span = unsafe Span(_unsafeStart: start, count: end &- first)
796+
fatalError("Span over the small string form is not supported yet.")
797+
return unsafe _overrideLifetime(span, borrowing: self)
798+
}
799+
_internalInvariant(_wholeGuts.isFastUTF8)
800+
var span = unsafe Span(_unsafeElements: _wholeGuts._object.fastUTF8)
801+
span = span._extracting(first..<end)
802+
return unsafe _overrideLifetime(span, borrowing: self)
803+
}
804+
}
805+
}
806+
752807
extension Substring {
753808
@inlinable
754809
public var utf8: UTF8View {

test/abi/macOS/arm64/stdlib.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,10 @@ Added: _$ss13KeyValuePairsV4spans4SpanVyx3key_q_5valuetGvpMV
837837
Added: _$ss15CollectionOfOneV4spans4SpanVyxGvpMV
838838
Added: _$ss15ContiguousArrayV4spans4SpanVyxGvpMV
839839
Added: _$ss4SpanVss15BitwiseCopyableRzlE5bytess03RawA0VvpMV
840+
Added: _$sSS8UTF8ViewV4spans4SpanVys5UInt8VGvg
841+
Added: _$sSS8UTF8ViewV4spans4SpanVys5UInt8VGvpMV
842+
Added: _$sSs8UTF8ViewV4spans4SpanVys5UInt8VGvg
843+
Added: _$sSs8UTF8ViewV4spans4SpanVys5UInt8VGvpMV
840844

841845
// SE-0467 mutableSpan properties
842846
Added: _$sSa11mutableSpans07MutableB0VyxGvr

test/abi/macOS/x86_64/stdlib.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,10 @@ Added: _$ss13KeyValuePairsV4spans4SpanVyx3key_q_5valuetGvpMV
838838
Added: _$ss15CollectionOfOneV4spans4SpanVyxGvpMV
839839
Added: _$ss15ContiguousArrayV4spans4SpanVyxGvpMV
840840
Added: _$ss4SpanVss15BitwiseCopyableRzlE5bytess03RawA0VvpMV
841+
Added: _$sSS8UTF8ViewV4spans4SpanVys5UInt8VGvg
842+
Added: _$sSS8UTF8ViewV4spans4SpanVys5UInt8VGvpMV
843+
Added: _$sSs8UTF8ViewV4spans4SpanVys5UInt8VGvg
844+
Added: _$sSs8UTF8ViewV4spans4SpanVys5UInt8VGvpMV
841845

842846
// SE-0467 mutableSpan properties
843847
Added: _$sSa11mutableSpans07MutableB0VyxGvr
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//===--- BridgedStringSpanTests.swift -------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
// RUN: %target-run-stdlib-swift -enable-experimental-feature LifetimeDependence
14+
15+
// REQUIRES: executable_test
16+
// REQUIRES: objc_interop
17+
// REQUIRES: swift_feature_LifetimeDependence
18+
19+
import StdlibUnittest
20+
21+
import Foundation
22+
23+
var suite = TestSuite("EagerLazyBridging String Tests")
24+
defer { runAllTests() }
25+
26+
let strings = [
27+
"Hello, World!",
28+
"A long ASCII string exceeding 16 code units.",
29+
"🇯🇵",
30+
"🏂☃❅❆❄︎⛄️❄️",
31+
// Enable the following once the small native string form is supported
32+
// "z",
33+
// "",
34+
]
35+
36+
strings.forEach { expected in
37+
suite.test("Span from Bridged String Stability: \(expected)")
38+
.require(.stdlib_6_2).code {
39+
guard #available(SwiftStdlib 6.2, *) else { return }
40+
41+
let string = NSString(utf8String: expected)
42+
guard let string else { expectNotNil(string); return }
43+
44+
let bridged = String(string).utf8
45+
var p: ObjectIdentifier? = nil
46+
for (i, j) in zip(0..<3, bridged.indices) {
47+
let span = bridged.span
48+
let c = span.withUnsafeBufferPointer {
49+
let o = unsafeBitCast($0.baseAddress, to: ObjectIdentifier.self)
50+
if p == nil {
51+
p = o
52+
} else {
53+
expectEqual(p, o)
54+
}
55+
return $0[i]
56+
}
57+
expectEqual(c, bridged[j])
58+
}
59+
}
60+
}
61+
62+
strings.forEach { expected in
63+
suite.test("Span from Bridged String: \(expected)")
64+
.require(.stdlib_6_2).code {
65+
guard #available(SwiftStdlib 6.2, *) else { return }
66+
67+
let string = NSString(utf8String: expected)
68+
guard let string else { expectNotNil(string); return }
69+
70+
let bridged = String(string)
71+
let utf8 = bridged.utf8
72+
let span = utf8.span
73+
expectEqual(span.count, expected.utf8.count)
74+
for (i,j) in zip(span.indices, expected.utf8.indices) {
75+
expectEqual(span[i], expected.utf8[j])
76+
}
77+
}
78+
}
79+
80+
strings.forEach { expected in
81+
suite.test("Span from Bridged String Substring: \(expected)")
82+
.require(.stdlib_6_2).code {
83+
guard #available(SwiftStdlib 6.2, *) else { return }
84+
85+
let string = NSString(utf8String: expected)
86+
guard let string else { expectNotNil(string); return }
87+
88+
let bridged = String(string).dropFirst()
89+
let utf8 = bridged.utf8
90+
let span = utf8.span
91+
let expected = expected.dropFirst()
92+
expectEqual(span.count, expected.utf8.count)
93+
for (i,j) in zip(span.indices, expected.utf8.indices) {
94+
expectEqual(span[i], expected.utf8[j])
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)