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