Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/react-native/ReactCommon/jsc/JSCRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ class JSCRuntime : public jsi::Runtime {

jsi::String createStringFromAscii(const char* str, size_t length) override;
jsi::String createStringFromUtf8(const uint8_t* utf8, size_t length) override;
jsi::String createStringFromUtf16(const char16_t* utf16, size_t length)
override;
std::string utf8(const jsi::String& /*str*/) override;
size_t length(const jsi::String& /*str*/) override;

Expand Down Expand Up @@ -742,6 +744,16 @@ jsi::String JSCRuntime::createStringFromUtf8(
return result;
}

jsi::String JSCRuntime::createStringFromUtf16(
const char16_t* utf16,
size_t length) {
JSStringRef stringRef = JSStringCreateWithCharacters(
reinterpret_cast<const JSChar*>(utf16), length);
auto result = createString(stringRef);
JSStringRelease(stringRef);
return result;
}

std::string JSCRuntime::utf8(const jsi::String& str) {
return JSStringToSTLString(stringRef(str));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#import <mutex>

#import <sstream>
#import <string>
#import <vector>

using namespace facebook;
Expand Down Expand Up @@ -60,7 +61,20 @@ static int32_t getUniqueId()

static jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value)
{
return jsi::String::createFromUtf8(runtime, ([value UTF8String] != nullptr) ? [value UTF8String] : "");
// Hand the JS runtime the string's own ASCII or UTF-16 characters when
// CoreFoundation can expose them, and copy them once otherwise.
CFStringRef string = (__bridge CFStringRef)value;
CFIndex length = CFStringGetLength(string);
if (const char *ascii = CFStringGetCStringPtr(string, kCFStringEncodingASCII)) {
return jsi::String::createFromAscii(runtime, ascii, static_cast<size_t>(length));
}
if (const UniChar *characters = CFStringGetCharactersPtr(string)) {
return jsi::String::createFromUtf16(
runtime, reinterpret_cast<const char16_t *>(characters), static_cast<size_t>(length));
}
std::u16string utf16(length, u'\0');
CFStringGetCharacters(string, CFRangeMake(0, length), reinterpret_cast<UniChar *>(utf16.data()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CFStringGetCharactersPtr would still be preferred here, although also claim it may return null...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. It now tries CFStringGetCharactersPtr first and only copies when that returns null. Each branch has its own test.

return jsi::String::createFromUtf16(runtime, utf16);
}

static jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime &runtime, NSDictionary *value)
Expand Down
4 changes: 4 additions & 0 deletions packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
E7DB218C22B41FCD005AC45F /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E7DB218B22B41FCD005AC45F /* XCTest.framework */; };
F0D621C32BBB9E38005960AC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F0D621C22BBB9E38005960AC /* PrivacyInfo.xcprivacy */; };
F1A0B1C23D4E5F6071829302 /* RCTTurboModuleArrayBufferTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F1A0B1C23D4E5F6071829301 /* RCTTurboModuleArrayBufferTests.mm */; };
F1A0B1C23D4E5F6071829322 /* RCTTurboModuleStringConversionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F1A0B1C23D4E5F6071829321 /* RCTTurboModuleStringConversionTests.mm */; };
D1A60AD11B9247A080734F02 /* RCTDevLoadingViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D1A60AD11B9247A080734F01 /* RCTDevLoadingViewTests.m */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -164,6 +165,7 @@
E7DB218B22B41FCD005AC45F /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = XCTest.framework; sourceTree = DEVELOPER_DIR; };
F0D621C22BBB9E38005960AC /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
F1A0B1C23D4E5F6071829301 /* RCTTurboModuleArrayBufferTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTTurboModuleArrayBufferTests.mm; sourceTree = "<group>"; };
F1A0B1C23D4E5F6071829321 /* RCTTurboModuleStringConversionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTTurboModuleStringConversionTests.mm; sourceTree = "<group>"; };
D1A60AD11B9247A080734F01 /* RCTDevLoadingViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTDevLoadingViewTests.m; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -345,6 +347,7 @@
E7DB20B022B2BAA4005AC45F /* RCTModuleInitTests.m */,
E7DB20CB22B2BAA5005AC45F /* RCTModuleMethodTests.mm */,
F1A0B1C23D4E5F6071829301 /* RCTTurboModuleArrayBufferTests.mm */,
F1A0B1C23D4E5F6071829321 /* RCTTurboModuleStringConversionTests.mm */,
E7DB20CF22B2BAA5005AC45F /* RCTMultipartStreamReaderTests.m */,
A975CA6B2C05EADE0043F72A /* RCTNetworkTaskTests.m */,
E7DB20BE22B2BAA4005AC45F /* RCTNativeAnimatedNodesManagerTests.m */,
Expand Down Expand Up @@ -762,6 +765,7 @@
E7DB20E022B2BAA6005AC45F /* RCTMethodArgumentTests.m in Sources */,
E7DB20E822B2BAA6005AC45F /* RCTModuleMethodTests.mm in Sources */,
F1A0B1C23D4E5F6071829302 /* RCTTurboModuleArrayBufferTests.mm in Sources */,
F1A0B1C23D4E5F6071829322 /* RCTTurboModuleStringConversionTests.mm in Sources */,
E7DB20E222B2BAA6005AC45F /* RCTGzipTests.m in Sources */,
E7DB20ED22B2BAA6005AC45F /* RCTURLUtilsTests.m in Sources */,
E7DB20D322B2BAA6005AC45F /* RCTBlobManagerTests.m in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import <XCTest/XCTest.h>

#import <ReactCommon/RCTTurboModule.h>
#import <hermes/hermes.h>

#import <memory>
#import <string>
#import <vector>

using namespace facebook;
using facebook::react::TurboModuleConvertUtils::convertObjCObjectToJSIValue;

// An NSString subclass that only implements the primitive methods, so
// CoreFoundation can't hand out a pointer to its characters.
@interface RCTTurboModuleStringConversionTestsString : NSString
- (instancetype)initWithCharacters:(std::vector<unichar>)characters;
@end

@implementation RCTTurboModuleStringConversionTestsString {
std::vector<unichar> _characters;
}

- (instancetype)initWithCharacters:(std::vector<unichar>)characters
{
if (self = [super init]) {
_characters = std::move(characters);
}
return self;
}

- (NSUInteger)length
{
return _characters.size();
}

- (unichar)characterAtIndex:(NSUInteger)index
{
return _characters[index];
}

@end

namespace {

std::u16string utf16FromNSString(NSString *string)
{
std::u16string utf16(string.length, u'\0');
[string getCharacters:reinterpret_cast<unichar *>(utf16.data()) range:NSMakeRange(0, string.length)];
return utf16;
}

// Strings read from data, like the text that `FileReader` returns.
NSString *stringFromUTF8Data(NSString *string)
{
return [[NSString alloc] initWithData:[string dataUsingEncoding:NSUTF8StringEncoding] encoding:NSUTF8StringEncoding];
}

NSString *stringWithCharacters(std::vector<unichar> characters)
{
return [NSString stringWithCharacters:characters.data() length:characters.size()];
}

bool hasASCIIPointer(NSString *string)
{
return CFStringGetCStringPtr((__bridge CFStringRef)string, kCFStringEncodingASCII) != nullptr;
}

bool hasUTF16Pointer(NSString *string)
{
return CFStringGetCharactersPtr((__bridge CFStringRef)string) != nullptr;
}

} // namespace

@interface RCTTurboModuleStringConversionTests : XCTestCase
@end

@implementation RCTTurboModuleStringConversionTests {
std::unique_ptr<jsi::Runtime> _runtime;
}

- (void)setUp
{
[super setUp];
_runtime = facebook::hermes::makeHermesRuntime();
}

- (void)tearDown
{
_runtime.reset();
[super tearDown];
}

- (void)assertConvertsToSameCharacters:(NSString *)string
{
jsi::Value value = convertObjCObjectToJSIValue(*_runtime, string);
XCTAssertTrue(value.isString(), @"Expected a JS string for \"%@\"", string);
XCTAssertTrue(
value.asString(*_runtime).utf16(*_runtime) == utf16FromNSString(string),
@"Expected the JS string to have the same characters as \"%@\"",
string);
}

- (void)testASCIIStringWithASCIIPointer
{
NSString *string = stringFromUTF8Data(@"com.facebook.react.uiapp");
XCTAssertTrue(hasASCIIPointer(string), @"Expected the string to expose its ASCII characters");
[self assertConvertsToSameCharacters:string];
[self assertConvertsToSameCharacters:@"constant"];
}

- (void)testTaggedPointerString
{
// Short strings like this one are tagged pointers, which have no character buffer.
NSString *string = stringWithCharacters({u'i', u'd'});
XCTAssertFalse(hasASCIIPointer(string), @"Expected the string to have no ASCII pointer");
XCTAssertFalse(hasUTF16Pointer(string), @"Expected the string to have no UTF-16 pointer");
[self assertConvertsToSameCharacters:string];
}

- (void)testNonASCIIString
{
NSString *string = stringFromUTF8Data(@"Zażółć gęślą jaźń");
XCTAssertTrue(hasUTF16Pointer(string), @"Expected the string to expose its UTF-16 characters");
[self assertConvertsToSameCharacters:string];
[self assertConvertsToSameCharacters:stringFromUTF8Data(@"日本語のテキスト")];
}

- (void)testStringWithSurrogatePairs
{
[self assertConvertsToSameCharacters:stringFromUTF8Data(@"Rocket 🚀 and family 👨‍👩‍👧")];
}

- (void)testEmptyString
{
[self assertConvertsToSameCharacters:@""];
}

- (void)testLargeString
{
NSMutableString *string = [NSMutableString new];
for (int i = 0; i < 20000; i++) {
[string appendFormat:@"{\"id\":%d,\"name\":\"Zażółć %d 🚀\"},", i, i];
}
[self assertConvertsToSameCharacters:stringFromUTF8Data(string)];
}

- (void)testStringSubclass
{
NSString *string =
[[RCTTurboModuleStringConversionTestsString alloc] initWithCharacters:{u'a', u'ż', 0xD83D, 0xDE80}];
XCTAssertFalse(hasASCIIPointer(string), @"Expected the string to have no ASCII pointer");
XCTAssertFalse(hasUTF16Pointer(string), @"Expected the string to have no UTF-16 pointer");
[self assertConvertsToSameCharacters:string];
}

- (void)testStringWithNullCharacterIsNotTruncated
{
NSString *ascii = [[NSString alloc] initWithBytes:"aaaaaaaaaaaaaaaaaaaa\0b" length:22 encoding:NSUTF8StringEncoding];
XCTAssertTrue(hasASCIIPointer(ascii), @"Expected the string to expose its ASCII characters");
[self assertConvertsToSameCharacters:ascii];
[self assertConvertsToSameCharacters:stringWithCharacters({u'a', 0, u'b'})];
[self assertConvertsToSameCharacters:stringWithCharacters({u'ż', 0, u'b'})];
}

- (void)testStringWithLoneSurrogateIsKept
{
NSString *string = stringWithCharacters({u'a', 0xD800, u'b'});
XCTAssertTrue(hasUTF16Pointer(string), @"Expected the string to expose its UTF-16 characters");
[self assertConvertsToSameCharacters:string];
[self assertConvertsToSameCharacters:stringWithCharacters({u'a', 0xDC00})];
}

- (void)testStringSubclassWithLoneSurrogateIsKept
{
NSString *string = [[RCTTurboModuleStringConversionTestsString alloc] initWithCharacters:{u'a', 0xD800, u'b'}];
XCTAssertFalse(hasUTF16Pointer(string), @"Expected the string to have no UTF-16 pointer");
[self assertConvertsToSameCharacters:string];
}

@end
Loading