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 return [self createHostFunction:name argsCount:argsCount block:^jsi::Value(jsi::Runtime &runtime, std::shared_ptr<react::CallInvoker> callInvoker, NSArray * _Nonnull arguments) { 90 return expo::convertObjCObjectToJSIValue(runtime, block(arguments)); 91 }]; 92} 93 94- (nonnull EXJavaScriptObject *)createAsyncFunction:(nonnull NSString *)name 95 argsCount:(NSInteger)argsCount 96 block:(nonnull JSAsyncFunctionBlock)block 97{ 98 return [self createHostFunction:name argsCount:argsCount block:^jsi::Value(jsi::Runtime &runtime, std::shared_ptr<react::CallInvoker> callInvoker, NSArray *arguments) { 99 if (!callInvoker) { 100 // In mocked environment the call invoker may be null so it's not supported to call async functions. 101 // Testing async functions is a bit more complicated anyway. See `init` description for more. 102 throw jsi::JSError(runtime, "Calling async functions is not supported when the call invoker is unavailable"); 103 } 104 // The function that is invoked as a setup of the EXJavaScript `Promise`. 105 auto promiseSetup = [callInvoker, block, arguments](jsi::Runtime &runtime, std::shared_ptr<Promise> promise) { 106 expo::callPromiseSetupWithBlock(runtime, callInvoker, promise, ^(RCTPromiseResolveBlock resolver, RCTPromiseRejectBlock rejecter) { 107 block(arguments, resolver, rejecter); 108 }); 109 }; 110 return createPromiseAsJSIValue(runtime, promiseSetup); 111 }]; 112} 113 114#pragma mark - Classes 115 116- (nonnull EXJavaScriptObject *)createClass:(nonnull NSString *)name 117 constructor:(nonnull ClassConstructorBlock)constructor 118{ 119 std::shared_ptr<jsi::Function> klass = expo::createClass(*_runtime, [name UTF8String], [self, constructor](jsi::Runtime &runtime, const jsi::Value &thisValue, jsi::Array args) { 120 std::shared_ptr<jsi::Object> thisPtr = std::make_shared<jsi::Object>(thisValue.asObject(runtime)); 121 EXJavaScriptObject *caller = [[EXJavaScriptObject alloc] initWith:thisPtr runtime:self]; 122 NSArray<EXJavaScriptValue *> *arguments = expo::convertJSIArrayToNSArray(runtime, args, self->_jsCallInvoker); 123 124 constructor(caller, arguments); 125 }); 126 return [[EXJavaScriptObject alloc] initWith:klass runtime:self]; 127} 128 129#pragma mark - Script evaluation 130 131- (nonnull EXJavaScriptValue *)evaluateScript:(nonnull NSString *)scriptSource 132{ 133 std::shared_ptr<jsi::StringBuffer> scriptBuffer = std::make_shared<jsi::StringBuffer>([scriptSource UTF8String]); 134 std::shared_ptr<jsi::Value> result; 135 136 try { 137 result = std::make_shared<jsi::Value>(_runtime->evaluateJavaScript(scriptBuffer, "<<evaluated>>")); 138 } catch (jsi::JSError &error) { 139 NSString *reason = [NSString stringWithUTF8String:error.getMessage().c_str()]; 140 NSString *stack = [NSString stringWithUTF8String:error.getStack().c_str()]; 141 142 @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{ 143 @"message": reason, 144 @"stack": stack, 145 }]; 146 } 147 return [[EXJavaScriptValue alloc] initWithRuntime:self value:result]; 148} 149 150#pragma mark - Private 151 152- (nonnull EXJavaScriptObject *)createHostFunction:(nonnull NSString *)name 153 argsCount:(NSInteger)argsCount 154 block:(nonnull JSHostFunctionBlock)block 155{ 156 jsi::PropNameID propNameId = jsi::PropNameID::forAscii(*_runtime, [name UTF8String], [name length]); 157 std::weak_ptr<react::CallInvoker> weakCallInvoker = _jsCallInvoker; 158 jsi::HostFunctionType function = [weakCallInvoker, block, self](jsi::Runtime &runtime, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value { 159 // Theoretically should check here whether the call invoker isn't null, but in mocked environment 160 // there is no need to care about that for synchronous calls, so it's ensured in `createAsyncFunction` instead. 161 auto callInvoker = weakCallInvoker.lock(); 162 NSArray<EXJavaScriptValue *> *arguments = expo::convertJSIValuesToNSArray(self, args, count); 163 return block(runtime, callInvoker, arguments); 164 }; 165 std::shared_ptr<jsi::Object> fnPtr = std::make_shared<jsi::Object>(jsi::Function::createFromHostFunction(*_runtime, propNameId, (unsigned int)argsCount, function)); 166 return [[EXJavaScriptObject alloc] initWith:fnPtr runtime:self]; 167} 168 169@end 170