1// Copyright 2018-present 650 Industries. All rights reserved. 2 3#import <jsi/jsi.h> 4 5#if __has_include(<reacthermes/HermesExecutorFactory.h>) 6#import <reacthermes/HermesExecutorFactory.h> 7#elif __has_include(<hermes/hermes.h>) 8#import <hermes/hermes.h> 9#else 10#import <jsi/JSCRuntime.h> 11#endif 12 13#import <ExpoModulesCore/EXJavaScriptRuntime.h> 14#import <ExpoModulesCore/ExpoModulesHostObject.h> 15#import <ExpoModulesCore/EXJSIUtils.h> 16#import <ExpoModulesCore/EXJSIConversions.h> 17#import <ExpoModulesCore/Swift.h> 18 19using namespace facebook; 20 21/** 22 Property name of the main object in the Expo JS runtime. 23 */ 24static NSString *mainObjectPropertyName = @"expo"; 25 26@implementation EXJavaScriptRuntime { 27 std::shared_ptr<jsi::Runtime> _runtime; 28 std::shared_ptr<react::CallInvoker> _jsCallInvoker; 29 EXJavaScriptObject *_mainObject; 30} 31 32/** 33 Initializes a runtime that is independent from React Native and its runtime initialization. 34 This flow is mostly intended for tests. The JS call invoker is unavailable thus calling async functions is not supported. 35 TODO: Implement the call invoker when it becomes necessary. 36 */ 37- (nonnull instancetype)init 38{ 39 if (self = [super init]) { 40#if __has_include(<reacthermes/HermesExecutorFactory.h>) || __has_include(<hermes/hermes.h>) 41 _runtime = hermes::makeHermesRuntime(); 42#else 43 _runtime = jsc::makeJSCRuntime(); 44#endif 45 _jsCallInvoker = nil; 46 [self initializeMainObject]; 47 } 48 return self; 49} 50 51- (nonnull instancetype)initWithRuntime:(nonnull jsi::Runtime *)runtime 52 callInvoker:(std::shared_ptr<react::CallInvoker>)callInvoker 53{ 54 if (self = [super init]) { 55 // Creating a shared pointer that points to the runtime but doesn't own it, thus doesn't release it. 56 // In this code flow, the runtime should be owned by something else like the RCTBridge. 57 // See explanation for constructor (8): https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr 58 _runtime = std::shared_ptr<jsi::Runtime>(std::shared_ptr<jsi::Runtime>(), runtime); 59 _jsCallInvoker = callInvoker; 60 [self initializeMainObject]; 61 } 62 return self; 63} 64 65- (nonnull jsi::Runtime *)get 66{ 67 return _runtime.get(); 68} 69 70- (std::shared_ptr<react::CallInvoker>)callInvoker 71{ 72 return _jsCallInvoker; 73} 74 75- (nonnull EXJavaScriptObject *)createObject 76{ 77 auto jsObjectPtr = std::make_shared<jsi::Object>(*_runtime); 78 return [[EXJavaScriptObject alloc] initWith:jsObjectPtr runtime:self]; 79} 80 81- (nonnull EXJavaScriptObject *)createHostObject:(std::shared_ptr<jsi::HostObject>)jsiHostObjectPtr 82{ 83 auto jsObjectPtr = std::make_shared<jsi::Object>(jsi::Object::createFromHostObject(*_runtime, jsiHostObjectPtr)); 84 return [[EXJavaScriptObject alloc] initWith:jsObjectPtr runtime:self]; 85} 86 87- (nonnull EXJavaScriptObject *)global 88{ 89 auto jsGlobalPtr = std::make_shared<jsi::Object>(_runtime->global()); 90 return [[EXJavaScriptObject alloc] initWith:jsGlobalPtr runtime:self]; 91} 92 93- (nonnull EXJavaScriptObject *)mainObject 94{ 95 return _mainObject; 96} 97 98- (nonnull EXJavaScriptObject *)createSyncFunction:(nonnull NSString *)name 99 argsCount:(NSInteger)argsCount 100 block:(nonnull JSSyncFunctionBlock)block 101{ 102 JSHostFunctionBlock hostFunctionBlock = ^jsi::Value( 103 jsi::Runtime &runtime, 104 std::shared_ptr<react::CallInvoker> callInvoker, 105 EXJavaScriptValue * _Nonnull thisValue, 106 NSArray<EXJavaScriptValue *> * _Nonnull arguments) { 107 NSError *error; 108 id result = block(thisValue, arguments, &error); 109 110 if (error == nil) { 111 return expo::convertObjCObjectToJSIValue(runtime, result); 112 } else { 113 throw jsi::JSError(runtime, [error.userInfo[@"message"] UTF8String]); 114 } 115 }; 116 return [self createHostFunction:name argsCount:argsCount block:hostFunctionBlock]; 117} 118 119- (nonnull EXJavaScriptObject *)createAsyncFunction:(nonnull NSString *)name 120 argsCount:(NSInteger)argsCount 121 block:(nonnull JSAsyncFunctionBlock)block 122{ 123 JSHostFunctionBlock hostFunctionBlock = ^jsi::Value( 124 jsi::Runtime &runtime, 125 std::shared_ptr<react::CallInvoker> callInvoker, 126 EXJavaScriptValue * _Nonnull thisValue, 127 NSArray<EXJavaScriptValue *> * _Nonnull arguments) { 128 if (!callInvoker) { 129 // In mocked environment the call invoker may be null so it's not supported to call async functions. 130 // Testing async functions is a bit more complicated anyway. See `init` description for more. 131 throw jsi::JSError(runtime, "Calling async functions is not supported when the call invoker is unavailable"); 132 } 133 // The function that is invoked as a setup of the EXJavaScript `Promise`. 134 auto promiseSetup = [callInvoker, block, thisValue, arguments](jsi::Runtime &runtime, std::shared_ptr<Promise> promise) { 135 expo::callPromiseSetupWithBlock(runtime, callInvoker, promise, ^(RCTPromiseResolveBlock resolver, RCTPromiseRejectBlock rejecter) { 136 block(thisValue, arguments, resolver, rejecter); 137 }); 138 }; 139 return createPromiseAsJSIValue(runtime, promiseSetup); 140 }; 141 return [self createHostFunction:name argsCount:argsCount block:hostFunctionBlock]; 142} 143 144#pragma mark - Classes 145 146- (nonnull EXJavaScriptObject *)createClass:(nonnull NSString *)name 147 constructor:(nonnull ClassConstructorBlock)constructor 148{ 149 expo::ClassConstructor jsConstructor = [self, constructor](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) { 150 std::shared_ptr<jsi::Object> thisPtr = std::make_shared<jsi::Object>(thisValue.asObject(runtime)); 151 EXJavaScriptObject *caller = [[EXJavaScriptObject alloc] initWith:thisPtr runtime:self]; 152 NSArray<EXJavaScriptValue *> *arguments = expo::convertJSIValuesToNSArray(self, args, count); 153 154 constructor(caller, arguments); 155 }; 156 std::shared_ptr<jsi::Function> klass = expo::createClass(*_runtime, [name UTF8String], jsConstructor); 157 return [[EXJavaScriptObject alloc] initWith:klass runtime:self]; 158} 159 160#pragma mark - Script evaluation 161 162- (nonnull EXJavaScriptValue *)evaluateScript:(nonnull NSString *)scriptSource 163{ 164 std::shared_ptr<jsi::StringBuffer> scriptBuffer = std::make_shared<jsi::StringBuffer>([scriptSource UTF8String]); 165 std::shared_ptr<jsi::Value> result; 166 167 try { 168 result = std::make_shared<jsi::Value>(_runtime->evaluateJavaScript(scriptBuffer, "<<evaluated>>")); 169 } catch (jsi::JSError &error) { 170 NSString *reason = [NSString stringWithUTF8String:error.getMessage().c_str()]; 171 NSString *stack = [NSString stringWithUTF8String:error.getStack().c_str()]; 172 173 @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{ 174 @"message": reason, 175 @"stack": stack, 176 }]; 177 } catch (jsi::JSIException &error) { 178 NSString *reason = [NSString stringWithUTF8String:error.what()]; 179 180 @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{ 181 @"message": reason 182 }]; 183 } 184 return [[EXJavaScriptValue alloc] initWithRuntime:self value:result]; 185} 186 187#pragma mark - Private 188 189- (void)initializeMainObject 190{ 191 if (!_mainObject) { 192 // Add the main object to the runtime (`global.expo`). 193 _mainObject = [self createObject]; 194 [[self global] defineProperty:mainObjectPropertyName value:_mainObject options:EXJavaScriptObjectPropertyDescriptorEnumerable]; 195 } 196} 197 198- (nonnull EXJavaScriptObject *)createHostFunction:(nonnull NSString *)name 199 argsCount:(NSInteger)argsCount 200 block:(nonnull JSHostFunctionBlock)block 201{ 202 jsi::PropNameID propNameId = jsi::PropNameID::forAscii(*_runtime, [name UTF8String], [name length]); 203 std::weak_ptr<react::CallInvoker> weakCallInvoker = _jsCallInvoker; 204 jsi::HostFunctionType function = [weakCallInvoker, block, self](jsi::Runtime &runtime, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value { 205 // Theoretically should check here whether the call invoker isn't null, but in mocked environment 206 // there is no need to care about that for synchronous calls, so it's ensured in `createAsyncFunction` instead. 207 auto callInvoker = weakCallInvoker.lock(); 208 NSArray<EXJavaScriptValue *> *arguments = expo::convertJSIValuesToNSArray(self, args, count); 209 std::shared_ptr<jsi::Value> thisValPtr = std::make_shared<jsi::Value>(runtime, std::move(thisVal)); 210 EXJavaScriptValue *thisValue = [[EXJavaScriptValue alloc] initWithRuntime:self value:thisValPtr]; 211 212 return block(runtime, callInvoker, thisValue, arguments); 213 }; 214 std::shared_ptr<jsi::Object> fnPtr = std::make_shared<jsi::Object>(jsi::Function::createFromHostFunction(*_runtime, propNameId, (unsigned int)argsCount, function)); 215 return [[EXJavaScriptObject alloc] initWith:fnPtr runtime:self]; 216} 217 218@end 219