From 475f797a51237e63e4d2bb4e5718e6aa73157767 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 10 Feb 2025 10:14:08 -0800 Subject: [PATCH] Properly handle null values coming from JS (#49250) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49250 The TurboModule System decided to ignore the Null values when they are coming to JS. However, in iOS, null value can be mapped to `[NSNull null];` and this value is a valid value that can be used on the native side. In the old architecture, when the user were sending a null value from JS to a native module, the Native side was receiving the value. In the New Architecture, the value was stripped away. This change allow us to handle the `null` value properly in the interop layer, to restore the usage of legacy modules in the New Arch. I also tried with a more radical approach, but several tests were crashing because some modules do not know how to handle `NSNull`. See discussion happening here: https://github.com/invertase/react-native-firebase/issues/8144#issuecomment-2548067344 ## Changelog: [iOS][Changed] - Properly handle `null` values coming from NativeModules. Reviewed By: sammy-SC Differential Revision: D69301396 fbshipit-source-id: be275185e2643092f6c3dc2481fe9381bbcf69e9 --- .../ios/ReactCommon/RCTInteropTurboModule.mm | 8 ++--- .../platform/ios/ReactCommon/RCTTurboModule.h | 5 +++ .../ios/ReactCommon/RCTTurboModule.mm | 36 ++++++++++++++----- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.mm b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.mm index be88130f050187..9ff56383832de5 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.mm @@ -346,7 +346,7 @@ T RCTConvertTo(SEL selector, id json) SEL selector = selectorForType(argumentType); if ([RCTConvert respondsToSelector:selector]) { - id objCArg = TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_); + id objCArg = TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_, YES); if (objCArgType == @encode(char)) { char arg = RCTConvertTo(selector, objCArg); @@ -500,7 +500,7 @@ T RCTConvertTo(SEL selector, id json) } RCTResponseSenderBlock arg = - (RCTResponseSenderBlock)TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_); + (RCTResponseSenderBlock)TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_, YES); if (arg) { [retainedObjectsForInvocation addObject:arg]; } @@ -515,7 +515,7 @@ T RCTConvertTo(SEL selector, id json) } RCTResponseSenderBlock senderBlock = - (RCTResponseSenderBlock)TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_); + (RCTResponseSenderBlock)TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_, YES); RCTResponseErrorBlock arg = ^(NSError *error) { senderBlock(@[ RCTJSErrorFromNSError(error) ]); }; @@ -545,7 +545,7 @@ T RCTConvertTo(SEL selector, id json) runtime, errorPrefix + "JavaScript argument must be a plain object. Got " + getType(runtime, jsiArg)); } - id arg = TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_); + id arg = TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_, YES); RCTManagedPointer *(*convert)(id, SEL, id) = (__typeof__(convert))objc_msgSend; RCTManagedPointer *box = convert([RCTCxxConvert class], selector, arg); diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h index f54e1751fb9465..8196ff9ab52736 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h @@ -32,6 +32,11 @@ using EventEmitterCallback = std::function; namespace TurboModuleConvertUtils { jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value); id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, std::shared_ptr jsInvoker); +id convertJSIValueToObjCObject( + jsi::Runtime &runtime, + const jsi::Value &value, + std::shared_ptr jsInvoker, + BOOL useNSNull); } // namespace TurboModuleConvertUtils template <> 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 812723f646ea1b..42067c0ff1b694 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 @@ -111,21 +111,27 @@ static int32_t getUniqueId() return [NSString stringWithUTF8String:value.utf8(runtime).c_str()]; } -static NSArray * -convertJSIArrayToNSArray(jsi::Runtime &runtime, const jsi::Array &value, std::shared_ptr jsInvoker) +static NSArray *convertJSIArrayToNSArray( + jsi::Runtime &runtime, + const jsi::Array &value, + std::shared_ptr jsInvoker, + BOOL useNSNull) { size_t size = value.size(runtime); NSMutableArray *result = [NSMutableArray new]; for (size_t i = 0; i < size; i++) { // Insert kCFNull when it's `undefined` value to preserve the indices. - id convertedObject = convertJSIValueToObjCObject(runtime, value.getValueAtIndex(runtime, i), jsInvoker); + id convertedObject = convertJSIValueToObjCObject(runtime, value.getValueAtIndex(runtime, i), jsInvoker, useNSNull); [result addObject:convertedObject ? convertedObject : (id)kCFNull]; } return [result copy]; } -static NSDictionary * -convertJSIObjectToNSDictionary(jsi::Runtime &runtime, const jsi::Object &value, std::shared_ptr jsInvoker) +static NSDictionary *convertJSIObjectToNSDictionary( + jsi::Runtime &runtime, + const jsi::Object &value, + std::shared_ptr jsInvoker, + BOOL useNSNull) { jsi::Array propertyNames = value.getPropertyNames(runtime); size_t size = propertyNames.size(runtime); @@ -133,7 +139,7 @@ static int32_t getUniqueId() for (size_t i = 0; i < size; i++) { jsi::String name = propertyNames.getValueAtIndex(runtime, i).getString(runtime); NSString *k = convertJSIStringToNSString(runtime, name); - id v = convertJSIValueToObjCObject(runtime, value.getProperty(runtime, name), jsInvoker); + id v = convertJSIValueToObjCObject(runtime, value.getProperty(runtime, name), jsInvoker, useNSNull); if (v) { result[k] = v; } @@ -161,9 +167,21 @@ static int32_t getUniqueId() id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, std::shared_ptr jsInvoker) { - if (value.isUndefined() || value.isNull()) { + return convertJSIValueToObjCObject(runtime, value, jsInvoker, NO); +} + +id convertJSIValueToObjCObject( + jsi::Runtime &runtime, + const jsi::Value &value, + std::shared_ptr jsInvoker, + BOOL useNSNull) +{ + if (value.isUndefined() || (value.isNull() && !useNSNull)) { return nil; } + if (value.isNull() && useNSNull) { + return [NSNull null]; + } if (value.isBool()) { return @(value.getBool()); } @@ -176,12 +194,12 @@ id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, s if (value.isObject()) { jsi::Object o = value.getObject(runtime); if (o.isArray(runtime)) { - return convertJSIArrayToNSArray(runtime, o.getArray(runtime), jsInvoker); + return convertJSIArrayToNSArray(runtime, o.getArray(runtime), jsInvoker, useNSNull); } if (o.isFunction(runtime)) { return convertJSIFunctionToCallback(runtime, o.getFunction(runtime), jsInvoker); } - return convertJSIObjectToNSDictionary(runtime, o, jsInvoker); + return convertJSIObjectToNSDictionary(runtime, o, jsInvoker, useNSNull); } throw std::runtime_error("Unsupported jsi::Value kind");