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      return expo::convertObjCObjectToJSIValue(runtime, block(thisValue, arguments));
95    };
96  return [self createHostFunction:name argsCount:argsCount block:hostFunctionBlock];
97}
98
99- (nonnull EXJavaScriptObject *)createAsyncFunction:(nonnull NSString *)name
100                                          argsCount:(NSInteger)argsCount
101                                              block:(nonnull JSAsyncFunctionBlock)block
102{
103  JSHostFunctionBlock hostFunctionBlock = ^jsi::Value(
104    jsi::Runtime &runtime,
105    std::shared_ptr<react::CallInvoker> callInvoker,
106    EXJavaScriptValue * _Nonnull thisValue,
107    NSArray<EXJavaScriptValue *> * _Nonnull arguments) {
108      if (!callInvoker) {
109        // In mocked environment the call invoker may be null so it's not supported to call async functions.
110        // Testing async functions is a bit more complicated anyway. See `init` description for more.
111        throw jsi::JSError(runtime, "Calling async functions is not supported when the call invoker is unavailable");
112      }
113      // The function that is invoked as a setup of the EXJavaScript `Promise`.
114      auto promiseSetup = [callInvoker, block, thisValue, arguments](jsi::Runtime &runtime, std::shared_ptr<Promise> promise) {
115        expo::callPromiseSetupWithBlock(runtime, callInvoker, promise, ^(RCTPromiseResolveBlock resolver, RCTPromiseRejectBlock rejecter) {
116          block(thisValue, arguments, resolver, rejecter);
117        });
118      };
119      return createPromiseAsJSIValue(runtime, promiseSetup);
120    };
121  return [self createHostFunction:name argsCount:argsCount block:hostFunctionBlock];
122}
123
124#pragma mark - Classes
125
126- (nonnull EXJavaScriptObject *)createClass:(nonnull NSString *)name
127                                constructor:(nonnull ClassConstructorBlock)constructor
128{
129  expo::ClassConstructor jsConstructor = [self, constructor](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) {
130    std::shared_ptr<jsi::Object> thisPtr = std::make_shared<jsi::Object>(thisValue.asObject(runtime));
131    EXJavaScriptObject *caller = [[EXJavaScriptObject alloc] initWith:thisPtr runtime:self];
132    NSArray<EXJavaScriptValue *> *arguments = expo::convertJSIValuesToNSArray(self, args, count);
133
134    constructor(caller, arguments);
135  };
136  std::shared_ptr<jsi::Function> klass = expo::createClass(*_runtime, [name UTF8String], jsConstructor);
137  return [[EXJavaScriptObject alloc] initWith:klass runtime:self];
138}
139
140#pragma mark - Script evaluation
141
142- (nonnull EXJavaScriptValue *)evaluateScript:(nonnull NSString *)scriptSource
143{
144  std::shared_ptr<jsi::StringBuffer> scriptBuffer = std::make_shared<jsi::StringBuffer>([scriptSource UTF8String]);
145  std::shared_ptr<jsi::Value> result;
146
147  try {
148    result = std::make_shared<jsi::Value>(_runtime->evaluateJavaScript(scriptBuffer, "<<evaluated>>"));
149  } catch (jsi::JSError &error) {
150    NSString *reason = [NSString stringWithUTF8String:error.getMessage().c_str()];
151    NSString *stack = [NSString stringWithUTF8String:error.getStack().c_str()];
152
153    @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{
154      @"message": reason,
155      @"stack": stack,
156    }];
157  } catch (jsi::JSIException &error) {
158    NSString *reason = [NSString stringWithUTF8String:error.what()];
159
160    @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{
161      @"message": reason
162    }];
163  }
164  return [[EXJavaScriptValue alloc] initWithRuntime:self value:result];
165}
166
167#pragma mark - Private
168
169- (nonnull EXJavaScriptObject *)createHostFunction:(nonnull NSString *)name
170                                         argsCount:(NSInteger)argsCount
171                                             block:(nonnull JSHostFunctionBlock)block
172{
173  jsi::PropNameID propNameId = jsi::PropNameID::forAscii(*_runtime, [name UTF8String], [name length]);
174  std::weak_ptr<react::CallInvoker> weakCallInvoker = _jsCallInvoker;
175  jsi::HostFunctionType function = [weakCallInvoker, block, self](jsi::Runtime &runtime, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value {
176    // Theoretically should check here whether the call invoker isn't null, but in mocked environment
177    // there is no need to care about that for synchronous calls, so it's ensured in `createAsyncFunction` instead.
178    auto callInvoker = weakCallInvoker.lock();
179    NSArray<EXJavaScriptValue *> *arguments = expo::convertJSIValuesToNSArray(self, args, count);
180    std::shared_ptr<jsi::Value> thisValPtr = std::make_shared<jsi::Value>(runtime, std::move(thisVal));
181    EXJavaScriptValue *thisValue = [[EXJavaScriptValue alloc] initWithRuntime:self value:thisValPtr];
182
183    return block(runtime, callInvoker, thisValue, arguments);
184  };
185  std::shared_ptr<jsi::Object> fnPtr = std::make_shared<jsi::Object>(jsi::Function::createFromHostFunction(*_runtime, propNameId, (unsigned int)argsCount, function));
186  return [[EXJavaScriptObject alloc] initWith:fnPtr runtime:self];
187}
188
189@end
190