1370fa39dSKudo Chien// Copyright 2018-present 650 Industries. All rights reserved.
2370fa39dSKudo Chien
3370fa39dSKudo Chien#import <jsi/jsi.h>
4370fa39dSKudo Chien
54bcb0d98STomasz Sapeta#if __has_include(<reacthermes/HermesExecutorFactory.h>)
64bcb0d98STomasz Sapeta#import <reacthermes/HermesExecutorFactory.h>
7d4cd4dfcSKudo Chien#elif __has_include(<React-jsc/JSCRuntime.h>)
8d4cd4dfcSKudo Chien// react-native@>=0.71 has a specific React-jsc pod
9d4cd4dfcSKudo Chien#import <React-jsc/JSCRuntime.h>
1032c7a27aSGabriel Donadel Dall'Agnol// use_frameworks! transforms the dash to underscore
1132c7a27aSGabriel Donadel Dall'Agnol#elif __has_include(<React_jsc/JSCRuntime.h>)
1232c7a27aSGabriel Donadel Dall'Agnol#import <React_jsc/JSCRuntime.h>
134bcb0d98STomasz Sapeta#else
144bcb0d98STomasz Sapeta#import <jsi/JSCRuntime.h>
154bcb0d98STomasz Sapeta#endif
16370fa39dSKudo Chien
174bcb0d98STomasz Sapeta#import <ExpoModulesCore/EXJavaScriptRuntime.h>
18370fa39dSKudo Chien#import <ExpoModulesCore/ExpoModulesHostObject.h>
19bd0cffe7STomasz Sapeta#import <ExpoModulesCore/EXJSIUtils.h>
20370fa39dSKudo Chien#import <ExpoModulesCore/EXJSIConversions.h>
21370fa39dSKudo Chien#import <ExpoModulesCore/Swift.h>
22370fa39dSKudo Chien
23370fa39dSKudo Chien@implementation EXJavaScriptRuntime {
244bcb0d98STomasz Sapeta  std::shared_ptr<jsi::Runtime> _runtime;
25370fa39dSKudo Chien  std::shared_ptr<react::CallInvoker> _jsCallInvoker;
26370fa39dSKudo Chien}
27370fa39dSKudo Chien
28f57544baSTomasz Sapeta/**
29f57544baSTomasz Sapeta Initializes a runtime that is independent from React Native and its runtime initialization.
30f57544baSTomasz Sapeta This flow is mostly intended for tests. The JS call invoker is unavailable thus calling async functions is not supported.
31f57544baSTomasz Sapeta TODO: Implement the call invoker when it becomes necessary.
32f57544baSTomasz Sapeta */
334bcb0d98STomasz Sapeta- (nonnull instancetype)init
34370fa39dSKudo Chien{
35370fa39dSKudo Chien  if (self = [super init]) {
36e68edf7aSKudo Chien#if __has_include(<reacthermes/HermesExecutorFactory.h>)
37e68edf7aSKudo Chien    _runtime = facebook::hermes::makeHermesRuntime();
384bcb0d98STomasz Sapeta#else
394bcb0d98STomasz Sapeta    _runtime = jsc::makeJSCRuntime();
404bcb0d98STomasz Sapeta#endif
414bcb0d98STomasz Sapeta    _jsCallInvoker = nil;
424bcb0d98STomasz Sapeta  }
434bcb0d98STomasz Sapeta  return self;
444bcb0d98STomasz Sapeta}
45370fa39dSKudo Chien
46ec62a260STomasz Sapeta- (nonnull instancetype)initWithRuntime:(nonnull jsi::Runtime *)runtime
47ec62a260STomasz Sapeta                            callInvoker:(std::shared_ptr<react::CallInvoker>)callInvoker
484bcb0d98STomasz Sapeta{
494bcb0d98STomasz Sapeta  if (self = [super init]) {
5060410a3bSTomasz Sapeta    // Creating a shared pointer that points to the runtime but doesn't own it, thus doesn't release it.
5160410a3bSTomasz Sapeta    // In this code flow, the runtime should be owned by something else like the RCTBridge.
5260410a3bSTomasz Sapeta    // See explanation for constructor (8): https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr
5360410a3bSTomasz Sapeta    _runtime = std::shared_ptr<jsi::Runtime>(std::shared_ptr<jsi::Runtime>(), runtime);
544bcb0d98STomasz Sapeta    _jsCallInvoker = callInvoker;
55370fa39dSKudo Chien  }
56370fa39dSKudo Chien  return self;
57370fa39dSKudo Chien}
58370fa39dSKudo Chien
59370fa39dSKudo Chien- (nonnull jsi::Runtime *)get
60370fa39dSKudo Chien{
614bcb0d98STomasz Sapeta  return _runtime.get();
62370fa39dSKudo Chien}
63370fa39dSKudo Chien
64370fa39dSKudo Chien- (std::shared_ptr<react::CallInvoker>)callInvoker
65370fa39dSKudo Chien{
66370fa39dSKudo Chien  return _jsCallInvoker;
67370fa39dSKudo Chien}
68370fa39dSKudo Chien
69370fa39dSKudo Chien- (nonnull EXJavaScriptObject *)createObject
70370fa39dSKudo Chien{
71370fa39dSKudo Chien  auto jsObjectPtr = std::make_shared<jsi::Object>(*_runtime);
72370fa39dSKudo Chien  return [[EXJavaScriptObject alloc] initWith:jsObjectPtr runtime:self];
73370fa39dSKudo Chien}
74370fa39dSKudo Chien
75370fa39dSKudo Chien- (nonnull EXJavaScriptObject *)createHostObject:(std::shared_ptr<jsi::HostObject>)jsiHostObjectPtr
76370fa39dSKudo Chien{
77370fa39dSKudo Chien  auto jsObjectPtr = std::make_shared<jsi::Object>(jsi::Object::createFromHostObject(*_runtime, jsiHostObjectPtr));
78370fa39dSKudo Chien  return [[EXJavaScriptObject alloc] initWith:jsObjectPtr runtime:self];
79370fa39dSKudo Chien}
80370fa39dSKudo Chien
81370fa39dSKudo Chien- (nonnull EXJavaScriptObject *)global
82370fa39dSKudo Chien{
834bcb0d98STomasz Sapeta  auto jsGlobalPtr = std::make_shared<jsi::Object>(_runtime->global());
844bcb0d98STomasz Sapeta  return [[EXJavaScriptObject alloc] initWith:jsGlobalPtr runtime:self];
85370fa39dSKudo Chien}
86370fa39dSKudo Chien
87447e3428STomasz Sapeta- (nonnull EXJavaScriptObject *)createSyncFunction:(nonnull NSString *)name
88370fa39dSKudo Chien                                         argsCount:(NSInteger)argsCount
89370fa39dSKudo Chien                                             block:(nonnull JSSyncFunctionBlock)block
90370fa39dSKudo Chien{
919b8bcdc4STomasz Sapeta  JSHostFunctionBlock hostFunctionBlock = ^jsi::Value(
929b8bcdc4STomasz Sapeta    jsi::Runtime &runtime,
939b8bcdc4STomasz Sapeta    std::shared_ptr<react::CallInvoker> callInvoker,
949b8bcdc4STomasz Sapeta    EXJavaScriptValue * _Nonnull thisValue,
959b8bcdc4STomasz Sapeta    NSArray<EXJavaScriptValue *> * _Nonnull arguments) {
964d7ea828STomasz Sapeta      NSError *error;
974d7ea828STomasz Sapeta      id result = block(thisValue, arguments, &error);
984d7ea828STomasz Sapeta
994d7ea828STomasz Sapeta      if (error == nil) {
1004d7ea828STomasz Sapeta        return expo::convertObjCObjectToJSIValue(runtime, result);
1014d7ea828STomasz Sapeta      } else {
1024d7ea828STomasz Sapeta        throw jsi::JSError(runtime, [error.userInfo[@"message"] UTF8String]);
1034d7ea828STomasz Sapeta      }
1049b8bcdc4STomasz Sapeta    };
1059b8bcdc4STomasz Sapeta  return [self createHostFunction:name argsCount:argsCount block:hostFunctionBlock];
106370fa39dSKudo Chien}
107370fa39dSKudo Chien
108447e3428STomasz Sapeta- (nonnull EXJavaScriptObject *)createAsyncFunction:(nonnull NSString *)name
109370fa39dSKudo Chien                                          argsCount:(NSInteger)argsCount
110370fa39dSKudo Chien                                              block:(nonnull JSAsyncFunctionBlock)block
111370fa39dSKudo Chien{
1129b8bcdc4STomasz Sapeta  JSHostFunctionBlock hostFunctionBlock = ^jsi::Value(
1139b8bcdc4STomasz Sapeta    jsi::Runtime &runtime,
1149b8bcdc4STomasz Sapeta    std::shared_ptr<react::CallInvoker> callInvoker,
1159b8bcdc4STomasz Sapeta    EXJavaScriptValue * _Nonnull thisValue,
1169b8bcdc4STomasz Sapeta    NSArray<EXJavaScriptValue *> * _Nonnull arguments) {
117f57544baSTomasz Sapeta      if (!callInvoker) {
118f57544baSTomasz Sapeta        // In mocked environment the call invoker may be null so it's not supported to call async functions.
119f57544baSTomasz Sapeta        // Testing async functions is a bit more complicated anyway. See `init` description for more.
120f57544baSTomasz Sapeta        throw jsi::JSError(runtime, "Calling async functions is not supported when the call invoker is unavailable");
121f57544baSTomasz Sapeta      }
122370fa39dSKudo Chien      // The function that is invoked as a setup of the EXJavaScript `Promise`.
1239b8bcdc4STomasz Sapeta      auto promiseSetup = [callInvoker, block, thisValue, arguments](jsi::Runtime &runtime, std::shared_ptr<Promise> promise) {
124370fa39dSKudo Chien        expo::callPromiseSetupWithBlock(runtime, callInvoker, promise, ^(RCTPromiseResolveBlock resolver, RCTPromiseRejectBlock rejecter) {
1259b8bcdc4STomasz Sapeta          block(thisValue, arguments, resolver, rejecter);
126370fa39dSKudo Chien        });
127370fa39dSKudo Chien      };
128370fa39dSKudo Chien      return createPromiseAsJSIValue(runtime, promiseSetup);
1299b8bcdc4STomasz Sapeta    };
1309b8bcdc4STomasz Sapeta  return [self createHostFunction:name argsCount:argsCount block:hostFunctionBlock];
131370fa39dSKudo Chien}
132370fa39dSKudo Chien
1337e0c1e78STomasz Sapeta#pragma mark - Classes
1347e0c1e78STomasz Sapeta
1357e0c1e78STomasz Sapeta- (nonnull EXJavaScriptObject *)createClass:(nonnull NSString *)name
1367e0c1e78STomasz Sapeta                                constructor:(nonnull ClassConstructorBlock)constructor
1377e0c1e78STomasz Sapeta{
1389b8bcdc4STomasz Sapeta  expo::ClassConstructor jsConstructor = [self, constructor](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) {
1397e0c1e78STomasz Sapeta    std::shared_ptr<jsi::Object> thisPtr = std::make_shared<jsi::Object>(thisValue.asObject(runtime));
1407e0c1e78STomasz Sapeta    EXJavaScriptObject *caller = [[EXJavaScriptObject alloc] initWith:thisPtr runtime:self];
1419b8bcdc4STomasz Sapeta    NSArray<EXJavaScriptValue *> *arguments = expo::convertJSIValuesToNSArray(self, args, count);
1427e0c1e78STomasz Sapeta
1437e0c1e78STomasz Sapeta    constructor(caller, arguments);
1449b8bcdc4STomasz Sapeta  };
1459b8bcdc4STomasz Sapeta  std::shared_ptr<jsi::Function> klass = expo::createClass(*_runtime, [name UTF8String], jsConstructor);
1467e0c1e78STomasz Sapeta  return [[EXJavaScriptObject alloc] initWith:klass runtime:self];
1477e0c1e78STomasz Sapeta}
1487e0c1e78STomasz Sapeta
149*7a261681STomasz Sapeta- (nullable EXJavaScriptObject *)createObjectWithPrototype:(nonnull EXJavaScriptObject *)prototype
150*7a261681STomasz Sapeta{
151*7a261681STomasz Sapeta  std::shared_ptr<jsi::Object> object = expo::createObjectWithPrototype(*_runtime, [prototype getShared]);
152*7a261681STomasz Sapeta  return object ? [[EXJavaScriptObject alloc] initWith:object runtime:self] : nil;
153*7a261681STomasz Sapeta}
154*7a261681STomasz Sapeta
1554bcb0d98STomasz Sapeta#pragma mark - Script evaluation
1564bcb0d98STomasz Sapeta
157ec62a260STomasz Sapeta- (nonnull EXJavaScriptValue *)evaluateScript:(nonnull NSString *)scriptSource
1584bcb0d98STomasz Sapeta{
15956c10913STomasz Sapeta  std::shared_ptr<jsi::StringBuffer> scriptBuffer = std::make_shared<jsi::StringBuffer>([scriptSource UTF8String]);
160d84b7ae9STomasz Sapeta  std::shared_ptr<jsi::Value> result;
161d84b7ae9STomasz Sapeta
162d84b7ae9STomasz Sapeta  try {
163d84b7ae9STomasz Sapeta    result = std::make_shared<jsi::Value>(_runtime->evaluateJavaScript(scriptBuffer, "<<evaluated>>"));
164d84b7ae9STomasz Sapeta  } catch (jsi::JSError &error) {
165d84b7ae9STomasz Sapeta    NSString *reason = [NSString stringWithUTF8String:error.getMessage().c_str()];
166d84b7ae9STomasz Sapeta    NSString *stack = [NSString stringWithUTF8String:error.getStack().c_str()];
167d84b7ae9STomasz Sapeta
168d84b7ae9STomasz Sapeta    @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{
169d84b7ae9STomasz Sapeta      @"message": reason,
170d84b7ae9STomasz Sapeta      @"stack": stack,
171d84b7ae9STomasz Sapeta    }];
172159a70f7SŁukasz Kosmaty  } catch (jsi::JSIException &error) {
173159a70f7SŁukasz Kosmaty    NSString *reason = [NSString stringWithUTF8String:error.what()];
174159a70f7SŁukasz Kosmaty
175159a70f7SŁukasz Kosmaty    @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{
176159a70f7SŁukasz Kosmaty      @"message": reason
177159a70f7SŁukasz Kosmaty    }];
178d84b7ae9STomasz Sapeta  }
179ec62a260STomasz Sapeta  return [[EXJavaScriptValue alloc] initWithRuntime:self value:result];
1804bcb0d98STomasz Sapeta}
1814bcb0d98STomasz Sapeta
182370fa39dSKudo Chien#pragma mark - Private
183370fa39dSKudo Chien
184447e3428STomasz Sapeta- (nonnull EXJavaScriptObject *)createHostFunction:(nonnull NSString *)name
185370fa39dSKudo Chien                                         argsCount:(NSInteger)argsCount
186370fa39dSKudo Chien                                             block:(nonnull JSHostFunctionBlock)block
187370fa39dSKudo Chien{
188370fa39dSKudo Chien  jsi::PropNameID propNameId = jsi::PropNameID::forAscii(*_runtime, [name UTF8String], [name length]);
189370fa39dSKudo Chien  std::weak_ptr<react::CallInvoker> weakCallInvoker = _jsCallInvoker;
190c4fc6f47STomasz Sapeta  jsi::HostFunctionType function = [weakCallInvoker, block, self](jsi::Runtime &runtime, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value {
191f57544baSTomasz Sapeta    // Theoretically should check here whether the call invoker isn't null, but in mocked environment
192f57544baSTomasz Sapeta    // there is no need to care about that for synchronous calls, so it's ensured in `createAsyncFunction` instead.
193f57544baSTomasz Sapeta    auto callInvoker = weakCallInvoker.lock();
194c4fc6f47STomasz Sapeta    NSArray<EXJavaScriptValue *> *arguments = expo::convertJSIValuesToNSArray(self, args, count);
1959b8bcdc4STomasz Sapeta    std::shared_ptr<jsi::Value> thisValPtr = std::make_shared<jsi::Value>(runtime, std::move(thisVal));
1969b8bcdc4STomasz Sapeta    EXJavaScriptValue *thisValue = [[EXJavaScriptValue alloc] initWithRuntime:self value:thisValPtr];
1979b8bcdc4STomasz Sapeta
1989b8bcdc4STomasz Sapeta    return block(runtime, callInvoker, thisValue, arguments);
199370fa39dSKudo Chien  };
200447e3428STomasz Sapeta  std::shared_ptr<jsi::Object> fnPtr = std::make_shared<jsi::Object>(jsi::Function::createFromHostFunction(*_runtime, propNameId, (unsigned int)argsCount, function));
201447e3428STomasz Sapeta  return [[EXJavaScriptObject alloc] initWith:fnPtr runtime:self];
202370fa39dSKudo Chien}
203370fa39dSKudo Chien
204370fa39dSKudo Chien@end
205