From 618107cc5d732c5e5a2f19ad7a18ba7820c895f7 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Sat, 24 Oct 2026 08:44:44 +0200 Subject: [PATCH 1/2] Convert iOS native module objects to JS with fewer lookups and retains TurboModule return values, promise results, callback arguments and codegen events are converted by convertObjCObjectToJSIValue. Dictionaries now read their keys and values at once instead of looking up each key again, keys are created as property names directly, nested values are retained once per container instead of once per call, and short strings without character storage are copied to the stack instead of the heap. Co-Authored-By: Claude Opus 5.5 --- .../ios/ReactCommon/RCTTurboModule.mm | 127 ++++++++++++++---- 1 file changed, 102 insertions(+), 25 deletions(-) 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..10df0f29fc0d 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 @@ -27,10 +27,12 @@ #import #import #import +#import #import #import #import +#import #import using namespace facebook; @@ -49,20 +51,39 @@ static int32_t getUniqueId() /** * All static helper functions are ObjC++ specific. */ -static jsi::Value convertNSNumberToJSIBoolean(jsi::Runtime &runtime, NSNumber *value) +static jsi::Value convertNSNumberToJSIBoolean(jsi::Runtime &runtime, __unsafe_unretained NSNumber *value) { return jsi::Value((bool)[value boolValue]); } -static jsi::Value convertNSNumberToJSINumber(jsi::Runtime &runtime, NSNumber *value) +static jsi::Value convertNSNumberToJSINumber(jsi::Runtime &runtime, __unsafe_unretained NSNumber *value) { return jsi::Value([value doubleValue]); } -static jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value) +// Creates a JS string or property name from the string's own ASCII or UTF-16 characters. +template +static T convertNSStringToJSI(jsi::Runtime &runtime, __unsafe_unretained NSString *value) { + auto fromAscii = [&](const char *ascii, size_t length) { + if constexpr (std::is_same_v) { + return jsi::PropNameID::forAscii(runtime, ascii, length); + } else { + return jsi::String::createFromAscii(runtime, ascii, length); + } + }; + auto fromUtf16 = [&](const UniChar *characters, size_t length) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + auto utf16 = reinterpret_cast(characters); + if constexpr (std::is_same_v) { + return jsi::PropNameID::forUtf16(runtime, utf16, length); + } else { + return jsi::String::createFromUtf16(runtime, utf16, length); + } + }; + if (value == nil) { - return jsi::String::createFromAscii(runtime, "", 0); + return fromAscii("", 0); } // Hand the JS runtime the string's own ASCII or UTF-16 characters when @@ -70,44 +91,95 @@ static int32_t getUniqueId() auto string = (__bridge CFStringRef)value; CFIndex length = CFStringGetLength(string); if (const char *ascii = CFStringGetCStringPtr(string, kCFStringEncodingASCII)) { - return jsi::String::createFromAscii(runtime, ascii, static_cast(length)); + return fromAscii(ascii, static_cast(length)); } if (const UniChar *characters = CFStringGetCharactersPtr(string)) { - return jsi::String::createFromUtf16( - runtime, - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - reinterpret_cast(characters), - static_cast(length)); + return fromUtf16(characters, static_cast(length)); + } + // Short strings, such as most keys, are often stored in the pointer itself, so copy them to the stack. + constexpr CFIndex kStackLength = 64; + if (length <= kStackLength) { + UInt8 ascii[kStackLength]; + CFIndex asciiLength = 0; + if (CFStringGetBytes( + string, CFRangeMake(0, length), kCFStringEncodingASCII, 0, false, ascii, length, &asciiLength) == length) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return fromAscii(reinterpret_cast(ascii), static_cast(asciiLength)); + } + UniChar characters[kStackLength]; + CFStringGetCharacters(string, CFRangeMake(0, length), characters); + return fromUtf16(characters, static_cast(length)); + } + std::unique_ptr characters(new UniChar[length]); + CFStringGetCharacters(string, CFRangeMake(0, length), characters.get()); + return fromUtf16(characters.get(), static_cast(length)); +} + +static jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value) +{ + return convertNSStringToJSI(runtime, value); +} + +// Doesn't retain `value`: the caller holds it, and containers hold what they contain while it is converted. +static jsi::Value convertUnretainedObjCObjectToJSIValue(jsi::Runtime &runtime, __unsafe_unretained id value); + +// Reads a container's objects at once and holds them while they are converted, because JS code +// that runs while properties are set could change the container. +template +static std::vector getRetainedObjects(NSUInteger count, ReadObjects readObjects) +{ + constexpr NSUInteger kStackCount = 32; + __unsafe_unretained id stackObjects[kStackCount]; + std::vector<__unsafe_unretained id> heapObjects; + __unsafe_unretained id *objects = stackObjects; + if (count > kStackCount) { + heapObjects.resize(count); + objects = heapObjects.data(); } - std::u16string utf16(length, u'\0'); - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - CFStringGetCharacters(string, CFRangeMake(0, length), reinterpret_cast(utf16.data())); - return jsi::String::createFromUtf16(runtime, utf16); + readObjects(objects); + return std::vector(objects, objects + count); } -static jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime &runtime, NSDictionary *value) +static jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime &runtime, __unsafe_unretained NSDictionary *value) { + NSUInteger count = value.count; + std::vector keysAndObjects = getRetainedObjects(count * 2, [&](__unsafe_unretained id *objects) { + [value getObjects:objects + count andKeys:objects count:count]; + }); jsi::Object result = jsi::Object(runtime); - for (NSString *k in value) { - result.setProperty(runtime, convertNSStringToJSIString(runtime, k), convertObjCObjectToJSIValue(runtime, value[k])); + for (NSUInteger i = 0; i < count; i++) { + result.setProperty( + runtime, + convertNSStringToJSI(runtime, keysAndObjects[i]), + convertUnretainedObjCObjectToJSIValue(runtime, keysAndObjects[count + i])); } return result; } -static jsi::Array convertNSArrayToJSIArray(jsi::Runtime &runtime, NSArray *value) +static std::vector getRetainedObjects(__unsafe_unretained NSArray *value) +{ + NSUInteger count = value.count; + return getRetainedObjects( + count, [&](__unsafe_unretained id *objects) { [value getObjects:objects range:NSMakeRange(0, count)]; }); +} + +static jsi::Array convertNSArrayToJSIArray(jsi::Runtime &runtime, __unsafe_unretained NSArray *value) { - jsi::Array result = jsi::Array(runtime, value.count); - for (size_t i = 0; i < value.count; i++) { - result.setValueAtIndex(runtime, i, convertObjCObjectToJSIValue(runtime, value[i])); + std::vector objects = getRetainedObjects(value); + jsi::Array result = jsi::Array(runtime, objects.size()); + for (size_t i = 0; i < objects.size(); i++) { + result.setValueAtIndex(runtime, i, convertUnretainedObjCObjectToJSIValue(runtime, objects[i])); } return result; } -static std::vector convertNSArrayToStdVector(jsi::Runtime &runtime, NSArray *value) +static std::vector convertNSArrayToStdVector(jsi::Runtime &runtime, __unsafe_unretained NSArray *value) { + std::vector objects = getRetainedObjects(value); std::vector result; - for (size_t i = 0; i < value.count; i++) { - result.emplace_back(convertObjCObjectToJSIValue(runtime, value[i])); + result.reserve(objects.size()); + for (id object : objects) { + result.emplace_back(convertUnretainedObjCObjectToJSIValue(runtime, object)); } return result; } @@ -152,9 +224,14 @@ size_t size() const override } jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value) +{ + return convertUnretainedObjCObjectToJSIValue(runtime, value); +} + +static jsi::Value convertUnretainedObjCObjectToJSIValue(jsi::Runtime &runtime, __unsafe_unretained id value) { if ([value isKindOfClass:[NSString class]]) { - return convertNSStringToJSIString(runtime, (NSString *)value); + return convertNSStringToJSI(runtime, value); } else if ([value isKindOfClass:[NSNumber class]]) { if ([value isKindOfClass:[@YES class]]) { return convertNSNumberToJSIBoolean(runtime, (NSNumber *)value); From da67a6cd4441ba5494edf505cd32b44fa637dd86 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Tue, 27 Oct 2026 10:36:53 +0100 Subject: [PATCH 2/2] Address review: no string wrapper, unretained objects, no stack copy - Call convertNSStringToJSI directly instead of convertNSStringToJSIString. - Read a container's objects into one vector and return it. - Don't retain the objects: the container holds them while they are converted. - Drop the stack copy of short strings. With it back, the benchmarks were within 1%, so it didn't help. Co-Authored-By: Claude Opus 5.5 --- .../ios/ReactCommon/RCTTurboModule.mm | 66 ++++++------------- 1 file changed, 19 insertions(+), 47 deletions(-) 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 10df0f29fc0d..fe5866c07027 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 @@ -27,7 +27,6 @@ #import #import #import -#import #import #import @@ -72,9 +71,7 @@ static T convertNSStringToJSI(jsi::Runtime &runtime, __unsafe_unretained NSStrin return jsi::String::createFromAscii(runtime, ascii, length); } }; - auto fromUtf16 = [&](const UniChar *characters, size_t length) { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - auto utf16 = reinterpret_cast(characters); + auto fromUtf16 = [&](const char16_t *utf16, size_t length) { if constexpr (std::is_same_v) { return jsi::PropNameID::forUtf16(runtime, utf16, length); } else { @@ -94,56 +91,31 @@ static T convertNSStringToJSI(jsi::Runtime &runtime, __unsafe_unretained NSStrin return fromAscii(ascii, static_cast(length)); } if (const UniChar *characters = CFStringGetCharactersPtr(string)) { - return fromUtf16(characters, static_cast(length)); - } - // Short strings, such as most keys, are often stored in the pointer itself, so copy them to the stack. - constexpr CFIndex kStackLength = 64; - if (length <= kStackLength) { - UInt8 ascii[kStackLength]; - CFIndex asciiLength = 0; - if (CFStringGetBytes( - string, CFRangeMake(0, length), kCFStringEncodingASCII, 0, false, ascii, length, &asciiLength) == length) { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - return fromAscii(reinterpret_cast(ascii), static_cast(asciiLength)); - } - UniChar characters[kStackLength]; - CFStringGetCharacters(string, CFRangeMake(0, length), characters); - return fromUtf16(characters, static_cast(length)); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return fromUtf16(reinterpret_cast(characters), static_cast(length)); } - std::unique_ptr characters(new UniChar[length]); - CFStringGetCharacters(string, CFRangeMake(0, length), characters.get()); - return fromUtf16(characters.get(), static_cast(length)); -} - -static jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value) -{ - return convertNSStringToJSI(runtime, value); + std::u16string utf16(length, u'\0'); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + CFStringGetCharacters(string, CFRangeMake(0, length), reinterpret_cast(utf16.data())); + return fromUtf16(utf16.data(), utf16.size()); } // Doesn't retain `value`: the caller holds it, and containers hold what they contain while it is converted. static jsi::Value convertUnretainedObjCObjectToJSIValue(jsi::Runtime &runtime, __unsafe_unretained id value); -// Reads a container's objects at once and holds them while they are converted, because JS code -// that runs while properties are set could change the container. +// Reads a container's objects at once. They aren't retained, as the container holds them. template -static std::vector getRetainedObjects(NSUInteger count, ReadObjects readObjects) +static std::vector<__unsafe_unretained id> getObjects(NSUInteger count, ReadObjects readObjects) { - constexpr NSUInteger kStackCount = 32; - __unsafe_unretained id stackObjects[kStackCount]; - std::vector<__unsafe_unretained id> heapObjects; - __unsafe_unretained id *objects = stackObjects; - if (count > kStackCount) { - heapObjects.resize(count); - objects = heapObjects.data(); - } - readObjects(objects); - return std::vector(objects, objects + count); + std::vector<__unsafe_unretained id> objects(count); + readObjects(objects.data()); + return objects; } static jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime &runtime, __unsafe_unretained NSDictionary *value) { NSUInteger count = value.count; - std::vector keysAndObjects = getRetainedObjects(count * 2, [&](__unsafe_unretained id *objects) { + std::vector<__unsafe_unretained id> keysAndObjects = getObjects(count * 2, [&](__unsafe_unretained id *objects) { [value getObjects:objects + count andKeys:objects count:count]; }); jsi::Object result = jsi::Object(runtime); @@ -156,16 +128,16 @@ static T convertNSStringToJSI(jsi::Runtime &runtime, __unsafe_unretained NSStrin return result; } -static std::vector getRetainedObjects(__unsafe_unretained NSArray *value) +static std::vector<__unsafe_unretained id> getObjects(__unsafe_unretained NSArray *value) { NSUInteger count = value.count; - return getRetainedObjects( + return getObjects( count, [&](__unsafe_unretained id *objects) { [value getObjects:objects range:NSMakeRange(0, count)]; }); } static jsi::Array convertNSArrayToJSIArray(jsi::Runtime &runtime, __unsafe_unretained NSArray *value) { - std::vector objects = getRetainedObjects(value); + std::vector<__unsafe_unretained id> objects = getObjects(value); jsi::Array result = jsi::Array(runtime, objects.size()); for (size_t i = 0; i < objects.size(); i++) { result.setValueAtIndex(runtime, i, convertUnretainedObjCObjectToJSIValue(runtime, objects[i])); @@ -175,10 +147,10 @@ static T convertNSStringToJSI(jsi::Runtime &runtime, __unsafe_unretained NSStrin static std::vector convertNSArrayToStdVector(jsi::Runtime &runtime, __unsafe_unretained NSArray *value) { - std::vector objects = getRetainedObjects(value); + std::vector<__unsafe_unretained id> objects = getObjects(value); std::vector result; result.reserve(objects.size()); - for (id object : objects) { + for (__unsafe_unretained id object : objects) { result.emplace_back(convertUnretainedObjCObjectToJSIValue(runtime, object)); } return result; @@ -716,7 +688,7 @@ TraceSection s( break; } case StringKind: { - returnValue = convertNSStringToJSIString(runtime, (NSString *)result); + returnValue = convertNSStringToJSI(runtime, (NSString *)result); break; } case ObjectKind: {