// Copyright © 2018 650 Industries. All rights reserved.
#import <UMCore/UMExportedModule.h>
#import <objc/runtime.h>
#define QUOTE(str) #str
#define EXPAND_AND_QUOTE(str) QUOTE(str)
#define UM_IS_METHOD_EXPORTED(methodName) \
[methodName hasPrefix:@EXPAND_AND_QUOTE(UM_EXPORTED_METHODS_PREFIX)]
static const NSString *noNameExceptionName = @"No custom +(const NSString *)exportedModuleName implementation.";
static const NSString *noNameExceptionReasonFormat = @"You've subclassed an UMExportedModule in %@, but didn't override the +(const NSString *)exportedModuleName method. Override this method and return a name for your exported module.";
static const NSRegularExpression *selectorRegularExpression = nil;
static dispatch_once_t selectorRegularExpressionOnceToken = 0;
@interface UMExportedModule ()
@property (nonatomic, strong) dispatch_queue_t methodQueue;
@property (nonatomic, assign) dispatch_once_t methodQueueSetupOnce;
@property (nonatomic, strong) NSDictionary<NSString *, NSString *> *exportedMethods;
@end
@implementation UMExportedModule
- (instancetype)init
{
return self = [super init];
}
- (instancetype)copyWithZone:(NSZone *)zone
{
return self;
}
+ (const NSArray<Protocol *> *)exportedInterfaces {
return nil;
}
+ (const NSString *)exportedModuleName
{
NSString *reason = [NSString stringWithFormat:(NSString *)noNameExceptionReasonFormat, [self description]];
@throw [NSException exceptionWithName:(NSString *)noNameExceptionName
reason:reason
userInfo:nil];
}
- (NSDictionary *)constantsToExport
{
return nil;
}
- (dispatch_queue_t)methodQueue
{
__weak UMExportedModule *weakSelf = self;
dispatch_once(&_methodQueueSetupOnce, ^{
__strong UMExportedModule *strongSelf = weakSelf;
if (strongSelf) {
NSString *queueName = [NSString stringWithFormat:@"org.unimodules.%@Queue", [[strongSelf class] exportedModuleName]];
strongSelf.methodQueue = dispatch_queue_create(queueName.UTF8String, DISPATCH_QUEUE_SERIAL);
}
});
return _methodQueue;
}
# pragma mark - Exported methods
- (NSDictionary<NSString *, NSString *> *)getExportedMethods
{
if (_exportedMethods) {
return _exportedMethods;
}
NSMutableDictionary<NSString *, NSString *> *exportedMethods = [NSMutableDictionary dictionary];
Class klass = [self class];
while (klass) {
unsigned int methodsCount;
Method *methodsDescriptions = class_copyMethodList(object_getClass(klass), &methodsCount);
@try {
for(int i = 0; i < methodsCount; i++) {
Method method = methodsDescriptions[i];
SEL methodSelector = method_getName(method);
NSString *methodName = NSStringFromSelector(methodSelector);
if (UM_IS_METHOD_EXPORTED(methodName)) {
IMP imp = method_getImplementation(method);
const UMMethodInfo *info = ((const UMMethodInfo *(*)(id, SEL))imp)(klass, methodSelector);
NSString *fullSelectorName = [NSString stringWithUTF8String:info->objcName];
// `objcName` constains a method declaration string
// (eg. `doSth:(NSString *)string options:(NSDictionary *)options`)
// We only need a selector string (eg. `doSth:options:`)
NSString *simpleSelectorName = [self selectorNameFromName:fullSelectorName];
exportedMethods[[NSString stringWithUTF8String:info->jsName]] = simpleSelectorName;
}
}
}
@finally {
free(methodsDescriptions);
}
klass = [klass superclass];
}
_exportedMethods = exportedMethods;
return _exportedMethods;
}
- (NSString *)selectorNameFromName:(NSString *)nameString
{
dispatch_once(&selectorRegularExpressionOnceToken, ^{
selectorRegularExpression = [NSRegularExpression regularExpressionWithPattern:@"\\(.+?\\).+?\\b\\s*" options:NSRegularExpressionCaseInsensitive error:nil];
});
return [selectorRegularExpression stringByReplacingMatchesInString:nameString options:0 range:NSMakeRange(0, [nameString length]) withTemplate:@""];
}
static const NSNumber *trueValue;
- (void)callExportedMethod:(NSString *)methodName withArguments:(NSArray *)arguments resolver:(UMPromiseResolveBlock)resolve rejecter:(UMPromiseRejectBlock)reject
{
trueValue = [NSNumber numberWithBool:YES];
const NSString *moduleName = [[self class] exportedModuleName];
NSString *methodDeclaration = _exportedMethods[methodName];
if (methodDeclaration == nil) {
NSString *reason = [NSString stringWithFormat:@"Module '%@' does not export method '%@'.", moduleName, methodName];
reject(@"E_NO_METHOD", reason, nil);
return;
}
SEL selector = NSSelectorFromString(methodDeclaration);
NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector];
if (methodSignature == nil) {
// This in fact should never happen -- if we have a methodDeclaration for an exported method
// it means that it has been exported with UM_IMPORT_METHOD and if we cannot find method signature
// for the cached selector either the macro or the -selectorNameFromName is faulty.
NSString *reason = [NSString stringWithFormat:@"Module '%@' does not implement method for selector '%@'.", moduleName, NSStringFromSelector(selector)];
reject(@"E_NO_METHOD", reason, nil);
return;
}
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setTarget:self];
[invocation setSelector:selector];
[arguments enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj != [NSNull null]) {
[invocation setArgument:&obj atIndex:(2 + idx)];
}
// According to objc.h, the BOOL type can be represented by `bool` or `signed char` so
// getArgumentTypeAtIndex can return _C_BOOL (when `bool`) or _C_CHR (when `signed char`)
#if OBJC_BOOL_IS_BOOL
if ([methodSignature getArgumentTypeAtIndex:(2 + idx)][0] == _C_BOOL) {
// We need this intermediary variable, see
// https://stackoverflow.com/questions/11061166/pointer-to-bool-in-objective-c
BOOL value = [obj boolValue];
[invocation setArgument:&value atIndex:(2 + idx)];
}
#else // BOOL is represented by `signed char`
if ([methodSignature getArgumentTypeAtIndex:(2 + idx)][0] == _C_CHR){
BOOL value = [obj charValue];
[invocation setArgument:&value atIndex:(2 + idx)];
}
#endif
}];
[invocation setArgument:&resolve atIndex:(2 + [arguments count])];
[invocation setArgument:&reject atIndex:([arguments count] + 2 + 1)];
[invocation retainArguments];
[invocation invoke];
}
@end