1 // Copyright © 2021-present 650 Industries, Inc. (aka Expo)
2 
3 #include "JavaScriptModuleObject.h"
4 #include "JSIInteropModuleRegistry.h"
5 #include "JSIUtils.h"
6 
7 #include <folly/dynamic.h>
8 #include <jsi/JSIDynamic.h>
9 #include <react/jni/ReadableNativeArray.h>
10 #include <fbjni/detail/Hybrid.h>
11 #include <ReactCommon/TurboModuleUtils.h>
12 #include <jni/JCallback.h>
13 #include <jsi/JSIDynamic.h>
14 #include <fbjni/fbjni.h>
15 #include <jsi/jsi.h>
16 
17 #include <utility>
18 #include <tuple>
19 #include <algorithm>
20 #include <sstream>
21 
22 namespace jni = facebook::jni;
23 namespace jsi = facebook::jsi;
24 namespace react = facebook::react;
25 
26 namespace expo {
27 
28 void decorateObjectWithFunctions(
29   jsi::Runtime &runtime,
30   JSIInteropModuleRegistry *jsiInteropModuleRegistry,
31   jsi::Object *jsObject,
32   JavaScriptModuleObject *objectData) {
33   for (auto &[name, method]: objectData->methodsMetadata) {
34     jsObject->setProperty(
35       runtime,
36       jsi::String::createFromUtf8(runtime, name),
37       jsi::Value(runtime, *method.toJSFunction(runtime, jsiInteropModuleRegistry))
38     );
39   }
40 }
41 
42 void decorateObjectWithProperties(
43   jsi::Runtime &runtime,
44   JSIInteropModuleRegistry *jsiInteropModuleRegistry,
45   jsi::Object *jsObject,
46   JavaScriptModuleObject *objectData) {
47   for (auto &[name, property]: objectData->properties) {
48     auto &[getter, setter] = property;
49 
50     auto descriptor = JavaScriptObject::preparePropertyDescriptor(runtime,
51                                                                   1 << 1 /* enumerable */);
52     descriptor.setProperty(
53       runtime,
54       "get",
55       jsi::Value(runtime, *getter.toJSFunction(runtime,
56                                                jsiInteropModuleRegistry))
57     );
58     descriptor.setProperty(
59       runtime,
60       "set",
61       jsi::Value(runtime, *setter.toJSFunction(runtime,
62                                                jsiInteropModuleRegistry))
63     );
64     common::definePropertyOnJSIObject(runtime, jsObject, name.c_str(), std::move(descriptor));
65   }
66 }
67 
68 void decorateObjectWithConstants(
69   jsi::Runtime &runtime,
70   JSIInteropModuleRegistry *jsiInteropModuleRegistry,
71   jsi::Object *jsObject,
72   JavaScriptModuleObject *objectData) {
73   for (const auto &[name, value]: objectData->constants) {
74     jsObject->setProperty(
75       runtime,
76       jsi::String::createFromUtf8(runtime, name),
77       jsi::valueFromDynamic(runtime, value)
78     );
79   }
80 }
81 
82 jni::local_ref<jni::HybridClass<JavaScriptModuleObject>::jhybriddata>
83 JavaScriptModuleObject::initHybrid(jni::alias_ref<jhybridobject> jThis) {
84   return makeCxxInstance(jThis);
85 }
86 
87 void JavaScriptModuleObject::registerNatives() {
88   registerHybrid({
89                    makeNativeMethod("initHybrid", JavaScriptModuleObject::initHybrid),
90                    makeNativeMethod("exportConstants", JavaScriptModuleObject::exportConstants),
91                    makeNativeMethod("registerSyncFunction",
92                                     JavaScriptModuleObject::registerSyncFunction),
93                    makeNativeMethod("registerAsyncFunction",
94                                     JavaScriptModuleObject::registerAsyncFunction),
95                    makeNativeMethod("registerProperty",
96                                     JavaScriptModuleObject::registerProperty),
97                    makeNativeMethod("registerClass",
98                                     JavaScriptModuleObject::registerClass),
99                    makeNativeMethod("registerViewPrototype",
100                                     JavaScriptModuleObject::registerViewPrototype)
101                  });
102 }
103 
104 std::shared_ptr<jsi::Object> JavaScriptModuleObject::getJSIObject(jsi::Runtime &runtime) {
105   if (auto object = jsiObject.lock()) {
106     return object;
107   }
108 
109   auto moduleObject = std::make_shared<jsi::Object>(runtime);
110 
111   decorateObjectWithConstants(
112     runtime,
113     jsiInteropModuleRegistry,
114     moduleObject.get(),
115     this
116   );
117   decorateObjectWithProperties(
118     runtime,
119     jsiInteropModuleRegistry,
120     moduleObject.get(),
121     this
122   );
123   decorateObjectWithFunctions(
124     runtime,
125     jsiInteropModuleRegistry,
126     moduleObject.get(),
127     this
128   );
129 
130   if (viewPrototype) {
131     auto viewPrototypeObject = viewPrototype->cthis();
132     viewPrototypeObject->jsiInteropModuleRegistry = jsiInteropModuleRegistry;
133     auto viewPrototypeJSIObject = viewPrototypeObject->getJSIObject(runtime);
134     moduleObject->setProperty(
135       runtime,
136       "ViewPrototype",
137       jsi::Value(runtime, *viewPrototypeJSIObject)
138     );
139   }
140 
141   for (auto &[name, classInfo]: classes) {
142     auto &[classRef, constructor] = classInfo;
143     auto classObject = classRef->cthis();
144     classObject->jsiInteropModuleRegistry = jsiInteropModuleRegistry;
145 
146     std::string nativeConstructorKey("__native_constructor__");
147 
148     // Create a string buffer of the source code to evaluate.
149     std::stringstream source;
150     source << "(function " << name << "(...args) { this." << nativeConstructorKey
151            << "(...args); return this; })";
152     std::shared_ptr<jsi::StringBuffer> sourceBuffer = std::make_shared<jsi::StringBuffer>(
153       source.str());
154 
155     // Evaluate the code and obtain returned value (the constructor function).
156     jsi::Object klass = runtime.evaluateJavaScript(sourceBuffer, "").asObject(runtime);
157 
158     // Set the native constructor in the prototype.
159     jsi::Object prototype = klass.getPropertyAsObject(runtime, "prototype");
160     jsi::PropNameID nativeConstructorPropId = jsi::PropNameID::forAscii(runtime,
161                                                                         nativeConstructorKey);
162     jsi::Function nativeConstructor = jsi::Function::createFromHostFunction(
163       runtime,
164       nativeConstructorPropId,
165       // The paramCount is not obligatory to match, it only affects the `length` property of the function.
166       0,
167       [classObject, &constructor = constructor, jsiInteropModuleRegistry = jsiInteropModuleRegistry](
168         jsi::Runtime &runtime,
169         const jsi::Value &thisValue,
170         const jsi::Value *args,
171         size_t count
172       ) -> jsi::Value {
173         auto thisObject = std::make_shared<jsi::Object>(thisValue.asObject(runtime));
174         decorateObjectWithProperties(runtime, jsiInteropModuleRegistry, thisObject.get(),
175                                      classObject);
176         try {
177           JNIEnv *env = jni::Environment::current();
178           /**
179           * This will push a new JNI stack frame for the LocalReferences in this
180           * function call. When the stack frame for this lambda is popped,
181           * all LocalReferences are deleted.
182           */
183           jni::JniLocalScope scope(env, (int) count);
184           auto result = constructor.callJNISync(
185             env,
186             runtime,
187             jsiInteropModuleRegistry,
188             thisValue,
189             args,
190             count
191           );
192           if (result == nullptr) {
193             return jsi::Value::undefined();
194           }
195           jobject unpackedResult = result.get();
196           jclass resultClass = env->GetObjectClass(unpackedResult);
197           if (env->IsAssignableFrom(
198             resultClass,
199             JavaReferencesCache::instance()->getJClass(
200               "expo/modules/kotlin/sharedobjects/SharedObject").clazz
201           )) {
202             auto jsThisObject = JavaScriptObject::newInstance(
203               jsiInteropModuleRegistry,
204               jsiInteropModuleRegistry->runtimeHolder,
205               thisObject
206             );
207             jsiInteropModuleRegistry->registerSharedObject(result, jsThisObject);
208           }
209         } catch (jni::JniException &jniException) {
210           rethrowAsCodedError(runtime, jniException);
211         }
212         return jsi::Value::undefined();
213       });
214 
215     auto descriptor = JavaScriptObject::preparePropertyDescriptor(runtime, 0);
216     descriptor.setProperty(runtime, "value", jsi::Value(runtime, nativeConstructor));
217 
218     common::definePropertyOnJSIObject(runtime, &prototype, nativeConstructorKey.c_str(),
219                                       std::move(descriptor));
220 
221     moduleObject->setProperty(
222       runtime,
223       jsi::String::createFromUtf8(runtime, name),
224       jsi::Value(runtime, klass.asFunction(runtime))
225     );
226 
227     decorateObjectWithFunctions(
228       runtime,
229       jsiInteropModuleRegistry,
230       &prototype,
231       classObject
232     );
233   }
234 
235   jsiObject = moduleObject;
236   return moduleObject;
237 }
238 
239 void JavaScriptModuleObject::exportConstants(
240   jni::alias_ref<react::NativeMap::javaobject> constants
241 ) {
242   auto dynamic = constants->cthis()->consume();
243   assert(dynamic.isObject());
244 
245   for (const auto &[key, value]: dynamic.items()) {
246     this->constants[key.asString()] = value;
247   }
248 }
249 
250 void JavaScriptModuleObject::registerSyncFunction(
251   jni::alias_ref<jstring> name,
252   jboolean takesOwner,
253   jint args,
254   jni::alias_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
255   jni::alias_ref<JNIFunctionBody::javaobject> body
256 ) {
257   std::string cName = name->toStdString();
258 
259   methodsMetadata.try_emplace(
260     cName,
261     cName,
262     takesOwner,
263     args,
264     false,
265     jni::make_local(expectedArgTypes),
266     jni::make_global(body)
267   );
268 }
269 
270 void JavaScriptModuleObject::registerAsyncFunction(
271   jni::alias_ref<jstring> name,
272   jboolean takesOwner,
273   jint args,
274   jni::alias_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
275   jni::alias_ref<JNIAsyncFunctionBody::javaobject> body
276 ) {
277   std::string cName = name->toStdString();
278 
279   methodsMetadata.try_emplace(
280     cName,
281     cName,
282     takesOwner,
283     args,
284     true,
285     jni::make_local(expectedArgTypes),
286     jni::make_global(body)
287   );
288 }
289 
290 void JavaScriptModuleObject::registerClass(
291   jni::alias_ref<jstring> name,
292   jni::alias_ref<JavaScriptModuleObject::javaobject> classObject,
293   jboolean takesOwner,
294   jint args,
295   jni::alias_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
296   jni::alias_ref<JNIFunctionBody::javaobject> body
297 ) {
298   std::string cName = name->toStdString();
299   MethodMetadata constructor(
300     "constructor",
301     takesOwner,
302     args,
303     false,
304     jni::make_local(expectedArgTypes),
305     jni::make_global(body)
306   );
307 
308   auto pair = std::make_pair(jni::make_global(classObject), std::move(constructor));
309 
310   classes.try_emplace(
311     cName,
312     std::move(pair)
313   );
314 }
315 
316 void JavaScriptModuleObject::registerViewPrototype(
317   jni::alias_ref<JavaScriptModuleObject::javaobject> viewPrototype
318 ) {
319   this->viewPrototype = jni::make_global(viewPrototype);
320 }
321 
322 void JavaScriptModuleObject::registerProperty(
323   jni::alias_ref<jstring> name,
324   jboolean getterTakesOwner,
325   jni::alias_ref<jni::JArrayClass<ExpectedType>> getterExpectedArgsTypes,
326   jni::alias_ref<JNIFunctionBody::javaobject> getter,
327   jboolean setterTakesOwner,
328   jni::alias_ref<jni::JArrayClass<ExpectedType>> setterExpectedArgsTypes,
329   jni::alias_ref<JNIFunctionBody::javaobject> setter
330 ) {
331   auto cName = name->toStdString();
332 
333   auto getterMetadata = MethodMetadata(
334     cName,
335     getterTakesOwner,
336     getterExpectedArgsTypes->size(),
337     false,
338     jni::make_local(getterExpectedArgsTypes),
339     jni::make_global(getter)
340   );
341 
342   auto setterMetadata = MethodMetadata(
343     cName,
344     setterTakesOwner,
345     setterExpectedArgsTypes->size(),
346     false,
347     jni::make_local(setterExpectedArgsTypes),
348     jni::make_global(setter)
349   );
350 
351   auto functions = std::make_pair(
352     std::move(getterMetadata),
353     std::move(setterMetadata)
354   );
355 
356   properties.insert({cName, std::move(functions)});
357 }
358 
359 JavaScriptModuleObject::JavaScriptModuleObject(jni::alias_ref<jhybridobject> jThis)
360   : javaPart_(jni::make_global(jThis)) {
361 }
362 } // namespace expo
363