[Bridge] Add support for JS async functions to RCT_EXPORT_METHOD

Summary:
Adds support for JS async methods and helps guide people writing native modules w.r.t. the callbacks. With this diff, on the native side you write:

```objc
RCT_EXPORT_METHOD(getValueAsync:(NSString *)key
                       resolver:(RCTPromiseResolver)resolve
                       rejecter:(RCTPromiseRejecter)reject)
{
  NSError *error = nil;
  id value = [_nativeDataStore valueForKey:key error:&error];

  // "resolve" and "reject" are automatically defined blocks that take
  // any object (nil is OK) and an NSError, respectively
  if (!error) {
    resolve(value);
  } else {
    reject(error);
  }
}
```

On the JS side, you can write:

```js
var {DemoDataStore} = require('react-native').NativeModules;
DemoDataStore.getValueAsync('sample-key').then((value) => {
  console.log('Got:', value);
}, (error) => {
  console.error(error);
  // "error" is an Error object whose message is the NSError's description.
  // The NSError's code and domain are also set, and the native trace i
Closes https://github.com/facebook/react-native/pull/1232
Github Author: James Ide <ide@jameside.com>

Test Plan: Imported from GitHub, without a `Test Plan:` line.
This commit is contained in:
James Ide 2015-06-09 14:26:40 -07:00
parent cf8c2693af
commit 90439cec26
4 changed files with 190 additions and 42 deletions

View File

@ -19,9 +19,17 @@ var slice = Array.prototype.slice;
var MethodTypes = keyMirror({ var MethodTypes = keyMirror({
remote: null, remote: null,
remoteAsync: null,
local: null, local: null,
}); });
type ErrorData = {
message: string;
domain: string;
code: number;
nativeStackIOS?: string;
};
/** /**
* Creates remotely invokable modules. * Creates remotely invokable modules.
*/ */
@ -36,21 +44,40 @@ var BatchedBridgeFactory = {
*/ */
_createBridgedModule: function(messageQueue, moduleConfig, moduleName) { _createBridgedModule: function(messageQueue, moduleConfig, moduleName) {
var remoteModule = mapObject(moduleConfig.methods, function(methodConfig, memberName) { var remoteModule = mapObject(moduleConfig.methods, function(methodConfig, memberName) {
return methodConfig.type === MethodTypes.local ? null : function() { switch (methodConfig.type) {
case MethodTypes.remote:
return function() {
var lastArg = arguments.length > 0 ? arguments[arguments.length - 1] : null; var lastArg = arguments.length > 0 ? arguments[arguments.length - 1] : null;
var secondLastArg = arguments.length > 1 ? arguments[arguments.length - 2] : null; var secondLastArg = arguments.length > 1 ? arguments[arguments.length - 2] : null;
var hasSuccCB = typeof lastArg === 'function'; var hasErrorCB = typeof lastArg === 'function';
var hasErrorCB = typeof secondLastArg === 'function'; var hasSuccCB = typeof secondLastArg === 'function';
hasErrorCB && invariant( hasSuccCB && invariant(
hasSuccCB, hasErrorCB,
'Cannot have a non-function arg after a function arg.' 'Cannot have a non-function arg after a function arg.'
); );
var numCBs = (hasSuccCB ? 1 : 0) + (hasErrorCB ? 1 : 0); var numCBs = (hasSuccCB ? 1 : 0) + (hasErrorCB ? 1 : 0);
var args = slice.call(arguments, 0, arguments.length - numCBs); var args = slice.call(arguments, 0, arguments.length - numCBs);
var onSucc = hasSuccCB ? lastArg : null; var onSucc = hasSuccCB ? secondLastArg : null;
var onFail = hasErrorCB ? secondLastArg : null; var onFail = hasErrorCB ? lastArg : null;
return messageQueue.call(moduleName, memberName, args, onFail, onSucc); messageQueue.call(moduleName, memberName, args, onSucc, onFail);
}; };
case MethodTypes.remoteAsync:
return function(...args) {
return new Promise((resolve, reject) => {
messageQueue.call(moduleName, memberName, args, resolve, (errorData) => {
var error = _createErrorFromErrorData(errorData);
reject(error);
});
});
};
case MethodTypes.local:
return null;
default:
throw new Error('Unknown bridge method type: ' + methodConfig.type);
}
}); });
for (var constName in moduleConfig.constants) { for (var constName in moduleConfig.constants) {
warning(!remoteModule[constName], 'saw constant and method named %s', constName); warning(!remoteModule[constName], 'saw constant and method named %s', constName);
@ -59,7 +86,6 @@ var BatchedBridgeFactory = {
return remoteModule; return remoteModule;
}, },
create: function(MessageQueue, modulesConfig, localModulesConfig) { create: function(MessageQueue, modulesConfig, localModulesConfig) {
var messageQueue = new MessageQueue(modulesConfig, localModulesConfig); var messageQueue = new MessageQueue(modulesConfig, localModulesConfig);
return { return {
@ -80,4 +106,14 @@ var BatchedBridgeFactory = {
} }
}; };
function _createErrorFromErrorData(errorData: ErrorData): Error {
var {
message,
...extraErrorInfo,
} = errorData;
var error = new Error(message);
error.framesToPop = 1;
return Object.assign(error, extraErrorInfo);
}
module.exports = BatchedBridgeFactory; module.exports = BatchedBridgeFactory;

View File

@ -441,14 +441,14 @@ var MessageQueueMixin = {
}, },
/** /**
* @param {Function} onFail Function to store in current thread for later
* lookup, when request fails.
* @param {Function} onSucc Function to store in current thread for later * @param {Function} onSucc Function to store in current thread for later
* lookup, when request succeeds. * lookup, when request succeeds.
* @param {Function} onFail Function to store in current thread for later
* lookup, when request fails.
* @param {Object?=} scope Scope to invoke `cb` with. * @param {Object?=} scope Scope to invoke `cb` with.
* @param {Object?=} res Resulting callback ids. Use `this._POOLED_CBIDS`. * @param {Object?=} res Resulting callback ids. Use `this._POOLED_CBIDS`.
*/ */
_storeCallbacksInCurrentThread: function(onFail, onSucc, scope) { _storeCallbacksInCurrentThread: function(onSucc, onFail, scope) {
invariant(onFail || onSucc, INTERNAL_ERROR); invariant(onFail || onSucc, INTERNAL_ERROR);
this._bookkeeping.allocateCallbackIDs(this._POOLED_CBIDS); this._bookkeeping.allocateCallbackIDs(this._POOLED_CBIDS);
var succCBID = this._POOLED_CBIDS.successCallbackID; var succCBID = this._POOLED_CBIDS.successCallbackID;
@ -506,7 +506,7 @@ var MessageQueueMixin = {
return ret; return ret;
}, },
call: function(moduleName, methodName, params, onFail, onSucc, scope) { call: function(moduleName, methodName, params, onSucc, onFail, scope) {
invariant( invariant(
(!onFail || typeof onFail === 'function') && (!onFail || typeof onFail === 'function') &&
(!onSucc || typeof onSucc === 'function'), (!onSucc || typeof onSucc === 'function'),
@ -514,10 +514,10 @@ var MessageQueueMixin = {
); );
// Store callback _before_ sending the request, just in case the MailBox // Store callback _before_ sending the request, just in case the MailBox
// returns the response in a blocking manner. // returns the response in a blocking manner.
if (onSucc) { if (onSucc || onFail) {
this._storeCallbacksInCurrentThread(onFail, onSucc, scope, this._POOLED_CBIDS); this._storeCallbacksInCurrentThread(onSucc, onFail, scope, this._POOLED_CBIDS);
onSucc && params.push(this._POOLED_CBIDS.successCallbackID);
onFail && params.push(this._POOLED_CBIDS.errorCallbackID); onFail && params.push(this._POOLED_CBIDS.errorCallbackID);
params.push(this._POOLED_CBIDS.successCallbackID);
} }
var moduleID = this._remoteModuleNameToModuleID[moduleName]; var moduleID = this._remoteModuleNameToModuleID[moduleName];
if (moduleID === undefined || moduleID === null) { if (moduleID === undefined || moduleID === null) {

View File

@ -47,6 +47,11 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) {
RCTBridgeFieldFlushDateMillis RCTBridgeFieldFlushDateMillis
}; };
typedef NS_ENUM(NSUInteger, RCTJavaScriptFunctionKind) {
RCTJavaScriptFunctionKindNormal,
RCTJavaScriptFunctionKindAsync,
};
#ifdef __LP64__ #ifdef __LP64__
typedef struct mach_header_64 *RCTHeaderValue; typedef struct mach_header_64 *RCTHeaderValue;
typedef struct section_64 RCTHeaderSection; typedef struct section_64 RCTHeaderSection;
@ -204,6 +209,27 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void)
return RCTModuleClassesByID; return RCTModuleClassesByID;
} }
// TODO: Can we just replace RCTMakeError with this function instead?
static NSDictionary *RCTJSErrorFromNSError(NSError *error)
{
NSString *errorMessage;
NSArray *stackTrace = [NSThread callStackSymbols];
NSMutableDictionary *errorInfo =
[NSMutableDictionary dictionaryWithObject:stackTrace forKey:@"nativeStackIOS"];
if (error) {
errorMessage = error.localizedDescription ?: @"Unknown error from a native module";
errorInfo[@"domain"] = error.domain ?: RCTErrorDomain;
errorInfo[@"code"] = @(error.code);
} else {
errorMessage = @"Unknown error from a native module";
errorInfo[@"domain"] = RCTErrorDomain;
errorInfo[@"code"] = @-1;
}
return RCTMakeError(errorMessage, nil, errorInfo);
}
@class RCTBatchedBridge; @class RCTBatchedBridge;
@interface RCTBridge () @interface RCTBridge ()
@ -239,6 +265,7 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void)
@property (nonatomic, copy, readonly) NSString *moduleClassName; @property (nonatomic, copy, readonly) NSString *moduleClassName;
@property (nonatomic, copy, readonly) NSString *JSMethodName; @property (nonatomic, copy, readonly) NSString *JSMethodName;
@property (nonatomic, assign, readonly) SEL selector; @property (nonatomic, assign, readonly) SEL selector;
@property (nonatomic, assign, readonly) RCTJavaScriptFunctionKind functionKind;
@end @end
@ -400,6 +427,48 @@ case _value: { \
} }
} else if ([argumentName isEqualToString:@"RCTResponseSenderBlock"]) { } else if ([argumentName isEqualToString:@"RCTResponseSenderBlock"]) {
addBlockArgument(); addBlockArgument();
} else if ([argumentName isEqualToString:@"RCTPromiseResolveBlock"]) {
RCTAssert(i == numberOfArguments - 2,
@"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]",
_moduleClassName, objCMethodName);
RCT_ARG_BLOCK(
if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise resolver ID", index,
json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName);
return;
}
// Marked as autoreleasing, because NSInvocation doesn't retain arguments
__autoreleasing RCTPromiseResolveBlock value = (^(id result) {
NSArray *arguments = result ? @[result] : @[];
[bridge _invokeAndProcessModule:@"BatchedBridge"
method:@"invokeCallbackAndReturnFlushedQueue"
arguments:@[json, arguments]
context:context];
});
)
_functionKind = RCTJavaScriptFunctionKindAsync;
} else if ([argumentName isEqualToString:@"RCTPromiseRejectBlock"]) {
RCTAssert(i == numberOfArguments - 1,
@"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]",
_moduleClassName, objCMethodName);
RCT_ARG_BLOCK(
if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise rejecter ID", index,
json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName);
return;
}
// Marked as autoreleasing, because NSInvocation doesn't retain arguments
__autoreleasing RCTPromiseRejectBlock value = (^(NSError *error) {
NSDictionary *errorJSON = RCTJSErrorFromNSError(error);
[bridge _invokeAndProcessModule:@"BatchedBridge"
method:@"invokeCallbackAndReturnFlushedQueue"
arguments:@[json, @[errorJSON]]
context:context];
});
)
_functionKind = RCTJavaScriptFunctionKindAsync;
} else { } else {
// Unknown argument type // Unknown argument type
@ -427,9 +496,18 @@ case _value: { \
// Safety check // Safety check
if (arguments.count != _argumentBlocks.count) { 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 (_functionKind == RCTJavaScriptFunctionKindAsync) {
actualCount -= 2;
expectedCount -= 2;
}
RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd", RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd",
RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName,
arguments.count, _argumentBlocks.count); actualCount, expectedCount);
return; return;
} }
} }
@ -535,7 +613,7 @@ static RCTSparseArray *RCTExportedMethodsByModuleID(void)
* }, * },
* "methodName2": { * "methodName2": {
* "methodID": 1, * "methodID": 1,
* "type": "remote" * "type": "remoteAsync"
* }, * },
* etc... * etc...
* }, * },
@ -559,7 +637,7 @@ static NSDictionary *RCTRemoteModulesConfig(NSDictionary *modulesByName)
[methods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger methodID, BOOL *_stop) { [methods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger methodID, BOOL *_stop) {
methodsByName[method.JSMethodName] = @{ methodsByName[method.JSMethodName] = @{
@"methodID": @(methodID), @"methodID": @(methodID),
@"type": @"remote", @"type": method.functionKind == RCTJavaScriptFunctionKindAsync ? @"remoteAsync" : @"remote",
}; };
}]; }];

View File

@ -17,6 +17,20 @@
*/ */
typedef void (^RCTResponseSenderBlock)(NSArray *response); typedef void (^RCTResponseSenderBlock)(NSArray *response);
/**
* Block that bridge modules use to resolve the JS promise waiting for a result.
* Nil results are supported and are converted to JS's undefined value.
*/
typedef void (^RCTPromiseResolveBlock)(id result);
/**
* Block that bridge modules use to reject the JS promise waiting for a result.
* The error may be nil but it is preferable to pass an NSError object for more
* precise error messages.
*/
typedef void (^RCTPromiseRejectBlock)(NSError *error);
/** /**
* This constant can be returned from +methodQueue to force module * This constant can be returned from +methodQueue to force module
* methods to be called on the JavaScript thread. This can have serious * methods to be called on the JavaScript thread. This can have serious
@ -37,7 +51,7 @@ extern const dispatch_queue_t RCTJSThread;
* A reference to the RCTBridge. Useful for modules that require access * A reference to the RCTBridge. Useful for modules that require access
* to bridge features, such as sending events or making JS calls. This * to bridge features, such as sending events or making JS calls. This
* will be set automatically by the bridge when it initializes the module. * will be set automatically by the bridge when it initializes the module.
* To implement this in your module, just add @synthesize bridge = _bridge; * To implement this in your module, just add @synthesize bridge = _bridge;
*/ */
@property (nonatomic, weak) RCTBridge *bridge; @property (nonatomic, weak) RCTBridge *bridge;
@ -70,6 +84,26 @@ extern const dispatch_queue_t RCTJSThread;
* { ... } * { ... }
* *
* and is exposed to JavaScript as `NativeModules.ModuleName.doSomething`. * and is exposed to JavaScript as `NativeModules.ModuleName.doSomething`.
*
* ## Promises
*
* Bridge modules can also define methods that are exported to JavaScript as
* methods that return a Promise, and are compatible with JS async functions.
*
* Declare the last two parameters of your native method to be a resolver block
* and a rejecter block. The resolver block must precede the rejecter block.
*
* For example:
*
* RCT_EXPORT_METHOD(doSomethingAsync:(NSString *)aString
* resolver:(RCTPromiseResolveBlock)resolve
* rejecter:(RCTPromiseRejectBlock)reject
* { ... }
*
* Calling `NativeModules.ModuleName.doSomethingAsync(aString)` from
* JavaScript will return a promise that is resolved or rejected when your
* native method implementation calls the respective block.
*
*/ */
#define RCT_EXPORT_METHOD(method) \ #define RCT_EXPORT_METHOD(method) \
RCT_REMAP_METHOD(, method) RCT_REMAP_METHOD(, method)
@ -118,7 +152,7 @@ extern const dispatch_queue_t RCTJSThread;
RCT_EXTERN_REMAP_MODULE(, objc_name, objc_supername) RCT_EXTERN_REMAP_MODULE(, objc_name, objc_supername)
/** /**
* Similar to RCT_EXTERN_MODULE but allows setting a custom JavaScript name * Like RCT_EXTERN_MODULE, but allows setting a custom JavaScript name.
*/ */
#define RCT_EXTERN_REMAP_MODULE(js_name, objc_name, objc_supername) \ #define RCT_EXTERN_REMAP_MODULE(js_name, objc_name, objc_supername) \
objc_name : objc_supername \ objc_name : objc_supername \
@ -136,7 +170,7 @@ extern const dispatch_queue_t RCTJSThread;
RCT_EXTERN_REMAP_METHOD(, method) RCT_EXTERN_REMAP_METHOD(, method)
/** /**
* Similar to RCT_EXTERN_REMAP_METHOD but allows setting a custom JavaScript name * Like RCT_EXTERN_REMAP_METHOD, but allows setting a custom JavaScript name.
*/ */
#define RCT_EXTERN_REMAP_METHOD(js_name, method) \ #define RCT_EXTERN_REMAP_METHOD(js_name, method) \
- (void)__rct_export__##method { \ - (void)__rct_export__##method { \