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 47 // Add the main object to the runtime (`global.expo`). 48 _mainObject = [self createObject]; 49 [[self global] defineProperty:mainObjectPropertyName value:_mainObject options:EXJavaScriptObjectPropertyDescriptorEnumerable]; 50 } 51 return self; 52} 53 54- (nonnull instancetype)initWithRuntime:(nonnull jsi::Runtime *)runtime 55 callInvoker:(std::shared_ptr<react::CallInvoker>)callInvoker 56{ 57 if (self = [super init]) { 58 // Creating a shared pointer that points to the runtime but doesn't own it, thus doesn't release it. 59 // In this code flow, the runtime should be owned by something else like the RCTBridge. 60 // See explanation for constructor (8): https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr 61 _runtime = std::shared_ptr<jsi::Runtime>(std::shared_ptr<jsi::Runtime>(), runtime); 62 _jsCallInvoker = callInvoker; 63 } 64 return self; 65} 66 67- (nonnull jsi::Runtime *)get 68{ 69 return _runtime.get(); 70} 71 72- (std::shared_ptr<react::CallInvoker>)callInvoker 73{ 74 return _jsCallInvoker; 75} 76 77- (nonnull EXJavaScriptObject *)createObject 78{ 79 auto jsObjectPtr = std::make_shared<jsi::Object>(*_runtime); 80 return [[EXJavaScriptObject alloc] initWith:jsObjectPtr runtime:self]; 81} 82 83- (nonnull EXJavaScriptObject *)createHostObject:(std::shared_ptr<jsi::HostObject>)jsiHostObjectPtr 84{ 85 auto jsObjectPtr = std::make_shared<jsi::Object>(jsi::Object::createFromHostObject(*_runtime, jsiHostObjectPtr)); 86 return [[EXJavaScriptObject alloc] initWith:jsObjectPtr runtime:self]; 87} 88 89- (nonnull EXJavaScriptObject *)global 90{ 91 auto jsGlobalPtr = std::make_shared<jsi::Object>(_runtime->global()); 92 return [[EXJavaScriptObject alloc] initWith:jsGlobalPtr runtime:self]; 93} 94 95- (nonnull EXJavaScriptObject *)mainObject 96{ 97 return _mainObject; 98} 99 100- (nonnull EXJavaScriptObject *)createSyncFunction:(nonnull NSString *)name 101 argsCount:(NSInteger)argsCount 102 block:(nonnull JSSyncFunctionBlock)block 103{ 104 JSHostFunctionBlock hostFunctionBlock = ^jsi::Value( 105 jsi::Runtime &runtime, 106 std::shared_ptr<react::CallInvoker> callInvoker, 107 EXJavaScriptValue * _Nonnull thisValue, 108 NSArray<EXJavaScriptValue *> * _Nonnull arguments) { 109 NSError *error; 110 id result = block(thisValue, arguments, &error); 111 112 if (error == nil) { 113 return expo::convertObjCObjectToJSIValue(runtime, result); 114 } else { 115 throw jsi::JSError(runtime, [error.userInfo[@"message"] UTF8String]); 116 } 117 }; 118 return [self createHostFunction:name argsCount:argsCount block:hostFunctionBlock]; 119} 120 121- (nonnull EXJavaScriptObject *)createAsyncFunction:(nonnull NSString *)name 122 argsCount:(NSInteger)argsCount 123 block:(nonnull JSAsyncFunctionBlock)block 124{ 125 JSHostFunctionBlock hostFunctionBlock = ^jsi::Value( 126 jsi::Runtime &runtime, 127 std::shared_ptr<react::CallInvoker> callInvoker, 128 EXJavaScriptValue * _Nonnull thisValue, 129 NSArray<EXJavaScriptValue *> * _Nonnull arguments) { 130 if (!callInvoker) { 131 // In mocked environment the call invoker may be null so it's not supported to call async functions. 132 // Testing async functions is a bit more complicated anyway. See `init` description for more. 133 throw jsi::JSError(runtime, "Calling async functions is not supported when the call invoker is unavailable"); 134 } 135 // The function that is invoked as a setup of the EXJavaScript `Promise`. 136 auto promiseSetup = [callInvoker, block, thisValue, arguments](jsi::Runtime &runtime, std::shared_ptr<Promise> promise) { 137 expo::callPromiseSetupWithBlock(runtime, callInvoker, promise, ^(RCTPromiseResolveBlock resolver, RCTPromiseRejectBlock rejecter) { 138 block(thisValue, arguments, resolver, rejecter); 139 }); 140 }; 141 return createPromiseAsJSIValue(runtime, promiseSetup); 142 }; 143 return [self createHostFunction:name argsCount:argsCount block:hostFunctionBlock]; 144} 145 146#pragma mark - Classes 147 148- (nonnull EXJavaScriptObject *)createClass:(nonnull NSString *)name 149 constructor:(nonnull ClassConstructorBlock)constructor 150{ 151 expo::ClassConstructor jsConstructor = [self, constructor](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) { 152 std::shared_ptr<jsi::Object> thisPtr = std::make_shared<jsi::Object>(thisValue.asObject(runtime)); 153 EXJavaScriptObject *caller = [[EXJavaScriptObject alloc] initWith:thisPtr runtime:self]; 154 NSArray<EXJavaScriptValue *> *arguments = expo::convertJSIValuesToNSArray(self, args, count); 155 156 constructor(caller, arguments); 157 }; 158 std::shared_ptr<jsi::Function> klass = expo::createClass(*_runtime, [name UTF8String], jsConstructor); 159 return [[EXJavaScriptObject alloc] initWith:klass runtime:self]; 160} 161 162#pragma mark - Script evaluation 163 164- (nonnull EXJavaScriptValue *)evaluateScript:(nonnull NSString *)scriptSource 165{ 166 std::shared_ptr<jsi::StringBuffer> scriptBuffer = std::make_shared<jsi::StringBuffer>([scriptSource UTF8String]); 167 std::shared_ptr<jsi::Value> result; 168 169 try { 170 result = std::make_shared<jsi::Value>(_runtime->evaluateJavaScript(scriptBuffer, "<<evaluated>>")); 171 } catch (jsi::JSError &error) { 172 NSString *reason = [NSString stringWithUTF8String:error.getMessage().c_str()]; 173 NSString *stack = [NSString stringWithUTF8String:error.getStack().c_str()]; 174 175 @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{ 176 @"message": reason, 177 @"stack": stack, 178 }]; 179 } catch (jsi::JSIException &error) { 180 NSString *reason = [NSString stringWithUTF8String:error.what()]; 181 182 @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{ 183 @"message": reason 184 }]; 185 } 186 return [[EXJavaScriptValue alloc] initWithRuntime:self value:result]; 187} 188 189#pragma mark - Private 190 191- (nonnull EXJavaScriptObject *)createHostFunction:(nonnull NSString *)name 192 argsCount:(NSInteger)argsCount 193 block:(nonnull JSHostFunctionBlock)block 194{ 195 jsi::PropNameID propNameId = jsi::PropNameID::forAscii(*_runtime, [name UTF8String], [name length]); 196 std::weak_ptr<react::CallInvoker> weakCallInvoker = _jsCallInvoker; 197 jsi::HostFunctionType function = [weakCallInvoker, block, self](jsi::Runtime &runtime, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value { 198 // Theoretically should check here whether the call invoker isn't null, but in mocked environment 199 // there is no need to care about that for synchronous calls, so it's ensured in `createAsyncFunction` instead. 200 auto callInvoker = weakCallInvoker.lock(); 201 NSArray<EXJavaScriptValue *> *arguments = expo::convertJSIValuesToNSArray(self, args, count); 202 std::shared_ptr<jsi::Value> thisValPtr = std::make_shared<jsi::Value>(runtime, std::move(thisVal)); 203 EXJavaScriptValue *thisValue = [[EXJavaScriptValue alloc] initWithRuntime:self value:thisValPtr]; 204 205 return block(runtime, callInvoker, thisValue, arguments); 206 }; 207 std::shared_ptr<jsi::Object> fnPtr = std::make_shared<jsi::Object>(jsi::Function::createFromHostFunction(*_runtime, propNameId, (unsigned int)argsCount, function)); 208 return [[EXJavaScriptObject alloc] initWith:fnPtr runtime:self]; 209} 210 211@end 212