Enable module lookup in TurboModules

Summary:
NativeModules are instantiated by the bridge. If they choose, they can capture the bridge instance that instantiated them. From within the NativeModule, the bridge can then be used to lookup other NativeModules. TurboModules have no way to do such a lookup.

Both NativeModules and TurboModules need to be able to query for one another. Therefore, we have four cases:
1. NativeModule accesses NativeModule.
2. NativeModule accesses TurboModule.
3. TurboModule accesses NativeModule.
4. TurboModule accesses TurboModule.

In summary, this solution extends the bridge to support querying TurboModules. It also introduces a `RCTTurboModuleLookupDelegate` protocol, which, implemented by `RCTTurboModuleManager`, supports querying TurboModules:
```
protocol RCTTurboModuleLookupDelegate <NSObject>
- (id)moduleForName:(NSString *)moduleName;
- (id)moduleForName:(NSString *)moduleName warnOnLookupFailure:(BOOL)warnOnLookupFailure;
- (BOOL)moduleIsInitialized:(NSString *)moduleName
end
```

If TurboModules want to query other TurboModules, then they need to implement this protocol and synthesize `turboModuleLookupDelegate`:

```
protocol RCTTurboModuleWithLookupCapabilities
property (nonatomic, weak) id<RCTTurboModuleLookupDelegate> turboModuleLookupDelegate;
end
```

NativeModules will continue to use `RCTBridge` to access other NativeModules. Nothing needs to change.

When we attach the bridge to `RCTTurboModuleManager`, we also attach `RCTTurboModuleManager` to the bridge as a `RCTTurboModuleLookupDelegate`. This allows the bridge to query TurboModules, which enables our NativeModules to transparently (i.e: without any NativeModule code modification) query TurboModules.

In an ideal world, all modules would be TurboModules. Until then, we're going to require that TurboModules use the bridge to query for NativeModules or TurboModules.

`RCTTurboModuleManager` keeps a map of all TurboModules that we instantiated. We simply search in this map and return the TurboModule.

This setup allows us to switch NativeModules to TurboModules without compromising their ability to use the bridge to search for other NativeModules (and TurboModules). When we write new TurboModules, we can have them use `RCTTurboModuleLookupDelegate` to do access other TurboModules. Eventually, after we migrate all NativeModules to TurboModules, we can migrate all old callsites to use `RCTTurboModuleLookupDelegate`.

Reviewed By: fkgozali

Differential Revision: D13553186

fbshipit-source-id: 4d0488eef081332c8b70782e1337eccf10717dae
This commit is contained in:
Ramanpreet Nara 2019-01-31 11:04:45 -08:00 committed by Facebook Github Bot
parent dfcbf9729f
commit 0ceefb40d5
7 changed files with 261 additions and 74 deletions

View File

@ -159,6 +159,12 @@ RCT_EXTERN void RCTEnableTurboModule(BOOL enabled);
// Note: This method lazily load the module as necessary.
- (id)moduleForClass:(Class)moduleClass;
/**
* When a NativeModule performs a lookup for a TurboModule, we need to query
* the lookupDelegate.
*/
- (void)setRCTTurboModuleLookupDelegate:(id<RCTTurboModuleLookupDelegate>)turboModuleLookupDelegate;
/**
* Convenience method for retrieving all modules conforming to a given protocol.
* Modules will be sychronously instantiated if they haven't already been,

View File

@ -226,6 +226,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
[self invalidate];
}
- (void)setRCTTurboModuleLookupDelegate:(id<RCTTurboModuleLookupDelegate>)turboModuleLookupDelegate
{
[self.batchedBridge setRCTTurboModuleLookupDelegate:turboModuleLookupDelegate];
}
- (void)didReceiveReloadCommand
{
[self reload];

View File

@ -322,6 +322,26 @@ RCT_EXTERN void RCTRegisterModule(Class); \
@end
/**
* A protocol that allows TurboModules to do lookup on other TurboModules.
* Calling these methods may cause a module to be synchronously instantiated.
*/
@protocol RCTTurboModuleLookupDelegate <NSObject>
- (id)moduleForName:(const char *)moduleName;
/**
* Rationale:
* When TurboModules lookup other modules by name, we first check the TurboModule
* registry to see if a TurboModule exists with the respective name. In this case,
* we don't want a RedBox to be raised if the TurboModule isn't found.
*
* This method is deprecated and will be deleted after the migration from
* TurboModules to TurboModules is complete.
*/
- (id)moduleForName:(const char *)moduleName warnOnLookupFailure:(BOOL)warnOnLookupFailure;
- (BOOL)moduleIsInitialized:(const char *)moduleName;
@end
/**
* Experimental.
* A protocol to declare that a class supports TurboModule.

View File

@ -172,6 +172,9 @@ struct RCTInstanceCallback : public InstanceCallback {
// This is uniquely owned, but weak_ptr is used.
std::shared_ptr<Instance> _reactInstance;
// Necessary for searching in TurboModuleRegistry
id<RCTTurboModuleLookupDelegate> _turboModuleLookupDelegate;
}
@synthesize bridgeDescription = _bridgeDescription;
@ -179,6 +182,11 @@ struct RCTInstanceCallback : public InstanceCallback {
@synthesize performanceLogger = _performanceLogger;
@synthesize valid = _valid;
- (void) setRCTTurboModuleLookupDelegate:(id<RCTTurboModuleLookupDelegate>)turboModuleLookupDelegate
{
_turboModuleLookupDelegate = turboModuleLookupDelegate;
}
- (std::shared_ptr<MessageQueueThread>)jsMessageThread
{
return _jsMessageThread;
@ -428,13 +436,23 @@ struct RCTInstanceCallback : public InstanceCallback {
- (id)moduleForName:(NSString *)moduleName
{
return _moduleDataByName[moduleName].instance;
return [self moduleForName:moduleName lazilyLoadIfNecessary:NO];
}
- (id)moduleForName:(NSString *)moduleName lazilyLoadIfNecessary:(BOOL)lazilyLoad
{
if (RCTTurboModuleEnabled() && _turboModuleLookupDelegate) {
const char* moduleNameCStr = [moduleName UTF8String];
if (lazilyLoad || [_turboModuleLookupDelegate moduleIsInitialized:moduleNameCStr]) {
id<RCTTurboModule> module = [_turboModuleLookupDelegate moduleForName:moduleNameCStr warnOnLookupFailure:NO];
if (module != nil) {
return module;
}
}
}
if (!lazilyLoad) {
return [self moduleForName:moduleName];
return _moduleDataByName[moduleName].instance;
}
RCTModuleData *moduleData = _moduleDataByName[moduleName];
@ -463,7 +481,16 @@ struct RCTInstanceCallback : public InstanceCallback {
- (BOOL)moduleIsInitialized:(Class)moduleClass
{
return _moduleDataByName[RCTBridgeModuleNameForClass(moduleClass)].hasInstance;
NSString* moduleName = RCTBridgeModuleNameForClass(moduleClass);
if (_moduleDataByName[moduleName].hasInstance) {
return YES;
}
if (_turboModuleLookupDelegate) {
return [_turboModuleLookupDelegate moduleIsInitialized:[moduleName UTF8String]];
}
return NO;
}
- (id)moduleForClass:(Class)moduleClass

View File

@ -39,9 +39,17 @@ public:
} // namespace facebook
@protocol RCTTurboModule <NSObject>
@optional
/**
* Used by TurboModules to get access to other TurboModules.
*
* Usage:
* Place `@synthesize turboModuleLookupDelegate = _turboModuleLookupDelegate`
* in the @implementation section of your TurboModule.
*/
@property (nonatomic, weak) id<RCTTurboModuleLookupDelegate> turboModuleLookupDelegate;
@optional
// This should be required, after migration is done.
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModuleWithJsInvoker:(std::shared_ptr<facebook::react::JSCallInvoker>)jsInvoker;

View File

@ -34,7 +34,7 @@
@end
@interface RCTTurboModuleManager : NSObject
@interface RCTTurboModuleManager : NSObject<RCTTurboModuleLookupDelegate>
- (instancetype)initWithRuntime:(facebook::jsi::Runtime *)runtime
bridge:(RCTBridge *)bridge

View File

@ -34,6 +34,14 @@ static Class getFallbackClassFromName(const char *name) {
std::shared_ptr<react::TurboModuleBinding> _binding;
__weak id<RCTTurboModuleManagerDelegate> _delegate;
__weak RCTBridge *_bridge;
/**
* TODO(rsnara):
* All modules are currently long-lived.
* We need to come up with a mechanism to allow modules to specify whether
* they want to be long-lived or short-lived.
*/
std::unordered_map<std::string, id<RCTTurboModule>> _rctTurboModuleCache;
std::unordered_map<std::string, std::shared_ptr<react::TurboModule>> _turboModuleCache;
}
- (instancetype)initWithRuntime:(jsi::Runtime *)runtime
@ -46,84 +54,24 @@ static Class getFallbackClassFromName(const char *name) {
_delegate = delegate;
_bridge = bridge;
// Necessary to allow NativeModules to lookup TurboModules
[bridge setRCTTurboModuleLookupDelegate:self];
__weak __typeof(self) weakSelf = self;
auto moduleProvider = [weakSelf](const std::string &name) -> std::shared_ptr<react::TurboModule> {
if (!weakSelf) {
return nullptr;
}
__strong __typeof(self) strongSelf = weakSelf;
// Pure C++ modules get priority.
if ([strongSelf->_delegate respondsToSelector:@selector(getTurboModule:jsInvoker:)]) {
std::shared_ptr<react::TurboModule> tm = [strongSelf->_delegate getTurboModule:name jsInvoker:strongSelf->_jsInvoker];
if (tm != nullptr) {
return tm;
}
}
Class moduleClass;
if ([strongSelf->_delegate respondsToSelector:@selector(getModuleClassFromName:)]) {
moduleClass = [strongSelf->_delegate getModuleClassFromName:name.c_str()];
} else {
moduleClass = getFallbackClassFromName(name.c_str());
}
assert(moduleClass);
id<RCTTurboModule> module;
if ([strongSelf->_delegate respondsToSelector:@selector(getModuleInstanceFromClass:)]) {
module = [strongSelf->_delegate getModuleInstanceFromClass:moduleClass];
} else {
module = [moduleClass new];
}
/**
* It is reasonable for NativeModules to not want/need the bridge.
* In such cases, they won't have `@synthesize bridge = _bridge` in their
* implementation, and a `- (RCTBridge *) bridge { ... }` method won't be
* generated by the ObjC runtime. The property will also not be backed
* by an ivar, which makes writing to it unsafe. Therefore, we check if
* this method exists to know if we can safely set the bridge to the
* NativeModule.
* By default, all TurboModules are long-lived.
* Additionally, if a TurboModule with the name `name` isn't found, then we
* trigger an assertion failure.
*/
if ([module respondsToSelector:@selector(bridge)] && strongSelf->_bridge) {
/**
* Just because a NativeModule has the `bridge` method, it doesn't mean
* that it has synthesized the bridge in its implementation. Therefore,
* we need to surround the code that sets the bridge to the NativeModule
* inside a try/catch. This catches the cases where the NativeModule
* author specifies a `bridge` method manually.
*/
@try {
/**
* RCTBridgeModule declares the bridge property as readonly.
* Therefore, when authors of NativeModules synthesize the bridge
* via @synthesize bridge = bridge;, the ObjC runtime generates
* only a - (RCTBridge *) bridge: { ... } method. No setter is
* generated, so we have have to rely on the KVC API of ObjC to set
* the bridge property of these NativeModules.
*/
[(id)module setValue:strongSelf->_bridge forKey:@"bridge"];
}
@catch (NSException *exception) {
RCTLogError(@"%@ has no setter or ivar for its bridge, which is not "
"permitted. You must either @synthesize the bridge property, "
"or provide your own setter method.", RCTBridgeModuleNameForClass(module));
}
}
if ([module respondsToSelector:@selector(getTurboModuleWithJsInvoker:)]) {
return [module getTurboModuleWithJsInvoker:strongSelf->_jsInvoker];
}
// RCTCxxModule compatibility layer.
if ([moduleClass isSubclassOfClass:RCTCxxModule.class]) {
// Use TurboCxxModule compat class to wrap the CxxModule instance.
// This is only for migration convenience, despite less performant.
return std::make_shared<react::TurboCxxModule>([((RCTCxxModule *)module) createModule], strongSelf->_jsInvoker);
}
// This may be needed for migration purpose in case the module class doesn't provide the static getter.
return [strongSelf->_delegate getTurboModule:name instance:module jsInvoker:strongSelf->_jsInvoker];
return [strongSelf provideTurboModule: name.c_str()];
};
_binding = std::make_shared<react::TurboModuleBinding>(moduleProvider);
@ -131,6 +79,156 @@ static Class getFallbackClassFromName(const char *name) {
return self;
}
/**
* Given a name for a TurboModule, return a C++ object which is the instance
* of that TurboModule C++ class. This class wraps the TurboModule's ObjC instance.
* If no TurboModule ObjC class exist with the provided name, abort program.
*
* Note: All TurboModule instances are cached, which means they're all long-lived
* (for now).
*/
- (std::shared_ptr<react::TurboModule>)provideTurboModule:(const char*)moduleName
{
auto turboModuleLookup = _turboModuleCache.find(moduleName);
if (turboModuleLookup != _turboModuleCache.end()) {
return turboModuleLookup->second;
}
/**
* Step 1: Look for pure C++ modules.
* Pure C++ modules get priority.
*/
if ([_delegate respondsToSelector:@selector(getTurboModule:jsInvoker:)]) {
auto turboModule = [_delegate getTurboModule:moduleName jsInvoker:_jsInvoker];
if (turboModule != nullptr) {
_turboModuleCache.insert({moduleName, turboModule});
return turboModule;
}
}
/**
* Step 2: Look for platform-specific modules.
*/
id<RCTTurboModule> module = [self provideRCTTurboModule:moduleName];
// If we request that a TurboModule be created, its respective ObjC class must exist
// If the class doesn't exist, then provideRCTTurboModule returns nil
// Therefore, module cannot be nil.
assert(module);
Class moduleClass = [module class];
// If RCTTurboModule supports creating its own C++ TurboModule object,
// allow it to do so.
if ([module respondsToSelector:@selector(getTurboModuleWithJsInvoker:)]) {
auto turboModule = [module getTurboModuleWithJsInvoker:_jsInvoker];
_turboModuleCache.insert({moduleName, turboModule});
return turboModule;
}
/**
* Step 2c: If the moduleClass is a legacy CxxModule, return a TurboCxxModule instance that
* wraps CxxModule.
*/
if ([moduleClass isSubclassOfClass:RCTCxxModule.class]) {
// Use TurboCxxModule compat class to wrap the CxxModule instance.
// This is only for migration convenience, despite less performant.
auto turboModule = std::make_shared<react::TurboCxxModule>([((RCTCxxModule *)module) createModule], _jsInvoker);
_turboModuleCache.insert({moduleName, turboModule});
return turboModule;
}
/**
* Step 2d: Return an exact sub-class of ObjC TurboModule
*/
auto turboModule = [_delegate getTurboModule:moduleName instance:module jsInvoker:_jsInvoker];
_turboModuleCache.insert({moduleName, turboModule});
return turboModule;
}
/**
* Given a name for a TurboModule, return an ObjC object which is the instance
* of that TurboModule ObjC class. If no TurboModule exist with the provided name,
* return nil.
*
* Note: All TurboModule instances are cached, which means they're all long-lived
* (for now).
*/
- (id<RCTTurboModule>)provideRCTTurboModule:(const char*)moduleName
{
auto rctTurboModuleCacheLookup = _rctTurboModuleCache.find(moduleName);
if (rctTurboModuleCacheLookup != _rctTurboModuleCache.end()) {
return rctTurboModuleCacheLookup->second;
}
/**
* Step 2a: Resolve platform-specific class.
*/
Class moduleClass;
if ([_delegate respondsToSelector:@selector(getModuleClassFromName:)]) {
moduleClass = [_delegate getModuleClassFromName:moduleName];
} else {
moduleClass = getFallbackClassFromName(moduleName);
}
if (!moduleClass) {
return nil;
}
/**
* Step 2b: Ask hosting application/delegate to instantiate this class
*/
id<RCTTurboModule> module = nil;
if ([_delegate respondsToSelector:@selector(getModuleInstanceFromClass:)]) {
module = [_delegate getModuleInstanceFromClass:moduleClass];
} else {
module = [moduleClass new];
}
/**
* It is reasonable for NativeModules to not want/need the bridge.
* In such cases, they won't have `@synthesize bridge = _bridge` in their
* implementation, and a `- (RCTBridge *) bridge { ... }` method won't be
* generated by the ObjC runtime. The property will also not be backed
* by an ivar, which makes writing to it unsafe. Therefore, we check if
* this method exists to know if we can safely set the bridge to the
* NativeModule.
*/
if ([module respondsToSelector:@selector(bridge)] && _bridge) {
/**
* Just because a NativeModule has the `bridge` method, it doesn't mean
* that it has synthesized the bridge in its implementation. Therefore,
* we need to surround the code that sets the bridge to the NativeModule
* inside a try/catch. This catches the cases where the NativeModule
* author specifies a `bridge` method manually.
*/
@try {
/**
* RCTBridgeModule declares the bridge property as readonly.
* Therefore, when authors of NativeModules synthesize the bridge
* via @synthesize bridge = bridge;, the ObjC runtime generates
* only a - (RCTBridge *) bridge: { ... } method. No setter is
* generated, so we have have to rely on the KVC API of ObjC to set
* the bridge property of these NativeModules.
*/
[(id)module setValue:_bridge forKey:@"bridge"];
}
@catch (NSException *exception) {
RCTLogError(@"%@ has no setter or ivar for its bridge, which is not "
"permitted. You must either @synthesize the bridge property, "
"or provide your own setter method.", RCTBridgeModuleNameForClass(module));
}
}
if ([module respondsToSelector:@selector(setTurboModuleLookupDelegate:)]) {
[module setTurboModuleLookupDelegate:self];
}
_rctTurboModuleCache.insert({moduleName, module});
return module;
}
- (void)installJSBinding
{
if (!_runtime) {
@ -146,4 +244,27 @@ static Class getFallbackClassFromName(const char *name) {
return _binding->getModule(name);
}
#pragma mark RCTTurboModuleLookupDelegate
- (id)moduleForName:(const char *)moduleName
{
return [self moduleForName:moduleName warnOnLookupFailure:YES];
}
- (id)moduleForName:(const char *)moduleName warnOnLookupFailure:(BOOL)warnOnLookupFailure
{
id<RCTTurboModule> module = [self provideRCTTurboModule:moduleName];
if (warnOnLookupFailure && !module) {
RCTLogError(@"Unable to find module for %@", [NSString stringWithUTF8String:moduleName]);
}
return module;
}
- (BOOL)moduleIsInitialized:(const char *)moduleName
{
return _rctTurboModuleCache.find(std::string(moduleName)) != _rctTurboModuleCache.end();
}
@end