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