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