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