1 #include "MethodMetadata.h"
2 #include "JSIInteropModuleRegistry.h"
3 #include "JavaScriptValue.h"
4 #include "JavaScriptObject.h"
5 #include "CachedReferencesRegistry.h"
6 #include "Exceptions.h"
7 
8 #include <utility>
9 
10 #include "react/jni/ReadableNativeMap.h"
11 #include "react/jni/ReadableNativeArray.h"
12 
13 namespace jni = facebook::jni;
14 namespace jsi = facebook::jsi;
15 namespace react = facebook::react;
16 
17 namespace expo {
18 
19 // Modified version of the RN implementation
20 // https://github.com/facebook/react-native/blob/7dceb9b63c0bfd5b13bf6d26f9530729506e9097/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp#L57
21 jni::local_ref<react::JCxxCallbackImpl::JavaPart> createJavaCallbackFromJSIFunction(
22   jsi::Function &&function,
23   jsi::Runtime &rt,
24   std::shared_ptr<react::CallInvoker> jsInvoker
25 ) {
26   auto weakWrapper = react::CallbackWrapper::createWeak(std::move(function), rt,
27                                                         std::move(jsInvoker));
28 
29   // This needs to be a shared_ptr because:
30   // 1. It cannot be unique_ptr. std::function is copyable but unique_ptr is
31   // not.
32   // 2. It cannot be weak_ptr since we need this object to live on.
33   // 3. It cannot be a value, because that would be deleted as soon as this
34   // function returns.
35   auto callbackWrapperOwner =
36     std::make_shared<react::RAIICallbackWrapperDestroyer>(weakWrapper);
37 
38   std::function<void(folly::dynamic)> fn =
39     [weakWrapper, callbackWrapperOwner, wrapperWasCalled = false](
40       folly::dynamic responses) mutable {
41       if (wrapperWasCalled) {
42         throw std::runtime_error(
43           "callback 2 arg cannot be called more than once");
44       }
45 
46       auto strongWrapper = weakWrapper.lock();
47       if (!strongWrapper) {
48         return;
49       }
50 
51       strongWrapper->jsInvoker().invokeAsync(
52         [weakWrapper, callbackWrapperOwner, responses]() mutable {
53           auto strongWrapper2 = weakWrapper.lock();
54           if (!strongWrapper2) {
55             return;
56           }
57 
58           jsi::Value args =
59             jsi::valueFromDynamic(strongWrapper2->runtime(), responses);
60           auto argsArray = args.getObject(strongWrapper2->runtime())
61             .asArray(strongWrapper2->runtime());
62           jsi::Value arg = argsArray.getValueAtIndex(strongWrapper2->runtime(), 0);
63 
64           strongWrapper2->callback().call(
65             strongWrapper2->runtime(),
66             (const jsi::Value *) &arg,
67             (size_t) 1
68           );
69 
70           callbackWrapperOwner.reset();
71         });
72 
73       wrapperWasCalled = true;
74     };
75 
76   return react::JCxxCallbackImpl::newObjectCxxArgs(fn);
77 }
78 
79 std::vector<jvalue> MethodMetadata::convertJSIArgsToJNI(
80   JSIInteropModuleRegistry *moduleRegistry,
81   JNIEnv *env,
82   jsi::Runtime &rt,
83   const jsi::Value *args,
84   size_t count,
85   bool returnGlobalReferences
86 ) {
87   std::vector<jvalue> result(count);
88 
89   auto makeGlobalIfNecessary = [env, returnGlobalReferences](jobject obj) -> jobject {
90     if (returnGlobalReferences) {
91       return env->NewGlobalRef(obj);
92     }
93     return obj;
94   };
95 
96   for (unsigned int argIndex = 0; argIndex < count; argIndex++) {
97     const jsi::Value *arg = &args[argIndex];
98     jvalue *jarg = &result[argIndex];
99     int desiredType = desiredTypes[argIndex];
100 
101     if (desiredType & CppType::JS_VALUE) {
102       jarg->l = makeGlobalIfNecessary(
103         JavaScriptValue::newObjectCxxArgs(
104           moduleRegistry->runtimeHolder->weak_from_this(),
105           // TODO(@lukmccall): make sure that copy here is necessary
106           std::make_shared<jsi::Value>(jsi::Value(rt, *arg))
107         ).release()
108       );
109     } else if (desiredType & CppType::JS_OBJECT) {
110       jarg->l = makeGlobalIfNecessary(
111         JavaScriptObject::newObjectCxxArgs(
112           moduleRegistry->runtimeHolder->weak_from_this(),
113           std::make_shared<jsi::Object>(arg->getObject(rt))
114         ).release()
115       );
116     } else if (arg->isNull() || arg->isUndefined()) {
117       jarg->l = nullptr;
118     } else if (arg->isNumber()) {
119       auto &doubleClass = CachedReferencesRegistry::instance()
120         ->getJClass("java/lang/Double");
121       jmethodID doubleConstructor = doubleClass.getMethod("<init>", "(D)V");
122       jarg->l = makeGlobalIfNecessary(
123         env->NewObject(doubleClass.clazz, doubleConstructor, arg->getNumber()));
124     } else if (arg->isBool()) {
125       auto &booleanClass = CachedReferencesRegistry::instance()
126         ->getJClass("java/lang/Boolean");
127       jmethodID booleanConstructor = booleanClass.getMethod("<init>", "(Z)V");
128       jarg->l = makeGlobalIfNecessary(
129         env->NewObject(booleanClass.clazz, booleanConstructor, arg->getBool()));
130     } else if (arg->isString()) {
131       jarg->l = makeGlobalIfNecessary(env->NewStringUTF(arg->getString(rt).utf8(rt).c_str()));
132     } else if (arg->isObject()) {
133       const jsi::Object object = arg->getObject(rt);
134 
135       // TODO(@lukmccall): stop using dynamic
136       auto dynamic = jsi::dynamicFromValue(rt, *arg);
137       if (arg->getObject(rt).isArray(rt)) {
138         jarg->l = makeGlobalIfNecessary(
139           react::ReadableNativeArray::newObjectCxxArgs(std::move(dynamic)).release());
140       } else {
141         jarg->l = makeGlobalIfNecessary(
142           react::ReadableNativeMap::createWithContents(std::move(dynamic)).release());
143       }
144     } else {
145       // TODO(@lukmccall): throw an exception
146       jarg->l = nullptr;
147     }
148   }
149 
150   return result;
151 }
152 
153 MethodMetadata::MethodMetadata(
154   std::string name,
155   int args,
156   bool isAsync,
157   std::unique_ptr<int[]> desiredTypes,
158   jni::global_ref<jobject> &&jBodyReference
159 ) : name(std::move(name)),
160     args(args),
161     isAsync(isAsync),
162     desiredTypes(std::move(desiredTypes)),
163     jBodyReference(std::move(jBodyReference)) {}
164 
165 std::shared_ptr<jsi::Function> MethodMetadata::toJSFunction(
166   jsi::Runtime &runtime,
167   JSIInteropModuleRegistry *moduleRegistry
168 ) {
169   if (body == nullptr) {
170     if (isAsync) {
171       body = std::make_shared<jsi::Function>(toAsyncFunction(runtime, moduleRegistry));
172     } else {
173       body = std::make_shared<jsi::Function>(toSyncFunction(runtime, moduleRegistry));
174     }
175   }
176 
177   return body;
178 }
179 
180 jsi::Function MethodMetadata::toSyncFunction(
181   jsi::Runtime &runtime,
182   JSIInteropModuleRegistry *moduleRegistry
183 ) {
184   return jsi::Function::createFromHostFunction(
185     runtime,
186     jsi::PropNameID::forAscii(runtime, name),
187     args,
188     [this, moduleRegistry](
189       jsi::Runtime &rt,
190       const jsi::Value &thisValue,
191       const jsi::Value *args,
192       size_t count
193     ) -> jsi::Value {
194       try {
195         return this->callSync(
196           rt,
197           moduleRegistry,
198           args,
199           count
200         );
201       } catch (jni::JniException &jniException) {
202         jni::local_ref<jni::JThrowable> unboxedThrowable = jniException.getThrowable();
203         if (unboxedThrowable->isInstanceOf(CodedException::javaClassLocal())) {
204           auto codedException = jni::static_ref_cast<CodedException>(unboxedThrowable);
205           auto code = codedException->getCode();
206           auto message = codedException->getLocalizedMessage();
207 
208           if (rt.global().hasProperty(rt, "ExpoModulesCore_CodedError")) {
209             auto jsCodedError = rt.global()
210               .getProperty(rt, "ExpoModulesCore_CodedError")
211               .asObject(rt)
212               .asFunction(rt);
213 
214             throw jsi::JSError(
215               message.value_or(""),
216               rt,
217               jsCodedError.callAsConstructor(
218                 rt,
219                 jsi::String::createFromUtf8(rt, code),
220                 jsi::String::createFromUtf8(rt, message.value_or(""))
221               )
222             );
223           }
224         }
225 
226         // Rethrow error if we can't wrap it.
227         throw;
228       }
229     });
230 }
231 
232 jsi::Value MethodMetadata::callSync(
233   jsi::Runtime &rt,
234   JSIInteropModuleRegistry *moduleRegistry,
235   const jsi::Value *args,
236   size_t count
237 ) {
238   if (this->jBodyReference == nullptr) {
239     return jsi::Value::undefined();
240   }
241 
242   JNIEnv *env = jni::Environment::current();
243 
244   /**
245    * This will push a new JNI stack frame for the LocalReferences in this
246    * function call. When the stack frame for this lambda is popped,
247    * all LocalReferences are deleted.
248    */
249   jni::JniLocalScope scope(env, (int) count);
250 
251   std::vector<jvalue> convertedArgs = convertJSIArgsToJNI(moduleRegistry, env, rt, args, count,
252                                                           false);
253 
254   // TODO(@lukmccall): Remove this temp array
255   auto tempArray = env->NewObjectArray(
256     convertedArgs.size(),
257     CachedReferencesRegistry::instance()->getJClass("java/lang/Object").clazz,
258     nullptr
259   );
260   for (size_t i = 0; i < convertedArgs.size(); i++) {
261     env->SetObjectArrayElement(tempArray, i, convertedArgs[i].l);
262   }
263 
264   // Cast in this place is safe, cause we know that this function is promise-less.
265   auto syncFunction = jni::static_ref_cast<JNIFunctionBody>(this->jBodyReference);
266   auto result = syncFunction->invoke(
267     tempArray
268   );
269 
270   if (result == nullptr) {
271     return jsi::Value::undefined();
272   }
273 
274   return jsi::valueFromDynamic(rt, result->cthis()->consume())
275     .asObject(rt)
276     .asArray(rt)
277     .getValueAtIndex(rt, 0);
278 }
279 
280 jsi::Function MethodMetadata::toAsyncFunction(
281   jsi::Runtime &runtime,
282   JSIInteropModuleRegistry *moduleRegistry
283 ) {
284   return jsi::Function::createFromHostFunction(
285     runtime,
286     jsi::PropNameID::forAscii(runtime, name),
287     args,
288     [this, moduleRegistry](
289       jsi::Runtime &rt,
290       const jsi::Value &thisValue,
291       const jsi::Value *args,
292       size_t count
293     ) -> jsi::Value {
294       JNIEnv *env = jni::Environment::current();
295 
296       /**
297        * This will push a new JNI stack frame for the LocalReferences in this
298        * function call. When the stack frame for this lambda is popped,
299        * all LocalReferences are deleted.
300        */
301       jni::JniLocalScope scope(env, (int) count);
302       std::vector<jvalue> convertedArgs = convertJSIArgsToJNI(moduleRegistry, env, rt, args, count,
303                                                               true);
304 
305       auto Promise = rt.global().getPropertyAsFunction(rt, "Promise");
306       // Creates a JSI promise
307       jsi::Value promise = Promise.callAsConstructor(
308         rt,
309         createPromiseBody(rt, moduleRegistry, std::move(convertedArgs))
310       );
311       return promise;
312     }
313   );
314 }
315 
316 jsi::Function MethodMetadata::createPromiseBody(
317   jsi::Runtime &runtime,
318   JSIInteropModuleRegistry *moduleRegistry,
319   std::vector<jvalue> &&args
320 ) {
321   return jsi::Function::createFromHostFunction(
322     runtime,
323     jsi::PropNameID::forAscii(runtime, "promiseFn"),
324     2,
325     [this, args = std::move(args), moduleRegistry](
326       jsi::Runtime &rt,
327       const jsi::Value &thisVal,
328       const jsi::Value *promiseConstructorArgs,
329       size_t promiseConstructorArgCount
330     ) {
331       if (promiseConstructorArgCount != 2) {
332         throw std::invalid_argument("Promise fn arg count must be 2");
333       }
334 
335       jsi::Function resolveJSIFn = promiseConstructorArgs[0].getObject(rt).getFunction(rt);
336       jsi::Function rejectJSIFn = promiseConstructorArgs[1].getObject(rt).getFunction(rt);
337 
338       auto &runtimeHolder = moduleRegistry->runtimeHolder;
339       jobject resolve = createJavaCallbackFromJSIFunction(
340         std::move(resolveJSIFn),
341         rt,
342         runtimeHolder->jsInvoker
343       ).release();
344 
345       jobject reject = createJavaCallbackFromJSIFunction(
346         std::move(rejectJSIFn),
347         rt,
348         runtimeHolder->jsInvoker
349       ).release();
350 
351       JNIEnv *env = jni::Environment::current();
352 
353       auto &jPromise = CachedReferencesRegistry::instance()->getJClass(
354         "com/facebook/react/bridge/PromiseImpl");
355       jmethodID jPromiseConstructor = jPromise.getMethod(
356         "<init>",
357         "(Lcom/facebook/react/bridge/Callback;Lcom/facebook/react/bridge/Callback;)V"
358       );
359 
360       // Creates a promise object
361       jobject promise = env->NewObject(
362         jPromise.clazz,
363         jPromiseConstructor,
364         resolve,
365         reject
366       );
367 
368       auto argsSize = args.size();
369       // TODO(@lukmccall): Remove this temp array
370       auto tempArray = env->NewObjectArray(
371         argsSize,
372         CachedReferencesRegistry::instance()->getJClass("java/lang/Object").clazz,
373         nullptr
374       );
375       for (size_t i = 0; i < argsSize; i++) {
376         env->SetObjectArrayElement(tempArray, i, args[i].l);
377       }
378 
379       // Cast in this place is safe, cause we know that this function expects promise.
380       auto asyncFunction = jni::static_ref_cast<JNIAsyncFunctionBody>(this->jBodyReference);
381       asyncFunction->invoke(
382         tempArray,
383         promise
384       );
385 
386       // We have to remove the local reference to the promise object.
387       // It doesn't mean that the promise will be deallocated, but rather that we move
388       // the ownership to the `JNIAsyncFunctionBody`.
389       env->DeleteLocalRef(promise);
390 
391       for (const auto &arg: args) {
392         env->DeleteGlobalRef(arg.l);
393       }
394       env->DeleteLocalRef(tempArray);
395 
396       return jsi::Value::undefined();
397     }
398   );
399 }
400 
401 } // namespace expo
402