2015-01-30 01:10:49 +00:00
// Copyright 2004 - present Facebook . All Rights Reserved .
# import "RCTBridge.h"
# import < objc / message . h >
# import "RCTModuleMethod.h"
# import "RCTInvalidating.h"
2015-02-04 00:02:36 +00:00
# import "RCTEventDispatcher.h"
2015-01-30 01:10:49 +00:00
# import "RCTLog.h"
# import "RCTModuleIDs.h"
# import "RCTUtils.h"
2015-02-04 00:12:07 +00:00
/ * *
* Functions are the one thing that aren ' t automatically converted to OBJC
* blocks , according to this revert : http : // trac . webkit . org / changeset / 144489
* They must be expressed as ` JSValue` s .
*
* But storing callbacks causes reference cycles !
* http : // stackoverflow . com / questions / 19202248 / how - can - i - use - jsmanagedvalue - to - avoid - a - reference - cycle - without - the - jsvalue - gett
* We ' ll live with the leak for now , but need to clean this up asap :
* Passing a reference to the ` context` to the bridge would make it easy to
* execute JS . We can add ` JSManagedValue` s to protect against this . The same
* needs to be done in ` RCTTiming` and friends .
* /
/ * *
* Must be kept in sync with ` MessageQueue . js` .
* /
typedef NS_ENUM ( NSUInteger , RCTBridgeFields ) {
RCTBridgeFieldRequestModuleIDs = 0 ,
RCTBridgeFieldMethodIDs ,
RCTBridgeFieldParamss ,
RCTBridgeFieldResponseCBIDs ,
RCTBridgeFieldResponseReturnValues ,
RCTBridgeFieldFlushDateMillis
} ;
static NSString * RCTModuleName ( Class moduleClass )
2015-01-30 01:10:49 +00:00
{
if ( [ moduleClass respondsToSelector : @ selector ( moduleName ) ] ) {
return [ moduleClass moduleName ] ;
} else {
// Default implementation , works in most cases
2015-02-04 00:12:07 +00:00
return NSStringFromClass ( moduleClass ) ;
2015-01-30 01:10:49 +00:00
}
}
2015-02-04 00:12:07 +00:00
static NSDictionary * RCTNativeModuleClasses ( void )
2015-01-30 01:10:49 +00:00
{
static NSMutableDictionary * modules ;
static dispatch_once _t onceToken ;
dispatch_once ( & onceToken , ^ {
modules = [ NSMutableDictionary dictionary ] ;
unsigned int classCount ;
Class * classes = objc_copyClassList ( & classCount ) ;
for ( unsigned int i = 0 ; i < classCount ; i + + ) {
Class cls = classes [ i ] ;
if ( ! class_getSuperclass ( cls ) ) {
// Class has no superclass - it ' s probably something weird
continue ;
}
if ( ! [ cls conformsToProtocol : @ protocol ( RCTNativeModule ) ] ) {
// Not an RCTNativeModule
continue ;
}
// Get module name
NSString * moduleName = RCTModuleName ( cls ) ;
// Check module name is unique
id existingClass = modules [ moduleName ] ;
RCTCAssert ( existingClass = = Nil , @ "Attempted to register RCTNativeModule class %@ for the name '%@', but name was already registered by class %@" , cls , moduleName , existingClass ) ;
modules [ moduleName ] = cls ;
}
free ( classes ) ;
} ) ;
return modules ;
}
@ implementation RCTBridge
{
NSMutableDictionary * _moduleInstances ;
NSDictionary * _javaScriptModulesConfig ;
id < RCTJavaScriptExecutor > _javaScriptExecutor ;
}
static id < RCTJavaScriptExecutor > _latestJSExecutor ;
- ( instancetype ) initWithJavaScriptExecutor : ( id < RCTJavaScriptExecutor > ) javaScriptExecutor
javaScriptModulesConfig : ( NSDictionary * ) javaScriptModulesConfig
{
if ( ( self = [ super init ] ) ) {
_javaScriptExecutor = javaScriptExecutor ;
_latestJSExecutor = _javaScriptExecutor ;
2015-02-04 00:02:36 +00:00
_eventDispatcher = [ [ RCTEventDispatcher alloc ] initWithBridge : self ] ;
2015-01-30 01:10:49 +00:00
_javaScriptModulesConfig = javaScriptModulesConfig ;
2015-02-04 00:12:07 +00:00
_shadowQueue = dispatch_queue _create ( "com.facebook.ReactKit.ShadowQueue" , DISPATCH_QUEUE _SERIAL ) ;
// Register modules
_moduleInstances = [ [ NSMutableDictionary alloc ] init ] ;
2015-01-30 01:10:49 +00:00
[ RCTNativeModuleClasses ( ) enumerateKeysAndObjectsUsingBlock : ^ ( NSString * moduleName , Class moduleClass , BOOL * stop ) {
if ( _moduleInstances [ moduleName ] = = nil ) {
2015-02-04 00:12:07 +00:00
if ( [ moduleClass instancesRespondToSelector : @ selector ( initWithBridge : ) ] ) {
_moduleInstances [ moduleName ] = [ [ moduleClass alloc ] initWithBridge : self ] ;
} else {
_moduleInstances [ moduleName ] = [ [ moduleClass alloc ] init ] ;
}
2015-01-30 01:10:49 +00:00
}
} ] ;
[ self doneRegisteringModules ] ;
}
return self ;
}
- ( void ) dealloc
{
RCTAssert ( ! self . valid , @ "must call -invalidate before -dealloc; TODO: why not call it here then?" ) ;
}
# pragma mark - RCTInvalidating
- ( BOOL ) isValid
{
return _javaScriptExecutor ! = nil ;
}
- ( void ) invalidate
{
if ( _latestJSExecutor = = _javaScriptExecutor ) {
_latestJSExecutor = nil ;
}
_javaScriptExecutor = nil ;
dispatch_sync ( _shadowQueue , ^ {
2015-02-04 00:12:07 +00:00
// Make sure all dispatchers have been executed before continuing
// TODO : is this still needed ?
2015-01-30 01:10:49 +00:00
} ) ;
2015-02-04 00:12:07 +00:00
2015-01-30 01:10:49 +00:00
for ( id target in _moduleInstances . objectEnumerator ) {
if ( [ target respondsToSelector : @ selector ( invalidate ) ] ) {
[ ( id < RCTInvalidating > ) target invalidate ] ;
}
}
[ _moduleInstances removeAllObjects ] ;
}
/ * *
* - TODO ( #5906496 ) : When we build a ` MessageQueue . m` , handling all the requests could
* cause both a queue of "responses" . We would flush them here . However , we
* currently just expect each objc block to handle its own response sending
* using a ` RCTResponseSenderBlock` .
* /
# pragma mark - RCTBridge methods
/ * *
* Like JS : : call , for objective - c .
* /
- ( void ) enqueueJSCall : ( NSUInteger ) moduleID methodID : ( NSUInteger ) methodID args : ( NSArray * ) args
{
RCTAssertMainThread ( ) ;
[ self _invokeRemoteJSModule : moduleID methodID : methodID args : args ] ;
}
- ( void ) enqueueApplicationScript : ( NSString * ) script url : ( NSURL * ) url onComplete : ( RCTJavaScriptCompleteBlock ) onComplete
{
RCTAssert ( onComplete ! = nil , @ "onComplete block passed in should be non-nil" ) ;
[ _javaScriptExecutor executeApplicationScript : script sourceURL : url onComplete : ^ ( NSError * scriptLoadError ) {
if ( scriptLoadError ) {
onComplete ( scriptLoadError ) ;
return ;
}
[ _javaScriptExecutor executeJSCall : @ "BatchedBridge"
method : @ "flushedQueue"
arguments : @ [ ]
callback : ^ ( id objcValue , NSError * error ) {
[ self _handleBuffer : objcValue ] ;
onComplete ( error ) ;
} ] ;
} ] ;
}
# pragma mark - Payload Generation
- ( void ) _invokeAndProcessModule : ( NSString * ) module method : ( NSString * ) method arguments : ( NSArray * ) args
{
NSTimeInterval startJS = RCTTGetAbsoluteTime ( ) ;
RCTJavaScriptCallback processResponse = ^ ( id objcValue , NSError * error ) {
NSTimeInterval startNative = RCTTGetAbsoluteTime ( ) ;
[ self _handleBuffer : objcValue ] ;
NSTimeInterval end = RCTTGetAbsoluteTime ( ) ;
NSTimeInterval timeJS = startNative - startJS ;
NSTimeInterval timeNative = end - startNative ;
// TODO : surface this performance information somewhere
[ [ NSNotificationCenter defaultCenter ] postNotificationName : @ "PERF" object : nil userInfo : @ { @ "JS" : @ ( timeJS * 1000000 ) , @ "Native" : @ ( timeNative * 1000000 ) } ] ;
} ;
[ _javaScriptExecutor executeJSCall : module
method : method
arguments : args
callback : processResponse ] ;
}
- ( void ) _invokeRemoteJSModule : ( NSUInteger ) moduleID methodID : ( NSUInteger ) methodID args : ( NSArray * ) args
{
[ self _invokeAndProcessModule : @ "BatchedBridge"
method : @ "callFunctionReturnFlushedQueue"
arguments : @ [ @ ( moduleID ) , @ ( methodID ) , args ] ] ;
}
/ * *
* TODO ( #5906496 ) : Have responses piggy backed on a round trip with ObjC -> JS requests .
* /
- ( void ) _sendResponseToJavaScriptCallbackID : ( NSInteger ) cbID args : ( NSArray * ) args
{
[ self _invokeAndProcessModule : @ "BatchedBridge"
method : @ "invokeCallbackAndReturnFlushedQueue"
arguments : @ [ @ ( cbID ) , args ] ] ;
}
# pragma mark - Payload Processing
- ( void ) _handleBuffer : ( id ) buffer
{
if ( buffer = = nil || buffer = = ( id ) kCFNull ) {
return ;
}
if ( ! [ buffer isKindOfClass : [ NSArray class ] ] ) {
RCTLogMustFix ( @ "Buffer must be an instance of NSArray, got %@" , NSStringFromClass ( [ buffer class ] ) ) ;
return ;
}
NSArray * requestsArray = ( NSArray * ) buffer ;
NSUInteger bufferRowCount = [ requestsArray count ] ;
NSUInteger expectedFieldsCount = RCTBridgeFieldResponseReturnValues + 1 ;
if ( bufferRowCount ! = expectedFieldsCount ) {
RCTLogMustFix ( @ "Must pass all fields to buffer - expected %zd, saw %zd" , expectedFieldsCount , bufferRowCount ) ;
return ;
}
for ( NSUInteger fieldIndex = RCTBridgeFieldRequestModuleIDs ; fieldIndex <= RCTBridgeFieldParamss ; fieldIndex + + ) {
id field = [ requestsArray objectAtIndex : fieldIndex ] ;
if ( ! [ field isKindOfClass : [ NSArray class ] ] ) {
RCTLogMustFix ( @ "Field at index %zd in buffer must be an instance of NSArray, got %@" , fieldIndex , NSStringFromClass ( [ field class ] ) ) ;
return ;
}
}
2015-02-04 00:12:07 +00:00
NSArray * moduleIDs = requestsArray [ RCTBridgeFieldRequestModuleIDs ] ;
NSArray * methodIDs = requestsArray [ RCTBridgeFieldMethodIDs ] ;
NSArray * paramsArrays = requestsArray [ RCTBridgeFieldParamss ] ;
2015-01-30 01:10:49 +00:00
NSUInteger numRequests = [ moduleIDs count ] ;
2015-02-04 00:12:07 +00:00
BOOL allSame = numRequests = = [ methodIDs count ] && numRequests = = [ paramsArrays count ] ;
2015-01-30 01:10:49 +00:00
if ( ! allSame ) {
RCTLogMustFix ( @ "Invalid data message - all must be length: %zd" , numRequests ) ;
return ;
}
for ( NSUInteger i = 0 ; i < numRequests ; i + + ) {
@ autoreleasepool {
[ self _handleRequestNumber : i
2015-02-04 00:12:07 +00:00
moduleID : [ moduleIDs [ i ] integerValue ]
methodID : [ methodIDs [ i ] integerValue ]
params : paramsArrays [ i ] ] ;
2015-01-30 01:10:49 +00:00
}
}
2015-02-04 00:12:07 +00:00
// TODO : only used by RCTUIManager - can we eliminate this special case ?
dispatch_async ( _shadowQueue , ^ {
for ( id target in _moduleInstances . objectEnumerator ) {
if ( [ target respondsToSelector : @ selector ( batchDidComplete ) ] ) {
2015-01-30 01:10:49 +00:00
[ target batchDidComplete ] ;
2015-02-04 00:12:07 +00:00
}
2015-01-30 01:10:49 +00:00
}
2015-02-04 00:12:07 +00:00
} ) ;
2015-01-30 01:10:49 +00:00
}
2015-02-04 00:12:07 +00:00
- ( BOOL ) _handleRequestNumber : ( NSUInteger ) i
moduleID : ( NSInteger ) moduleID
methodID : ( NSInteger ) methodID
params : ( NSArray * ) params
2015-01-30 01:10:49 +00:00
{
2015-02-04 00:12:07 +00:00
if ( ! [ params isKindOfClass : [ NSArray class ] ] ) {
2015-01-30 01:10:49 +00:00
RCTLogMustFix ( @ "Invalid module/method/params tuple for request #%zd" , i ) ;
2015-02-04 00:12:07 +00:00
return NO ;
2015-01-30 01:10:49 +00:00
}
2015-02-04 00:12:07 +00:00
2015-01-30 01:10:49 +00:00
if ( moduleID < 0 || moduleID >= RCTExportedMethodsByModule ( ) . count ) {
return NO ;
}
2015-02-04 00:12:07 +00:00
2015-01-30 01:10:49 +00:00
NSString * moduleName = RCTExportedModuleNameAtSortedIndex ( moduleID ) ;
NSArray * methods = RCTExportedMethodsByModule ( ) [ moduleName ] ;
if ( methodID < 0 || methodID >= methods . count ) {
return NO ;
}
2015-02-04 00:12:07 +00:00
2015-01-30 01:10:49 +00:00
RCTModuleMethod * method = methods [ methodID ] ;
NSUInteger methodArity = method . arity ;
if ( params . count ! = methodArity ) {
2015-02-04 00:12:07 +00:00
RCTLogMustFix ( @ "Expected %tu arguments but got %tu invoking %@.%@" ,
methodArity ,
params . count ,
moduleName ,
method . JSMethodName ) ;
2015-01-30 01:10:49 +00:00
return NO ;
}
2015-02-04 00:12:07 +00:00
2015-01-30 01:10:49 +00:00
__weak RCTBridge * weakSelf = self ;
dispatch_async ( _shadowQueue , ^ {
__strong RCTBridge * strongSelf = weakSelf ;
2015-02-04 00:12:07 +00:00
2015-01-30 01:10:49 +00:00
if ( ! strongSelf . isValid ) {
// strongSelf has been invalidated since the dispatch_async call and this
// invocation should not continue .
return ;
}
2015-02-04 00:12:07 +00:00
2015-01-30 01:10:49 +00:00
NSInvocation * invocation = [ RCTBridge invocationForAdditionalArguments : methodArity ] ;
// TODO : we should just store module instances by index , since that ' s how we look them up anyway
id target = strongSelf -> _moduleInstances [ moduleName ] ;
RCTAssert ( target ! = nil , @ "No module found for name '%@'" , moduleName ) ;
[ invocation setArgument : & target atIndex : 0 ] ;
2015-02-04 00:12:07 +00:00
2015-01-30 01:10:49 +00:00
SEL selector = method . selector ;
[ invocation setArgument : & selector atIndex : 1 ] ;
2015-02-04 00:12:07 +00:00
2015-01-30 01:10:49 +00:00
// Retain used blocks until after invocation completes .
NSMutableArray * blocks = [ NSMutableArray array ] ;
2015-02-04 00:12:07 +00:00
2015-01-30 01:10:49 +00:00
[ params enumerateObjectsUsingBlock : ^ ( id param , NSUInteger idx , BOOL * stop ) {
if ( [ param isEqual : [ NSNull null ] ] ) {
param = nil ;
} else if ( [ method . blockArgumentIndexes containsIndex : idx ] ) {
id block = [ strongSelf createResponseSenderBlock : [ param integerValue ] ] ;
[ blocks addObject : block ] ;
param = block ;
}
2015-02-04 00:12:07 +00:00
2015-01-30 01:10:49 +00:00
[ invocation setArgument : & param atIndex : idx + 2 ] ;
} ] ;
2015-02-04 00:12:07 +00:00
2015-01-30 01:10:49 +00:00
@ try {
[ invocation invoke ] ;
}
@ catch ( NSException * exception ) {
RCTLogMustFix ( @ "Exception thrown while invoking %@ on target %@ with params %@: %@" , method . JSMethodName , target , params , exception ) ;
}
@ finally {
// Force ` blocks` to remain alive until here .
blocks = nil ;
}
} ) ;
2015-02-04 00:12:07 +00:00
2015-01-30 01:10:49 +00:00
return YES ;
}
2015-02-04 00:12:07 +00:00
/ * *
* Returns a callback that reports values back to the JS thread .
* TODO ( #5906496 ) : These responses should go into their own queue ` MessageQueue . m` that
* mirrors the JS queue and protocol . For now , we speak the "language" of the JS
* queue by packing it into an array that matches the wire protocol .
* /
- ( RCTResponseSenderBlock ) createResponseSenderBlock : ( NSInteger ) cbID
{
if ( ! cbID ) {
return nil ;
}
return ^ ( NSArray * args ) {
[ self _sendResponseToJavaScriptCallbackID : cbID args : args ] ;
} ;
}
+ ( NSInvocation * ) invocationForAdditionalArguments : ( NSUInteger ) argCount
{
static NSMutableDictionary * invocations ;
static dispatch_once _t onceToken ;
dispatch_once ( & onceToken , ^ {
invocations = [ NSMutableDictionary dictionary ] ;
} ) ;
id key = @ ( argCount ) ;
NSInvocation * invocation = invocations [ key ] ;
if ( invocation = = nil ) {
NSString * objCTypes = [ @ "v@:" stringByPaddingToLength : 3 + argCount withString : @ "@" startingAtIndex : 0 ] ;
NSMethodSignature * methodSignature = [ NSMethodSignature signatureWithObjCTypes : objCTypes . UTF8String ] ;
invocation = [ NSInvocation invocationWithMethodSignature : methodSignature ] ;
invocations [ key ] = invocation ;
}
return invocation ;
}
2015-01-30 01:10:49 +00:00
- ( void ) doneRegisteringModules
{
RCTAssertMainThread ( ) ;
RCTAssert ( _javaScriptModulesConfig ! = nil , @ "JS module config not loaded in APP" ) ;
NSMutableDictionary * objectsToInject = [ NSMutableDictionary dictionary ] ;
// Dictionary of { moduleName0 : { moduleID : 0 , methods : { methodName0 : { methodID : 0 , type : remote } , methodName1 : { . . . } , . . . } , . . . }
NSUInteger moduleCount = RCTExportedMethodsByModule ( ) . count ;
NSMutableDictionary * moduleConfigs = [ NSMutableDictionary dictionaryWithCapacity : RCTExportedMethodsByModule ( ) . count ] ;
for ( NSUInteger i = 0 ; i < moduleCount ; i + + ) {
NSString * moduleName = RCTExportedModuleNameAtSortedIndex ( i ) ;
NSArray * rawMethods = RCTExportedMethodsByModule ( ) [ moduleName ] ;
NSMutableDictionary * methods = [ NSMutableDictionary dictionaryWithCapacity : rawMethods . count ] ;
[ rawMethods enumerateObjectsUsingBlock : ^ ( RCTModuleMethod * method , NSUInteger methodID , BOOL * stop ) {
methods [ method . JSMethodName ] = @ {
@ "methodID" : @ ( methodID ) ,
@ "type" : @ "remote" ,
} ;
} ] ;
NSMutableDictionary * moduleConfig = [ NSMutableDictionary dictionary ] ;
moduleConfig [ @ "moduleID" ] = @ ( i ) ;
moduleConfig [ @ "methods" ] = methods ;
id target = [ _moduleInstances objectForKey : moduleName ] ;
2015-02-04 00:12:07 +00:00
if ( [ target respondsToSelector : @ selector ( constantsToExport ) ] ) {
2015-01-30 01:10:49 +00:00
moduleConfig [ @ "constants" ] = [ target constantsToExport ] ;
}
moduleConfigs [ moduleName ] = moduleConfig ;
}
NSDictionary * batchedBridgeConfig = @ {
@ "remoteModuleConfig" : moduleConfigs ,
@ "localModulesConfig" : _javaScriptModulesConfig
} ;
NSString * configJSON = RCTJSONStringify ( batchedBridgeConfig , NULL ) ;
objectsToInject [ @ "__fbBatchedBridgeConfig" ] = configJSON ;
dispatch_semaphore _t semaphore = dispatch_semaphore _create ( 0 ) ;
[ objectsToInject enumerateKeysAndObjectsUsingBlock : ^ ( NSString * objectName , NSString * script , BOOL * stop ) {
[ _javaScriptExecutor injectJSONText : script asGlobalObjectNamed : objectName callback : ^ ( id err ) {
dispatch_semaphore _signal ( semaphore ) ;
} ] ;
} ] ;
for ( NSUInteger i = 0 , count = objectsToInject . count ; i < count ; i + + ) {
if ( dispatch_semaphore _wait ( semaphore , dispatch_time ( DISPATCH_TIME _NOW , NSEC_PER _SEC ) ) ! = 0 ) {
RCTLogMustFix ( @ "JavaScriptExecutor take too long to inject JSON object" ) ;
}
}
}
2015-02-04 00:12:07 +00:00
- ( void ) registerRootView : ( RCTRootView * ) rootView
{
// TODO : only used by RCTUIManager - can we eliminate this special case ?
for ( id target in _moduleInstances . objectEnumerator ) {
if ( [ target respondsToSelector : @ selector ( registerRootView : ) ] ) {
[ target registerRootView : rootView ] ;
}
}
}
2015-01-30 01:10:49 +00:00
+ ( BOOL ) hasValidJSExecutor
{
return ( _latestJSExecutor ! = nil && [ _latestJSExecutor isValid ] ) ;
}
+ ( void ) log : ( NSArray * ) objects level : ( NSString * ) level
{
if ( ! _latestJSExecutor || ! [ _latestJSExecutor isValid ] ) {
RCTLogError ( @ "%@" , RCTLogFormatString ( @ "ERROR: No valid JS executor to log %@." , objects ) ) ;
return ;
}
NSMutableArray * args = [ NSMutableArray arrayWithObject : level ] ;
// TODO ( #5906496 ) : Find out and document why we skip the first object
for ( id ob in [ objects subarrayWithRange : ( NSRange ) { 1 , [ objects count ] - 1 } ] ) {
if ( [ NSJSONSerialization isValidJSONObject : @ [ ob ] ] ) {
[ args addObject : ob ] ;
} else {
[ args addObject : [ ob description ] ] ;
}
}
// Note the js executor could get invalidated while we ' re trying to call this . . . need to watch out for that .
[ _latestJSExecutor executeJSCall : @ "RCTLog"
method : @ "logIfNoNativeHook"
arguments : args
callback : ^ ( id objcValue , NSError * error ) { } ] ;
}
@ end