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