Added support for synchronous methods in native modules on iOS
Reviewed By: javache Differential Revision: D4947556 fbshipit-source-id: 0ef73dc5d741201e59fef1fc048809afc65c75b5
This commit is contained in:
parent
2d3a272e0e
commit
db0c22192c
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
* 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<NSString *> *)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]; \
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<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 @@ typedef BOOL (^RCTArgumentBlock)(RCTBridge *, NSUInteger, id);
|
|||
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 @@ SEL RCTParseMethodSignature(NSString *methodSignature, NSArray<RCTMethodArgument
|
|||
}
|
||||
}
|
||||
|
||||
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 @@ SEL RCTParseMethodSignature(NSString *methodSignature, NSArray<RCTMethodArgument
|
|||
- (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 @@ SEL RCTParseMethodSignature(NSString *methodSignature, NSArray<RCTMethodArgument
|
|||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
id result = nil;
|
||||
|
||||
if (_isSync) {
|
||||
void *pointer;
|
||||
[_invocation getReturnValue:&pointer];
|
||||
result = (__bridge id)pointer;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSString *)methodName
|
||||
|
@ -539,8 +561,10 @@ SEL RCTParseMethodSignature(NSString *methodSignature, NSArray<RCTMethodArgument
|
|||
|
||||
- (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 &¶ms);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ std::vector<MethodDescriptor> RCTNativeModule::getMethods() {
|
|||
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 @@ 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<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 @@ 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<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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue