diff --git a/packages/react-native/ReactCommon/jsc/JSCRuntime.cpp b/packages/react-native/ReactCommon/jsc/JSCRuntime.cpp index 8e666c022416..f19b6fcf2bc3 100644 --- a/packages/react-native/ReactCommon/jsc/JSCRuntime.cpp +++ b/packages/react-native/ReactCommon/jsc/JSCRuntime.cpp @@ -181,6 +181,10 @@ class JSCRuntime : public jsi::Runtime { jsi::String createStringFromUtf16(const char16_t* utf16, size_t length) override; std::string utf8(const jsi::String& /*str*/) override; + void getStringData( + const jsi::String& str, + void* ctx, + void (*cb)(void* ctx, bool ascii, const void* data, size_t num)) override; size_t length(const jsi::String& /*str*/) override; jsi::Object createObject() override; @@ -760,6 +764,15 @@ std::string JSCRuntime::utf8(const jsi::String& str) { return JSStringToSTLString(stringRef(str)); } +void JSCRuntime::getStringData( + const jsi::String& str, + void* ctx, + void (*cb)(void* ctx, bool ascii, const void* data, size_t num)) { + // A JSString holds UTF-16, so pass its characters without converting them. + JSStringRef ref = stringRef(str); + cb(ctx, false, JSStringGetCharactersPtr(ref), JSStringGetLength(ref)); +} + size_t JSCRuntime::length(const jsi::String& str) { return JSStringGetLength(stringRef(str)); } diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm index 701122cc63f5..f5b70c0b31e0 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm @@ -172,10 +172,57 @@ size_t size() const override return jsi::Value::undefined(); } +static bool hasLoneSurrogate(const char16_t *chars, size_t length) +{ + bool hasSurrogate = false; + for (size_t i = 0; i < length; i++) { + hasSurrogate |= (chars[i] & 0xF800) == 0xD800; + } + if (!hasSurrogate) { + return false; + } + for (size_t i = 0; i < length; i++) { + if ((chars[i] & 0xFC00) == 0xD800 && i + 1 < length && (chars[i + 1] & 0xFC00) == 0xDC00) { + i++; + } else if ((chars[i] & 0xF800) == 0xD800) { + return true; + } + } + return false; +} + static NSString *convertJSIStringToNSString(jsi::Runtime &runtime, const jsi::String &value) { - auto utf8 = value.utf8(runtime); - NSString *result = [[NSString alloc] initWithBytes:utf8.data() length:utf8.size() encoding:NSUTF8StringEncoding]; + // Copy the string's own ASCII or UTF-16 characters instead of converting them to UTF-8 and back. + NSString *result = nil; + size_t chunkCount = 0; + bool needsUTF8 = false; + auto readChunk = [&](bool ascii, const void *data, size_t length) { + if (++chunkCount > 1) { + needsUTF8 = true; + return; + } + if (ascii) { + result = [[NSString alloc] initWithBytes:data length:length encoding:NSASCIIStringEncoding]; + return; + } + const auto *chars = static_cast(data); + // A lone surrogate can't be encoded as UTF-8, and native code often needs UTF-8, so such strings + // keep going through UTF-8, which replaces it as before. So does a leading byte order mark, + // which decoding UTF-8 drops. + if ((length > 0 && chars[0] == 0xFEFF) || hasLoneSurrogate(chars, length)) { + needsUTF8 = true; + return; + } + result = [[NSString alloc] initWithCharacters:reinterpret_cast(chars) length:length]; + }; + value.getStringData(runtime, readChunk); + + // Strings that arrive in several chunks also take the UTF-8 path. No runtime here splits strings. + if (needsUTF8 || (result == nil && chunkCount > 0)) { + auto utf8 = value.utf8(runtime); + result = [[NSString alloc] initWithBytes:utf8.data() length:utf8.size() encoding:NSUTF8StringEncoding]; + } return result != nil ? result : @""; }