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