Skip to content

Commit 0cdcfab

Browse files
committed
[RFC][Bridge] Add support for JS async functions, backed by RCT_EXPORT_PROMISE
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_PROMISE(getValueAsync:(NSString *)key) { 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 is // available under }); ``` And if you take a time machine or use Babel w/stage 1, you can write: ```js try { var value = await DemoDataStore.getValueAsync('sample-key'); console.log('Got:', value); } catch (error) { console.error(error); } ``` For now the macro is defined as RCT_EXPORT_PROMISE_EXPERIMENTAL since I'd like to merge this and get real-world feedback on the API before committing to it.
1 parent 41612f3 commit 0cdcfab

File tree

6 files changed

+229
-28
lines changed

6 files changed

+229
-28
lines changed

Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js

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

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

@@ -36,21 +37,40 @@ var BatchedBridgeFactory = {
3637
*/
3738
_createBridgedModule: function(messageQueue, moduleConfig, moduleName) {
3839
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-
};
40+
switch (methodConfig.type) {
41+
case MethodTypes.remote:
42+
return function() {
43+
var lastArg = arguments.length > 0 ? arguments[arguments.length - 1] : null;
44+
var secondLastArg = arguments.length > 1 ? arguments[arguments.length - 2] : null;
45+
var hasSuccCB = typeof lastArg === 'function';
46+
var hasErrorCB = typeof secondLastArg === 'function';
47+
hasErrorCB && invariant(
48+
hasSuccCB,
49+
'Cannot have a non-function arg after a function arg.'
50+
);
51+
var numCBs = (hasSuccCB ? 1 : 0) + (hasErrorCB ? 1 : 0);
52+
var args = slice.call(arguments, 0, arguments.length - numCBs);
53+
var onSucc = hasSuccCB ? lastArg : null;
54+
var onFail = hasErrorCB ? secondLastArg : null;
55+
messageQueue.call(moduleName, memberName, args, onFail, onSucc);
56+
};
57+
58+
case MethodTypes.remoteAsync:
59+
return function(...args) {
60+
return new Promise((resolve, reject) => {
61+
messageQueue.call(moduleName, memberName, args, (errorData) => {
62+
var error = _createErrorFromErrorData(errorData);
63+
reject(error);
64+
}, resolve);
65+
});
66+
};
67+
68+
case MethodTypes.local:
69+
return null;
70+
71+
default:
72+
throw new Error('Unknown bridge method type: ' + methodConfig.type);
73+
}
5474
});
5575
for (var constName in moduleConfig.constants) {
5676
warning(!remoteModule[constName], 'saw constant and method named %s', constName);
@@ -59,7 +79,6 @@ var BatchedBridgeFactory = {
5979
return remoteModule;
6080
},
6181

62-
6382
create: function(MessageQueue, modulesConfig, localModulesConfig) {
6483
var messageQueue = new MessageQueue(modulesConfig, localModulesConfig);
6584
return {
@@ -80,4 +99,13 @@ var BatchedBridgeFactory = {
8099
}
81100
};
82101

102+
function _createErrorFromErrorData(errorData: ErrorData): Error {
103+
var {
104+
message,
105+
...extraErrorInfo,
106+
} = errorData;
107+
var error = new Error(message);
108+
return Object.assign(error, extraErrorInfo);
109+
}
110+
83111
module.exports = BatchedBridgeFactory;

Libraries/Utilities/MessageQueue.js

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,17 @@ type ModulesConfig = {
3131
methodID: number;
3232
}};
3333
}
34-
}
34+
};
35+
36+
type ErrorData = {
37+
message: string;
38+
domain: string;
39+
code: number;
40+
nativeStackIOS?: Array<string>;
41+
};
3542

36-
type NameToID = {[key:string]: number}
37-
type IDToName = {[key:number]: string}
43+
type NameToID = {[key:string]: number};
44+
type IDToName = {[key:number]: string};
3845

3946
/**
4047
* So as not to confuse static build system.
@@ -290,6 +297,40 @@ var MessageQueueMixin = {
290297
);
291298
},
292299

300+
invokePromiseRejecterAndReturnFlushedQueue: function(cbID, errorData: ErrorData) {
301+
if (this._enableLogging) {
302+
this._loggedIncomingItems.push([new Date().getTime(), cbID, errorData]);
303+
}
304+
return guardReturn(
305+
this._invokePromiseRejecter,
306+
[cbID, errorData],
307+
this._flushedQueueUnguarded,
308+
this
309+
);
310+
},
311+
312+
_invokePromiseRejecter(cbID, errorData: ErrorData) {
313+
try {
314+
var error = this._createErrorFromErrorData(errorData);
315+
} catch (e) {
316+
// Clear out the related resources. Normally this is handled after
317+
// invoking the callback but we haven't gone that far in this code path.
318+
this._freeResourcesForCallbackID(cbID);
319+
throw e;
320+
}
321+
322+
this._invokeCallback(cbID, [error]);
323+
},
324+
325+
_createErrorFromErrorData: function(errorData: ErrorData): Error {
326+
var {
327+
message,
328+
...extraErrorInfo,
329+
} = errorData;
330+
var error = new Error(message);
331+
return Object.assign(error, extraErrorInfo);
332+
},
333+
293334
callFunction: function(moduleID, methodID, params) {
294335
return guardReturn(this._callFunction, [moduleID, methodID, params], null, this);
295336
},

React/Base/RCTBridge.m

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#import <mach-o/dyld.h>
1717
#import <mach-o/getsect.h>
1818

19+
#import "RCTBridgeModule_Internal.h"
1920
#import "RCTContextExecutor.h"
2021
#import "RCTConvert.h"
2122
#import "RCTEventDispatcher.h"
@@ -240,6 +241,7 @@ @interface RCTModuleMethod : NSObject
240241
@property (nonatomic, copy, readonly) NSString *moduleClassName;
241242
@property (nonatomic, copy, readonly) NSString *JSMethodName;
242243
@property (nonatomic, assign, readonly) SEL selector;
244+
@property (nonatomic, assign, readonly) RCTBridgeFunctionKind functionKind;
243245

244246
@end
245247

@@ -266,8 +268,10 @@ @implementation RCTModuleMethod
266268
- (instancetype)initWithReactMethodName:(NSString *)reactMethodName
267269
objCMethodName:(NSString *)objCMethodName
268270
JSMethodName:(NSString *)JSMethodName
271+
functionKind:(RCTBridgeFunctionKind)functionKind
269272
{
270273
if ((self = [super init])) {
274+
_functionKind = functionKind;
271275
_methodName = reactMethodName;
272276
NSArray *parts = [[reactMethodName substringWithRange:(NSRange){2, reactMethodName.length - 3}] componentsSeparatedByString:@" "];
273277

@@ -421,6 +425,43 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
421425
} else if ([argumentName isEqualToString:@"RCTResponseSenderBlock"]) {
422426
addBlockArgument();
423427
useFallback = NO;
428+
} else if ([argumentName isEqualToString:@"RCTPromiseResolverBlock"]) {
429+
RCT_ARG_BLOCK(
430+
if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
431+
RCTLogError(@"Argument %tu (%@) of %@.%@ must be a Promise resolver ID", index,
432+
json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName);
433+
return;
434+
}
435+
436+
// Marked as autoreleasing, because NSInvocation doesn't retain arguments
437+
__autoreleasing RCTPromiseResolverBlock value = (^(id result) {
438+
NSArray *arguments = result ? @[result] : @[];
439+
[bridge _invokeAndProcessModule:@"BatchedBridge"
440+
method:@"invokeCallbackAndReturnFlushedQueue"
441+
arguments:@[json, arguments]
442+
context:context];
443+
});
444+
)
445+
useFallback = NO;
446+
} else if ([argumentName isEqualToString:@"RCTPromiseRejecterBlock"]) {
447+
RCT_ARG_BLOCK(
448+
if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
449+
RCTLogError(@"Argument %tu (%@) of %@.%@ must be a Promise resolver ID", index,
450+
json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName);
451+
return;
452+
}
453+
454+
// Marked as autoreleasing, because NSInvocation doesn't retain arguments
455+
__autoreleasing RCTPromiseRejecterBlock value = (^(NSError *error) {
456+
NSDictionary *errorData = [RCTModuleMethod dictionaryFromError:error
457+
stackTrace:[NSThread callStackSymbols]];
458+
[bridge _invokeAndProcessModule:@"BatchedBridge"
459+
method:@"invokeCallbackAndReturnFlushedQueue"
460+
arguments:@[json, @[errorData]]
461+
context:context];
462+
});
463+
)
464+
useFallback = NO;
424465
}
425466
}
426467

@@ -499,9 +540,18 @@ - (void)invokeWithBridge:(RCTBridge *)bridge
499540

500541
// Safety check
501542
if (arguments.count != _argumentBlocks.count) {
543+
NSInteger actualCount = arguments.count;
544+
NSInteger expectedCount = _argumentBlocks.count;
545+
546+
// Subtract the implicit Promise resolver and rejecter functions for implementations of async functions
547+
if (_functionKind == RCTBridgeFunctionKindAsync) {
548+
actualCount -= 2;
549+
expectedCount -= 2;
550+
}
551+
502552
RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd",
503553
RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName,
504-
arguments.count, _argumentBlocks.count);
554+
actualCount, expectedCount);
505555
return;
506556
}
507557
}
@@ -529,6 +579,26 @@ - (NSString *)description
529579
return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@;>", NSStringFromClass(self.class), self, _methodName, _JSMethodName];
530580
}
531581

582+
+ (NSDictionary *)dictionaryFromError:(NSError *)error stackTrace:(NSArray *)stackTrace
583+
{
584+
NSString *errorMessage;
585+
NSMutableDictionary *errorInfo = [NSMutableDictionary dictionaryWithDictionary:@{
586+
@"nativeStackIOS": stackTrace,
587+
}];
588+
589+
if (error) {
590+
errorMessage = error.localizedDescription ?: @"Unknown error from a native module";
591+
errorInfo[@"domain"] = error.domain ?: RCTErrorDomain;
592+
errorInfo[@"code"] = @(error.code);
593+
} else {
594+
errorMessage = @"Unknown error from a native module";
595+
errorInfo[@"domain"] = RCTErrorDomain;
596+
errorInfo[@"code"] = @(-1);
597+
}
598+
599+
return RCTMakeError(errorMessage, nil, errorInfo);
600+
}
601+
532602
@end
533603

534604
/**
@@ -557,7 +627,7 @@ - (NSString *)description
557627

558628
for (RCTHeaderValue addr = section->offset;
559629
addr < section->offset + section->size;
560-
addr += sizeof(const char **) * 3) {
630+
addr += sizeof(const char **) * 4) {
561631

562632
// Get data entry
563633
const char **entries = (const char **)(mach_header + addr);
@@ -569,11 +639,13 @@ - (NSString *)description
569639
// Legacy support for RCT_EXPORT()
570640
moduleMethod = [[RCTModuleMethod alloc] initWithReactMethodName:@(entries[0])
571641
objCMethodName:@(entries[0])
572-
JSMethodName:strlen(entries[1]) ? @(entries[1]) : nil];
642+
JSMethodName:strlen(entries[1]) ? @(entries[1]) : nil
643+
functionKind:(RCTBridgeFunctionKind)entries[3]];
573644
} else {
574645
moduleMethod = [[RCTModuleMethod alloc] initWithReactMethodName:@(entries[0])
575646
objCMethodName:strlen(entries[1]) ? @(entries[1]) : nil
576-
JSMethodName:strlen(entries[2]) ? @(entries[2]) : nil];
647+
JSMethodName:strlen(entries[2]) ? @(entries[2]) : nil
648+
functionKind:(RCTBridgeFunctionKind)entries[3]];
577649
}
578650

579651
// Cache method
@@ -607,7 +679,7 @@ - (NSString *)description
607679
* },
608680
* "methodName2": {
609681
* "methodID": 1,
610-
* "type": "remote"
682+
* "type": "remoteAsync"
611683
* },
612684
* etc...
613685
* },
@@ -631,7 +703,7 @@ - (NSString *)description
631703
[methods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger methodID, BOOL *_stop) {
632704
methodsByName[method.JSMethodName] = @{
633705
@"methodID": @(methodID),
634-
@"type": @"remote",
706+
@"type": method.functionKind == RCTBridgeFunctionKindAsync ? @"remoteAsync" : @"remote",
635707
};
636708
}];
637709

React/Base/RCTBridgeModule.h

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
#import <Foundation/Foundation.h>
1111

12+
#import "RCTBridgeModule_Internal.h"
13+
1214
@class RCTBridge;
1315

1416
/**
@@ -17,6 +19,11 @@
1719
*/
1820
typedef void (^RCTResponseSenderBlock)(NSArray *response);
1921

22+
typedef void (^RCTPromiseResolverBlock)(id result);
23+
24+
typedef void (^RCTPromiseRejecterBlock)(NSError *error);
25+
26+
2027
/**
2128
* This constant can be returned from +methodQueue to force module
2229
* methods to be called on the JavaScript thread. This can have serious
@@ -83,7 +90,28 @@ extern const dispatch_queue_t RCTJSThread;
8390
* { ... }
8491
*/
8592
#define RCT_REMAP_METHOD(js_name, method) \
86-
RCT_EXTERN_REMAP_METHOD(js_name, method) \
93+
_RCT_REMAP_METHOD_INTERNAL(js_name, method, RCTBridgeFunctionKindNormal) \
94+
95+
#define RCT_EXPORT_PROMISE_EXPERIMENTAL(method) \
96+
RCT_REMAP_PROMISE_EXPERIMENTAL(, method)
97+
98+
// XXX(ide): is there a trick to support 0-arg methods just one macro?
99+
#define RCT_EXPORT_NULLARY_PROMISE_EXPERIMENTAL(method) \
100+
_RCT_REMAP_METHOD_INTERNAL( \
101+
, \
102+
method:(RCTPromiseRejecterBlock)reject __rct_resolver:(RCTPromiseResolverBlock)resolve, \
103+
RCTBridgeFunctionKindAsync \
104+
)
105+
106+
#define RCT_REMAP_PROMISE_EXPERIMENTAL(js_name, method) \
107+
_RCT_REMAP_METHOD_INTERNAL( \
108+
js_name, \
109+
method __rct_rejecter:(RCTPromiseRejecterBlock)reject __rct_resolver:(RCTPromiseResolverBlock)resolve, \
110+
RCTBridgeFunctionKindAsync \
111+
)
112+
113+
#define _RCT_REMAP_METHOD_INTERNAL(js_name, method, functionKind) \
114+
_RCT_EXTERN_REMAP_METHOD_INTERNAL(js_name, method, functionKind) \
87115
- (void)method
88116

89117
/**
@@ -139,10 +167,25 @@ extern const dispatch_queue_t RCTJSThread;
139167
* Similar to RCT_EXTERN_REMAP_METHOD but allows setting a custom JavaScript name
140168
*/
141169
#define RCT_EXTERN_REMAP_METHOD(js_name, method) \
170+
_RCT_EXTERN_REMAP_METHOD_INTERNAL(js_name, method, RCTBridgeFunctionKindNormal)
171+
172+
/**
173+
* Internal implementation of RCTExport
174+
*
175+
* @param js_name method name exposed to JavaScript
176+
* @param method name of the Objective-C method being exported
177+
* @param functionKind kind of JavaScript function to define; one of RCTBridgeFunctionKind
178+
*/
179+
#define _RCT_EXTERN_REMAP_METHOD_INTERNAL(js_name, method, functionKind) \
142180
- (void)__rct_export__##method { \
143181
__attribute__((used, section("__DATA,RCTExport"))) \
144182
__attribute__((__aligned__(1))) \
145-
static const char *__rct_export_entry__[] = { __func__, #method, #js_name }; \
183+
static const char *__rct_export_entry__[] = { \
184+
__func__, \
185+
#method, \
186+
#js_name, \
187+
(char *)functionKind \
188+
}; \
146189
}
147190

148191
/**
@@ -152,7 +195,7 @@ extern const dispatch_queue_t RCTJSThread;
152195
_Pragma("message(\"RCT_EXPORT is deprecated. Use RCT_EXPORT_METHOD instead.\")") \
153196
__attribute__((used, section("__DATA,RCTExport"))) \
154197
__attribute__((__aligned__(1))) \
155-
static const char *__rct_export_entry__[] = { __func__, #js_name, NULL }
198+
static const char *__rct_export_entry__[] = { __func__, #js_name, NULL, RCTBridgeFunctionKindNormal }
156199

157200
/**
158201
* The queue that will be used to call all exported methods. If omitted, this

0 commit comments

Comments
 (0)