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