From 8618a5824f3eca6d2dd6ba13399a2e0f96e2f6c9 Mon Sep 17 00:00:00 2001 From: Ramanpreet Nara Date: Fri, 22 Mar 2019 15:58:50 -0700 Subject: [PATCH] Add support for argument conversion via RCTConvert Summary: With our current infra, we support automatic conversion of method arguments using `RCTConvert`. ``` RCT_EXPORT_METHOD(foo:(RCTSound*) sound) { //... } ``` ``` interface RCTConvert (RCTSound) + (RCTSound *) RCTSound: (NSDictionary *) dict; end implementation RCTConvert (RCTSound) + (RCTSound *) RCTSound: (NSDictionary *) dict { //... } end ``` ``` export interface Spec extends TurboModule { +foo: (dict: Object) => void, } ``` With this setup, when we call the foo method on the TurboModule in JS, we'd first convert `dict` from a JS Object to an `NSDictionary`. Then, because the `foo` method has an argument of type`RCTSound*`, and because `RCTConvert` has a method called `RCTSound`, before we invoke the `foo` NativeModule native method, we first convert the `NSDictionary` to `RCTSound` using `[RCTConvert RCTSound:obj]`. Essentially, if an argument type of a TurboModule method is neither a primitive type nor a struct (i.e: is an identifier), and it corresponds to a selector on `RCTConvert`, we call `[RCTConvert argumentType:obj]` to convert `obj` to the type `argumentType` before passing in `obj` as an argument to the NativeModule method call. **Note:** I originally planned on using `NSMethodSignature` to get the argument types. Unfortunately, while the Objective C Runtime lets us know that the type is an identifier, it doesn't inform us which identifier it is. In other words, at runtime, we can't determine whether identifier represents `RCTSound *` or some other Objective C class. I figure this also the reason why the old code relies on the `RCT_EXPORT_METHOD` macros to implement this very same feature: https://git.io/fjJsC. It uses `NSMethodSignature` to switch on the argument type, and then uses the `RCTMethodInfo` struct to parse the argument type name, from which it constructs the RCTConvert selector. One caveat of the current solution is that it won't work work unless we decorate our TurboModule methods with `RCT_EXPORT_METHOD`. Reviewed By: fkgozali Differential Revision: D14582661 fbshipit-source-id: 3c7dfb2059f031dba7495f12cbdf406b14f0b5b4 --- React/Base/RCTModuleMethod.h | 2 + React/Base/RCTModuleMethod.mm | 1 - .../core/platform/ios/RCTTurboModule.h | 27 ++++-- .../core/platform/ios/RCTTurboModule.mm | 93 ++++++++++++++++++- 4 files changed, 111 insertions(+), 12 deletions(-) diff --git a/React/Base/RCTModuleMethod.h b/React/Base/RCTModuleMethod.h index 472b755afc..647440ca5b 100644 --- a/React/Base/RCTModuleMethod.h +++ b/React/Base/RCTModuleMethod.h @@ -30,3 +30,5 @@ moduleClass:(Class)moduleClass NS_DESIGNATED_INITIALIZER; @end + +RCT_EXTERN NSString *RCTParseMethodSignature(const char *input, NSArray **arguments); diff --git a/React/Base/RCTModuleMethod.mm b/React/Base/RCTModuleMethod.mm index 3d93a785b3..14963b19a6 100644 --- a/React/Base/RCTModuleMethod.mm +++ b/React/Base/RCTModuleMethod.mm @@ -129,7 +129,6 @@ static BOOL checkCallbackMultipleInvocations(BOOL *didInvoke) { } #endif -extern NSString *RCTParseMethodSignature(const char *input, NSArray **arguments); NSString *RCTParseMethodSignature(const char *input, NSArray **arguments) { RCTSkipWhitespace(&input); diff --git a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.h b/ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.h index cc4ed4d0ee..da78e21e11 100644 --- a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.h +++ b/ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.h @@ -11,6 +11,7 @@ #import #import +#import #import #import #import @@ -41,17 +42,25 @@ public: protected: void setMethodArgConversionSelector(NSString *methodName, int argIndex, NSString *fnName); private: + /** + * TODO(ramanpreet): + * Investigate an optimization that'll let us get rid of this NSMutableDictionary. + */ NSMutableDictionary *methodArgConversionSelectors_; + NSDictionary *> *methodArgumentTypeNames_; + NSString* getArgumentTypeName(NSString* methodName, int argIndex); + NSInvocation *getMethodInvocation( - jsi::Runtime &runtime, - TurboModuleMethodValueKind valueKind, - const id module, - std::shared_ptr jsInvoker, - const std::string& methodName, - SEL selector, - const jsi::Value *args, - size_t count, - NSMutableArray *retainedObjectsForInvocation); + jsi::Runtime &runtime, + TurboModuleMethodValueKind valueKind, + const id module, + std::shared_ptr jsInvoker, + const std::string& methodName, + SEL selector, + const jsi::Value *args, + size_t count, + NSMutableArray *retainedObjectsForInvocation); + BOOL hasMethodArgConversionSelector(NSString *methodName, int argIndex); SEL getMethodArgConversionSelector(NSString *methodName, int argIndex); }; diff --git a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.mm b/ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.mm index 1cd17d6f63..9cae5a3517 100644 --- a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.mm +++ b/ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.mm @@ -12,7 +12,9 @@ #import #import +#import #import +#import #import #import #import @@ -406,6 +408,63 @@ jsi::Value performMethodInvocation( } // namespace +/** + * Given a method name, and an argument index, return type type of that argument. + * Prerequisite: You must wrap the method declaration inside some variant of the + * RCT_EXPORT_METHOD macro. + * + * This method returns nil if the method for which you're querying the argument type + * is not wrapped in an RCT_EXPORT_METHOD. + * + * Note: This is only being introduced for backward compatibility. It will be removed + * in the future. + */ +NSString* ObjCTurboModule::getArgumentTypeName(NSString* methodName, int argIndex) { + if (!methodArgumentTypeNames_) { + NSMutableDictionary *> *methodArgumentTypeNames = [NSMutableDictionary new]; + + unsigned int numberOfMethods; + Class cls = [instance_ class]; + Method *methods = class_copyMethodList(object_getClass(cls), &numberOfMethods); + + if (methods) { + for (unsigned int i = 0; i < numberOfMethods; i++) { + SEL s = method_getName(methods[i]); + NSString* mName = NSStringFromSelector(s); + if (![mName hasPrefix:@"__rct_export__"]) { + continue; + } + + // Message dispatch logic from old infra + RCTMethodInfo *(*getMethodInfo)(id, SEL) = (__typeof__(getMethodInfo))objc_msgSend; + RCTMethodInfo *methodInfo = getMethodInfo(cls, s); + + NSArray *arguments; + NSString *otherMethodName = RCTParseMethodSignature(methodInfo->objcName, &arguments); + + NSMutableArray* argumentTypes = [NSMutableArray arrayWithCapacity:[arguments count]]; + for (int j = 0; j < [arguments count]; j += 1) { + [argumentTypes addObject:arguments[j].type]; + } + + NSString *normalizedOtherMethodName = [otherMethodName stringByReplacingOccurrencesOfString:@":" withString:@""]; + methodArgumentTypeNames[normalizedOtherMethodName] = argumentTypes; + } + + free(methods); + } + + methodArgumentTypeNames_ = methodArgumentTypeNames; + } + + if (methodArgumentTypeNames_[methodName]) { + assert([methodArgumentTypeNames_[methodName] count] > argIndex); + return methodArgumentTypeNames_[methodName][argIndex]; + } + + return nil; +} + NSInvocation *ObjCTurboModule::getMethodInvocation( jsi::Runtime &runtime, TurboModuleMethodValueKind valueKind, @@ -430,6 +489,36 @@ NSInvocation *ObjCTurboModule::getMethodInvocation( id v = convertJSIValueToObjCObject(runtime, *arg, jsInvoker); NSString *methodNameObjc = @(methodName.c_str()); + NSMethodSignature *methodSignature = [[module class] instanceMethodSignatureForSelector:selector]; + const char *objcType = [methodSignature getArgumentTypeAtIndex:i]; + + if (objcType[0] == _C_ID) { + NSString* argumentType = getArgumentTypeName(methodNameObjc, i); + + /** + * When argumentType is nil, it means that the method hasn't been wrapped with + * an RCT_EXPORT_METHOD macro. Therefore, we do not support converting the method + * arguments using RCTConvert. + */ + if (argumentType != nil) { + NSString *rctConvertMethodName = [NSString stringWithFormat:@"%@:", argumentType]; + SEL rctConvertSelector = NSSelectorFromString(rctConvertMethodName); + + if ([RCTConvert respondsToSelector: rctConvertSelector]) { + // Message dispatch logic from old infra + id (*convert)(id, SEL, id) = (__typeof__(convert))objc_msgSend; + v = convert([RCTConvert class], rctConvertSelector, v); + + /** + * TODO(ramanpreet): + * Investigate whether we can avoid inserting to retainedObjectsForInvocation. + * Otherwise, NSInvocation raises a BAD_ACCESS when we invoke the retainArguments method. + **/ + [retainedObjectsForInvocation addObject:v]; + } + } + } + if ([v isKindOfClass:[NSDictionary class]] && hasMethodArgConversionSelector(methodNameObjc, i)) { SEL methodArgConversionSelector = getMethodArgConversionSelector(methodNameObjc, i); @@ -488,12 +577,12 @@ jsi::Value ObjCTurboModule::invokeMethod( BOOL ObjCTurboModule::hasMethodArgConversionSelector(NSString *methodName, int argIndex) { return methodArgConversionSelectors_ && methodArgConversionSelectors_[methodName] && ![methodArgConversionSelectors_[methodName][argIndex] isEqual:[NSNull null]]; } - + SEL ObjCTurboModule::getMethodArgConversionSelector(NSString *methodName, int argIndex) { assert(hasMethodArgConversionSelector(methodName, argIndex)); return (SEL)((NSValue *)methodArgConversionSelectors_[methodName][argIndex]).pointerValue; } - + void ObjCTurboModule::setMethodArgConversionSelector(NSString *methodName, int argIndex, NSString *fnName) { if (!methodArgConversionSelectors_) { methodArgConversionSelectors_ = [NSMutableDictionary new];