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- (nullable EXJavaScriptObject *)createObjectWithPrototype:(nonnull EXJavaScriptObject *)prototype 164{ 165 std::shared_ptr<jsi::Object> object = expo::createObjectWithPrototype(*_runtime, [prototype getShared]); 166 return object ? [[EXJavaScriptObject alloc] initWith:object runtime:self] : nil; 167} 168 169#pragma mark - Script evaluation 170 171- (nonnull EXJavaScriptValue *)evaluateScript:(nonnull NSString *)scriptSource 172{ 173 std::shared_ptr<jsi::StringBuffer> scriptBuffer = std::make_shared<jsi::StringBuffer>([scriptSource UTF8String]); 174 std::shared_ptr<jsi::Value> result; 175 176 try { 177 result = std::make_shared<jsi::Value>(_runtime->evaluateJavaScript(scriptBuffer, "<<evaluated>>")); 178 } catch (jsi::JSError &error) { 179 NSString *reason = [NSString stringWithUTF8String:error.getMessage().c_str()]; 180 NSString *stack = [NSString stringWithUTF8String:error.getStack().c_str()]; 181 182 @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{ 183 @"message": reason, 184 @"stack": stack, 185 }]; 186 } catch (jsi::JSIException &error) { 187 NSString *reason = [NSString stringWithUTF8String:error.what()]; 188 189 @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{ 190 @"message": reason 191 }]; 192 } 193 return [[EXJavaScriptValue alloc] initWithRuntime:self value:result]; 194} 195 196#pragma mark - Private 197 198- (void)initializeMainObject 199{ 200 if (!_mainObject) { 201 // Add the main object to the runtime (`global.expo`). 202 _mainObject = [self createObject]; 203 [[self global] defineProperty:mainObjectPropertyName value:_mainObject options:EXJavaScriptObjectPropertyDescriptorEnumerable]; 204 } 205} 206 207- (nonnull EXJavaScriptObject *)createHostFunction:(nonnull NSString *)name 208 argsCount:(NSInteger)argsCount 209 block:(nonnull JSHostFunctionBlock)block 210{ 211 jsi::PropNameID propNameId = jsi::PropNameID::forAscii(*_runtime, [name UTF8String], [name length]); 212 std::weak_ptr<react::CallInvoker> weakCallInvoker = _jsCallInvoker; 213 jsi::HostFunctionType function = [weakCallInvoker, block, self](jsi::Runtime &runtime, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value { 214 // Theoretically should check here whether the call invoker isn't null, but in mocked environment 215 // there is no need to care about that for synchronous calls, so it's ensured in `createAsyncFunction` instead. 216 auto callInvoker = weakCallInvoker.lock(); 217 NSArray<EXJavaScriptValue *> *arguments = expo::convertJSIValuesToNSArray(self, args, count); 218 std::shared_ptr<jsi::Value> thisValPtr = std::make_shared<jsi::Value>(runtime, std::move(thisVal)); 219 EXJavaScriptValue *thisValue = [[EXJavaScriptValue alloc] initWithRuntime:self value:thisValPtr]; 220 221 return block(runtime, callInvoker, thisValue, arguments); 222 }; 223 std::shared_ptr<jsi::Object> fnPtr = std::make_shared<jsi::Object>(jsi::Function::createFromHostFunction(*_runtime, propNameId, (unsigned int)argsCount, function)); 224 return [[EXJavaScriptObject alloc] initWith:fnPtr runtime:self]; 225} 226 227@end 228