Permalink
Browse files

Added support for synchronous methods in native modules on iOS

Reviewed By: javache

Differential Revision: D4947556

fbshipit-source-id: 0ef73dc5d741201e59fef1fc048809afc65c75b5
  • Loading branch information...
fromcelticpark authored and facebook-github-bot committed Apr 27, 2017
1 parent 2d3a272 commit db0c22192c68967f3b69e5069ac9c94e73d0fce2
@@ -151,7 +151,7 @@ - (void)testModuleMethodsAreDeallocated
{
__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");
}
@@ -41,16 +41,21 @@ @implementation RCTModuleMethodTests
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 { }
- (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,39 +78,31 @@ - (void)testNumbersNonnull
// 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]];
}));
}
{
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]]];
}));
}
{
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]]];
}));
}
{
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 @@ - (void)testNumbersNonnull
- (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 @@ - (void)testWhitespaceTolerance
__block RCTModuleMethod *method;
XCTAssertFalse(RCTLogsError(^{
method = [[RCTModuleMethod alloc] initWithMethodSignature:methodSignature
JSMethodName:nil
moduleClass:[self class]];
method = buildDefaultMethodWithMethodSignature(methodSignature);
}));
XCTAssertEqualObjects(method.JSMethodName, @"doFoo");
@@ -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 <NSObject>
@property (nonatomic, copy, readonly) NSString *JSMethodName;
@@ -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

This comment has been minimized.

Show comment
Hide comment
@ssssssssssss

ssssssssssss Apr 28, 2017

What's the typical use case here? Wrapping UITableView at ease?

@ssssssssssss

ssssssssssss Apr 28, 2017

What's the typical use case here? Wrapping UITableView at ease?

* 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)
/**
* Use this macro in accordance with RCT_EXTERN_MODULE to export methods
* of an external module that should be invoked synchronously.
*/
#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.
* 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) \
+ (NSArray<NSString *> *)RCT_CONCAT(__rct_export__, \
#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]; \
}
/**
@@ -268,11 +268,12 @@ - (NSString *)name
SEL selector = method_getName(method);
if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) {
IMP imp = method_getImplementation(method);
NSArray<NSString *> *entries =
((NSArray<NSString *> *(*)(id, SEL))imp)(_moduleClass, selector);
NSArray *entries =
((NSArray *(*)(id, SEL))imp)(_moduleClass, selector);
id<RCTBridgeMethod> moduleMethod =
[[RCTModuleMethod alloc] initWithMethodSignature:entries[1]
JSMethodName:entries[0]
isSync:((NSNumber *)entries[2]).boolValue
moduleClass:_moduleClass];
[moduleMethods addObject:moduleMethod];
@@ -29,6 +29,7 @@
- (instancetype)initWithMethodSignature:(NSString *)objCMethodName
JSMethodName:(NSString *)JSMethodName
isSync:(BOOL)isSync
moduleClass:(Class)moduleClass NS_DESIGNATED_INITIALIZER;
@end
@@ -45,6 +45,7 @@ @implementation RCTModuleMethod
NSArray<RCTArgumentBlock> *_argumentBlocks;
NSString *_methodSignature;
SEL _selector;
BOOL _isSync;
}
@synthesize JSMethodName = _JSMethodName;
@@ -165,12 +166,14 @@ SEL RCTParseMethodSignature(NSString *methodSignature, NSArray<RCTMethodArgument
- (instancetype)initWithMethodSignature:(NSString *)methodSignature
JSMethodName:(NSString *)JSMethodName
isSync:(BOOL)isSync
moduleClass:(Class)moduleClass
{
if (self = [super init]) {
_moduleClass = moduleClass;
_methodSignature = [methodSignature copy];
_JSMethodName = [JSMethodName copy];
_isSync = isSync;
}
return self;
@@ -417,6 +420,13 @@ - (void)processMethodSignature
}
}
if (RCT_DEBUG) {
const char *objcType = _invocation.methodSignature.methodReturnType;
if (_isSync && objcType[0] != _C_ID)
RCTLogError(@"Return type of %@.%@ should be (id) as the method is \"sync\"",
RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName);
}
_argumentBlocks = [argumentBlocks copy];
}
@@ -450,7 +460,11 @@ - (NSString *)JSMethodName
- (RCTFunctionType)functionType
{
if ([_methodSignature rangeOfString:@"RCTPromise"].length) {
RCTAssert(_isSync == NO, @"Promises cannot be used in sync functions");
return RCTFunctionTypePromise;
} else if (_isSync) {
return RCTFunctionTypeSync;
} else {
return RCTFunctionTypeNormal;
}
@@ -525,7 +539,15 @@ - (id)invokeWithBridge:(RCTBridge *)bridge
}
}
return nil;
id result = nil;
if (_isSync) {
void *pointer;
[_invocation getReturnValue:&pointer];
result = (__bridge id)pointer;
}
return result;
}
- (NSString *)methodName
@@ -539,8 +561,10 @@ - (NSString *)methodName
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@()>",
[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
@@ -26,6 +26,7 @@ class RCTNativeModule : public NativeModule {
private:
__weak RCTBridge *m_bridge;
RCTModuleData *m_moduleData;
MethodCallResult invokeInner(unsigned int methodId, const folly::dynamic &&params);
};
}
@@ -34,7 +34,7 @@
for (id<RCTBridgeMethod> method in m_moduleData.methods) {
descs.emplace_back(
method.JSMethodName.UTF8String,
method.functionType == RCTFunctionTypePromise ? "promise" : "async"
RCTFunctionDescriptorFromType(method.functionType)
);
}
@@ -54,40 +54,12 @@
// 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<folly::dynamic>(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<RCTBridgeMethod> 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 @@
}
MethodCallResult RCTNativeModule::callSerializableNativeHook(unsigned int reactMethodId, folly::dynamic &&params) {
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 &&params) {
id<RCTBridgeMethod> 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));
}
}
}
}

0 comments on commit db0c221

Please sign in to comment.