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- (nonnull instancetype)init
27{
28  if (self = [super init]) {
29#if __has_include(<reacthermes/HermesExecutorFactory.h>) || __has_include(<hermes/hermes.h>)
30    _runtime = hermes::makeHermesRuntime();
31#else
32    _runtime = jsc::makeJSCRuntime();
33#endif
34    _jsCallInvoker = nil;
35  }
36  return self;
37}
38
39- (nonnull instancetype)initWithRuntime:(nonnull jsi::Runtime *)runtime
40                            callInvoker:(std::shared_ptr<react::CallInvoker>)callInvoker
41{
42  if (self = [super init]) {
43    // Creating a shared pointer that points to the runtime but doesn't own it, thus doesn't release it.
44    // In this code flow, the runtime should be owned by something else like the RCTBridge.
45    // See explanation for constructor (8): https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr
46    _runtime = std::shared_ptr<jsi::Runtime>(std::shared_ptr<jsi::Runtime>(), runtime);
47    _jsCallInvoker = callInvoker;
48  }
49  return self;
50}
51
52- (nonnull jsi::Runtime *)get
53{
54  return _runtime.get();
55}
56
57- (std::shared_ptr<react::CallInvoker>)callInvoker
58{
59  return _jsCallInvoker;
60}
61
62- (nonnull EXJavaScriptObject *)createObject
63{
64  auto jsObjectPtr = std::make_shared<jsi::Object>(*_runtime);
65  return [[EXJavaScriptObject alloc] initWith:jsObjectPtr runtime:self];
66}
67
68- (nonnull EXJavaScriptObject *)createHostObject:(std::shared_ptr<jsi::HostObject>)jsiHostObjectPtr
69{
70  auto jsObjectPtr = std::make_shared<jsi::Object>(jsi::Object::createFromHostObject(*_runtime, jsiHostObjectPtr));
71  return [[EXJavaScriptObject alloc] initWith:jsObjectPtr runtime:self];
72}
73
74- (nonnull EXJavaScriptObject *)global
75{
76  auto jsGlobalPtr = std::make_shared<jsi::Object>(_runtime->global());
77  return [[EXJavaScriptObject alloc] initWith:jsGlobalPtr runtime:self];
78}
79
80- (jsi::Function)createSyncFunction:(nonnull NSString *)name
81                          argsCount:(NSInteger)argsCount
82                              block:(nonnull JSSyncFunctionBlock)block
83{
84  return [self createHostFunction:name argsCount:argsCount block:^jsi::Value(jsi::Runtime &runtime, std::shared_ptr<react::CallInvoker> callInvoker, NSArray * _Nonnull arguments) {
85    return expo::convertObjCObjectToJSIValue(runtime, block(arguments));
86  }];
87}
88
89- (jsi::Function)createAsyncFunction:(nonnull NSString *)name
90                           argsCount:(NSInteger)argsCount
91                               block:(nonnull JSAsyncFunctionBlock)block
92{
93  return [self createHostFunction:name argsCount:argsCount block:^jsi::Value(jsi::Runtime &runtime, std::shared_ptr<react::CallInvoker> callInvoker, NSArray *arguments) {
94    // The function that is invoked as a setup of the EXJavaScript `Promise`.
95    auto promiseSetup = [callInvoker, block, arguments](jsi::Runtime &runtime, std::shared_ptr<Promise> promise) {
96      expo::callPromiseSetupWithBlock(runtime, callInvoker, promise, ^(RCTPromiseResolveBlock resolver, RCTPromiseRejectBlock rejecter) {
97        block(arguments, resolver, rejecter);
98      });
99    };
100    return createPromiseAsJSIValue(runtime, promiseSetup);
101  }];
102}
103
104#pragma mark - Script evaluation
105
106- (nonnull EXJavaScriptValue *)evaluateScript:(nonnull NSString *)scriptSource
107{
108  std::shared_ptr<jsi::StringBuffer> scriptBuffer = std::make_shared<jsi::StringBuffer>([[NSString stringWithFormat:@"(%@)", scriptSource] UTF8String]);
109  std::shared_ptr<jsi::Value> result;
110
111  try {
112    result = std::make_shared<jsi::Value>(_runtime->evaluateJavaScript(scriptBuffer, "<<evaluated>>"));
113  } catch (jsi::JSError &error) {
114    NSString *reason = [NSString stringWithUTF8String:error.getMessage().c_str()];
115    NSString *stack = [NSString stringWithUTF8String:error.getStack().c_str()];
116
117    @throw [NSException exceptionWithName:@"ScriptEvaluationException" reason:reason userInfo:@{
118      @"message": reason,
119      @"stack": stack,
120    }];
121  }
122  return [[EXJavaScriptValue alloc] initWithRuntime:self value:result];
123}
124
125#pragma mark - Private
126
127typedef jsi::Value (^JSHostFunctionBlock)(jsi::Runtime &runtime, std::shared_ptr<react::CallInvoker> callInvoker, NSArray * _Nonnull arguments);
128
129- (jsi::Function)createHostFunction:(nonnull NSString *)name
130                          argsCount:(NSInteger)argsCount
131                              block:(nonnull JSHostFunctionBlock)block
132{
133  jsi::PropNameID propNameId = jsi::PropNameID::forAscii(*_runtime, [name UTF8String], [name length]);
134  std::weak_ptr<react::CallInvoker> weakCallInvoker = _jsCallInvoker;
135  jsi::HostFunctionType function = [weakCallInvoker, block](jsi::Runtime &runtime, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value {
136    if (auto callInvoker = weakCallInvoker.lock()) {
137      NSArray *arguments = expo::convertJSIValuesToNSArray(runtime, args, count, callInvoker);
138      return block(runtime, callInvoker, arguments);
139    }
140    // TODO: We should throw some kind of error.
141    return jsi::Value::undefined();
142  };
143  return jsi::Function::createFromHostFunction(*_runtime, propNameId, (unsigned int)argsCount, function);
144}
145
146@end
147