diff --git a/stdlib/public/core/SmallString.swift b/stdlib/public/core/SmallString.swift index 8594b29a31a0c..84b749cabe792 100644 --- a/stdlib/public/core/SmallString.swift +++ b/stdlib/public/core/SmallString.swift @@ -365,6 +365,27 @@ extension _SmallString { } self._invariantCheck() } + + @_effects(readonly) // @opaque + internal init?(taggedASCIICocoa cocoa: AnyObject) { + self.init() + var success = true + self.withMutableCapacity { + /* + For regular NSTaggedPointerStrings we will always succeed here, but + tagged NSLocalizedStrings may not fit in a SmallString + */ + if let len = _bridgeTaggedASCII(cocoa, intoUTF8: $0) { + return len + } + success = false + return 0 + } + if !success { + return nil + } + self._invariantCheck() + } } #endif diff --git a/stdlib/public/core/StringBridge.swift b/stdlib/public/core/StringBridge.swift index e8d24b5115f24..2aae9ed797a85 100644 --- a/stdlib/public/core/StringBridge.swift +++ b/stdlib/public/core/StringBridge.swift @@ -167,8 +167,9 @@ internal func _cocoaStringSubscript( } @_effects(releasenone) -private func _NSStringCopyUTF8( +private func _NSStringCopyBytes( _ o: _StringSelectorHolder, + encoding: UInt, into bufPtr: UnsafeMutableRawBufferPointer ) -> Int? { let ptr = bufPtr.baseAddress._unsafelyUnwrappedUnchecked @@ -179,7 +180,7 @@ private func _NSStringCopyUTF8( ptr, maxLength: bufPtr.count, usedLength: &usedLen, - encoding: _cocoaUTF8Encoding, + encoding: encoding, options: 0, range: _SwiftNSRange(location: 0, length: len), remaining: &remainingRange @@ -195,7 +196,23 @@ internal func _cocoaStringCopyUTF8( _ target: _CocoaString, into bufPtr: UnsafeMutableRawBufferPointer ) -> Int? { - return _NSStringCopyUTF8(_objc(target), into: bufPtr) + return _NSStringCopyBytes( + _objc(target), + encoding: _cocoaUTF8Encoding, + into: bufPtr + ) +} + +@_effects(releasenone) +internal func _cocoaStringCopyASCII( + _ target: _CocoaString, + into bufPtr: UnsafeMutableRawBufferPointer +) -> Int? { + return _NSStringCopyBytes( + _objc(target), + encoding: _cocoaASCIIEncoding, + into: bufPtr + ) } @_effects(readonly) @@ -346,17 +363,30 @@ internal func _bridgeTagged( _internalInvariant(_isObjCTaggedPointer(cocoa)) return _cocoaStringCopyUTF8(cocoa, into: bufPtr) } + +@_effects(releasenone) // @opaque +internal func _bridgeTaggedASCII( + _ cocoa: _CocoaString, + intoUTF8 bufPtr: UnsafeMutableRawBufferPointer +) -> Int? { + _internalInvariant(_isObjCTaggedPointer(cocoa)) + return _cocoaStringCopyASCII(cocoa, into: bufPtr) +} #endif @_effects(readonly) private func _NSStringASCIIPointer(_ str: _StringSelectorHolder) -> UnsafePointer? { - // TODO(String bridging): Is there a better interface here? Ideally we'd be - // able to ask for UTF8 rather than just ASCII //TODO(String bridging): Unconditionally asking for nul-terminated contents is // overly conservative and hurts perf with some NSStrings return str._fastCStringContents(1)?._asUInt8 } +@_effects(readonly) +private func _NSStringUTF8Pointer(_ str: _StringSelectorHolder) -> UnsafePointer? { + //We don't have a way to ask for UTF8 here currently + return _NSStringASCIIPointer(str) +} + @_effects(readonly) // @opaque private func _withCocoaASCIIPointer( _ str: _CocoaString, @@ -371,7 +401,7 @@ private func _withCocoaASCIIPointer( if requireStableAddress { return nil // tagged pointer strings don't support _fastCStringContents } - if let smol = _SmallString(taggedCocoa: str) { + if let smol = _SmallString(taggedASCIICocoa: str) { return _StringGuts(smol).withFastUTF8 { work($0.baseAddress._unsafelyUnwrappedUnchecked) } @@ -385,6 +415,34 @@ private func _withCocoaASCIIPointer( return nil } +@_effects(readonly) // @opaque +private func _withCocoaUTF8Pointer( + _ str: _CocoaString, + requireStableAddress: Bool, + work: (UnsafePointer) -> R? +) -> R? { + #if !(arch(i386) || arch(arm) || arch(arm64_32)) + if _isObjCTaggedPointer(str) { + if let ptr = getConstantTaggedCocoaContents(str)?.asciiContentsPointer { + return work(ptr) + } + if requireStableAddress { + return nil // tagged pointer strings don't support _fastCStringContents + } + if let smol = _SmallString(taggedCocoa: str) { + return _StringGuts(smol).withFastUTF8 { + work($0.baseAddress._unsafelyUnwrappedUnchecked) + } + } + } + #endif + defer { _fixLifetime(str) } + if let ptr = _NSStringUTF8Pointer(_objc(str)) { + return work(ptr) + } + return nil +} + @_effects(readonly) // @opaque internal func withCocoaASCIIPointer( _ str: _CocoaString, @@ -393,12 +451,26 @@ internal func withCocoaASCIIPointer( return _withCocoaASCIIPointer(str, requireStableAddress: false, work: work) } +@_effects(readonly) // @opaque +internal func withCocoaUTF8Pointer( + _ str: _CocoaString, + work: (UnsafePointer) -> R? +) -> R? { + return _withCocoaUTF8Pointer(str, requireStableAddress: false, work: work) +} + @_effects(readonly) internal func stableCocoaASCIIPointer(_ str: _CocoaString) -> UnsafePointer? { return _withCocoaASCIIPointer(str, requireStableAddress: true, work: { $0 }) } +@_effects(readonly) +internal func stableCocoaUTF8Pointer(_ str: _CocoaString) + -> UnsafePointer? { + return _withCocoaUTF8Pointer(str, requireStableAddress: true, work: { $0 }) +} + private enum CocoaStringPointer { case ascii(UnsafePointer) case utf8(UnsafePointer) diff --git a/stdlib/public/core/StringObject.swift b/stdlib/public/core/StringObject.swift index 0030a71f95610..10d158c02344d 100644 --- a/stdlib/public/core/StringObject.swift +++ b/stdlib/public/core/StringObject.swift @@ -915,7 +915,7 @@ extension _StringObject { _internalInvariant(largeFastIsShared) #if _runtime(_ObjC) if largeIsCocoa { - return stableCocoaASCIIPointer(cocoaObject)._unsafelyUnwrappedUnchecked + return stableCocoaUTF8Pointer(cocoaObject)._unsafelyUnwrappedUnchecked } #endif diff --git a/stdlib/public/core/StringStorageBridge.swift b/stdlib/public/core/StringStorageBridge.swift index 6301199cf9f0f..9771c24a7ddc1 100644 --- a/stdlib/public/core/StringStorageBridge.swift +++ b/stdlib/public/core/StringStorageBridge.swift @@ -14,8 +14,8 @@ import SwiftShims #if _runtime(_ObjC) -internal let _cocoaASCIIEncoding:UInt = 1 /* NSASCIIStringEncoding */ -internal let _cocoaUTF8Encoding:UInt = 4 /* NSUTF8StringEncoding */ +internal var _cocoaASCIIEncoding:UInt { 1 } /* NSASCIIStringEncoding */ +internal var _cocoaUTF8Encoding:UInt { 4 } /* NSUTF8StringEncoding */ extension String { @available(SwiftStdlib 5.6, *) diff --git a/test/stdlib/NSSlowTaggedLocalizedString.swift b/test/stdlib/NSSlowTaggedLocalizedString.swift index 31634c7526b8e..bed726f32c6b3 100644 --- a/test/stdlib/NSSlowTaggedLocalizedString.swift +++ b/test/stdlib/NSSlowTaggedLocalizedString.swift @@ -14,6 +14,8 @@ import StdlibUnittest let longTaggedTests = TestSuite("NonContiguousTaggedStrings") var constant = "Send Message to different Team" +//doesn't fit in a tagged pointer because of ', but does fit in a SmallString +var shortNonTagged = "Don\u{2019}t Save" func runEqualLongTagged() { @@ -39,5 +41,27 @@ longTaggedTests.test("EqualLongTagged") { runEqualLongTagged() } +longTaggedTests.test("EqualNonASCIISubsetSmall") { + if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { + if MemoryLayout.size != 8 { + return //no tagged pointers + } + + var native = shortNonTagged.withUTF8 { String(decoding: $0, as: UTF8.self) } + native.reserveCapacity(30) //force into non-small form so we can reverse bridge below + let longTagged = NSSlowTaggedLocalizedString.createTest()! + shortNonTagged.withCString { + NSSlowTaggedLocalizedString.setContents($0) + } + defer { + NSSlowTaggedLocalizedString.setContents(nil) + } + let reverseBridged = unsafeBitCast(native._guts._object.largeAddressBits, to: AnyObject.self) + let eq = reverseBridged.isEqual(to: longTagged) + expectEqual(eq, 1) + _fixLifetime(native) + } +} + runAllTests()