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