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