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