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