diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.h b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.h new file mode 100644 index 00000000000..914153bff44 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.h @@ -0,0 +1,95 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#import +#import + +#import +#import + +#import "RCTTurboModule.h" + +namespace facebook { +namespace react { + +class JSI_EXPORT ObjCInteropTurboModule : public ObjCTurboModule { + public: + struct MethodDescriptor { + std::string methodName; + SEL selector; + int jsArgCount; + TurboModuleMethodValueKind jsReturnKind; + }; + + ObjCInteropTurboModule(const ObjCTurboModule::InitParams ¶ms); + + std::vector getPropertyNames(facebook::jsi::Runtime &runtime) override; + + protected: + jsi::Value create(jsi::Runtime &runtime, const jsi::PropNameID &propName) override; + + /** + * Why is this overriden? + * + * Purpose: Converts native module method returns from Objective C values to JavaScript values. + * + * ObjCTurboModule converts returns by returnType. But, Legacy native modules convert returns by the Objective C type: + * React Native cannot infer a method's returnType from the RCT_EXPORT_METHOD annotations. + */ + jsi::Value convertReturnIdToJSIValue( + jsi::Runtime &runtime, + const char *methodName, + TurboModuleMethodValueKind returnType, + id result) override; + + /** + * Why is this overriden? + * + * Purpose: Get a native module method's argument's type, given the method name, and argument index. + * + * This override is meant to serve as a performance optimization. + * + * ObjCTurboModule computes the method argument types from the RCT_EXPORT_METHOD macros lazily. + * ObjCInteropTurboModule computes all the method argument types eagerly on module init. + * + * ObjCInteropTurboModule overrides getArgumentTypeName, so ObjCTurboModule doesn't end up re-computing the argument + * type names again. + */ + NSString *getArgumentTypeName(jsi::Runtime &runtime, NSString *methodName, int argIndex) override; + + /** + * Why is this overriden? + * + * Purpose: Convert arguments from JavaScript values to Objective C values. Assign the Objective C argument to the + * method invocation. + * + * ObjCTurboModule tries to minimize reliance on RCTConvert for argument conversion. Why: RCTConvert relies on the + * RCT_EXPORT_METHOD macros, which we want to remove long term. But, Legacy native modules rely heavily on RCTConvert + * for argument conversion. + */ + void setInvocationArg( + jsi::Runtime &runtime, + const char *methodName, + const std::string &objCArgType, + const jsi::Value &arg, + size_t i, + NSInvocation *inv, + NSMutableArray *retainedObjectsForInvocation) override; + + private: + std::vector methodDescriptors_; + NSDictionary *> *methodArgumentTypeNames_; + jsi::Value constantsCache_; + + const jsi::Value &getConstants(jsi::Runtime &runtime); + bool exportsConstants(); +}; + +} // namespace react +} // namespace facebook 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 new file mode 100644 index 00000000000..1425907c5da --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.mm @@ -0,0 +1,654 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "RCTInteropTurboModule.h" + +#import +#import + +#import +#import +#import +#import +#import +#import + +namespace facebook { +namespace react { + +namespace { + +// This is used for generating short exception strings. +std::string getType(jsi::Runtime &rt, const jsi::Value &v) +{ + if (v.isUndefined()) { + return "undefined"; + } else if (v.isNull()) { + return "null"; + } else if (v.isBool()) { + return v.getBool() ? "true" : "false"; + } else if (v.isNumber()) { + return "number"; + } else if (v.isString()) { + return "string"; + } else if (v.isSymbol()) { + return "symbol"; + } else if (v.isBigInt()) { + return "bigint"; + } else if (v.isObject()) { + jsi::Object vObj = v.getObject(rt); + return vObj.isFunction(rt) ? "function" : vObj.isArray(rt) ? "array" : "object"; + } else { + return "unknown"; + } +} + +std::vector getMethodInfos(Class moduleClass) +{ + std::vector methodInfos; + + Class cls = moduleClass; + while (cls && cls != [NSObject class] && cls != [NSProxy class]) { + unsigned int methodCount; + Method *methods = class_copyMethodList(object_getClass(cls), &methodCount); + + for (unsigned int i = 0; i < methodCount; i++) { + Method method = methods[i]; + SEL selector = method_getName(method); + if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) { + IMP imp = method_getImplementation(method); + const RCTMethodInfo *methodInfo = ((const RCTMethodInfo *(*)(id, SEL))imp)(moduleClass, selector); + methodInfos.push_back(methodInfo); + } + } + + free(methods); + cls = class_getSuperclass(cls); + } + + return methodInfos; +} + +NSString *getJSMethodName(const RCTMethodInfo *methodInfo) +{ + std::string jsName = methodInfo->jsName; + if (jsName != "") { + return @(jsName.c_str()); + } + + NSString *methodName = @(methodInfo->objcName); + NSRange colonRange = [methodName rangeOfString:@":"]; + if (colonRange.location != NSNotFound) { + methodName = [methodName substringToIndex:colonRange.location]; + } + methodName = [methodName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + RCTAssert( + methodName.length, + @"%s is not a valid JS function name, please" + " supply an alternative using RCT_REMAP_METHOD()", + methodInfo->objcName); + + return methodName; +} + +class ObjCInteropTurboModuleParseException : public std::runtime_error { + public: + ObjCInteropTurboModuleParseException(std::string moduleName, std::string methodName, std::string message) + : std::runtime_error( + "Failed to create module \"" + moduleName + "\": Error while parsing method " + moduleName + "." + + methodName + ": " + message) + { + } +}; + +struct ExportedMethod { + NSString *methodName; + NSArray *argumentTypes; + std::string returnType; + SEL selector; +}; + +std::vector parseExportedMethods(std::string moduleName, Class moduleClass) +{ + std::vector methodInfos = getMethodInfos(moduleClass); + std::vector methods; + methods.reserve(methodInfos.size()); + + for (const RCTMethodInfo *methodInfo : methodInfos) { + NSString *jsMethodName = getJSMethodName(methodInfo); + NSArray *arguments; + SEL objCMethodSelector = NSSelectorFromString(RCTParseMethodSignature(methodInfo->objcName, &arguments)); + NSMethodSignature *objCMethodSignature = [moduleClass instanceMethodSignatureForSelector:objCMethodSelector]; + std::string objCMethodReturnType = [objCMethodSignature methodReturnType]; + + if (objCMethodSignature.numberOfArguments - 2 != [arguments count]) { + std::string message = "Parsed argument count (i.e: " + std::to_string([arguments count]) + + ") != Objective C method signature argument count (i.e: " + + std::to_string(objCMethodSignature.numberOfArguments - 2) + ")."; + throw ObjCInteropTurboModuleParseException(moduleName, [jsMethodName UTF8String], message); + } + + NSMutableArray *argumentTypes = [NSMutableArray new]; + for (NSUInteger i = 0; i < [arguments count]; i += 1) { + [argumentTypes addObject:arguments[i].type]; + } + + if ([argumentTypes count] == 1) { + std::string lastArgType = [argumentTypes[[argumentTypes count] - 1] UTF8String]; + if (lastArgType == "RCTPromiseResolveBlock" || lastArgType == "RCTPromiseRejectBlock") { + std::string message = + "Methods that return promises must accept a RCTPromiseResolveBlock followed by a RCTPromiseRejectBlock. This method just accepts a " + + lastArgType + "."; + throw ObjCInteropTurboModuleParseException(moduleName, [jsMethodName UTF8String], message); + } + } else if ([argumentTypes count] > 1) { + std::string lastArgType = [argumentTypes[[argumentTypes count] - 1] UTF8String]; + std::string secondLastArgType = [argumentTypes[[argumentTypes count] - 2] UTF8String]; + if ((secondLastArgType == "RCTPromiseResolveBlock" && lastArgType != "RCTPromiseRejectBlock") || + (secondLastArgType != "RCTPromiseResolveBlock" && lastArgType == "RCTPromiseRejectBlock")) { + std::string message = + "Methods that return promises must accept a RCTPromiseResolveBlock followed by a RCTPromiseRejectBlock. This method accepts a " + + secondLastArgType + " followed by a " + lastArgType + "."; + throw ObjCInteropTurboModuleParseException(moduleName, [jsMethodName UTF8String], message); + } + } + + methods.push_back( + {.methodName = jsMethodName, + .argumentTypes = argumentTypes, + .returnType = objCMethodReturnType, + .selector = objCMethodSelector}); + } + + return methods; +} + +SEL selectorForType(NSString *type) +{ + const char *input = type.UTF8String; + return NSSelectorFromString([RCTParseType(&input) stringByAppendingString:@":"]); +} + +template +T RCTConvertTo(SEL selector, id json) +{ + T (*convert)(id, SEL, id) = (__typeof__(convert))objc_msgSend; + return convert([RCTConvert class], selector, json); +} + +} // namespace + +ObjCInteropTurboModule::ObjCInteropTurboModule(const ObjCTurboModule::InitParams ¶ms) + : ObjCTurboModule(params), constantsCache_(jsi::Value::undefined()) +{ + std::vector methods = parseExportedMethods(name_, [params.instance class]); + methodDescriptors_.reserve(methods.size()); + + NSMutableDictionary *> *methodArgTypeNames = [NSMutableDictionary new]; + methodArgumentTypeNames_ = methodArgTypeNames; + + for (const ExportedMethod &method : methods) { + const int numArgs = [method.argumentTypes count]; + const bool isPromiseMethod = + numArgs >= 2 && [method.argumentTypes[numArgs - 1] isEqualToString:@"RCTPromiseRejectBlock"]; + + const int jsArgCount = isPromiseMethod ? numArgs - 2 : numArgs; + + /** + * In the TurboModule system, only promises and voids are special. So, set those. + * In the else case, just assume ObjectKind. This will be ignored by the interop layer. + * In the else case, the interop layer will just call into ::convertReturnIdToJSIValue() + */ + const TurboModuleMethodValueKind returnKind = isPromiseMethod ? PromiseKind + : method.returnType == @encode(void) ? VoidKind + : ObjectKind; + + methodMap_[[method.methodName UTF8String]] = MethodMetadata{static_cast(jsArgCount), nullptr}; + + for (NSUInteger i = 0; i < numArgs; i += 1) { + NSString *typeName = method.argumentTypes[i]; + + if ([typeName hasPrefix:@"JS::"]) { + NSString *rctCxxConvertSelector = + [[typeName stringByReplacingOccurrencesOfString:@"::" withString:@"_"] stringByAppendingString:@":"]; + setMethodArgConversionSelector(method.methodName, i, rctCxxConvertSelector); + } + } + + methodArgTypeNames[method.methodName] = method.argumentTypes; + methodDescriptors_.push_back({ + .methodName = [method.methodName UTF8String], + .selector = method.selector, + .jsArgCount = jsArgCount, + .jsReturnKind = returnKind, + }); + } + + if ([params.instance respondsToSelector:@selector(constantsToExport)]) { + methodDescriptors_.push_back({ + .methodName = "getConstants", .selector = @selector(constantsToExport), .jsArgCount = 0, + .jsReturnKind = ObjectKind, + }); + } else { + static SEL getConstantsSelector = NSSelectorFromString(@"getConstants"); + if ([params.instance respondsToSelector:getConstantsSelector]) { + methodDescriptors_.push_back({ + .methodName = "getConstants", + .selector = getConstantsSelector, + .jsArgCount = 0, + .jsReturnKind = ObjectKind, + }); + } + } +} + +jsi::Value ObjCInteropTurboModule::create(jsi::Runtime &runtime, const jsi::PropNameID &propName) +{ + for (size_t i = 0; i < methodDescriptors_.size(); i += 1) { + if (methodDescriptors_[i].methodName == propName.utf8(runtime)) { + if (propName.utf8(runtime) == "getConstants") { + return jsi::Function::createFromHostFunction( + runtime, + propName, + static_cast(methodDescriptors_[i].jsArgCount), + [this, i](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) mutable { + if (!this->constantsCache_.isUndefined()) { + return jsi::Value(rt, this->constantsCache_); + } + + // TODO: Dispatch getConstants to the main queue, if the module requires main queue setup + jsi::Value ret = this->invokeObjCMethod( + rt, + this->methodDescriptors_[i].jsReturnKind, + this->methodDescriptors_[i].methodName, + this->methodDescriptors_[i].selector, + args, + count); + + bool isRetValid = ret.isUndefined() || ret.isNull() || + (ret.isObject() && !ret.asObject(rt).isFunction(rt) && !ret.asObject(rt).isArray(rt)); + + if (!isRetValid) { + std::string methodJsSignature = name_ + ".getConstants()"; + std::string errorPrefix = methodJsSignature + ": "; + throw jsi::JSError( + rt, + errorPrefix + "Expected return value to be null, undefined, or a plain object. But, got: " + + getType(rt, ret)); + } + + if (ret.isUndefined() || ret.isNull()) { + this->constantsCache_ = jsi::Object(rt); + } else { + this->constantsCache_ = jsi::Value(rt, ret); + } + + return ret; + }); + } + + return jsi::Function::createFromHostFunction( + runtime, + propName, + static_cast(methodDescriptors_[i].jsArgCount), + [this, i](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) { + return this->invokeObjCMethod( + rt, + this->methodDescriptors_[i].jsReturnKind, + this->methodDescriptors_[i].methodName, + this->methodDescriptors_[i].selector, + args, + count); + }); + } + } + + jsi::Object constants = getConstants(runtime).asObject(runtime); + jsi::Value constant = constants.getProperty(runtime, propName); + + if (!constant.isUndefined()) { + // TODO(T145105887): Output warning. Tried to access a constant as a + // property on the native module object. Please migrate to getConstants(). + } + + return constant; +} + +void ObjCInteropTurboModule::setInvocationArg( + jsi::Runtime &runtime, + const char *methodNameCStr, + const std::string &objCArgType, + const jsi::Value &jsiArg, + size_t index, + NSInvocation *inv, + NSMutableArray *retainedObjectsForInvocation) +{ + NSString *methodName = @(methodNameCStr); + std::string methodJsSignature = name_ + "." + methodNameCStr + "()"; + + NSString *argumentType = getArgumentTypeName(runtime, methodName, index); + std::string errorPrefix = methodJsSignature + ": Error while converting JavaScript argument " + + std::to_string(index) + " to Objective C type " + [argumentType UTF8String] + ". "; + + SEL selector = selectorForType(argumentType); + + if ([RCTConvert respondsToSelector:selector]) { + id objCArg = TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_); + + if (objCArgType == @encode(char)) { + char arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(unsigned char)) { + unsigned char arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(short)) { + short arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(unsigned short)) { + unsigned short arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(int)) { + int arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(unsigned int)) { + unsigned int arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(long)) { + long arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(unsigned long)) { + unsigned long arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(long long)) { + long long arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(unsigned long long)) { + unsigned long long arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(float)) { + float arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(double)) { + double arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(BOOL)) { + BOOL arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(SEL)) { + SEL arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(const char *)) { + const char *arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(void *)) { + void *arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(id)) { + id arg = RCTConvertTo(selector, objCArg); + if (arg) { + [retainedObjectsForInvocation addObject:arg]; + } + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType[0] == _C_STRUCT_B) { + NSMethodSignature *typeSignature = [RCTConvert methodSignatureForSelector:selector]; + NSInvocation *typeInvocation = [NSInvocation invocationWithMethodSignature:typeSignature]; + typeInvocation.selector = selector; + typeInvocation.target = [RCTConvert class]; + + void *returnValue = malloc(typeSignature.methodReturnLength); + if (!returnValue) { + // CWE - 391 : Unchecked error condition + // https://www.cvedetails.com/cwe-details/391/Unchecked-Error-Condition.html + // https://eli.thegreenplace.net/2009/10/30/handling-out-of-memory-conditions-in-c + abort(); + } + [typeInvocation setArgument:&objCArg atIndex:2]; + [typeInvocation invoke]; + + [typeInvocation getReturnValue:returnValue]; + [inv setArgument:returnValue atIndex:index + 2]; + free(returnValue); + return; + } + + const char *BLOCK_TYPE = @encode(__typeof__(^{ + })); + + if (objCArgType == BLOCK_TYPE) { + /** + * RCTModuleMethod doesn't actually call into RCTConvert in this case. + */ + id arg = [objCArg copy]; + if (arg) { + [retainedObjectsForInvocation addObject:arg]; + } + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + throw jsi::JSError(runtime, errorPrefix + "Objective C type " + [argumentType UTF8String] + " is unsupported."); + } + + if ([argumentType isEqualToString:@"RCTResponseSenderBlock"]) { + if (!(jsiArg.isObject() && jsiArg.asObject(runtime).isFunction(runtime))) { + throw jsi::JSError( + runtime, errorPrefix + "JavaScript argument must be a function. Got " + getType(runtime, jsiArg)); + } + + RCTResponseSenderBlock arg = + (RCTResponseSenderBlock)TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_); + if (arg) { + [retainedObjectsForInvocation addObject:arg]; + } + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if ([argumentType isEqualToString:@"RCTResponseErrorBlock"]) { + if (!(jsiArg.isObject() && jsiArg.asObject(runtime).isFunction(runtime))) { + throw jsi::JSError( + runtime, errorPrefix + "JavaScript argument must be a function. Got " + getType(runtime, jsiArg)); + } + + RCTResponseSenderBlock senderBlock = + (RCTResponseSenderBlock)TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_); + RCTResponseErrorBlock arg = ^(NSError *error) { + senderBlock(@[ RCTJSErrorFromNSError(error) ]); + }; + [retainedObjectsForInvocation addObject:arg]; + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if ([argumentType isEqualToString:@"RCTPromiseResolveBlock"] || + [argumentType isEqualToString:@"RCTPromiseRejectBlock"]) { + throw jsi::JSError( + runtime, + errorPrefix + "The TurboModule interop layer should not convert JavaScript arguments to " + + [argumentType UTF8String] + + " inside ObjCinteropTurboModule::setInvocationArg(). Please report this as an issue."); + } + + if ([argumentType hasPrefix:@"JS::"]) { + NSString *selectorNameForCxxType = + [[argumentType stringByReplacingOccurrencesOfString:@"::" withString:@"_"] stringByAppendingString:@":"]; + selector = NSSelectorFromString(selectorNameForCxxType); + + bool isPlainObject = jsiArg.isObject() && !jsiArg.asObject(runtime).isFunction(runtime) && + !jsiArg.asObject(runtime).isArray(runtime); + if (!isPlainObject) { + throw jsi::JSError( + runtime, errorPrefix + "JavaScript argument must be an object. Got " + getType(runtime, jsiArg)); + } + + id arg = TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_); + + RCTManagedPointer *(*convert)(id, SEL, id) = (__typeof__(convert))objc_msgSend; + RCTManagedPointer *box = convert([RCTCxxConvert class], selector, arg); + + void *pointer = box.voidPointer; + [inv setArgument:&pointer atIndex:index + 2]; + [retainedObjectsForInvocation addObject:box]; + return; + } +} + +jsi::Value ObjCInteropTurboModule::convertReturnIdToJSIValue( + jsi::Runtime &runtime, + const char *methodNameCStr, + TurboModuleMethodValueKind returnType, + id result) +{ + std::string methodJsSignature = name_ + "." + methodNameCStr + "()"; + std::string errorPrefix = + methodJsSignature + ": Error while converting return Objective C value to JavaScript type. "; + + if (returnType == VoidKind) { + return jsi::Value::undefined(); + } + + if (result == (id)kCFNull || result == nil) { + return jsi::Value::null(); + } + + jsi::Value returnValue = TurboModuleConvertUtils::convertObjCObjectToJSIValue(runtime, result); + if (!returnValue.isUndefined()) { + return returnValue; + } + + throw jsi::JSError(runtime, methodJsSignature + "Objective C type was unsupported."); +} + +NSString *ObjCInteropTurboModule::getArgumentTypeName(jsi::Runtime &runtime, NSString *methodName, int argIndex) +{ + const char *methodNameCStr = [methodName UTF8String]; + std::string methodJsSignature = name_ + "." + methodNameCStr + "()"; + std::string errorPrefix = + methodJsSignature + ": Error while trying to get Objective C type of parameter " + std::to_string(argIndex) + "."; + + if (methodArgumentTypeNames_[methodName] == nil) { + throw jsi::JSError(runtime, errorPrefix + "No parameter types found for method."); + } + + if ([methodArgumentTypeNames_[methodName] count] <= argIndex) { + int paramCount = [methodArgumentTypeNames_[methodName] count]; + throw jsi::JSError(runtime, errorPrefix + "Method has only " + std::to_string(paramCount) + " parameter types."); + } + + return methodArgumentTypeNames_[methodName][argIndex]; +} + +bool ObjCInteropTurboModule::exportsConstants() +{ + for (size_t i = 0; i < methodDescriptors_.size(); i += 1) { + if (methodDescriptors_[i].methodName == "getConstants") { + return true; + } + } + + return false; +} + +const jsi::Value &ObjCInteropTurboModule::getConstants(jsi::Runtime &runtime) +{ + if (!constantsCache_.isUndefined()) { + return constantsCache_; + } + + if (!exportsConstants()) { + constantsCache_ = jsi::Object(runtime); + return constantsCache_; + } + + jsi::Value getConstantsProp = get(runtime, jsi::PropNameID::forAscii(runtime, "getConstants")); + + if (getConstantsProp.isObject()) { + jsi::Object getConstantsObj = getConstantsProp.asObject(runtime); + if (getConstantsObj.isFunction(runtime)) { + jsi::Function getConstantsFn = getConstantsObj.asFunction(runtime); + getConstantsFn.call(runtime); + return constantsCache_; + } + } + + // Unable to invoke the getConstants() method. + // Maybe the module didn't define a getConstants() method. + // Default constants to {}, so no constants are spread into the NativeModule + constantsCache_ = jsi::Object(runtime); + return constantsCache_; +} + +std::vector ObjCInteropTurboModule::getPropertyNames(facebook::jsi::Runtime &runtime) +{ + std::vector propNames = ObjCTurboModule::getPropertyNames(runtime); + + jsi::Object constants = getConstants(runtime).asObject(runtime); + jsi::Array constantNames = constants.getPropertyNames(runtime); + + for (size_t i = 0; i < constantNames.size(runtime); i += 1) { + jsi::Value constantName = constantNames.getValueAtIndex(runtime, i); + if (constantName.isString()) { + propNames.push_back(jsi::PropNameID::forString(runtime, constantName.asString(runtime))); + } + } + + return propNames; +} + +} // namespace react +} // namespace facebook 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 4dc6b8f2330..5b91c5b949d 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 @@ -30,6 +30,7 @@ class Instance; namespace TurboModuleConvertUtils { jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value); +id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, std::shared_ptr jsInvoker); } /** @@ -61,12 +62,52 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule { protected: void setMethodArgConversionSelector(NSString *methodName, int argIndex, NSString *fnName); + + /** + * Why is this virtual? + * + * Purpose: Converts native module method returns from Objective C values to JavaScript values. + * + * ObjCTurboModule uses TurboModuleMethodValueKind to convert returns from Objective C values to JavaScript values. + * ObjCInteropTurboModule just blindly converts returns from Objective C values to JavaScript values by runtime type, + * because it cannot infer TurboModuleMethodValueKind from the RCT_EXPORT_METHOD annotations. + */ virtual jsi::Value convertReturnIdToJSIValue( jsi::Runtime &runtime, const char *methodName, TurboModuleMethodValueKind returnType, id result); + /** + * Why is this virtual? + * + * Purpose: Get a native module method's argument's type, given the method name, and argument index. + * + * ObjCInteropTurboModule computes the argument type names eagerly on module init. So, make this method virtual. That + * way, ObjCInteropTurboModule doesn't end up computing the argument types twice: once on module init, and second on + * method dispatch. + */ + virtual NSString *getArgumentTypeName(jsi::Runtime &runtime, NSString *methodName, int argIndex); + + /** + * Why is this virtual? + * + * Purpose: Convert arguments from JavaScript values to Objective C values. Assign the Objective C argument to the + * method invocation. + * + * ObjCInteropTurboModule relies heavily on RCTConvert to convert arguments from JavaScript values to Objective C + * values. ObjCTurboModule tries to minimize reliance on RCTConvert: RCTConvert uses the RCT_EXPORT_METHOD macros, + * which we want to remove long term from React Native. + */ + virtual void setInvocationArg( + jsi::Runtime &runtime, + const char *methodName, + const std::string &objCArgType, + const jsi::Value &arg, + size_t i, + NSInvocation *inv, + NSMutableArray *retainedObjectsForInvocation); + private: // Does the NativeModule dispatch async methods to the JS thread? const bool isSyncModule_; @@ -74,6 +115,8 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule { /** * TODO(ramanpreet): * Investigate an optimization that'll let us get rid of this NSMutableDictionary. + * Perhaps, have the code-generated TurboModule subclass implement + * getMethodArgConversionSelector below. */ NSMutableDictionary *methodArgConversionSelectors_; NSDictionary *> *methodArgumentTypeNames_; @@ -81,7 +124,6 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule { bool isMethodSync(TurboModuleMethodValueKind returnType); BOOL hasMethodArgConversionSelector(NSString *methodName, int argIndex); SEL getMethodArgConversionSelector(NSString *methodName, int argIndex); - NSString *getArgumentTypeName(jsi::Runtime &runtime, NSString *methodName, int argIndex); NSInvocation *createMethodInvocation( jsi::Runtime &runtime, bool isSync, @@ -90,14 +132,6 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule { const jsi::Value *args, size_t count, NSMutableArray *retainedObjectsForInvocation); - void setInvocationArg( - jsi::Runtime &runtime, - const char *methodName, - const std::string &objCArgType, - const jsi::Value &arg, - size_t i, - NSInvocation *inv, - NSMutableArray *retainedObjectsForInvocation); id performMethodInvocation( jsi::Runtime &runtime, bool isSync, 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 93271d2f818..ce9a0ffcf30 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 @@ -105,8 +105,6 @@ jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value) return jsi::Value::undefined(); } -static id -convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, std::shared_ptr jsInvoker); static NSString *convertJSIStringToNSString(jsi::Runtime &runtime, const jsi::String &value) { return [NSString stringWithUTF8String:value.utf8(runtime).c_str()]; @@ -144,8 +142,7 @@ convertJSIObjectToNSDictionary(jsi::Runtime &runtime, const jsi::Object &value, static RCTResponseSenderBlock convertJSIFunctionToCallback(jsi::Runtime &runtime, const jsi::Function &value, std::shared_ptr jsInvoker); -static 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) { if (value.isUndefined() || value.isNull()) { return nil; diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm index 2319a9c0d6d..18d8459cdcd 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm @@ -23,6 +23,7 @@ #import #import #import +#import #import #import #import