react-native/React/Base/RCTModuleMethod.m

547 lines
18 KiB
Objective-C

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTModuleMethod.h"
#import <objc/message.h>
#import "RCTAssert.h"
#import "RCTBridge+Private.h"
#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTLog.h"
#import "RCTParserUtils.h"
#import "RCTProfile.h"
#import "RCTUtils.h"
typedef BOOL (^RCTArgumentBlock)(RCTBridge *, NSUInteger, id);
@implementation RCTMethodArgument
- (instancetype)initWithType:(NSString *)type
nullability:(RCTNullability)nullability
unused:(BOOL)unused
{
if (self = [super init]) {
_type = [type copy];
_nullability = nullability;
_unused = unused;
}
return self;
}
@end
@implementation RCTModuleMethod
{
Class _moduleClass;
NSInvocation *_invocation;
NSArray<RCTArgumentBlock> *_argumentBlocks;
NSString *_methodSignature;
SEL _selector;
}
@synthesize JSMethodName = _JSMethodName;
static void RCTLogArgumentError(RCTModuleMethod *method, NSUInteger index,
id valueOrType, const char *issue)
{
RCTLogError(@"Argument %tu (%@) of %@.%@ %s", index, valueOrType,
RCTBridgeModuleNameForClass(method->_moduleClass),
method->_JSMethodName, issue);
}
RCT_NOT_IMPLEMENTED(- (instancetype)init)
// returns YES if the selector ends in a colon (indicating that there is at
// least one argument, and maybe more selector parts) or NO if it doesn't.
static BOOL RCTParseSelectorPart(const char **input, NSMutableString *selector)
{
NSString *selectorPart;
if (RCTParseIdentifier(input, &selectorPart)) {
[selector appendString:selectorPart];
}
RCTSkipWhitespace(input);
if (RCTReadChar(input, ':')) {
[selector appendString:@":"];
RCTSkipWhitespace(input);
return YES;
}
return NO;
}
static BOOL RCTParseUnused(const char **input)
{
return RCTReadString(input, "__unused") ||
RCTReadString(input, "__attribute__((unused))");
}
static RCTNullability RCTParseNullability(const char **input)
{
if (RCTReadString(input, "nullable")) {
return RCTNullable;
} else if (RCTReadString(input, "nonnull")) {
return RCTNonnullable;
}
return RCTNullabilityUnspecified;
}
static RCTNullability RCTParseNullabilityPostfix(const char **input)
{
if (RCTReadString(input, "_Nullable")) {
return RCTNullable;
} else if (RCTReadString(input, "_Nonnull")) {
return RCTNonnullable;
}
return RCTNullabilityUnspecified;
}
// returns YES if execution is safe to proceed (enqueue callback invocation), NO if callback has already been invoked
static BOOL RCTCheckCallbackMultipleInvocations(BOOL *didInvoke) {
if (*didInvoke) {
RCTFatal(RCTErrorWithMessage(@"Illegal callback invocation from native module. This callback type only permits a single invocation from native code."));
return NO;
} else {
*didInvoke = YES;
return YES;
}
}
SEL RCTParseMethodSignature(NSString *, NSArray<RCTMethodArgument *> **);
SEL RCTParseMethodSignature(NSString *methodSignature, NSArray<RCTMethodArgument *> **arguments)
{
const char *input = methodSignature.UTF8String;
RCTSkipWhitespace(&input);
NSMutableArray *args;
NSMutableString *selector = [NSMutableString new];
while (RCTParseSelectorPart(&input, selector)) {
if (!args) {
args = [NSMutableArray new];
}
// Parse type
if (RCTReadChar(&input, '(')) {
RCTSkipWhitespace(&input);
BOOL unused = RCTParseUnused(&input);
RCTSkipWhitespace(&input);
RCTNullability nullability = RCTParseNullability(&input);
RCTSkipWhitespace(&input);
NSString *type = RCTParseType(&input);
RCTSkipWhitespace(&input);
if (nullability == RCTNullabilityUnspecified) {
nullability = RCTParseNullabilityPostfix(&input);
}
[args addObject:[[RCTMethodArgument alloc] initWithType:type
nullability:nullability
unused:unused]];
RCTSkipWhitespace(&input);
RCTReadChar(&input, ')');
RCTSkipWhitespace(&input);
} else {
// Type defaults to id if unspecified
[args addObject:[[RCTMethodArgument alloc] initWithType:@"id"
nullability:RCTNullable
unused:NO]];
}
// Argument name
RCTParseIdentifier(&input, NULL);
RCTSkipWhitespace(&input);
}
*arguments = [args copy];
return NSSelectorFromString(selector);
}
- (instancetype)initWithMethodSignature:(NSString *)methodSignature
JSMethodName:(NSString *)JSMethodName
moduleClass:(Class)moduleClass
{
if (self = [super init]) {
_moduleClass = moduleClass;
_methodSignature = [methodSignature copy];
_JSMethodName = [JSMethodName copy];
}
return self;
}
- (void)processMethodSignature
{
NSArray<RCTMethodArgument *> *arguments;
_selector = RCTParseMethodSignature(_methodSignature, &arguments);
RCTAssert(_selector, @"%@ is not a valid selector", _methodSignature);
// Create method invocation
NSMethodSignature *methodSignature = [_moduleClass instanceMethodSignatureForSelector:_selector];
RCTAssert(methodSignature, @"%@ is not a recognized Objective-C method.", _methodSignature);
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.selector = _selector;
_invocation = invocation;
// Process arguments
NSUInteger numberOfArguments = methodSignature.numberOfArguments;
NSMutableArray<RCTArgumentBlock> *argumentBlocks =
[[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2];
#define RCT_ARG_BLOCK(_logic) \
[argumentBlocks addObject:^(__unused RCTBridge *bridge, NSUInteger index, id json) { \
_logic \
[invocation setArgument:&value atIndex:(index) + 2]; \
return YES; \
}];
/**
* Explicitly copy the block and retain it, since NSInvocation doesn't retain them.
*/
#define RCT_BLOCK_ARGUMENT(block...) \
id value = json ? [block copy] : (id)^(__unused NSArray *_){}; \
CFBridgingRetain(value)
__weak RCTModuleMethod *weakSelf = self;
void (^addBlockArgument)(void) = ^{
RCT_ARG_BLOCK(
if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) {
RCTLogArgumentError(weakSelf, index, json, "should be a function");
return NO;
}
__block BOOL didInvoke = NO;
RCT_BLOCK_ARGUMENT(^(NSArray *args) {
if (RCTCheckCallbackMultipleInvocations(&didInvoke)) {
[bridge enqueueCallback:json args:args];
}
});
)
};
for (NSUInteger i = 2; i < numberOfArguments; i++) {
const char *objcType = [methodSignature getArgumentTypeAtIndex:i];
BOOL isNullableType = NO;
RCTMethodArgument *argument = arguments[i - 2];
NSString *typeName = argument.type;
SEL selector = RCTConvertSelectorForType(typeName);
if ([RCTConvert respondsToSelector:selector]) {
switch (objcType[0]) {
#define RCT_CASE(_value, _type) \
case _value: { \
_type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \
RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \
break; \
}
RCT_CASE(_C_CHR, char)
RCT_CASE(_C_UCHR, unsigned char)
RCT_CASE(_C_SHT, short)
RCT_CASE(_C_USHT, unsigned short)
RCT_CASE(_C_INT, int)
RCT_CASE(_C_UINT, unsigned int)
RCT_CASE(_C_LNG, long)
RCT_CASE(_C_ULNG, unsigned long)
RCT_CASE(_C_LNG_LNG, long long)
RCT_CASE(_C_ULNG_LNG, unsigned long long)
RCT_CASE(_C_FLT, float)
RCT_CASE(_C_DBL, double)
RCT_CASE(_C_BOOL, BOOL)
#define RCT_NULLABLE_CASE(_value, _type) \
case _value: { \
isNullableType = YES; \
_type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \
RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \
break; \
}
RCT_NULLABLE_CASE(_C_SEL, SEL)
RCT_NULLABLE_CASE(_C_CHARPTR, const char *)
RCT_NULLABLE_CASE(_C_PTR, void *)
case _C_ID: {
isNullableType = YES;
id (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend;
RCT_ARG_BLOCK(
id value = convert([RCTConvert class], selector, json);
CFBridgingRetain(value);
)
break;
}
case _C_STRUCT_B: {
NSMethodSignature *typeSignature = [RCTConvert methodSignatureForSelector:selector];
NSInvocation *typeInvocation = [NSInvocation invocationWithMethodSignature:typeSignature];
typeInvocation.selector = selector;
typeInvocation.target = [RCTConvert class];
[argumentBlocks addObject:^(__unused RCTBridge *bridge, NSUInteger index, id json) {
void *returnValue = malloc(typeSignature.methodReturnLength);
[typeInvocation setArgument:&json atIndex:2];
[typeInvocation invoke];
[typeInvocation getReturnValue:returnValue];
[invocation setArgument:returnValue atIndex:index + 2];
free(returnValue);
return YES;
}];
break;
}
default: {
static const char *blockType = @encode(typeof(^{}));
if (!strcmp(objcType, blockType)) {
addBlockArgument();
} else {
RCTLogError(@"Unsupported argument type '%@' in method %@.",
typeName, [self methodName]);
}
}
}
} else if ([typeName isEqualToString:@"RCTResponseSenderBlock"]) {
addBlockArgument();
} else if ([typeName isEqualToString:@"RCTResponseErrorBlock"]) {
RCT_ARG_BLOCK(
if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) {
RCTLogArgumentError(weakSelf, index, json, "should be a function");
return NO;
}
__block BOOL didInvoke = NO;
RCT_BLOCK_ARGUMENT(^(NSError *error) {
if (RCTCheckCallbackMultipleInvocations(&didInvoke)) {
[bridge enqueueCallback:json args:@[RCTJSErrorFromNSError(error)]];
}
});
)
} else if ([typeName isEqualToString:@"RCTPromiseResolveBlock"]) {
RCTAssert(i == numberOfArguments - 2,
@"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]",
_moduleClass, _methodSignature);
RCT_ARG_BLOCK(
if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
RCTLogArgumentError(weakSelf, index, json, "should be a promise resolver function");
return NO;
}
__block BOOL didInvoke = NO;
RCT_BLOCK_ARGUMENT(^(id result) {
if (RCTCheckCallbackMultipleInvocations(&didInvoke)) {
[bridge enqueueCallback:json args:result ? @[result] : @[]];
}
});
)
} else if ([typeName isEqualToString:@"RCTPromiseRejectBlock"]) {
RCTAssert(i == numberOfArguments - 1,
@"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]",
_moduleClass, _methodSignature);
RCT_ARG_BLOCK(
if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
RCTLogArgumentError(weakSelf, index, json, "should be a promise rejecter function");
return NO;
}
__block BOOL didInvoke = NO;
RCT_BLOCK_ARGUMENT(^(NSString *code, NSString *message, NSError *error) {
if (RCTCheckCallbackMultipleInvocations(&didInvoke)) {
NSDictionary *errorJSON = RCTJSErrorFromCodeMessageAndNSError(code, message, error);
[bridge enqueueCallback:json args:@[errorJSON]];
}
});
)
} else {
// Unknown argument type
RCTLogError(@"Unknown argument type '%@' in method %@. Extend RCTConvert"
" to support this type.", typeName, [self methodName]);
}
if (RCT_DEBUG) {
RCTNullability nullability = argument.nullability;
if (!isNullableType) {
if (nullability == RCTNullable) {
RCTLogArgumentError(weakSelf, i - 2, typeName, "is marked as "
"nullable, but is not a nullable type.");
}
nullability = RCTNonnullable;
}
/**
* Special case - Numbers are not nullable in Android, so we
* don't support this for now. In future we may allow it.
*/
if ([typeName isEqualToString:@"NSNumber"]) {
BOOL unspecified = (nullability == RCTNullabilityUnspecified);
if (!argument.unused && (nullability == RCTNullable || unspecified)) {
RCTLogArgumentError(weakSelf, i - 2, typeName,
[unspecified ? @"has unspecified nullability" : @"is marked as nullable"
stringByAppendingString: @" but React requires that all NSNumber "
"arguments are explicitly marked as `nonnull` to ensure "
"compatibility with Android."].UTF8String);
}
nullability = RCTNonnullable;
}
if (nullability == RCTNonnullable) {
RCTArgumentBlock oldBlock = argumentBlocks[i - 2];
argumentBlocks[i - 2] = ^(RCTBridge *bridge, NSUInteger index, id json) {
if (json != nil) {
if (!oldBlock(bridge, index, json)) {
return NO;
}
if (isNullableType) {
// Check converted value wasn't null either, as method probably
// won't gracefully handle a nil vallue for a nonull argument
void *value;
[invocation getArgument:&value atIndex:index + 2];
if (value == NULL) {
return NO;
}
}
return YES;
}
RCTLogArgumentError(weakSelf, index, typeName, "must not be null");
return NO;
};
}
}
}
_argumentBlocks = [argumentBlocks copy];
}
- (SEL)selector
{
if (_selector == NULL) {
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"", (@{ @"module": NSStringFromClass(_moduleClass),
@"method": _methodSignature }));
[self processMethodSignature];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
return _selector;
}
- (NSString *)JSMethodName
{
NSString *methodName = _JSMethodName;
if (methodName.length == 0) {
methodName = _methodSignature;
NSRange colonRange = [methodName rangeOfString:@":"];
if (colonRange.location != NSNotFound) {
methodName = [methodName substringToIndex:colonRange.location];
}
methodName = [methodName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
RCTAssert(methodName.length, @"%@ is not a valid JS function name, please"
" supply an alternative using RCT_REMAP_METHOD()", _methodSignature);
}
return methodName;
}
- (RCTFunctionType)functionType
{
if ([_methodSignature rangeOfString:@"RCTPromise"].length) {
return RCTFunctionTypePromise;
} else {
return RCTFunctionTypeNormal;
}
}
- (id)invokeWithBridge:(RCTBridge *)bridge
module:(id)module
arguments:(NSArray *)arguments
{
if (_argumentBlocks == nil) {
[self processMethodSignature];
}
if (RCT_DEBUG) {
// Sanity check
RCTAssert([module class] == _moduleClass, @"Attempted to invoke method \
%@ on a module of class %@", [self methodName], [module class]);
// Safety check
if (arguments.count != _argumentBlocks.count) {
NSInteger actualCount = arguments.count;
NSInteger expectedCount = _argumentBlocks.count;
// Subtract the implicit Promise resolver and rejecter functions for implementations of async functions
if (self.functionType == RCTFunctionTypePromise) {
actualCount -= 2;
expectedCount -= 2;
}
RCTLogError(@"%@.%@ was called with %zd arguments but expects %zd arguments. "
@"If you haven\'t changed this method yourself, this usually means that "
@"your versions of the native code and JavaScript code are out of sync. "
@"Updating both should make this error go away.",
RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName,
actualCount, expectedCount);
return nil;
}
}
// Set arguments
NSUInteger index = 0;
for (id json in arguments) {
RCTArgumentBlock block = _argumentBlocks[index];
if (!block(bridge, index, RCTNilIfNull(json))) {
// Invalid argument, abort
RCTLogArgumentError(self, index, json,
"could not be processed. Aborting method call.");
return nil;
}
index++;
}
// Invoke method
[_invocation invokeWithTarget:module];
RCTAssert(
@encode(RCTArgumentBlock)[0] == _C_ID,
@"Block type encoding has changed, it won't be released. A check for the block"
"type encoding (%s) has to be added below.",
@encode(RCTArgumentBlock)
);
index = 2;
for (NSUInteger length = _invocation.methodSignature.numberOfArguments; index < length; index++) {
if ([_invocation.methodSignature getArgumentTypeAtIndex:index][0] == _C_ID) {
__unsafe_unretained id value;
[_invocation getArgument:&value atIndex:index];
if (value) {
CFRelease((__bridge CFTypeRef)value);
}
}
}
return nil;
}
- (NSString *)methodName
{
if (_selector == NULL) {
[self processMethodSignature];
}
return [NSString stringWithFormat:@"-[%@ %@]", _moduleClass,
NSStringFromSelector(_selector)];
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@()>",
[self class], self, [self methodName], self.JSMethodName];
}
@end