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#pragma mark - Script evaluation
164
165- (nonnull EXJavaScriptValue *)evaluateScript:(nonnull NSString *)scriptSource
166{
167  std::shared_ptr<jsi::StringBuffer> scriptBuffer = std::make_shared<jsi::StringBuffer>([scriptSource UTF8String]);
168  std::shared_ptr<jsi::Value> result;
169
170  try {
171    result = std::make_shared<jsi::Value>(_runtime->evaluateJavaScript(scriptBuffer, "<<evaluated>>"));
172  } catch (jsi::JSError &error) {
173    NSString *reason = [NSString stringWithUTF8String:error.getMessage().c_str()];
174    NSString *stack = [NSString stringWithUTF8String:error.getStack().c_str()];
175
176    @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{
177      @"message": reason,
178      @"stack": stack,
179    }];
180  } catch (jsi::JSIException &error) {
181    NSString *reason = [NSString stringWithUTF8String:error.what()];
182
183    @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{
184      @"message": reason
185    }];
186  }
187  return [[EXJavaScriptValue alloc] initWithRuntime:self value:result];
188}
189
190#pragma mark - Private
191
192- (void)initializeMainObject
193{
194  if (!_mainObject) {
195    // Add the main object to the runtime (`global.expo`).
196    _mainObject = [self createObject];
197    [[self global] defineProperty:mainObjectPropertyName value:_mainObject options:EXJavaScriptObjectPropertyDescriptorEnumerable];
198  }
199}
200
201- (nonnull EXJavaScriptObject *)createHostFunction:(nonnull NSString *)name
202                                         argsCount:(NSInteger)argsCount
203                                             block:(nonnull JSHostFunctionBlock)block
204{
205  jsi::PropNameID propNameId = jsi::PropNameID::forAscii(*_runtime, [name UTF8String], [name length]);
206  std::weak_ptr<react::CallInvoker> weakCallInvoker = _jsCallInvoker;
207  jsi::HostFunctionType function = [weakCallInvoker, block, self](jsi::Runtime &runtime, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value {
208    // Theoretically should check here whether the call invoker isn't null, but in mocked environment
209    // there is no need to care about that for synchronous calls, so it's ensured in `createAsyncFunction` instead.
210    auto callInvoker = weakCallInvoker.lock();
211    NSArray<EXJavaScriptValue *> *arguments = expo::convertJSIValuesToNSArray(self, args, count);
212    std::shared_ptr<jsi::Value> thisValPtr = std::make_shared<jsi::Value>(runtime, std::move(thisVal));
213    EXJavaScriptValue *thisValue = [[EXJavaScriptValue alloc] initWithRuntime:self value:thisValPtr];
214
215    return block(runtime, callInvoker, thisValue, arguments);
216  };
217  std::shared_ptr<jsi::Object> fnPtr = std::make_shared<jsi::Object>(jsi::Function::createFromHostFunction(*_runtime, propNameId, (unsigned int)argsCount, function));
218  return [[EXJavaScriptObject alloc] initWith:fnPtr runtime:self];
219}
220
221@end
222