1// Copyright © 2018 650 Industries. All rights reserved.
2
3#import <objc/runtime.h>
4
5#import <ExpoModulesCore/EXExportedModule.h>
6
7#define QUOTE(str) #str
8#define EXPAND_AND_QUOTE(str) QUOTE(str)
9
10#define EX_IS_METHOD_EXPORTED(methodName) \
11[methodName hasPrefix:@EXPAND_AND_QUOTE(EX_EXPORTED_METHODS_PREFIX)]
12
13static const NSString *noNameExceptionName = @"No custom +(const NSString *)exportedModuleName implementation.";
14static const NSString *noNameExceptionReasonFormat = @"You've subclassed an EXExportedModule in %@, but didn't override the +(const NSString *)exportedModuleName method. Override this method and return a name for your exported module.";
15
16static const NSRegularExpression *selectorRegularExpression = nil;
17static dispatch_once_t selectorRegularExpressionOnceToken = 0;
18
19@interface EXExportedModule ()
20
21@property (nonatomic, strong) dispatch_queue_t methodQueue;
22@property (nonatomic, strong) NSDictionary<NSString *, NSString *> *exportedMethods;
23
24@end
25
26@implementation EXExportedModule
27
28- (instancetype)init
29{
30  return self = [super init];
31}
32
33- (instancetype)copyWithZone:(NSZone *)zone
34{
35  return self;
36}
37
38+ (const NSArray<Protocol *> *)exportedInterfaces {
39  return nil;
40}
41
42
43+ (const NSString *)exportedModuleName
44{
45  NSString *reason = [NSString stringWithFormat:(NSString *)noNameExceptionReasonFormat, [self description]];
46  @throw [NSException exceptionWithName:(NSString *)noNameExceptionName
47                                 reason:reason
48                               userInfo:nil];
49}
50
51- (NSDictionary *)constantsToExport
52{
53  return nil;
54}
55
56- (dispatch_queue_t)methodQueue
57{
58  if (!_methodQueue) {
59    NSString *queueName = [NSString stringWithFormat:@"expo.modules.%@Queue", [[self class] exportedModuleName]];
60    _methodQueue = dispatch_queue_create(queueName.UTF8String, DISPATCH_QUEUE_SERIAL);
61  }
62  return _methodQueue;
63}
64
65# pragma mark - Exported methods
66
67- (NSDictionary<NSString *, NSString *> *)getExportedMethods
68{
69  if (_exportedMethods) {
70    return _exportedMethods;
71  }
72
73  NSMutableDictionary<NSString *, NSString *> *exportedMethods = [NSMutableDictionary dictionary];
74
75  Class klass = [self class];
76
77  while (klass) {
78    unsigned int methodsCount;
79    Method *methodsDescriptions = class_copyMethodList(object_getClass(klass), &methodsCount);
80
81    @try {
82      for(int i = 0; i < methodsCount; i++) {
83        Method method = methodsDescriptions[i];
84        SEL methodSelector = method_getName(method);
85        NSString *methodName = NSStringFromSelector(methodSelector);
86        if (EX_IS_METHOD_EXPORTED(methodName)) {
87          IMP imp = method_getImplementation(method);
88          const EXMethodInfo *info = ((const EXMethodInfo *(*)(id, SEL))imp)(klass, methodSelector);
89          NSString *fullSelectorName = [NSString stringWithUTF8String:info->objcName];
90          // `objcName` constains a method declaration string
91          // (eg. `doSth:(NSString *)string options:(NSDictionary *)options`)
92          // We only need a selector string  (eg. `doSth:options:`)
93          NSString *simpleSelectorName = [self selectorNameFromName:fullSelectorName];
94          exportedMethods[[NSString stringWithUTF8String:info->jsName]] = simpleSelectorName;
95        }
96      }
97    }
98    @finally {
99      free(methodsDescriptions);
100    }
101
102    klass = [klass superclass];
103  }
104
105  _exportedMethods = exportedMethods;
106
107  return _exportedMethods;
108}
109
110- (NSString *)selectorNameFromName:(NSString *)nameString
111{
112  dispatch_once(&selectorRegularExpressionOnceToken, ^{
113    selectorRegularExpression = [NSRegularExpression regularExpressionWithPattern:@"\\(.+?\\).+?\\b\\s*" options:NSRegularExpressionCaseInsensitive error:nil];
114  });
115  return [selectorRegularExpression stringByReplacingMatchesInString:nameString options:0 range:NSMakeRange(0, [nameString length]) withTemplate:@""];
116}
117
118static const NSNumber *trueValue;
119
120- (void)callExportedMethod:(NSString *)methodName withArguments:(NSArray *)arguments resolver:(EXPromiseResolveBlock)resolve rejecter:(EXPromiseRejectBlock)reject
121{
122  trueValue = [NSNumber numberWithBool:YES];
123  const NSString *moduleName = [[self class] exportedModuleName];
124  NSString *methodDeclaration = _exportedMethods[methodName];
125  if (methodDeclaration == nil) {
126    NSString *reason = [NSString stringWithFormat:@"Module '%@' does not export method '%@'.", moduleName, methodName];
127    reject(@"E_NO_METHOD", reason, nil);
128    return;
129  }
130  SEL selector = NSSelectorFromString(methodDeclaration);
131  NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector];
132  if (methodSignature == nil) {
133    // This in fact should never happen -- if we have a methodDeclaration for an exported method
134    // it means that it has been exported with EX_IMPORT_METHOD and if we cannot find method signature
135    // for the cached selector either the macro or the -selectorNameFromName is faulty.
136    NSString *reason = [NSString stringWithFormat:@"Module '%@' does not implement method for selector '%@'.", moduleName, NSStringFromSelector(selector)];
137    reject(@"E_NO_METHOD", reason, nil);
138    return;
139  }
140
141  NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
142  [invocation setTarget:self];
143  [invocation setSelector:selector];
144  [arguments enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
145    if (obj != [NSNull null]) {
146      [invocation setArgument:&obj atIndex:(2 + idx)];
147    }
148
149    // According to objc.h, the BOOL type can be represented by `bool` or `signed char` so
150    // getArgumentTypeAtIndex can return _C_BOOL (when `bool`) or _C_CHR (when `signed char`)
151#if OBJC_BOOL_IS_BOOL
152    if ([methodSignature getArgumentTypeAtIndex:(2 + idx)][0] == _C_BOOL) {
153      // We need this intermediary variable, see
154      // https://stackoverflow.com/questions/11061166/pointer-to-bool-in-objective-c
155      BOOL value = [obj boolValue];
156      [invocation setArgument:&value atIndex:(2 + idx)];
157    }
158#else // BOOL is represented by `signed char`
159    if ([methodSignature getArgumentTypeAtIndex:(2 + idx)][0] == _C_CHR){
160      BOOL value = [obj charValue];
161      [invocation setArgument:&value atIndex:(2 + idx)];
162    }
163#endif
164  }];
165  [invocation setArgument:&resolve atIndex:(2 + [arguments count])];
166  [invocation setArgument:&reject atIndex:([arguments count] + 2 + 1)];
167  [invocation retainArguments];
168  [invocation invoke];
169}
170
171@end
172