From db0c22192c68967f3b69e5069ac9c94e73d0fce2 Mon Sep 17 00:00:00 2001 From: Alex Dvornikov Date: Thu, 27 Apr 2017 11:49:49 -0700 Subject: [PATCH] Added support for synchronous methods in native modules on iOS Reviewed By: javache Differential Revision: D4947556 fbshipit-source-id: 0ef73dc5d741201e59fef1fc048809afc65c75b5 --- .../UIExplorerUnitTests/RCTAllocationTests.m | 2 +- .../RCTModuleMethodTests.m | 35 ++++------ React/Base/RCTBridgeMethod.h | 11 ++++ React/Base/RCTBridgeModule.h | 51 +++++++++++++-- React/Base/RCTModuleData.mm | 5 +- React/Base/RCTModuleMethod.h | 1 + React/Base/RCTModuleMethod.m | 30 ++++++++- React/CxxModule/RCTNativeModule.h | 1 + React/CxxModule/RCTNativeModule.mm | 65 +++++++++---------- 9 files changed, 134 insertions(+), 67 deletions(-) diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m index 08b98449c0b..4b3f96fe883 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m @@ -151,7 +151,7 @@ RCT_EXPORT_METHOD(test:(__unused NSString *)a { __weak RCTModuleMethod *weakMethod; @autoreleasepool { - __autoreleasing RCTModuleMethod *method = [[RCTModuleMethod alloc] initWithMethodSignature:@"test:(NSString *)a :(nonnull NSNumber *)b :(RCTResponseSenderBlock)c :(RCTResponseErrorBlock)d" JSMethodName:@"" moduleClass:[AllocationTestModule class]]; + __autoreleasing RCTModuleMethod *method = [[RCTModuleMethod alloc] initWithMethodSignature:@"test:(NSString *)a :(nonnull NSNumber *)b :(RCTResponseSenderBlock)c :(RCTResponseErrorBlock)d" JSMethodName:@"" isSync:NO moduleClass:[AllocationTestModule class]]; weakMethod = method; XCTAssertNotNil(method, @"RCTModuleMethod should have been created"); } diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m index 37104083260..18ee3f38dc5 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m @@ -41,6 +41,13 @@ static BOOL RCTLogsError(void (^block)(void)) CGRect _s; } +static RCTModuleMethod *buildDefaultMethodWithMethodSignature(NSString *methodSignature) { + return [[RCTModuleMethod alloc] initWithMethodSignature:methodSignature + JSMethodName:nil + isSync:NO + moduleClass:[RCTModuleMethodTests class]]; +} + + (NSString *)moduleName { return nil; } - (void)doFooWithBar:(__unused NSString *)bar { } @@ -48,9 +55,7 @@ static BOOL RCTLogsError(void (^block)(void)) - (void)testNonnull { NSString *methodSignature = @"doFooWithBar:(nonnull NSString *)bar"; - RCTModuleMethod *method = [[RCTModuleMethod alloc] initWithMethodSignature:methodSignature - JSMethodName:nil - moduleClass:[self class]]; + RCTModuleMethod *method = buildDefaultMethodWithMethodSignature(methodSignature); XCTAssertFalse(RCTLogsError(^{ [method invokeWithBridge:nil module:self arguments:@[@"Hello World"]]; })); @@ -73,9 +78,7 @@ static BOOL RCTLogsError(void (^block)(void)) // Specifying an NSNumber param without nonnull isn't allowed XCTAssertTrue(RCTLogsError(^{ NSString *methodSignature = @"doFooWithNumber:(NSNumber *)n"; - RCTModuleMethod *method = [[RCTModuleMethod alloc] initWithMethodSignature:methodSignature - JSMethodName:nil - moduleClass:[self class]]; + RCTModuleMethod *method = buildDefaultMethodWithMethodSignature(methodSignature); // Invoke method to trigger parsing [method invokeWithBridge:nil module:self arguments:@[@1]]; })); @@ -83,9 +86,7 @@ static BOOL RCTLogsError(void (^block)(void)) { NSString *methodSignature = @"doFooWithNumber:(nonnull NSNumber *)n"; - RCTModuleMethod *method = [[RCTModuleMethod alloc] initWithMethodSignature:methodSignature - JSMethodName:nil - moduleClass:[self class]]; + RCTModuleMethod *method = buildDefaultMethodWithMethodSignature(methodSignature); XCTAssertTrue(RCTLogsError(^{ [method invokeWithBridge:nil module:self arguments:@[[NSNull null]]]; })); @@ -93,9 +94,7 @@ static BOOL RCTLogsError(void (^block)(void)) { NSString *methodSignature = @"doFooWithDouble:(double)n"; - RCTModuleMethod *method = [[RCTModuleMethod alloc] initWithMethodSignature:methodSignature - JSMethodName:nil - moduleClass:[self class]]; + RCTModuleMethod *method = buildDefaultMethodWithMethodSignature(methodSignature); XCTAssertTrue(RCTLogsError(^{ [method invokeWithBridge:nil module:self arguments:@[[NSNull null]]]; })); @@ -103,9 +102,7 @@ static BOOL RCTLogsError(void (^block)(void)) { NSString *methodSignature = @"doFooWithInteger:(NSInteger)n"; - RCTModuleMethod *method = [[RCTModuleMethod alloc] initWithMethodSignature:methodSignature - JSMethodName:nil - moduleClass:[self class]]; + RCTModuleMethod *method = buildDefaultMethodWithMethodSignature(methodSignature); XCTAssertTrue(RCTLogsError(^{ [method invokeWithBridge:nil module:self arguments:@[[NSNull null]]]; })); @@ -115,9 +112,7 @@ static BOOL RCTLogsError(void (^block)(void)) - (void)testStructArgument { NSString *methodSignature = @"doFooWithCGRect:(CGRect)s"; - RCTModuleMethod *method = [[RCTModuleMethod alloc] initWithMethodSignature:methodSignature - JSMethodName:nil - moduleClass:[self class]]; + RCTModuleMethod *method = buildDefaultMethodWithMethodSignature(methodSignature); CGRect r = CGRectMake(10, 20, 30, 40); [method invokeWithBridge:nil module:self arguments:@[@[@10, @20, @30, @40]]]; @@ -130,9 +125,7 @@ static BOOL RCTLogsError(void (^block)(void)) __block RCTModuleMethod *method; XCTAssertFalse(RCTLogsError(^{ - method = [[RCTModuleMethod alloc] initWithMethodSignature:methodSignature - JSMethodName:nil - moduleClass:[self class]]; + method = buildDefaultMethodWithMethodSignature(methodSignature); })); XCTAssertEqualObjects(method.JSMethodName, @"doFoo"); diff --git a/React/Base/RCTBridgeMethod.h b/React/Base/RCTBridgeMethod.h index 657e80a86c0..37ca1bb4368 100644 --- a/React/Base/RCTBridgeMethod.h +++ b/React/Base/RCTBridgeMethod.h @@ -17,6 +17,17 @@ typedef NS_ENUM(NSUInteger, RCTFunctionType) { RCTFunctionTypeSync, }; +static inline const char *RCTFunctionDescriptorFromType(RCTFunctionType type) { + switch (type) { + case RCTFunctionTypeNormal: + return "async"; + case RCTFunctionTypePromise: + return "promise"; + case RCTFunctionTypeSync: + return "sync"; + } +}; + @protocol RCTBridgeMethod @property (nonatomic, copy, readonly) NSString *JSMethodName; diff --git a/React/Base/RCTBridgeModule.h b/React/Base/RCTBridgeModule.h index 4bfa4b3210a..8e49ae31c82 100644 --- a/React/Base/RCTBridgeModule.h +++ b/React/Base/RCTBridgeModule.h @@ -144,6 +144,25 @@ RCT_EXTERN void RCTRegisterModule(Class); \ #define RCT_EXPORT_METHOD(method) \ RCT_REMAP_METHOD(, method) +/** + * Same as RCT_EXPORT_METHOD but the method is called from JS + * synchronously **on the JS thread**, possibly returning a result. + * + * WARNING: in the vast majority of cases, you should use RCT_EXPORT_METHOD which + * allows your native module methods to be called asynchronously: calling + * methods synchronously can have strong performance penalties and introduce + * threading-related bugs to your native modules. + * + * The return type must be of object type (id) and should be serializable + * to JSON. This means that the hook can only return nil or JSON values + * (e.g. NSNumber, NSString, NSArray, NSDictionary). + * + * Calling these methods when running under the websocket executor + * is currently not supported. + */ +#define RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(method) \ + RCT_REMAP_BLOCKING_SYNCHRONOUS_METHOD(, method) + /** * Similar to RCT_EXPORT_METHOD but lets you set the JS name of the exported * method. Example usage: @@ -153,9 +172,21 @@ RCT_EXTERN void RCTRegisterModule(Class); \ * { ... } */ #define RCT_REMAP_METHOD(js_name, method) \ - RCT_EXTERN_REMAP_METHOD(js_name, method) \ + RCT_EXTERN_REMAP_METHOD(js_name, method, NO) \ - (void)method; +/** + * Similar to RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD but lets you set + * the JS name of the exported method. Example usage: + * + * RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(executeQueryWithParameters, + * executeQuery:(NSString *)query parameters:(NSDictionary *)parameters) + * { ... } + */ +#define RCT_REMAP_BLOCKING_SYNCHRONOUS_METHOD(js_name, method) \ + RCT_EXTERN_REMAP_METHOD(js_name, method, YES) \ + - (id)method; + /** * Use this macro in a private Objective-C implementation file to automatically * register an external module with the bridge when it loads. This allows you to @@ -203,15 +234,23 @@ RCT_EXTERN void RCTRegisterModule(Class); \ * of an external module. */ #define RCT_EXTERN_METHOD(method) \ - RCT_EXTERN_REMAP_METHOD(, method) + RCT_EXTERN_REMAP_METHOD(, method, NO) /** - * Like RCT_EXTERN_REMAP_METHOD, but allows setting a custom JavaScript name. + * Use this macro in accordance with RCT_EXTERN_MODULE to export methods + * of an external module that should be invoked synchronously. */ -#define RCT_EXTERN_REMAP_METHOD(js_name, method) \ - + (NSArray *)RCT_CONCAT(__rct_export__, \ +#define RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(method) \ + RCT_EXTERN_REMAP_METHOD(, method, YES) + +/** + * Like RCT_EXTERN_REMAP_METHOD, but allows setting a custom JavaScript name + * and also whether this method is synchronous. + */ +#define RCT_EXTERN_REMAP_METHOD(js_name, method, is_blocking_synchronous_method) \ + + (NSArray *)RCT_CONCAT(__rct_export__, \ RCT_CONCAT(js_name, RCT_CONCAT(__LINE__, __COUNTER__))) { \ - return @[@#js_name, @#method]; \ + return @[@#js_name, @#method, @is_blocking_synchronous_method]; \ } /** diff --git a/React/Base/RCTModuleData.mm b/React/Base/RCTModuleData.mm index e3ad7641a26..cc70c13e181 100644 --- a/React/Base/RCTModuleData.mm +++ b/React/Base/RCTModuleData.mm @@ -268,11 +268,12 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init); SEL selector = method_getName(method); if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) { IMP imp = method_getImplementation(method); - NSArray *entries = - ((NSArray *(*)(id, SEL))imp)(_moduleClass, selector); + NSArray *entries = + ((NSArray *(*)(id, SEL))imp)(_moduleClass, selector); id moduleMethod = [[RCTModuleMethod alloc] initWithMethodSignature:entries[1] JSMethodName:entries[0] + isSync:((NSNumber *)entries[2]).boolValue moduleClass:_moduleClass]; [moduleMethods addObject:moduleMethod]; diff --git a/React/Base/RCTModuleMethod.h b/React/Base/RCTModuleMethod.h index 977b6c13dc2..3f2800b23cc 100644 --- a/React/Base/RCTModuleMethod.h +++ b/React/Base/RCTModuleMethod.h @@ -29,6 +29,7 @@ - (instancetype)initWithMethodSignature:(NSString *)objCMethodName JSMethodName:(NSString *)JSMethodName + isSync:(BOOL)isSync moduleClass:(Class)moduleClass NS_DESIGNATED_INITIALIZER; @end diff --git a/React/Base/RCTModuleMethod.m b/React/Base/RCTModuleMethod.m index f598ace7787..21e753e248a 100644 --- a/React/Base/RCTModuleMethod.m +++ b/React/Base/RCTModuleMethod.m @@ -45,6 +45,7 @@ typedef BOOL (^RCTArgumentBlock)(RCTBridge *, NSUInteger, id); NSArray *_argumentBlocks; NSString *_methodSignature; SEL _selector; + BOOL _isSync; } @synthesize JSMethodName = _JSMethodName; @@ -165,12 +166,14 @@ SEL RCTParseMethodSignature(NSString *methodSignature, NSArray", - [self class], self, [self methodName], self.JSMethodName]; + NSString *descriptor = [NSString stringWithCString:RCTFunctionDescriptorFromType(self.functionType) + encoding:NSString.defaultCStringEncoding]; + return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@(); type: %@>", + [self class], self, [self methodName], self.JSMethodName, descriptor]; } @end diff --git a/React/CxxModule/RCTNativeModule.h b/React/CxxModule/RCTNativeModule.h index 16b97593355..e4d1b0892be 100644 --- a/React/CxxModule/RCTNativeModule.h +++ b/React/CxxModule/RCTNativeModule.h @@ -26,6 +26,7 @@ class RCTNativeModule : public NativeModule { private: __weak RCTBridge *m_bridge; RCTModuleData *m_moduleData; + MethodCallResult invokeInner(unsigned int methodId, const folly::dynamic &¶ms); }; } diff --git a/React/CxxModule/RCTNativeModule.mm b/React/CxxModule/RCTNativeModule.mm index 9690c5458c9..3402bc098a8 100644 --- a/React/CxxModule/RCTNativeModule.mm +++ b/React/CxxModule/RCTNativeModule.mm @@ -34,7 +34,7 @@ std::vector RCTNativeModule::getMethods() { for (id method in m_moduleData.methods) { descs.emplace_back( method.JSMethodName.UTF8String, - method.functionType == RCTFunctionTypePromise ? "promise" : "async" + RCTFunctionDescriptorFromType(method.functionType) ); } @@ -54,40 +54,12 @@ void RCTNativeModule::invoke(unsigned int methodId, folly::dynamic &¶ms) { // The BatchedBridge version of this buckets all the callbacks by thread, and // queues one block on each. This is much simpler; we'll see how it goes and // iterate. - - // There is no flow event handling here until I can understand it. - - auto sparams = std::make_shared(std::move(params)); - - __weak RCTBridge *bridge = m_bridge; - - dispatch_block_t block = ^{ - if (!bridge || !bridge.valid) { + dispatch_block_t block = [this, methodId, params=std::move(params)] { + if (!m_bridge.valid) { return; } - id method = m_moduleData.methods[methodId]; - if (RCT_DEBUG && !method) { - RCTLogError(@"Unknown methodID: %ud for module: %@", - methodId, m_moduleData.name); - } - - NSArray *objcParams = convertFollyDynamicToId(*sparams); - - @try { - [method invokeWithBridge:bridge module:m_moduleData.instance arguments:objcParams]; - } - @catch (NSException *exception) { - // Pass on JS exceptions - if ([exception.name hasPrefix:RCTFatalExceptionName]) { - @throw exception; - } - - NSString *message = [NSString stringWithFormat: - @"Exception '%@' was thrown while invoking %@ on target %@ with params %@", - exception, method.JSMethodName, m_moduleData.name, objcParams]; - RCTFatal(RCTErrorWithMessage(message)); - } + invokeInner(methodId, std::move(params)); }; dispatch_queue_t queue = m_moduleData.methodQueue; @@ -100,10 +72,35 @@ void RCTNativeModule::invoke(unsigned int methodId, folly::dynamic &¶ms) { } MethodCallResult RCTNativeModule::callSerializableNativeHook(unsigned int reactMethodId, folly::dynamic &¶ms) { - RCTFatal(RCTErrorWithMessage(@"callSerializableNativeHook is not yet supported on iOS")); - return folly::none; + return invokeInner(reactMethodId, std::move(params)); } +MethodCallResult RCTNativeModule::invokeInner(unsigned int methodId, const folly::dynamic &¶ms) { + id method = m_moduleData.methods[methodId]; + if (RCT_DEBUG && !method) { + RCTLogError(@"Unknown methodID: %ud for module: %@", + methodId, m_moduleData.name); + } + + NSArray *objcParams = convertFollyDynamicToId(params); + + @try { + id result = [method invokeWithBridge:m_bridge module:m_moduleData.instance arguments:objcParams]; + return convertIdToFollyDynamic(result); + } + @catch (NSException *exception) { + // Pass on JS exceptions + if ([exception.name hasPrefix:RCTFatalExceptionName]) { + @throw exception; + } + + NSString *message = [NSString stringWithFormat: + @"Exception '%@' was thrown while invoking %@ on target %@ with params %@", + exception, method.JSMethodName, m_moduleData.name, objcParams]; + RCTFatal(RCTErrorWithMessage(message)); + } + +} } }