Skip to content

Commit

Permalink
[Bridge] RCT_REMAP_METHOD(js_name, selector)
Browse files Browse the repository at this point in the history
Summary:
cc @a2 @nicklockwood

This diff introduces a new macro called `RCT_EXPORT_NAMED_METHOD`, which is like `RCT_EXPORT_METHOD` but lets you choose the name of the method in JS. This diff is backwards compatible with the `RCT_EXPORT_METHOD` and legacy `RCT_EXPORT` macros.

The entries in the data segment now contain `__func__`, the Obj-C selector signature, and the JS name. If the JS name is `NULL`, we take the legacy `RCT_EXPORT` code path. If the JS name is an empty string, we use the Obj-C selector's name up to the first colon (that is, the behavior of `RCT_EXPORT_METHOD`).

Since there are three values in each data segment entry, the macros now specify 1-byte alignment. Without the byte alignment, the compiler defaults to 2-byte alignment meaning that each entry takes up 4 bytes instead of 3. The extra byte isn't a concern but being explicit about the alignment should reduce compiler surprises.
Closes #802
Github Author: James Ide <[email protected]>

Test Plan: Imported from GitHub, without a `Test Plan:` line.
  • Loading branch information
ide committed Apr 14, 2015
1 parent 5bbb351 commit e193a13
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 102 deletions.
159 changes: 86 additions & 73 deletions React/Base/RCTBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -219,20 +219,22 @@ @implementation RCTModuleMethod

static Class _globalExecutorClass;

NS_INLINE NSString *RCTStringUpToFirstArgument(NSString *methodName) {
static NSString *RCTStringUpToFirstArgument(NSString *methodName)
{
NSRange colonRange = [methodName rangeOfString:@":"];
if (colonRange.length) {
methodName = [methodName substringToIndex:colonRange.location];
}
return methodName;
}

- (instancetype)initWithMethodName:(NSString *)methodName
JSMethodName:(NSString *)JSMethodName
- (instancetype)initWithReactMethodName:(NSString *)reactMethodName
objCMethodName:(NSString *)objCMethodName
JSMethodName:(NSString *)JSMethodName
{
if ((self = [super init])) {
_methodName = methodName;
NSArray *parts = [[methodName substringWithRange:(NSRange){2, methodName.length - 3}] componentsSeparatedByString:@" "];
_methodName = reactMethodName;
NSArray *parts = [[reactMethodName substringWithRange:(NSRange){2, reactMethodName.length - 3}] componentsSeparatedByString:@" "];

// Parse class and method
_moduleClassName = parts[0];
Expand All @@ -246,7 +248,7 @@ - (instancetype)initWithMethodName:(NSString *)methodName
// New format
NSString *selectorString = [parts[1] substringFromIndex:14];
_selector = NSSelectorFromString(selectorString);
_JSMethodName = RCTStringUpToFirstArgument(selectorString);
_JSMethodName = JSMethodName ?: RCTStringUpToFirstArgument(selectorString);

static NSRegularExpression *regExp;
if (!regExp) {
Expand All @@ -258,8 +260,8 @@ - (instancetype)initWithMethodName:(NSString *)methodName
}

argumentNames = [NSMutableArray array];
[regExp enumerateMatchesInString:JSMethodName options:0 range:NSMakeRange(0, JSMethodName.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
NSString *argumentName = [JSMethodName substringWithRange:[result rangeAtIndex:1]];
[regExp enumerateMatchesInString:objCMethodName options:0 range:NSMakeRange(0, objCMethodName.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
NSString *argumentName = [objCMethodName substringWithRange:[result rangeAtIndex:1]];
[(NSMutableArray *)argumentNames addObject:argumentName];
}];
} else {
Expand All @@ -270,14 +272,15 @@ - (instancetype)initWithMethodName:(NSString *)methodName
}

// Extract class and method details
_isClassMethod = [methodName characterAtIndex:0] == '+';
_isClassMethod = [reactMethodName characterAtIndex:0] == '+';
_moduleClass = NSClassFromString(_moduleClassName);

#if DEBUG

// Sanity check
RCTAssert([_moduleClass conformsToProtocol:@protocol(RCTBridgeModule)],
@"You are attempting to export the method %@, but %@ does not \
conform to the RCTBridgeModule Protocol", methodName, _moduleClassName);
conform to the RCTBridgeModule Protocol", objCMethodName, _moduleClassName);
#endif

// Get method signature
Expand All @@ -290,26 +293,26 @@ - (instancetype)initWithMethodName:(NSString *)methodName
NSMutableArray *argumentBlocks = [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2];

#define RCT_ARG_BLOCK(_logic) \
[argumentBlocks addObject:^(RCTBridge *bridge, NSInvocation *invocation, NSUInteger index, id json) { \
_logic \
[invocation setArgument:&value atIndex:index]; \
}]; \
[argumentBlocks addObject:^(RCTBridge *bridge, NSInvocation *invocation, NSUInteger index, id json) { \
_logic \
[invocation setArgument:&value atIndex:index]; \
}]; \

void (^addBlockArgument)(void) = ^{
RCT_ARG_BLOCK(
if (json && ![json isKindOfClass:[NSNumber class]]) {
RCTLogError(@"Argument %tu (%@) of %@.%@ should be a number", index,
json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName);
return;
}

// Marked as autoreleasing, because NSInvocation doesn't retain arguments
__autoreleasing id value = (json ? ^(NSArray *args) {
[bridge _invokeAndProcessModule:@"BatchedBridge"
method:@"invokeCallbackAndReturnFlushedQueue"
arguments:@[json, args]];
} : ^(NSArray *unused) {});
)
if (json && ![json isKindOfClass:[NSNumber class]]) {
RCTLogError(@"Argument %tu (%@) of %@.%@ should be a number", index,
json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName);
return;
}

// Marked as autoreleasing, because NSInvocation doesn't retain arguments
__autoreleasing id value = (json ? ^(NSArray *args) {
[bridge _invokeAndProcessModule:@"BatchedBridge"
method:@"invokeCallbackAndReturnFlushedQueue"
arguments:@[json, args]];
} : ^(NSArray *unused) {});
)
};

void (^defaultCase)(const char *) = ^(const char *argumentType) {
Expand All @@ -333,11 +336,11 @@ - (instancetype)initWithMethodName:(NSString *)methodName
switch (argumentType[0]) {

#define RCT_CONVERT_CASE(_value, _type) \
case _value: { \
_type (*convert)(id, SEL, id) = (typeof(convert))[RCTConvert methodForSelector:selector]; \
RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \
break; \
}
case _value: { \
_type (*convert)(id, SEL, id) = (typeof(convert))[RCTConvert methodForSelector:selector]; \
RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \
break; \
}

RCT_CONVERT_CASE(':', SEL)
RCT_CONVERT_CASE('*', const char *)
Expand Down Expand Up @@ -371,33 +374,33 @@ - (instancetype)initWithMethodName:(NSString *)methodName
switch (argumentType[0]) {

#define RCT_CASE(_value, _class, _logic) \
case _value: { \
RCT_ARG_BLOCK( \
if (json && ![json isKindOfClass:[_class class]]) { \
RCTLogError(@"Argument %tu (%@) of %@.%@ should be of type %@", index, \
json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, [_class class]); \
return; \
} \
_logic \
) \
break; \
}
case _value: { \
RCT_ARG_BLOCK( \
if (json && ![json isKindOfClass:[_class class]]) { \
RCTLogError(@"Argument %tu (%@) of %@.%@ should be of type %@", index, \
json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, [_class class]); \
return; \
} \
_logic \
) \
break; \
}

RCT_CASE(':', NSString, SEL value = NSSelectorFromString(json); )
RCT_CASE('*', NSString, const char *value = [json UTF8String]; )

#define RCT_SIMPLE_CASE(_value, _type, _selector) \
case _value: { \
RCT_ARG_BLOCK( \
if (json && ![json respondsToSelector:@selector(_selector)]) { \
RCTLogError(@"Argument %tu (%@) of %@.%@ does not respond to selector: %@", \
index, json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, @#_selector); \
return; \
} \
_type value = [json _selector]; \
) \
break; \
}
case _value: { \
RCT_ARG_BLOCK( \
if (json && ![json respondsToSelector:@selector(_selector)]) { \
RCTLogError(@"Argument %tu (%@) of %@.%@ does not respond to selector: %@", \
index, json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, @#_selector); \
return; \
} \
_type value = [json _selector]; \
) \
break; \
}

RCT_SIMPLE_CASE('c', char, charValue)
RCT_SIMPLE_CASE('C', unsigned char, unsignedCharValue)
Expand Down Expand Up @@ -432,6 +435,7 @@ - (void)invokeWithBridge:(RCTBridge *)bridge
{

#if DEBUG

// Sanity check
RCTAssert([module class] == _moduleClass, @"Attempted to invoke method \
%@ on a module of class %@", _methodName, [module class]);
Expand Down Expand Up @@ -496,15 +500,24 @@ - (NSString *)description

for (RCTHeaderValue addr = section->offset;
addr < section->offset + section->size;
addr += sizeof(const char **) * 2) {
addr += sizeof(const char **) * 3) {

// Get data entry
const char **entries = (const char **)(mach_header + addr);

// Create method
RCTModuleMethod *moduleMethod =
[[RCTModuleMethod alloc] initWithMethodName:@(entries[0])
JSMethodName:strlen(entries[1]) ? @(entries[1]) : nil];
RCTModuleMethod *moduleMethod;
if (entries[2] == NULL) {

// Legacy support for RCT_EXPORT()
moduleMethod = [[RCTModuleMethod alloc] initWithReactMethodName:@(entries[0])
objCMethodName:@(entries[0])
JSMethodName:strlen(entries[1]) ? @(entries[1]) : nil];
} else {
moduleMethod = [[RCTModuleMethod alloc] initWithReactMethodName:@(entries[0])
objCMethodName:strlen(entries[1]) ? @(entries[1]) : nil
JSMethodName:strlen(entries[2]) ? @(entries[2]) : nil];
}

// Cache method
NSArray *methods = methodsByModuleClassName[moduleMethod.moduleClassName];
Expand Down Expand Up @@ -560,15 +573,15 @@ - (NSString *)description
NSMutableDictionary *methodsByName = [NSMutableDictionary dictionaryWithCapacity:methods.count];
[methods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger methodID, BOOL *_stop) {
methodsByName[method.JSMethodName] = @{
@"methodID": @(methodID),
@"type": @"remote",
};
@"methodID": @(methodID),
@"type": @"remote",
};
}];

NSDictionary *module = @{
@"moduleID": @(moduleID),
@"methods": methodsByName
};
@"moduleID": @(moduleID),
@"methods": methodsByName
};

remoteModuleConfigByClassName[NSStringFromClass(moduleClass)] = module;
}];
Expand Down Expand Up @@ -639,9 +652,9 @@ - (NSString *)description
NSDictionary *module = localModules[moduleName];
if (!module) {
module = @{
@"moduleID": @(localModules.count),
@"methods": [[NSMutableDictionary alloc] init]
};
@"moduleID": @(localModules.count),
@"methods": [[NSMutableDictionary alloc] init]
};
localModules[moduleName] = module;
}

Expand All @@ -650,9 +663,9 @@ - (NSString *)description
NSMutableDictionary *methods = module[@"methods"];
if (!methods[methodName]) {
methods[methodName] = @{
@"methodID": @(methods.count),
@"type": @"local"
};
@"methodID": @(methods.count),
@"type": @"local"
};
}

// Add module and method lookup
Expand Down Expand Up @@ -741,9 +754,9 @@ - (void)setUp

// Inject module data into JS context
NSString *configJSON = RCTJSONStringify(@{
@"remoteModuleConfig": RCTRemoteModulesConfig(_modulesByName),
@"localModulesConfig": RCTLocalModulesConfig()
}, NULL);
@"remoteModuleConfig": RCTRemoteModulesConfig(_modulesByName),
@"localModulesConfig": RCTLocalModulesConfig()
}, NULL);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[_javaScriptExecutor injectJSONText:configJSON
asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback:^(id err) {
Expand Down
43 changes: 26 additions & 17 deletions React/Base/RCTBridgeModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ typedef void (^RCTResponseSenderBlock)(NSArray *response);
@property (nonatomic, weak) RCTBridge *bridge;

/**
* Place this macro in your class implementation, to automatically register
* Place this macro in your class implementation to automatically register
* your module with the bridge when it loads. The optional js_name argument
* will be used as the JS module name. If omitted, the JS module name will
* match the Objective-C class name.
Expand All @@ -41,23 +41,11 @@ typedef void (^RCTResponseSenderBlock)(NSArray *response);
+ (NSString *)moduleName { __attribute__((used, section("__DATA,RCTExportModule" \
))) static const char *__rct_export_entry__ = { __func__ }; return @#js_name; }

/**
* Place this macro inside the method body of any method you want to expose
* to JS. The optional js_name argument will be used as the JS method name
* (the method will be namespaced to the module name, as specified above).
* If omitted, the JS method name will match the first part of the Objective-C
* method selector name (up to the first colon).
*/
#define RCT_EXPORT(js_name) \
_Pragma("message(\"RCT_EXPORT is deprecated. Use RCT_EXPORT_METHOD instead.\")") \
__attribute__((used, section("__DATA,RCTExport"))) \
static const char *__rct_export_entry__[] = { __func__, #js_name }

/**
* Wrap the parameter line of your method implementation with this macro to
* expose it to JS. Unlike the deprecated RCT_EXPORT, this macro does not take
* a js_name argument and the exposed method will match the first part of the
* Objective-C method selector name (up to the first colon).
* expose it to JS. By default the exposed method will match the first part of
* the Objective-C method selector name (up to the first colon). Use
* RCT_REMAP_METHOD to specify the JS name of the method.
*
* For example, in ModuleName.m:
*
Expand All @@ -74,12 +62,33 @@ typedef void (^RCTResponseSenderBlock)(NSArray *response);
* and is exposed to JavaScript as `NativeModules.ModuleName.doSomething`.
*/
#define RCT_EXPORT_METHOD(method) \
RCT_REMAP_METHOD(, method)

/**
* Similar to RCT_EXPORT_METHOD but lets you set the JS name of the exported
* method. Example usage:
*
* RCT_REMAP_METHOD(executeQueryWithParameters,
* executeQuery:(NSString *)query parameters:(NSDictionary *)parameters)
* { ... }
*/
#define RCT_REMAP_METHOD(js_name, method) \
- (void)__rct_export__##method { \
__attribute__((used, section("__DATA,RCTExport"))) \
static const char *__rct_export_entry__[] = { __func__, #method }; \
__attribute__((__aligned__(1))) \
static const char *__rct_export_entry__[] = { __func__, #method, #js_name }; \
} \
- (void)method

/**
* Deprecated, do not use.
*/
#define RCT_EXPORT(js_name) \
_Pragma("message(\"RCT_EXPORT is deprecated. Use RCT_EXPORT_METHOD instead.\")") \
__attribute__((used, section("__DATA,RCTExport"))) \
__attribute__((__aligned__(1))) \
static const char *__rct_export_entry__[] = { __func__, #js_name, NULL }

/**
* Injects constants into JS. These constants are made accessible via
* NativeModules.ModuleName.X. This method is called when the module is
Expand Down
11 changes: 7 additions & 4 deletions React/Base/RCTConvert.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ BOOL RCTSetProperty(id target, NSString *keyPath, SEL type, id json);
*/
BOOL RCTCopyProperty(id target, id source, NSString *keyPath);

/**
* Underlying implementation of RCT_ENUM_CONVERTER macro. Ignore this.
*/
NSNumber *RCTConverterEnumValue(const char *, NSDictionary *, NSNumber *, id);

#ifdef __cplusplus
}
#endif
Expand Down Expand Up @@ -169,13 +174,11 @@ RCT_CUSTOM_CONVERTER(type, name, [json getter])
/**
* This macro is similar to RCT_CONVERTER, but specifically geared towards
* numeric types. It will handle string input correctly, and provides more
* detailed error reporting if a wrong value is passed in.
* detailed error reporting if an invalid value is passed in.
*/
#define RCT_NUMBER_CONVERTER(type, getter) \
RCT_CUSTOM_CONVERTER(type, type, [[self NSNumber:json] getter])

NSNumber *RCTEnumConverterImpl(const char *typeName, NSDictionary *values, NSNumber *defaultValue, id json);

/**
* This macro is used for creating converters for enum types.
*/
Expand All @@ -187,7 +190,7 @@ NSNumber *RCTEnumConverterImpl(const char *typeName, NSDictionary *values, NSNum
dispatch_once(&onceToken, ^{ \
mapping = values; \
}); \
NSNumber *converted = RCTEnumConverterImpl(#type, mapping, @(default), json); \
NSNumber *converted = RCTConverterEnumValue(#type, mapping, @(default), json); \
return ((type(*)(id, SEL))objc_msgSend)(converted, @selector(getter)); \
}

Expand Down
Loading

0 comments on commit e193a13

Please sign in to comment.