Files
react-native/React/Base/RCTModuleMethod.mm
Ramanpreet Nara 8618a5824f 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
2019-03-22 16:23:40 -07:00

596 lines
21 KiB
Plaintext

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTModuleMethod.h"
#import <objc/message.h>
#import "RCTAssert.h"
#import "RCTBridge+Private.h"
#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTCxxConvert.h"
#import "RCTLog.h"
#import "RCTManagedPointer.h"
#import "RCTParserUtils.h"
#import "RCTProfile.h"
#import "RCTUtils.h"
typedef BOOL (^RCTArgumentBlock)(RCTBridge *, NSUInteger, id);
/**
* Get the converter function for the specified type
*/
static SEL selectorForType(NSString *type)
{
const char *input = type.UTF8String;
return NSSelectorFromString([RCTParseType(&input) stringByAppendingString:@":"]);
}
@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;
const RCTMethodInfo *_methodInfo;
NSString *_JSMethodName;
SEL _selector;
NSInvocation *_invocation;
NSArray<RCTArgumentBlock> *_argumentBlocks;
NSMutableArray *_retainedObjects;
}
static void RCTLogArgumentError(RCTModuleMethod *method, NSUInteger index,
id valueOrType, const char *issue)
{
RCTLogError(@"Argument %tu (%@) of %@.%s %s", index, valueOrType,
RCTBridgeModuleNameForClass(method->_moduleClass),
method.JSMethodName, issue);
}
RCT_NOT_IMPLEMENTED(- (instancetype)init)
RCT_EXTERN_C_BEGIN
// 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 (RCTParseSelectorIdentifier(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, "__attribute__((unused))") ||
RCTReadString(input, "__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") ||
RCTReadString(input, "__nullable")) {
return RCTNullable;
} else if (RCTReadString(input, "_Nonnull") ||
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
#if RCT_DEBUG
static BOOL checkCallbackMultipleInvocations(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;
}
}
#endif
NSString *RCTParseMethodSignature(const char *input, NSArray<RCTMethodArgument *> **arguments)
{
RCTSkipWhitespace(&input);
NSMutableArray *args;
NSMutableString *selector = [NSMutableString new];
while (RCTParseSelectorPart(&input, selector)) {
if (!args) {
args = [NSMutableArray new];
}
// Parse type
if (RCTReadChar(&input, '(')) {
RCTSkipWhitespace(&input);
// 5 cases that both nullable and __unused exist
// 1: foo:(nullable __unused id)foo 2: foo:(nullable id __unused)foo
// 3: foo:(__unused id _Nullable)foo 4: foo:(id __unused _Nullable)foo
// 5: foo:(id _Nullable __unused)foo
RCTNullability nullability = RCTParseNullability(&input);
RCTSkipWhitespace(&input);
BOOL unused = RCTParseUnused(&input);
RCTSkipWhitespace(&input);
NSString *type = RCTParseType(&input);
RCTSkipWhitespace(&input);
if (nullability == RCTNullabilityUnspecified) {
nullability = RCTParseNullabilityPostfix(&input);
RCTSkipWhitespace(&input);
if (!unused) {
unused = RCTParseUnused(&input);
RCTSkipWhitespace(&input);
if (unused && nullability == RCTNullabilityUnspecified) {
nullability = RCTParseNullabilityPostfix(&input);
RCTSkipWhitespace(&input);
}
}
} else if (!unused) {
unused = RCTParseUnused(&input);
RCTSkipWhitespace(&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
RCTParseArgumentIdentifier(&input, NULL);
RCTSkipWhitespace(&input);
}
*arguments = [args copy];
return selector;
}
RCT_EXTERN_C_END
- (instancetype)initWithExportedMethod:(const RCTMethodInfo *)exportedMethod
moduleClass:(Class)moduleClass
{
if (self = [super init]) {
_moduleClass = moduleClass;
_methodInfo = exportedMethod;
}
return self;
}
- (void)processMethodSignature
{
NSArray<RCTMethodArgument *> *arguments;
_selector = NSSelectorFromString(RCTParseMethodSignature(_methodInfo->objcName, &arguments));
RCTAssert(_selector, @"%s is not a valid selector", _methodInfo->objcName);
// Create method invocation
NSMethodSignature *methodSignature = [_moduleClass instanceMethodSignatureForSelector:_selector];
RCTAssert(methodSignature, @"%s is not a recognized Objective-C method.", sel_getName(_selector));
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.selector = _selector;
_invocation = invocation;
NSMutableArray *retainedObjects = [NSMutableArray array];
_retainedObjects = retainedObjects;
// Process arguments
NSUInteger numberOfArguments = methodSignature.numberOfArguments;
NSMutableArray<RCTArgumentBlock> *argumentBlocks =
[[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2];
#if RCT_DEBUG
__weak RCTModuleMethod *weakSelf = self;
#endif
#define RCT_RETAINED_ARG_BLOCK(_logic) \
[argumentBlocks addObject:^(__unused __weak RCTBridge *bridge, NSUInteger index, id json) { \
_logic \
[invocation setArgument:&value atIndex:(index) + 2]; \
if (value) { \
[retainedObjects addObject:value]; \
} \
return YES; \
}]
#define __PRIMITIVE_CASE(_type, _nullable) { \
isNullableType = _nullable; \
_type (*convert)(id, SEL, id) = (__typeof__(convert))objc_msgSend; \
[argumentBlocks addObject:^(__unused RCTBridge *bridge, NSUInteger index, id json) { \
_type value = convert([RCTConvert class], selector, json); \
[invocation setArgument:&value atIndex:(index) + 2]; \
return YES; \
}]; \
break; \
}
#define PRIMITIVE_CASE(_type) __PRIMITIVE_CASE(_type, NO)
#define NULLABLE_PRIMITIVE_CASE(_type) __PRIMITIVE_CASE(_type, YES)
// Explicitly copy the block
#define __COPY_BLOCK(block...) \
id value = [block copy]; \
if (value) { \
[retainedObjects addObject:value]; \
} \
#if RCT_DEBUG
#define BLOCK_CASE(_block_args, _block) RCT_RETAINED_ARG_BLOCK( \
if (json && ![json isKindOfClass:[NSNumber class]]) { \
RCTLogArgumentError(weakSelf, index, json, "should be a function"); \
return NO; \
} \
__block BOOL didInvoke = NO; \
__COPY_BLOCK(^_block_args { \
if (checkCallbackMultipleInvocations(&didInvoke)) _block \
}); \
)
#else
#define BLOCK_CASE(_block_args, _block) \
RCT_RETAINED_ARG_BLOCK( __COPY_BLOCK(^_block_args { _block }); )
#endif
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 = selectorForType(typeName);
if ([RCTConvert respondsToSelector:selector]) {
switch (objcType[0]) {
// Primitives
case _C_CHR: PRIMITIVE_CASE(char)
case _C_UCHR: PRIMITIVE_CASE(unsigned char)
case _C_SHT: PRIMITIVE_CASE(short)
case _C_USHT: PRIMITIVE_CASE(unsigned short)
case _C_INT: PRIMITIVE_CASE(int)
case _C_UINT: PRIMITIVE_CASE(unsigned int)
case _C_LNG: PRIMITIVE_CASE(long)
case _C_ULNG: PRIMITIVE_CASE(unsigned long)
case _C_LNG_LNG: PRIMITIVE_CASE(long long)
case _C_ULNG_LNG: PRIMITIVE_CASE(unsigned long long)
case _C_FLT: PRIMITIVE_CASE(float)
case _C_DBL: PRIMITIVE_CASE(double)
case _C_BOOL: PRIMITIVE_CASE(BOOL)
case _C_SEL: NULLABLE_PRIMITIVE_CASE(SEL)
case _C_CHARPTR: NULLABLE_PRIMITIVE_CASE(const char *)
case _C_PTR: NULLABLE_PRIMITIVE_CASE(void *)
case _C_ID: {
isNullableType = YES;
id (*convert)(id, SEL, id) = (__typeof__(convert))objc_msgSend;
RCT_RETAINED_ARG_BLOCK(
id value = convert([RCTConvert class], selector, json);
);
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);
if (!returnValue) {
// CWE - 391 : Unchecked error condition
// https://www.cvedetails.com/cwe-details/391/Unchecked-Error-Condition.html
// https://eli.thegreenplace.net/2009/10/30/handling-out-of-memory-conditions-in-c
abort();
}
[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)) {
BLOCK_CASE((NSArray *args), {
[bridge enqueueCallback:json args:args];
});
} else {
RCTLogError(@"Unsupported argument type '%@' in method %@.",
typeName, [self methodName]);
}
}
}
} else if ([typeName isEqualToString:@"RCTResponseSenderBlock"]) {
BLOCK_CASE((NSArray *args), {
[bridge enqueueCallback:json args:args];
});
} else if ([typeName isEqualToString:@"RCTResponseErrorBlock"]) {
BLOCK_CASE((NSError *error), {
[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 %@",
[self methodName]);
BLOCK_CASE((id result), {
[bridge enqueueCallback:json args:result ? @[result] : @[]];
});
} else if ([typeName isEqualToString:@"RCTPromiseRejectBlock"]) {
RCTAssert(i == numberOfArguments - 1,
@"The RCTPromiseRejectBlock must be the last parameter in %@",
[self methodName]);
BLOCK_CASE((NSString *code, NSString *message, NSError *error), {
NSDictionary *errorJSON = RCTJSErrorFromCodeMessageAndNSError(code, message, error);
[bridge enqueueCallback:json args:@[errorJSON]];
});
} else if ([typeName hasPrefix:@"JS::"]) {
NSString *selectorNameForCxxType =
[[typeName stringByReplacingOccurrencesOfString:@"::" withString:@"_"]
stringByAppendingString:@":"];
selector = NSSelectorFromString(selectorNameForCxxType);
[argumentBlocks addObject:^(__unused RCTBridge *bridge, NSUInteger index, id json) {
RCTManagedPointer *(*convert)(id, SEL, id) = (__typeof__(convert))objc_msgSend;
RCTManagedPointer *box = convert([RCTCxxConvert class], selector, json);
void *pointer = box.voidPointer;
[invocation setArgument:&pointer atIndex:index + 2];
[retainedObjects addObject:box];
return YES;
}];
} 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;
};
}
#endif
}
#if RCT_DEBUG
const char *objcType = _invocation.methodSignature.methodReturnType;
if (_methodInfo->isSync && objcType[0] != _C_ID) {
RCTLogError(@"Return type of %@.%s should be (id) as the method is \"sync\"",
RCTBridgeModuleNameForClass(_moduleClass), self.JSMethodName);
}
#endif
_argumentBlocks = argumentBlocks;
}
- (SEL)selector
{
if (_selector == NULL) {
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"", (@{ @"module": NSStringFromClass(_moduleClass),
@"method": @(_methodInfo->objcName) }));
[self processMethodSignature];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
return _selector;
}
- (const char *)JSMethodName
{
NSString *methodName = _JSMethodName;
if (!methodName) {
const char *jsName = _methodInfo->jsName;
if (jsName && strlen(jsName) > 0) {
methodName = @(jsName);
} else {
methodName = @(_methodInfo->objcName);
NSRange colonRange = [methodName rangeOfString:@":"];
if (colonRange.location != NSNotFound) {
methodName = [methodName substringToIndex:colonRange.location];
}
methodName = [methodName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
RCTAssert(methodName.length, @"%s is not a valid JS function name, please"
" supply an alternative using RCT_REMAP_METHOD()", _methodInfo->objcName);
}
_JSMethodName = methodName;
}
return methodName.UTF8String;
}
- (RCTFunctionType)functionType
{
if (strstr(_methodInfo->objcName, "RCTPromise") != NULL) {
RCTAssert(!_methodInfo->isSync, @"Promises cannot be used in sync functions");
return RCTFunctionTypePromise;
} else if (_methodInfo->isSync) {
return RCTFunctionTypeSync;
} 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(@"%@.%s was called with %lld arguments but expects %lld 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), self.JSMethodName,
(long long)actualCount, (long long)expectedCount);
return nil;
}
#endif
// 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
#ifdef RCT_MAIN_THREAD_WATCH_DOG_THRESHOLD
if (RCTIsMainQueue()) {
CFTimeInterval start = CACurrentMediaTime();
[_invocation invokeWithTarget:module];
CFTimeInterval duration = CACurrentMediaTime() - start;
if (duration > RCT_MAIN_THREAD_WATCH_DOG_THRESHOLD) {
RCTLogWarn(
@"Main Thread Watchdog: Invocation of %@ blocked the main thread for %dms. "
"Consider using background-threaded modules and asynchronous calls "
"to spend less time on the main thread and keep the app's UI responsive.",
[self methodName],
(int)(duration * 1000)
);
}
} else {
[_invocation invokeWithTarget:module];
}
#else
[_invocation invokeWithTarget:module];
#endif
[_retainedObjects removeAllObjects];
if (_methodInfo->isSync) {
void *returnValue;
[_invocation getReturnValue:&returnValue];
return (__bridge id)returnValue;
}
return nil;
}
- (NSString *)methodName
{
if (!_selector) {
[self processMethodSignature];
}
return [NSString stringWithFormat:@"-[%@ %s]", _moduleClass, sel_getName(_selector)];
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@: %p; exports %@ as %s(); type: %s>",
[self class], self, [self methodName], self.JSMethodName, RCTFunctionDescriptorFromType(self.functionType)];
}
@end