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