Repository navigation
Convert iOS native module objects to JS with fewer lookups and retains #59450
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,10 +27,12 @@ | |
| #import <objc/runtime.h> | ||
| #import <atomic> | ||
| #import <iostream> | ||
| #import <memory> | ||
| #import <mutex> | ||
|
|
||
| #import <sstream> | ||
| #import <string> | ||
| #import <type_traits> | ||
| #import <vector> | ||
|
|
||
| using namespace facebook; | ||
|
|
@@ -49,65 +51,135 @@ 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 <typename T> | ||
| static T convertNSStringToJSI(jsi::Runtime &runtime, __unsafe_unretained NSString *value) | ||
| { | ||
| auto fromAscii = [&](const char *ascii, size_t length) { | ||
| if constexpr (std::is_same_v<T, jsi::PropNameID>) { | ||
| 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<const char16_t *>(characters); | ||
| if constexpr (std::is_same_v<T, jsi::PropNameID>) { | ||
| 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 | ||
| // CoreFoundation can expose them, and copy them once otherwise. | ||
| auto 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)); | ||
| return fromAscii(ascii, static_cast<size_t>(length)); | ||
| } | ||
| if (const UniChar *characters = CFStringGetCharactersPtr(string)) { | ||
| return jsi::String::createFromUtf16( | ||
| runtime, | ||
| // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) | ||
| reinterpret_cast<const char16_t *>(characters), | ||
| static_cast<size_t>(length)); | ||
| return fromUtf16(characters, static_cast<size_t>(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<const char *>(ascii), static_cast<size_t>(asciiLength)); | ||
| } | ||
| UniChar characters[kStackLength]; | ||
| CFStringGetCharacters(string, CFRangeMake(0, length), characters); | ||
| return fromUtf16(characters, static_cast<size_t>(length)); | ||
| } | ||
| std::unique_ptr<UniChar[]> characters(new UniChar[length]); | ||
| CFStringGetCharacters(string, CFRangeMake(0, length), characters.get()); | ||
| return fromUtf16(characters.get(), static_cast<size_t>(length)); | ||
| } | ||
|
|
||
| static jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value) | ||
| { | ||
| return convertNSStringToJSI<jsi::String>(runtime, value); | ||
| } | ||
|
Comment on lines
+118
to
+121
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Call the templated helper directly in callers of |
||
|
|
||
| // 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 <typename ReadObjects> | ||
| static std::vector<id> getRetainedObjects(NSUInteger count, ReadObjects readObjects) | ||
| { | ||
| constexpr NSUInteger kStackCount = 32; | ||
| __unsafe_unretained id stackObjects[kStackCount]; | ||
| std::vector<__unsafe_unretained id> heapObjects; | ||
|
Comment on lines
+132
to
+133
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The stack optimization isn't helpful here, since we end having to copy it in the return value anyway. Just use |
||
| __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<UniChar *>(utf16.data())); | ||
| return jsi::String::createFromUtf16(runtime, utf16); | ||
| readObjects(objects); | ||
| return std::vector<id>(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<id> 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<jsi::PropNameID>(runtime, keysAndObjects[i]), | ||
| convertUnretainedObjCObjectToJSIValue(runtime, keysAndObjects[count + i])); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| static jsi::Array convertNSArrayToJSIArray(jsi::Runtime &runtime, NSArray *value) | ||
| static std::vector<id> 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<id> objects = getRetainedObjects(value); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to retain them? The NSArray who the caller is holding on to is already doing so. |
||
| 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<jsi::Value> convertNSArrayToStdVector(jsi::Runtime &runtime, NSArray *value) | ||
| static std::vector<jsi::Value> convertNSArrayToStdVector(jsi::Runtime &runtime, __unsafe_unretained NSArray *value) | ||
| { | ||
| std::vector<id> objects = getRetainedObjects(value); | ||
| std::vector<jsi::Value> 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<jsi::String>(runtime, value); | ||
| } else if ([value isKindOfClass:[NSNumber class]]) { | ||
| if ([value isKindOfClass:[@YES class]]) { | ||
| return convertNSNumberToJSIBoolean(runtime, (NSNumber *)value); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this path observed to be making things more efficient?