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