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, classRef]: classes) {
141     auto classObject = jni::static_ref_cast<JavaScriptModuleObject::javaobject>(
142       jni::make_local(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, 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 = thisValue.asObject(runtime);
173         decorateObjectWithProperties(runtime, jsiInteropModuleRegistry, &thisObject, classObject);
174         // TODO(@lukmccall): call the constructor from the class definition
175         return jsi::Value::undefined();
176       });
177 
178     auto descriptor = JavaScriptObject::preparePropertyDescriptor(runtime, 0);
179     descriptor.setProperty(runtime, "value", jsi::Value(runtime, nativeConstructor));
180 
181     JavaScriptObject::defineProperty(runtime, &prototype, nativeConstructorKey,
182                                      std::move(descriptor));
183 
184     moduleObject->setProperty(
185       runtime,
186       jsi::String::createFromUtf8(runtime, name),
187       jsi::Value(runtime, klass.asFunction(runtime))
188     );
189 
190     decorateObjectWithFunctions(
191       runtime,
192       jsiInteropModuleRegistry,
193       &prototype,
194       classObject
195     );
196   }
197 
198   jsiObject = moduleObject;
199   return moduleObject;
200 }
201 
202 void JavaScriptModuleObject::exportConstants(
203   jni::alias_ref<react::NativeMap::javaobject> constants
204 ) {
205   auto dynamic = constants->cthis()->consume();
206   assert(dynamic.isObject());
207 
208   for (const auto &[key, value]: dynamic.items()) {
209     this->constants[key.asString()] = value;
210   }
211 }
212 
213 void JavaScriptModuleObject::registerSyncFunction(
214   jni::alias_ref<jstring> name,
215   jboolean takesOwner,
216   jint args,
217   jni::alias_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
218   jni::alias_ref<JNIFunctionBody::javaobject> body
219 ) {
220   std::string cName = name->toStdString();
221 
222   methodsMetadata.try_emplace(
223     cName,
224     longLivedObjectCollection_,
225     cName,
226     takesOwner,
227     args,
228     false,
229     jni::make_local(expectedArgTypes),
230     jni::make_global(body)
231   );
232 }
233 
234 void JavaScriptModuleObject::registerAsyncFunction(
235   jni::alias_ref<jstring> name,
236   jboolean takesOwner,
237   jint args,
238   jni::alias_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
239   jni::alias_ref<JNIAsyncFunctionBody::javaobject> body
240 ) {
241   std::string cName = name->toStdString();
242 
243   methodsMetadata.try_emplace(
244     cName,
245     longLivedObjectCollection_,
246     cName,
247     takesOwner,
248     args,
249     true,
250     jni::make_local(expectedArgTypes),
251     jni::make_global(body)
252   );
253 }
254 
255 void JavaScriptModuleObject::registerClass(
256   jni::alias_ref<jstring> name,
257   jni::alias_ref<JavaScriptModuleObject::javaobject> classObject
258 ) {
259   std::string cName = name->toStdString();
260   classes[cName] = jni::make_global(classObject);
261 }
262 
263 void JavaScriptModuleObject::registerViewPrototype(
264   jni::alias_ref<JavaScriptModuleObject::javaobject> viewPrototype
265 ) {
266   this->viewPrototype = jni::make_global(viewPrototype);
267 }
268 
269 void JavaScriptModuleObject::registerProperty(
270   jni::alias_ref<jstring> name,
271   jni::alias_ref<ExpectedType> expectedArgType,
272   jni::alias_ref<JNIFunctionBody::javaobject> getter,
273   jni::alias_ref<JNIFunctionBody::javaobject> setter
274 ) {
275   auto cName = name->toStdString();
276 
277   auto getterMetadata = MethodMetadata(
278     longLivedObjectCollection_,
279     cName,
280     false,
281     0,
282     false,
283     std::vector<std::unique_ptr<AnyType>>(),
284     jni::make_global(getter)
285   );
286 
287   auto types = std::vector<std::unique_ptr<AnyType>>();
288   types.push_back(std::make_unique<AnyType>(jni::make_local(expectedArgType)));
289   auto setterMetadata = MethodMetadata(
290     longLivedObjectCollection_,
291     cName,
292     false,
293     1,
294     false,
295     std::move(types),
296     jni::make_global(setter)
297   );
298 
299   auto functions = std::make_pair(
300     std::move(getterMetadata),
301     std::move(setterMetadata)
302   );
303 
304   properties.insert({cName, std::move(functions)});
305 }
306 
307 JavaScriptModuleObject::JavaScriptModuleObject(jni::alias_ref<jhybridobject> jThis)
308   : javaPart_(jni::make_global(jThis)) {
309   longLivedObjectCollection_ = std::make_shared<react::LongLivedObjectCollection>();
310 }
311 } // namespace expo
312