// Copyright 2018-present 650 Industries. All rights reserved. #import #if __has_include() #import #else #import #endif #import #import #import #import #import /** Property name of the main object in the Expo JS runtime. */ static NSString *mainObjectPropertyName = @"expo"; @implementation EXJavaScriptRuntime { std::shared_ptr _runtime; std::shared_ptr _jsCallInvoker; EXJavaScriptObject *_mainObject; } /** Initializes a runtime that is independent from React Native and its runtime initialization. This flow is mostly intended for tests. The JS call invoker is unavailable thus calling async functions is not supported. TODO: Implement the call invoker when it becomes necessary. */ - (nonnull instancetype)init { if (self = [super init]) { #if __has_include() _runtime = facebook::hermes::makeHermesRuntime(); #else _runtime = jsc::makeJSCRuntime(); #endif _jsCallInvoker = nil; [self initializeMainObject]; } return self; } - (nonnull instancetype)initWithRuntime:(nonnull jsi::Runtime *)runtime callInvoker:(std::shared_ptr)callInvoker { if (self = [super init]) { // Creating a shared pointer that points to the runtime but doesn't own it, thus doesn't release it. // In this code flow, the runtime should be owned by something else like the RCTBridge. // See explanation for constructor (8): https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr _runtime = std::shared_ptr(std::shared_ptr(), runtime); _jsCallInvoker = callInvoker; [self initializeMainObject]; } return self; } - (nonnull jsi::Runtime *)get { return _runtime.get(); } - (std::shared_ptr)callInvoker { return _jsCallInvoker; } - (nonnull EXJavaScriptObject *)createObject { auto jsObjectPtr = std::make_shared(*_runtime); return [[EXJavaScriptObject alloc] initWith:jsObjectPtr runtime:self]; } - (nonnull EXJavaScriptObject *)createHostObject:(std::shared_ptr)jsiHostObjectPtr { auto jsObjectPtr = std::make_shared(jsi::Object::createFromHostObject(*_runtime, jsiHostObjectPtr)); return [[EXJavaScriptObject alloc] initWith:jsObjectPtr runtime:self]; } - (nonnull EXJavaScriptObject *)global { auto jsGlobalPtr = std::make_shared(_runtime->global()); return [[EXJavaScriptObject alloc] initWith:jsGlobalPtr runtime:self]; } - (nonnull EXJavaScriptObject *)mainObject { return _mainObject; } - (nonnull EXJavaScriptObject *)createSyncFunction:(nonnull NSString *)name argsCount:(NSInteger)argsCount block:(nonnull JSSyncFunctionBlock)block { JSHostFunctionBlock hostFunctionBlock = ^jsi::Value( jsi::Runtime &runtime, std::shared_ptr callInvoker, EXJavaScriptValue * _Nonnull thisValue, NSArray * _Nonnull arguments) { NSError *error; id result = block(thisValue, arguments, &error); if (error == nil) { return expo::convertObjCObjectToJSIValue(runtime, result); } else { throw jsi::JSError(runtime, [error.userInfo[@"message"] UTF8String]); } }; return [self createHostFunction:name argsCount:argsCount block:hostFunctionBlock]; } - (nonnull EXJavaScriptObject *)createAsyncFunction:(nonnull NSString *)name argsCount:(NSInteger)argsCount block:(nonnull JSAsyncFunctionBlock)block { JSHostFunctionBlock hostFunctionBlock = ^jsi::Value( jsi::Runtime &runtime, std::shared_ptr callInvoker, EXJavaScriptValue * _Nonnull thisValue, NSArray * _Nonnull arguments) { if (!callInvoker) { // In mocked environment the call invoker may be null so it's not supported to call async functions. // Testing async functions is a bit more complicated anyway. See `init` description for more. throw jsi::JSError(runtime, "Calling async functions is not supported when the call invoker is unavailable"); } // The function that is invoked as a setup of the EXJavaScript `Promise`. auto promiseSetup = [callInvoker, block, thisValue, arguments](jsi::Runtime &runtime, std::shared_ptr promise) { expo::callPromiseSetupWithBlock(runtime, callInvoker, promise, ^(RCTPromiseResolveBlock resolver, RCTPromiseRejectBlock rejecter) { block(thisValue, arguments, resolver, rejecter); }); }; return createPromiseAsJSIValue(runtime, promiseSetup); }; return [self createHostFunction:name argsCount:argsCount block:hostFunctionBlock]; } #pragma mark - Classes - (nonnull EXJavaScriptObject *)createClass:(nonnull NSString *)name constructor:(nonnull ClassConstructorBlock)constructor { expo::ClassConstructor jsConstructor = [self, constructor](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) { std::shared_ptr thisPtr = std::make_shared(thisValue.asObject(runtime)); EXJavaScriptObject *caller = [[EXJavaScriptObject alloc] initWith:thisPtr runtime:self]; NSArray *arguments = expo::convertJSIValuesToNSArray(self, args, count); constructor(caller, arguments); }; std::shared_ptr klass = expo::createClass(*_runtime, [name UTF8String], jsConstructor); return [[EXJavaScriptObject alloc] initWith:klass runtime:self]; } #pragma mark - Script evaluation - (nonnull EXJavaScriptValue *)evaluateScript:(nonnull NSString *)scriptSource { std::shared_ptr scriptBuffer = std::make_shared([scriptSource UTF8String]); std::shared_ptr result; try { result = std::make_shared(_runtime->evaluateJavaScript(scriptBuffer, "<>")); } catch (jsi::JSError &error) { NSString *reason = [NSString stringWithUTF8String:error.getMessage().c_str()]; NSString *stack = [NSString stringWithUTF8String:error.getStack().c_str()]; @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{ @"message": reason, @"stack": stack, }]; } catch (jsi::JSIException &error) { NSString *reason = [NSString stringWithUTF8String:error.what()]; @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{ @"message": reason }]; } return [[EXJavaScriptValue alloc] initWithRuntime:self value:result]; } #pragma mark - Private - (void)initializeMainObject { if (!_mainObject) { // Add the main object to the runtime (`global.expo`). _mainObject = [self createObject]; [[self global] defineProperty:mainObjectPropertyName value:_mainObject options:EXJavaScriptObjectPropertyDescriptorEnumerable]; } } - (nonnull EXJavaScriptObject *)createHostFunction:(nonnull NSString *)name argsCount:(NSInteger)argsCount block:(nonnull JSHostFunctionBlock)block { jsi::PropNameID propNameId = jsi::PropNameID::forAscii(*_runtime, [name UTF8String], [name length]); std::weak_ptr weakCallInvoker = _jsCallInvoker; jsi::HostFunctionType function = [weakCallInvoker, block, self](jsi::Runtime &runtime, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value { // Theoretically should check here whether the call invoker isn't null, but in mocked environment // there is no need to care about that for synchronous calls, so it's ensured in `createAsyncFunction` instead. auto callInvoker = weakCallInvoker.lock(); NSArray *arguments = expo::convertJSIValuesToNSArray(self, args, count); std::shared_ptr thisValPtr = std::make_shared(runtime, std::move(thisVal)); EXJavaScriptValue *thisValue = [[EXJavaScriptValue alloc] initWithRuntime:self value:thisValPtr]; return block(runtime, callInvoker, thisValue, arguments); }; std::shared_ptr fnPtr = std::make_shared(jsi::Function::createFromHostFunction(*_runtime, propNameId, (unsigned int)argsCount, function)); return [[EXJavaScriptObject alloc] initWith:fnPtr runtime:self]; } @end