Skip to content

Commit d548c85

Browse files
committed
[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 facebook/react-native#1232 Github Author: James Ide <ide@jameside.com> Test Plan: Imported from GitHub, without a `Test Plan:` line.
1 parent 2b4daf2 commit d548c85

4 files changed

Lines changed: 181 additions & 30 deletions

File tree

Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,17 @@ var slice = Array.prototype.slice;
1919

2020
var MethodTypes = keyMirror({
2121
remote: null,
22+
remoteAsync: null,
2223
local: null,
2324
});
2425

26+
type ErrorData = {
27+
message: string;
28+
domain: string;
29+
code: number;
30+
nativeStackIOS?: string;
31+
};
32+
2533
/**
2634
* Creates remotely invokable modules.
2735
*/
@@ -36,21 +44,40 @@ var BatchedBridgeFactory = {
3644
*/
3745
_createBridgedModule: function(messageQueue, moduleConfig, moduleName) {
3846
var remoteModule = mapObject(moduleConfig.methods, function(methodConfig, memberName) {
39-
return methodConfig.type === MethodTypes.local ? null : function() {
40-
var lastArg = arguments.length > 0 ? arguments[arguments.length - 1] : null;
41-
var secondLastArg = arguments.length > 1 ? arguments[arguments.length - 2] : null;
42-
var hasSuccCB = typeof lastArg === 'function';
43-
var hasErrorCB = typeof secondLastArg === 'function';
44-
hasErrorCB && invariant(
45-
hasSuccCB,
46-
'Cannot have a non-function arg after a function arg.'
47-
);
48-
var numCBs = (hasSuccCB ? 1 : 0) + (hasErrorCB ? 1 : 0);
49-
var args = slice.call(arguments, 0, arguments.length - numCBs);
50-
var onSucc = hasSuccCB ? lastArg : null;
51-
var onFail = hasErrorCB ? secondLastArg : null;
52-
return messageQueue.call(moduleName, memberName, args, onFail, onSucc);
53-
};
47+
switch (methodConfig.type) {
48+
case MethodTypes.remote:
49+
return function() {
50+
var lastArg = arguments.length > 0 ? arguments[arguments.length - 1] : null;
51+
var secondLastArg = arguments.length > 1 ? arguments[arguments.length - 2] : null;
52+
var hasErrorCB = typeof lastArg === 'function';
53+
var hasSuccCB = typeof secondLastArg === 'function';
54+
hasSuccCB && invariant(
55+
hasErrorCB,
56+
'Cannot have a non-function arg after a function arg.'
57+
);
58+
var numCBs = (hasSuccCB ? 1 : 0) + (hasErrorCB ? 1 : 0);
59+
var args = slice.call(arguments, 0, arguments.length - numCBs);
60+
var onSucc = hasSuccCB ? secondLastArg : null;
61+
var onFail = hasErrorCB ? lastArg : null;
62+
messageQueue.call(moduleName, memberName, args, onSucc, onFail);
63+
};
64+
65+
case MethodTypes.remoteAsync:
66+
return function(...args) {
67+
return new Promise((resolve, reject) => {
68+
messageQueue.call(moduleName, memberName, args, resolve, (errorData) => {
69+
var error = _createErrorFromErrorData(errorData);
70+
reject(error);
71+
});
72+
});
73+
};
74+
75+
case MethodTypes.local:
76+
return null;
77+
78+
default:
79+
throw new Error('Unknown bridge method type: ' + methodConfig.type);
80+
}
5481
});
5582
for (var constName in moduleConfig.constants) {
5683
warning(!remoteModule[constName], 'saw constant and method named %s', constName);
@@ -59,7 +86,6 @@ var BatchedBridgeFactory = {
5986
return remoteModule;
6087
},
6188

62-
6389
create: function(MessageQueue, modulesConfig, localModulesConfig) {
6490
var messageQueue = new MessageQueue(modulesConfig, localModulesConfig);
6591
return {
@@ -80,4 +106,14 @@ var BatchedBridgeFactory = {
80106
}
81107
};
82108

109+
function _createErrorFromErrorData(errorData: ErrorData): Error {
110+
var {
111+
message,
112+
...extraErrorInfo,
113+
} = errorData;
114+
var error = new Error(message);
115+
error.framesToPop = 1;
116+
return Object.assign(error, extraErrorInfo);
117+
}
118+
83119
module.exports = BatchedBridgeFactory;

Libraries/Utilities/MessageQueue.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -431,14 +431,14 @@ var MessageQueueMixin = {
431431
},
432432

433433
/**
434-
* @param {Function} onFail Function to store in current thread for later
435-
* lookup, when request fails.
436434
* @param {Function} onSucc Function to store in current thread for later
437435
* lookup, when request succeeds.
436+
* @param {Function} onFail Function to store in current thread for later
437+
* lookup, when request fails.
438438
* @param {Object?=} scope Scope to invoke `cb` with.
439439
* @param {Object?=} res Resulting callback ids. Use `this._POOLED_CBIDS`.
440440
*/
441-
_storeCallbacksInCurrentThread: function(onFail, onSucc, scope) {
441+
_storeCallbacksInCurrentThread: function(onSucc, onFail, scope) {
442442
invariant(onFail || onSucc, INTERNAL_ERROR);
443443
this._bookkeeping.allocateCallbackIDs(this._POOLED_CBIDS);
444444
var succCBID = this._POOLED_CBIDS.successCallbackID;
@@ -494,18 +494,18 @@ var MessageQueueMixin = {
494494
return ret;
495495
},
496496

497-
call: function(moduleName, methodName, params, onFail, onSucc, scope) {
497+
call: function(moduleName, methodName, params, onSucc, onFail, scope) {
498498
invariant(
499499
(!onFail || typeof onFail === 'function') &&
500500
(!onSucc || typeof onSucc === 'function'),
501501
'Callbacks must be functions'
502502
);
503503
// Store callback _before_ sending the request, just in case the MailBox
504504
// returns the response in a blocking manner.
505-
if (onSucc) {
506-
this._storeCallbacksInCurrentThread(onFail, onSucc, scope, this._POOLED_CBIDS);
505+
if (onSucc || onFail) {
506+
this._storeCallbacksInCurrentThread(onSucc, onFail, scope, this._POOLED_CBIDS);
507+
onSucc && params.push(this._POOLED_CBIDS.successCallbackID);
507508
onFail && params.push(this._POOLED_CBIDS.errorCallbackID);
508-
params.push(this._POOLED_CBIDS.successCallbackID);
509509
}
510510
var moduleID = this._remoteModuleNameToModuleID[moduleName];
511511
if (moduleID === undefined || moduleID === null) {

React/Base/RCTBridge.m

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) {
4747
RCTBridgeFieldFlushDateMillis
4848
};
4949

50+
typedef NS_ENUM(NSUInteger, RCTJavaScriptFunctionKind) {
51+
RCTJavaScriptFunctionKindNormal,
52+
RCTJavaScriptFunctionKindAsync,
53+
};
54+
5055
#ifdef __LP64__
5156
typedef uint64_t RCTHeaderValue;
5257
typedef struct section_64 RCTHeaderSection;
@@ -204,6 +209,27 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) {
204209
return RCTModuleClassesByID;
205210
}
206211

212+
// TODO: Can we just replace RCTMakeError with this function instead?
213+
static NSDictionary *RCTJSErrorFromNSError(NSError *error)
214+
{
215+
NSString *errorMessage;
216+
NSArray *stackTrace = [NSThread callStackSymbols];
217+
NSMutableDictionary *errorInfo =
218+
[NSMutableDictionary dictionaryWithObject:stackTrace forKey:@"nativeStackIOS"];
219+
220+
if (error) {
221+
errorMessage = error.localizedDescription ?: @"Unknown error from a native module";
222+
errorInfo[@"domain"] = error.domain ?: RCTErrorDomain;
223+
errorInfo[@"code"] = @(error.code);
224+
} else {
225+
errorMessage = @"Unknown error from a native module";
226+
errorInfo[@"domain"] = RCTErrorDomain;
227+
errorInfo[@"code"] = @-1;
228+
}
229+
230+
return RCTMakeError(errorMessage, nil, errorInfo);
231+
}
232+
207233
@class RCTBatchedBridge;
208234

209235
@interface RCTBridge ()
@@ -239,6 +265,7 @@ @interface RCTModuleMethod : NSObject
239265
@property (nonatomic, copy, readonly) NSString *moduleClassName;
240266
@property (nonatomic, copy, readonly) NSString *JSMethodName;
241267
@property (nonatomic, assign, readonly) SEL selector;
268+
@property (nonatomic, assign, readonly) RCTJavaScriptFunctionKind functionKind;
242269

243270
@end
244271

@@ -420,6 +447,50 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
420447
} else if ([argumentName isEqualToString:@"RCTResponseSenderBlock"]) {
421448
addBlockArgument();
422449
useFallback = NO;
450+
} else if ([argumentName isEqualToString:@"RCTPromiseResolveBlock"]) {
451+
RCTAssert(i == numberOfArguments - 2,
452+
@"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]",
453+
_moduleClassName, objCMethodName);
454+
RCT_ARG_BLOCK(
455+
if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
456+
RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise resolver ID", index,
457+
json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName);
458+
return;
459+
}
460+
461+
// Marked as autoreleasing, because NSInvocation doesn't retain arguments
462+
__autoreleasing RCTPromiseResolveBlock value = (^(id result) {
463+
NSArray *arguments = result ? @[result] : @[];
464+
[bridge _invokeAndProcessModule:@"BatchedBridge"
465+
method:@"invokeCallbackAndReturnFlushedQueue"
466+
arguments:@[json, arguments]
467+
context:context];
468+
});
469+
)
470+
useFallback = NO;
471+
_functionKind = RCTJavaScriptFunctionKindAsync;
472+
} else if ([argumentName isEqualToString:@"RCTPromiseRejectBlock"]) {
473+
RCTAssert(i == numberOfArguments - 1,
474+
@"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]",
475+
_moduleClassName, objCMethodName);
476+
RCT_ARG_BLOCK(
477+
if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
478+
RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise rejecter ID", index,
479+
json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName);
480+
return;
481+
}
482+
483+
// Marked as autoreleasing, because NSInvocation doesn't retain arguments
484+
__autoreleasing RCTPromiseRejectBlock value = (^(NSError *error) {
485+
NSDictionary *errorJSON = RCTJSErrorFromNSError(error);
486+
[bridge _invokeAndProcessModule:@"BatchedBridge"
487+
method:@"invokeCallbackAndReturnFlushedQueue"
488+
arguments:@[json, @[errorJSON]]
489+
context:context];
490+
});
491+
)
492+
useFallback = NO;
493+
_functionKind = RCTJavaScriptFunctionKindAsync;
423494
}
424495
}
425496

@@ -498,9 +569,18 @@ - (void)invokeWithBridge:(RCTBridge *)bridge
498569

499570
// Safety check
500571
if (arguments.count != _argumentBlocks.count) {
572+
NSInteger actualCount = arguments.count;
573+
NSInteger expectedCount = _argumentBlocks.count;
574+
575+
// Subtract the implicit Promise resolver and rejecter functions for implementations of async functions
576+
if (_functionKind == RCTJavaScriptFunctionKindAsync) {
577+
actualCount -= 2;
578+
expectedCount -= 2;
579+
}
580+
501581
RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd",
502582
RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName,
503-
arguments.count, _argumentBlocks.count);
583+
actualCount, expectedCount);
504584
return;
505585
}
506586
}
@@ -525,7 +605,8 @@ - (void)invokeWithBridge:(RCTBridge *)bridge
525605

526606
- (NSString *)description
527607
{
528-
return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@;>", NSStringFromClass(self.class), self, _methodName, _JSMethodName];
608+
return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@;>",
609+
NSStringFromClass(self.class), self, _methodName, _JSMethodName];
529610
}
530611

531612
@end
@@ -606,7 +687,7 @@ - (NSString *)description
606687
* },
607688
* "methodName2": {
608689
* "methodID": 1,
609-
* "type": "remote"
690+
* "type": "remoteAsync"
610691
* },
611692
* etc...
612693
* },
@@ -630,7 +711,7 @@ - (NSString *)description
630711
[methods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger methodID, BOOL *_stop) {
631712
methodsByName[method.JSMethodName] = @{
632713
@"methodID": @(methodID),
633-
@"type": @"remote",
714+
@"type": method.functionKind == RCTJavaScriptFunctionKindAsync ? @"remoteAsync" : @"remote",
634715
};
635716
}];
636717

React/Base/RCTBridgeModule.h

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@
1717
*/
1818
typedef void (^RCTResponseSenderBlock)(NSArray *response);
1919

20+
/**
21+
* Block that bridge modules use to resolve the JS promise waiting for a result.
22+
* Nil results are supported and are converted to JS's undefined value.
23+
*/
24+
typedef void (^RCTPromiseResolveBlock)(id result);
25+
26+
/**
27+
* Block that bridge modules use to reject the JS promise waiting for a result.
28+
* The error may be nil but it is preferable to pass an NSError object for more
29+
* precise error messages.
30+
*/
31+
typedef void (^RCTPromiseRejectBlock)(NSError *error);
32+
33+
2034
/**
2135
* This constant can be returned from +methodQueue to force module
2236
* methods to be called on the JavaScript thread. This can have serious
@@ -37,7 +51,7 @@ extern const dispatch_queue_t RCTJSThread;
3751
* A reference to the RCTBridge. Useful for modules that require access
3852
* to bridge features, such as sending events or making JS calls. This
3953
* will be set automatically by the bridge when it initializes the module.
40-
* To implement this in your module, just add @synthesize bridge = _bridge;
54+
* To implement this in your module, just add @synthesize bridge = _bridge;
4155
*/
4256
@property (nonatomic, weak) RCTBridge *bridge;
4357

@@ -70,6 +84,26 @@ extern const dispatch_queue_t RCTJSThread;
7084
* { ... }
7185
*
7286
* and is exposed to JavaScript as `NativeModules.ModuleName.doSomething`.
87+
*
88+
* ## Promises
89+
*
90+
* Bridge modules can also define methods that are exported to JavaScript as
91+
* methods that return a Promise, and are compatible with JS async functions.
92+
*
93+
* Declare the last two parameters of your native method to be a resolver block
94+
* and a rejecter block. The resolver block must precede the rejecter block.
95+
*
96+
* For example:
97+
*
98+
* RCT_EXPORT_METHOD(doSomethingAsync:(NSString *)aString
99+
* resolver:(RCTPromiseResolveBlock)resolve
100+
* rejecter:(RCTPromiseRejectBlock)reject
101+
* { ... }
102+
*
103+
* Calling `NativeModules.ModuleName.doSomethingAsync(aString)` from
104+
* JavaScript will return a promise that is resolved or rejected when your
105+
* native method implementation calls the respective block.
106+
*
73107
*/
74108
#define RCT_EXPORT_METHOD(method) \
75109
RCT_REMAP_METHOD(, method)
@@ -118,7 +152,7 @@ extern const dispatch_queue_t RCTJSThread;
118152
RCT_EXTERN_REMAP_MODULE(, objc_name, objc_supername)
119153

120154
/**
121-
* Similar to RCT_EXTERN_MODULE but allows setting a custom JavaScript name
155+
* Like RCT_EXTERN_MODULE, but allows setting a custom JavaScript name.
122156
*/
123157
#define RCT_EXTERN_REMAP_MODULE(js_name, objc_name, objc_supername) \
124158
objc_name : objc_supername \
@@ -136,7 +170,7 @@ extern const dispatch_queue_t RCTJSThread;
136170
RCT_EXTERN_REMAP_METHOD(, method)
137171

138172
/**
139-
* Similar to RCT_EXTERN_REMAP_METHOD but allows setting a custom JavaScript name
173+
* Like RCT_EXTERN_REMAP_METHOD, but allows setting a custom JavaScript name.
140174
*/
141175
#define RCT_EXTERN_REMAP_METHOD(js_name, method) \
142176
- (void)__rct_export__##method { \

0 commit comments

Comments
 (0)