1 // Copyright © 2021-present 650 Industries, Inc. (aka Expo)
2 
3 #include "JavaScriptModuleObject.h"
4 #include "JSIInteropModuleRegistry.h"
5 #include "ObjectDeallocator.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     JavaScriptObject::defineProperty(runtime, jsObject, name, 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::newObjectCxxArgs(
203               jsiInteropModuleRegistry->runtimeHolder,
204               thisObject
205             );
206             jsiInteropModuleRegistry->registerSharedObject(result, jsThisObject);
207           }
208         } catch (jni::JniException &jniException) {
209           rethrowAsCodedError(runtime, jniException);
210         }
211         return jsi::Value::undefined();
212       });
213 
214     auto descriptor = JavaScriptObject::preparePropertyDescriptor(runtime, 0);
215     descriptor.setProperty(runtime, "value", jsi::Value(runtime, nativeConstructor));
216 
217     JavaScriptObject::defineProperty(runtime, &prototype, nativeConstructorKey,
218                                      std::move(descriptor));
219 
220     moduleObject->setProperty(
221       runtime,
222       jsi::String::createFromUtf8(runtime, name),
223       jsi::Value(runtime, klass.asFunction(runtime))
224     );
225 
226     decorateObjectWithFunctions(
227       runtime,
228       jsiInteropModuleRegistry,
229       &prototype,
230       classObject
231     );
232   }
233 
234   jsiObject = moduleObject;
235   return moduleObject;
236 }
237 
238 void JavaScriptModuleObject::exportConstants(
239   jni::alias_ref<react::NativeMap::javaobject> constants
240 ) {
241   auto dynamic = constants->cthis()->consume();
242   assert(dynamic.isObject());
243 
244   for (const auto &[key, value]: dynamic.items()) {
245     this->constants[key.asString()] = value;
246   }
247 }
248 
249 void JavaScriptModuleObject::registerSyncFunction(
250   jni::alias_ref<jstring> name,
251   jboolean takesOwner,
252   jint args,
253   jni::alias_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
254   jni::alias_ref<JNIFunctionBody::javaobject> body
255 ) {
256   std::string cName = name->toStdString();
257 
258   methodsMetadata.try_emplace(
259     cName,
260     cName,
261     takesOwner,
262     args,
263     false,
264     jni::make_local(expectedArgTypes),
265     jni::make_global(body)
266   );
267 }
268 
269 void JavaScriptModuleObject::registerAsyncFunction(
270   jni::alias_ref<jstring> name,
271   jboolean takesOwner,
272   jint args,
273   jni::alias_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
274   jni::alias_ref<JNIAsyncFunctionBody::javaobject> body
275 ) {
276   std::string cName = name->toStdString();
277 
278   methodsMetadata.try_emplace(
279     cName,
280     cName,
281     takesOwner,
282     args,
283     true,
284     jni::make_local(expectedArgTypes),
285     jni::make_global(body)
286   );
287 }
288 
289 void JavaScriptModuleObject::registerClass(
290   jni::alias_ref<jstring> name,
291   jni::alias_ref<JavaScriptModuleObject::javaobject> classObject,
292   jboolean takesOwner,
293   jint args,
294   jni::alias_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
295   jni::alias_ref<JNIFunctionBody::javaobject> body
296 ) {
297   std::string cName = name->toStdString();
298   MethodMetadata constructor(
299     "constructor",
300     takesOwner,
301     args,
302     false,
303     jni::make_local(expectedArgTypes),
304     jni::make_global(body)
305   );
306 
307   auto pair = std::make_pair(jni::make_global(classObject), std::move(constructor));
308 
309   classes.try_emplace(
310     cName,
311     std::move(pair)
312   );
313 }
314 
315 void JavaScriptModuleObject::registerViewPrototype(
316   jni::alias_ref<JavaScriptModuleObject::javaobject> viewPrototype
317 ) {
318   this->viewPrototype = jni::make_global(viewPrototype);
319 }
320 
321 void JavaScriptModuleObject::registerProperty(
322   jni::alias_ref<jstring> name,
323   jni::alias_ref<ExpectedType> expectedArgType,
324   jni::alias_ref<JNIFunctionBody::javaobject> getter,
325   jni::alias_ref<JNIFunctionBody::javaobject> setter
326 ) {
327   auto cName = name->toStdString();
328 
329   auto getterMetadata = MethodMetadata(
330     cName,
331     false,
332     0,
333     false,
334     std::vector<std::unique_ptr<AnyType>>(),
335     jni::make_global(getter)
336   );
337 
338   auto types = std::vector<std::unique_ptr<AnyType>>();
339   types.push_back(std::make_unique<AnyType>(jni::make_local(expectedArgType)));
340   auto setterMetadata = MethodMetadata(
341     cName,
342     false,
343     1,
344     false,
345     std::move(types),
346     jni::make_global(setter)
347   );
348 
349   auto functions = std::make_pair(
350     std::move(getterMetadata),
351     std::move(setterMetadata)
352   );
353 
354   properties.insert({cName, std::move(functions)});
355 }
356 
357 JavaScriptModuleObject::JavaScriptModuleObject(jni::alias_ref<jhybridobject> jThis)
358   : javaPart_(jni::make_global(jThis)) {
359 }
360 } // namespace expo
361