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