// Copyright 2018-present 650 Industries. All rights reserved. #import #if __has_include() #import #elif __has_include() // react-native@>=0.71 has a specific React-jsc pod #import // use_frameworks! transforms the dash to underscore #elif __has_include() #import #else #import #endif #import #import #import #import #import @implementation EXJavaScriptRuntime { std::shared_ptr _runtime; std::shared_ptr _jsCallInvoker; } /** 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; } 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; } 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 *)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]; } - (nullable EXJavaScriptObject *)createObjectWithPrototype:(nonnull EXJavaScriptObject *)prototype { std::shared_ptr object = expo::createObjectWithPrototype(*_runtime, [prototype getShared]); return object ? [[EXJavaScriptObject alloc] initWith:object runtime:self] : nil; } #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 - (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