Repository navigation
Convert iOS native module event arguments to JS without UTF-8 - #59449
hryhoriiK97 wants to merge 3 commits into
Conversation
Events sent with RCTEventEmitter went through folly::dynamic: every string was encoded as UTF-8 on the sending thread and decoded again on the JS thread. RCTInstance now reads the arguments on the sending thread into a small tree with the same structure, where strings keep their ASCII or UTF-16 characters, and the JS thread creates the JS values from it. ReactInstance gets a callFunctionOnModule overload that takes an ArgFactory, which creates the arguments on the JS thread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hi @javache, this one is similar to #59325, but for events. On iOS, event arguments don't go through If it's easier to review, I can move |
Visual regressionNo visual differences at |
|
Hi @javache, the iOS visual diff isn't from this change: a system overlay covers the top of the crop, and the content matches the baseline. On an iPhone 15 Pro, before and after are pixel-identical. Could you apply |
| if ([value isKindOfClass:[NSString class]]) { | ||
| kind_ = Kind::String; | ||
| setString(value); | ||
| } else if ([value isKindOfClass:[NSNumber class]]) { | ||
| setNumber(value); | ||
| } else if ([value isKindOfClass:[NSArray class]]) { | ||
| kind_ = Kind::Array; | ||
| children_.reserve([value count]); | ||
| for (id element in value) { | ||
| children_.emplace_back(element); | ||
| } | ||
| } else if ([value isKindOfClass:[NSDictionary class]]) { | ||
| kind_ = Kind::Object; | ||
| children_.reserve([value count] * 2); | ||
| for (id key in value) { | ||
| if ([key isKindOfClass:[NSString class]]) { | ||
| children_.emplace_back(key); | ||
| } else if (NSString *numberKey = numberKeyString(key)) { | ||
| children_.emplace_back(numberKey); | ||
| } else { | ||
| continue; | ||
| } | ||
| children_.emplace_back(value[key]); | ||
| } | ||
| } |
There was a problem hiding this comment.
This is very similar to convertObjCObjectToJSIValue in RCTTurboModule.
Can we extract it a single shared helper that we use across both paths.
There was a problem hiding this comment.
Thank you for the comment! Done 👍
| } else { | ||
| continue; | ||
| } | ||
| children_.emplace_back(value[key]); |
There was a problem hiding this comment.
Why do this conversion up-front (requiring additional allocation) instead of lazily in the toJSValue path?
Drop CallArgument and convert the arguments on the JS thread with the TurboModule helper, like codegen's AsyncEventEmitter does. The helper now turns number keys into strings and skips other keys that aren't strings, as convertIdToFollyDynamic does, instead of treating every key as an NSString. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| [moduleName UTF8String] ?: "", | ||
| [method UTF8String] ?: "", |
There was a problem hiding this comment.
Early return if either moduleName or method is the empty string.
There was a problem hiding this comment.
Sorry, I wanted to avoid the ?: "" fallback here entirely, that should never happen.
That can be an early return inside callFunctionOnModule?
| moduleName, | ||
| methodName, | ||
| [&](std::vector<jsi::Value>& jsArgs) { | ||
| for (auto& arg : args) { |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@javache has imported this pull request. If you are a Meta employee, you can view this in D127583929. |
Summary:
Native modules send events to JS with
RCTEventEmitter sendEventWithName:body:, which ends inRCTInstance -callFunctionOnJSModule:method:args:. Core modules use it too: for example,RCTNetworkingsends the whole text of an XHR response indidReceiveNetworkData, andRCTWebSocketModulesends each message.Current behavior. The arguments go through
folly::dynamic. On the sending thread,convertIdToFollyDynamicencodes every string as UTF-8. On the JS thread,jsi::valueFromDynamicdecodes it again into a JS string. Text that isn't ASCII is the expensive case: a 1 MB response with accented text costs about 3.9 ms on the JS thread and 3.2 ms on the sending thread.With this change.
RCTInstancereads the arguments into a small tree with the structure thatconvertIdToFollyDynamiccreates. Strings keep their own ASCII or UTF-16 characters instead of being encoded as UTF-8. When CoreFoundation exposes the characters, the tree points at them in an immutable copy of the string; otherwise it copies them.valueFromDynamicdoes.ReactInstancegets acallFunctionOnModuleoverload that takes anArgFactory, which creates the arguments on the JS thread. It has the same shape asTurboModule::ArgFactory, whichemitDeviceEventuses. Both overloads share the module lookup, so Android and other callers of thefolly::dynamicoverload run the same code as before.JS receives the same values as before:
convertIdToFollyDynamic.null."".The order of properties in event objects now follows the
NSDictionary. Before, it followed thefolly::dynamichash map, so it was arbitrary in both cases. On JSC, a string with both a NUL and non-ASCII characters is no longer cut off at the NUL, asJSStringCreateWithUTF8CStringdid. Hermes output is the same.Changelog:
[IOS] [CHANGED] - Events sent by native modules are converted to JS values without a UTF-8 round trip
Test Plan:
clang-format --dry-run --Werroron the changed files passes.xcodebuildin Release and run on a physical iPhone 15 Pro (iOS 26.6.1).ReactInstanceTest.testCallFunctionOnModule_withArgFactoryandtestCallFunctionOnModule_withEmptyArgFactoryfor the new overload. I couldn't run the C++ tests locally. I updated the C++ API snapshots by hand, because I couldn't run Doxygen here.BOOL,char,unsigned char,short,int, negative,unsigned long long, 2^60,float,double, NaN andNSDecimalNumbernumbers;NSNull,NSDateandNSURLvalues;NSNullkeys.GETofdata:URLs with ASCII or accented text: 1000 requests at 1 KB, 30 at 100 KB and 10 at 1 MB. Also 1000 small events sent from a background queue: a keyboard-like payload, and one with short keys and values created at run time.valueFromDynamicbefore and the tree conversion after.convertIdToFollyDynamicbefore and building the tree after.didReceiveNetworkData, accented, 1 KBdidReceiveNetworkData, accented, 100 KBdidReceiveNetworkData, accented, 1 MBdidReceiveNetworkData, ASCII, 1 KBdidReceiveNetworkData, ASCII, 100 KBdidReceiveNetworkData, ASCII, 1 MBdidReceiveNetworkResponse(headers)🤖 Generated with Claude Code