[ReactNative] Remove module info from the data section + allow external modules

Summary:
@public

The information we required about the exported methods were previously stored
on the binary's DATA section, which didn't allow to access methods on different
static libraries, or in any dynamic library at all. Instead of fetching information
from all the DATA segments, this diff changes the macro in order to create a
new method, that returns the required information about the original method. The
module itself is registered at load time, and on the bridge initialization all
the auto-generated methods are called to gather the methods' information.

Test Plan:
UIExplorer previously had a dependency on `RCTTest`, because it had a `TestModule`
that had to be on the same library. `RCTTest` is now a dependency of
`UIExplorerIntegrationTests`. So the tests themselves running should test it.
This commit is contained in:
Tadeu Zagallo 2015-06-10 03:43:55 -07:00
parent 4690983c10
commit e9095b2f42
5 changed files with 151 additions and 172 deletions

View File

@ -30,6 +30,7 @@
1497CFB21B21F5E400C1F8F2 /* RCTSparseArrayTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1497CFAA1B21F5E400C1F8F2 /* RCTSparseArrayTests.m */; };
1497CFB31B21F5E400C1F8F2 /* RCTUIManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1497CFAB1B21F5E400C1F8F2 /* RCTUIManagerTests.m */; };
14AADF051AC3DBB1002390C9 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14AADF041AC3DB95002390C9 /* libReact.a */; };
14B6DA821B276C5900BF4DD1 /* libRCTTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58005BEE1ABA80530062E044 /* libRCTTest.a */; };
14D6D7111B220EB3001FB087 /* libOCMock.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14D6D7101B220EB3001FB087 /* libOCMock.a */; };
14D6D71E1B2222EF001FB087 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; };
14D6D71F1B2222EF001FB087 /* libRCTAdSupport.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1344545A1AAFCAAE003F0779 /* libRCTAdSupport.a */; };
@ -44,7 +45,6 @@
14D6D7281B2222EF001FB087 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDED91B0651EA00C62182 /* libRCTWebSocket.a */; };
14D6D7291B2222EF001FB087 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14AADF041AC3DB95002390C9 /* libReact.a */; };
14DC67F41AB71881001358AB /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14DC67F11AB71876001358AB /* libRCTPushNotification.a */; };
58005BF21ABA80A60062E044 /* libRCTTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58005BEE1ABA80530062E044 /* libRCTTest.a */; };
834C36EC1AF8DED70019C93C /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 834C36D21AF8DA610019C93C /* libRCTSettings.a */; };
D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */; };
/* End PBXBuildFile section */
@ -229,7 +229,6 @@
1341802C1AA9178B003F314A /* libRCTNetwork.a in Frameworks */,
14DC67F41AB71881001358AB /* libRCTPushNotification.a in Frameworks */,
834C36EC1AF8DED70019C93C /* libRCTSettings.a in Frameworks */,
58005BF21ABA80A60062E044 /* libRCTTest.a in Frameworks */,
134180011AA9153C003F314A /* libRCTText.a in Frameworks */,
D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */,
139FDEDB1B0651FB00C62182 /* libRCTWebSocket.a in Frameworks */,
@ -240,6 +239,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
14B6DA821B276C5900BF4DD1 /* libRCTTest.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -865,7 +865,7 @@
"$(SRCROOT)/../../React/**",
);
INFOPLIST_FILE = "$(SRCROOT)/UIExplorer/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited)";
LIBRARY_SEARCH_PATHS = "$(inherited)";
OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = UIExplorer;
@ -883,7 +883,7 @@
"$(SRCROOT)/../../React/**",
);
INFOPLIST_FILE = "$(SRCROOT)/UIExplorer/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited)";
LIBRARY_SEARCH_PATHS = "$(inherited)";
OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = UIExplorer;
@ -907,6 +907,12 @@
INFOPLIST_FILE = UIExplorerIntegrationTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 8.3;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
OTHER_LDFLAGS = (
"$(inherited)",
"-framework",
XCTest,
"-ObjC",
);
PRODUCT_NAME = "$(TARGET_NAME)";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UIExplorer.app/UIExplorer";
};
@ -926,6 +932,12 @@
INFOPLIST_FILE = UIExplorerIntegrationTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 8.3;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
OTHER_LDFLAGS = (
"$(inherited)",
"-framework",
XCTest,
"-ObjC",
);
PRODUCT_NAME = "$(TARGET_NAME)";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UIExplorer.app/UIExplorer";
};

View File

@ -21,17 +21,17 @@
/**
* This notification triggers a reload of all bridges currently running.
*/
extern NSString *const RCTReloadNotification;
RCT_EXTERN NSString *const RCTReloadNotification;
/**
* This notification fires when the bridge has finished loading.
*/
extern NSString *const RCTJavaScriptDidLoadNotification;
RCT_EXTERN NSString *const RCTJavaScriptDidLoadNotification;
/**
* This notification fires when the bridge failed to load.
*/
extern NSString *const RCTJavaScriptDidFailToLoadNotification;
RCT_EXTERN NSString *const RCTJavaScriptDidFailToLoadNotification;
/**
* This block can be used to instantiate modules that require additional
@ -43,6 +43,13 @@ extern NSString *const RCTJavaScriptDidFailToLoadNotification;
*/
typedef NSArray *(^RCTBridgeModuleProviderBlock)(void);
/**
* Register the given class as a bridge module. All modules must be registered
* prior to the first bridge initialization.
*
*/
RCT_EXTERN void RCTRegisterModule(Class);
/**
* This function returns the module name for a given class.
*/

View File

@ -70,6 +70,30 @@ typedef struct section RCTHeaderSection;
NSString *const RCTEnqueueNotification = @"RCTEnqueueNotification";
NSString *const RCTDequeueNotification = @"RCTDequeueNotification";
static NSDictionary *RCTModuleIDsByName;
static NSArray *RCTModuleNamesByID;
static NSArray *RCTModuleClassesByID;
void RCTRegisterModule(Class moduleClass)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTModuleIDsByName = [[NSMutableDictionary alloc] init];
RCTModuleNamesByID = [[NSMutableArray alloc] init];
RCTModuleClassesByID = [[NSMutableArray alloc] init];
});
RCTAssert([moduleClass conformsToProtocol:@protocol(RCTBridgeModule)],
@"%@ does not conform to the RCTBridgeModule protocol",
NSStringFromClass(moduleClass));
// Register module
NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);
((NSMutableDictionary *)RCTModuleIDsByName)[moduleName] = @(RCTModuleNamesByID.count);
[(NSMutableArray *)RCTModuleNamesByID addObject:moduleName];
[(NSMutableArray *)RCTModuleClassesByID addObject:moduleClass];
}
/**
* This function returns the module name for a given class.
*/
@ -122,93 +146,6 @@ static NSArray *RCTJSMethods(void)
return JSMethods;
}
/**
* This function scans all exported modules available at runtime and returns an
* array. As a backup, it also scans all classes that implement the
* RTCBridgeModule protocol to ensure they've been exported. This scanning
* functionality is disabled in release mode to improve startup performance.
*/
static NSDictionary *RCTModuleIDsByName;
static NSArray *RCTModuleNamesByID;
static NSArray *RCTModuleClassesByID;
static NSArray *RCTBridgeModuleClassesByModuleID(void)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTModuleIDsByName = [[NSMutableDictionary alloc] init];
RCTModuleNamesByID = [[NSMutableArray alloc] init];
RCTModuleClassesByID = [[NSMutableArray alloc] init];
Dl_info info;
dladdr(&RCTBridgeModuleClassesByModuleID, &info);
const RCTHeaderValue mach_header = (RCTHeaderValue)info.dli_fbase;
unsigned long size;
const uint8_t *sectionData = getsectiondata(mach_header, "__DATA", "RCTExportModule", &size);
if (sectionData) {
for (const uint8_t *addr = sectionData;
addr < sectionData + size;
addr += sizeof(const char **)) {
// Get data entry
NSString *entry = @(*(const char **)addr);
NSArray *parts = [[entry substringWithRange:(NSRange){2, entry.length - 3}]
componentsSeparatedByString:@" "];
// Parse class name
NSString *moduleClassName = parts[0];
NSRange categoryRange = [moduleClassName rangeOfString:@"("];
if (categoryRange.length) {
moduleClassName = [moduleClassName substringToIndex:categoryRange.location];
}
// Get class
Class cls = NSClassFromString(moduleClassName);
RCTAssert([cls conformsToProtocol:@protocol(RCTBridgeModule)],
@"%@ does not conform to the RCTBridgeModule protocol",
NSStringFromClass(cls));
// Register module
NSString *moduleName = RCTBridgeModuleNameForClass(cls);
((NSMutableDictionary *)RCTModuleIDsByName)[moduleName] = @(RCTModuleNamesByID.count);
[(NSMutableArray *)RCTModuleNamesByID addObject:moduleName];
[(NSMutableArray *)RCTModuleClassesByID addObject:cls];
}
}
if (RCT_DEBUG) {
// We may be able to get rid of this check in future, once people
// get used to the new registration system. That would potentially
// allow you to create modules that are not automatically registered
static unsigned int classCount;
Class *classes = objc_copyClassList(&classCount);
for (unsigned int i = 0; i < classCount; i++)
{
Class cls = classes[i];
Class superclass = cls;
while (superclass)
{
if (class_conformsToProtocol(superclass, @protocol(RCTBridgeModule)))
{
if (![RCTModuleClassesByID containsObject:cls]) {
RCTLogError(@"Class %@ was not exported. Did you forget to use RCT_EXPORT_MODULE()?", NSStringFromClass(cls));
}
break;
}
superclass = class_getSuperclass(superclass);
}
}
free(classes);
}
});
return RCTModuleClassesByID;
}
// TODO: Can we just replace RCTMakeError with this function instead?
static NSDictionary *RCTJSErrorFromNSError(NSError *error)
{
@ -278,25 +215,42 @@ static NSDictionary *RCTJSErrorFromNSError(NSError *error)
dispatch_block_t _methodQueue;
}
- (instancetype)initWithReactMethodName:(NSString *)reactMethodName
objCMethodName:(NSString *)objCMethodName
JSMethodName:(NSString *)JSMethodName
- (instancetype)initWithObjCMethodName:(NSString *)objCMethodName
JSMethodName:(NSString *)JSMethodName
moduleClass:(Class)moduleClass
{
if ((self = [super init])) {
static NSRegularExpression *typeRegex;
static NSRegularExpression *selectorRegex;
if (!typeRegex) {
NSString *unusedPattern = @"(?:(?:__unused|__attribute__\\(\\(unused\\)\\)))";
NSString *constPattern = @"(?:const)";
NSString *constUnusedPattern = [NSString stringWithFormat:@"(?:(?:%@|%@)\\s*)", unusedPattern, constPattern];
NSString *pattern = [NSString stringWithFormat:@"\\(%1$@?(\\w+?)(?:\\s*\\*)?%1$@?\\)", constUnusedPattern];
typeRegex = [[NSRegularExpression alloc] initWithPattern:pattern options:0 error:NULL];
NSArray *parts = [[reactMethodName substringWithRange:(NSRange){2, reactMethodName.length - 3}] componentsSeparatedByString:@" "];
// Parse class and method
_moduleClassName = parts[0];
NSRange categoryRange = [_moduleClassName rangeOfString:@"("];
if (categoryRange.length) {
_moduleClassName = [_moduleClassName substringToIndex:categoryRange.location];
selectorRegex = [[NSRegularExpression alloc] initWithPattern:@"(?<=:).*?(?=[a-zA-Z_]+:|$)" options:0 error:NULL];
}
NSString *selectorString = [parts[1] substringFromIndex:14];
_selector = NSSelectorFromString(selectorString);
_JSMethodName = JSMethodName ?: ({
NSString *methodName = selectorString;
NSMutableArray *argumentNames = [NSMutableArray array];
[typeRegex enumerateMatchesInString:objCMethodName options:0 range:NSMakeRange(0, objCMethodName.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
NSString *argumentName = [objCMethodName substringWithRange:[result rangeAtIndex:1]];
[argumentNames addObject:argumentName];
}];
// Remove the parameters' type and name
objCMethodName = [selectorRegex stringByReplacingMatchesInString:objCMethodName
options:0
range:NSMakeRange(0, objCMethodName.length)
withTemplate:@""];
// Remove any spaces since `selector : (Type)name` is a valid syntax
objCMethodName = [objCMethodName stringByReplacingOccurrencesOfString:@" " withString:@""];
_moduleClass = moduleClass;
_moduleClassName = NSStringFromClass(_moduleClass);
_selector = NSSelectorFromString(objCMethodName);
_JSMethodName = JSMethodName.length > 0 ? JSMethodName : ({
NSString *methodName = NSStringFromSelector(_selector);
NSRange colonRange = [methodName rangeOfString:@":"];
if (colonRange.length) {
methodName = [methodName substringToIndex:colonRange.location];
@ -304,31 +258,6 @@ static NSDictionary *RCTJSErrorFromNSError(NSError *error)
methodName;
});
static NSRegularExpression *regExp;
if (!regExp) {
NSString *unusedPattern = @"(?:(?:__unused|__attribute__\\(\\(unused\\)\\)))";
NSString *constPattern = @"(?:const)";
NSString *constUnusedPattern = [NSString stringWithFormat:@"(?:(?:%@|%@)\\s*)", unusedPattern, constPattern];
NSString *pattern = [NSString stringWithFormat:@"\\(%1$@?(\\w+?)(?:\\s*\\*)?%1$@?\\)", constUnusedPattern];
regExp = [[NSRegularExpression alloc] initWithPattern:pattern options:0 error:NULL];
}
NSMutableArray *argumentNames = [NSMutableArray array];
[regExp enumerateMatchesInString:objCMethodName options:0 range:NSMakeRange(0, objCMethodName.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
NSString *argumentName = [objCMethodName substringWithRange:[result rangeAtIndex:1]];
[argumentNames addObject:argumentName];
}];
// Extract class and method details
_moduleClass = NSClassFromString(_moduleClassName);
if (RCT_DEBUG) {
// Sanity check
RCTAssert([_moduleClass conformsToProtocol:@protocol(RCTBridgeModule)],
@"You are attempting to export the method %@, but %@ does not \
conform to the RCTBridgeModule Protocol", objCMethodName, _moduleClassName);
}
// Get method signature
_methodSignature = [_moduleClass instanceMethodSignatureForSelector:_selector];
@ -554,44 +483,33 @@ static RCTSparseArray *RCTExportedMethodsByModuleID(void)
static RCTSparseArray *methodsByModuleID;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
methodsByModuleID = [[RCTSparseArray alloc] initWithCapacity:[RCTModuleClassesByID count]];
Dl_info info;
dladdr(&RCTExportedMethodsByModuleID, &info);
[RCTModuleClassesByID enumerateObjectsUsingBlock:^(Class moduleClass, NSUInteger moduleID, BOOL *stop) {
const RCTHeaderValue mach_header = (RCTHeaderValue)info.dli_fbase;
methodsByModuleID[moduleID] = [[NSMutableArray alloc] init];
unsigned long size;
const uint8_t *sectionData = getsectiondata(mach_header, "__DATA", "RCTExport", &size);
unsigned int methodCount;
Method *methods = class_copyMethodList(object_getClass(moduleClass), &methodCount);
if (sectionData == NULL) {
return;
}
for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
SEL selector = method_getName(method);
if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) {
NSArray *entries = ((NSArray *(*)(id, SEL))objc_msgSend)(moduleClass, selector);
RCTModuleMethod *moduleMethod =
[[RCTModuleMethod alloc] initWithObjCMethodName:entries[1]
JSMethodName:entries[0]
moduleClass:moduleClass];
NSArray *classes = RCTBridgeModuleClassesByModuleID();
NSMutableDictionary *methodsByModuleClassName = [NSMutableDictionary dictionaryWithCapacity:[classes count]];
[methodsByModuleID[moduleID] addObject:moduleMethod];
}
}
for (const uint8_t *addr = sectionData;
addr < sectionData + size;
addr += sizeof(const char **) * 3) {
free(methods);
// Get data entry
const char **entries = (const char **) addr;
// Create method
RCTModuleMethod *moduleMethod =
[[RCTModuleMethod alloc] initWithReactMethodName:@(entries[0])
objCMethodName:@(entries[1])
JSMethodName:strlen(entries[2]) ? @(entries[2]) : nil];
// Cache method
NSArray *methods = methodsByModuleClassName[moduleMethod.moduleClassName];
methodsByModuleClassName[moduleMethod.moduleClassName] =
methods ? [methods arrayByAddingObject:moduleMethod] : @[moduleMethod];
}
methodsByModuleID = [[RCTSparseArray alloc] initWithCapacity:[classes count]];
[classes enumerateObjectsUsingBlock:^(Class moduleClass, NSUInteger moduleID, BOOL *stop) {
methodsByModuleID[moduleID] = methodsByModuleClassName[NSStringFromClass(moduleClass)];
}];
});
return methodsByModuleID;
@ -630,7 +548,7 @@ static NSDictionary *RCTRemoteModulesConfig(NSDictionary *modulesByName)
dispatch_once(&onceToken, ^{
remoteModuleConfigByClassName = [[NSMutableDictionary alloc] init];
[RCTBridgeModuleClassesByModuleID() enumerateObjectsUsingBlock:^(Class moduleClass, NSUInteger moduleID, BOOL *stop) {
[RCTModuleClassesByID enumerateObjectsUsingBlock:^(Class moduleClass, NSUInteger moduleID, BOOL *stop) {
NSArray *methods = RCTExportedMethodsByModuleID()[moduleID];
NSMutableDictionary *methodsByName = [NSMutableDictionary dictionaryWithCapacity:methods.count];
@ -768,6 +686,38 @@ static NSDictionary *RCTLocalModulesConfig()
static id<RCTJavaScriptExecutor> _latestJSExecutor;
#if RCT_DEBUG
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
static unsigned int classCount;
Class *classes = objc_copyClassList(&classCount);
for (unsigned int i = 0; i < classCount; i++)
{
Class cls = classes[i];
Class superclass = cls;
while (superclass)
{
if (class_conformsToProtocol(superclass, @protocol(RCTBridgeModule)))
{
if (![RCTModuleClassesByID containsObject:cls]) {
RCTLogError(@"Class %@ was not exported. Did you forget to use RCT_EXPORT_MODULE()?", NSStringFromClass(cls));
}
break;
}
superclass = class_getSuperclass(superclass);
}
}
free(classes);
});
}
#endif
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
moduleProvider:(RCTBridgeModuleProviderBlock)block
launchOptions:(NSDictionary *)launchOptions
@ -1006,7 +956,7 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin
// Instantiate modules
_modulesByID = [[RCTSparseArray alloc] init];
NSMutableDictionary *modulesByName = [preregisteredModules mutableCopy];
[RCTBridgeModuleClassesByModuleID() enumerateObjectsUsingBlock:^(Class moduleClass, NSUInteger moduleID, BOOL *stop) {
[RCTModuleClassesByID enumerateObjectsUsingBlock:^(Class moduleClass, NSUInteger moduleID, BOOL *stop) {
NSString *moduleName = RCTModuleNamesByID[moduleID];
// Check if module instance has already been registered for this name
id<RCTBridgeModule> module = modulesByName[moduleName];

View File

@ -9,6 +9,8 @@
#import <Foundation/Foundation.h>
#import "RCTDefines.h"
@class RCTBridge;
/**
@ -62,8 +64,9 @@ extern const dispatch_queue_t RCTJSThread;
* match the Objective-C class name.
*/
#define RCT_EXPORT_MODULE(js_name) \
+ (NSString *)moduleName { __attribute__((used, section("__DATA,RCTExportModule" \
))) static const char *__rct_export_entry__ = { __func__ }; return @#js_name; }
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule([self class]); }
/**
* Wrap the parameter line of your method implementation with this macro to
@ -173,11 +176,10 @@ extern const dispatch_queue_t RCTJSThread;
* Like RCT_EXTERN_REMAP_METHOD, but allows setting a custom JavaScript name.
*/
#define RCT_EXTERN_REMAP_METHOD(js_name, method) \
- (void)__rct_export__##method { \
__attribute__((used, section("__DATA,RCTExport"))) \
__attribute__((__aligned__(1))) \
static const char *__rct_export_entry__[] = { __func__, #method, #js_name }; \
}
+ (NSArray *)RCT_CONCAT(__rct_export__, __COUNTER__) { \
return @[@#js_name, @#method]; \
} \
/**
* The queue that will be used to call all exported methods. If omitted, this

View File

@ -74,3 +74,11 @@
#define RCT_NSASSERT 0
#endif
#endif
/**
* Concat two literals. Supports macro expansions
*
* i.e. RCT_CONCAT(foo, __FILE__)
*/
#define RCT_CONCAT2(A, B) A ## B
#define RCT_CONCAT(A, B) RCT_CONCAT2(A, B)