1 // Copyright © 2021-present 650 Industries, Inc. (aka Expo)
2 
3 #pragma once
4 
5 #include "JSIObjectWrapper.h"
6 #include "JSITypeConverter.h"
7 #include "JavaScriptRuntime.h"
8 #include "WeakRuntimeHolder.h"
9 #include "JNIFunctionBody.h"
10 #include "JNIDeallocator.h"
11 
12 #include <fbjni/fbjni.h>
13 #include <jsi/jsi.h>
14 
15 #include <memory>
16 
17 namespace jni = facebook::jni;
18 namespace jsi = facebook::jsi;
19 
20 namespace expo {
21 class JavaScriptValue;
22 
23 class JavaScriptFunction;
24 
25 /**
26  * Represents any JavaScript object. Its purpose is to exposes `jsi::Object` API back to Kotlin.
27  */
28 class JavaScriptObject : public jni::HybridClass<JavaScriptObject, Destructible>, JSIObjectWrapper {
29 public:
30   static auto constexpr
31     kJavaDescriptor = "Lexpo/modules/kotlin/jni/JavaScriptObject;";
32   static auto constexpr TAG = "JavaScriptObject";
33 
34   static void registerNatives();
35 
36   static jni::local_ref<JavaScriptObject::javaobject> newInstance(
37     JSIInteropModuleRegistry *jsiInteropModuleRegistry,
38     std::weak_ptr<JavaScriptRuntime> runtime,
39     std::shared_ptr<jsi::Object> jsObject
40   );
41 
42   JavaScriptObject(
43     std::weak_ptr<JavaScriptRuntime> runtime,
44     std::shared_ptr<jsi::Object> jsObject
45   );
46 
47   JavaScriptObject(
48     WeakRuntimeHolder runtime,
49     std::shared_ptr<jsi::Object> jsObject
50   );
51 
52   std::shared_ptr<jsi::Object> get() override;
53 
54   /**
55    * @return a bool whether the object has a property with the given name
56    */
57   bool hasProperty(const std::string &name);
58 
59   /**
60    * @return the property of the object with the given name.
61    * If the name isn't a property on the object, returns the `jsi::Value::undefined` value.
62    */
63   jsi::Value getProperty(const std::string &name);
64 
65   /**
66    * @return a vector consisting of all enumerable property names in the object and its prototype chain.
67    */
68   std::vector<std::string> getPropertyNames();
69 
70   void setProperty(const std::string &name, jsi::Value value);
71 
72   static jsi::Object preparePropertyDescriptor(jsi::Runtime &jsRuntime, int options);
73 
74   static void defineProperty(
75     jsi::Runtime &runtime,
76     jsi::Object *jsthis,
77     const std::string &name,
78     jsi::Object descriptor
79   );
80 
81   void defineNativeDeallocator(
82     jni::alias_ref<JNIFunctionBody::javaobject> deallocator
83   );
84 
85 protected:
86   WeakRuntimeHolder runtimeHolder;
87   std::shared_ptr<jsi::Object> jsObject;
88 
89 private:
90   friend HybridBase;
91 
92   bool jniHasProperty(jni::alias_ref<jstring> name);
93 
94   jni::local_ref<jni::HybridClass<JavaScriptValue, Destructible>::javaobject> jniGetProperty(
95     jni::alias_ref<jstring> name
96   );
97 
98   jni::local_ref<jni::JArrayClass<jstring>> jniGetPropertyNames();
99 
100   jni::local_ref<jni::HybridClass<JavaScriptFunction, Destructible>::javaobject> jniAsFunction();
101 
102   /**
103    * Unsets property with the given name.
104    */
105   void unsetProperty(jni::alias_ref<jstring> name);
106 
107   /**
108    * A template to generate different versions of the `setProperty` method based on the `jsi_type_converter` trait.
109    * Those generated methods will be exported and visible in the Kotlin codebase.
110    * On the other hand, we could just make one function that would take a generic Java Object,
111    * but then we would have to decide what to do with it and how to convert it to jsi::Value
112    * in cpp. That would be expensive. So it's easier to ensure that
113    * we call the correct version of `setProperty` in the Kotlin code.
114    *
115    * This template will work only if the jsi_type_converter exists for a given type.
116    */
117   template<
118     class T,
119     typename = std::enable_if_t<is_jsi_type_converter_defined<T>>
120   >
121   void setProperty(jni::alias_ref<jstring> name, T value) {
122     jsi::Runtime &jsRuntime = runtimeHolder.getJSRuntime();
123 
124     auto cName = name->toStdString();
125     jsObject->setProperty(
126       jsRuntime,
127       cName.c_str(),
128       jsi_type_converter<T>::convert(jsRuntime, value)
129     );
130   }
131 
132   template<
133     class T,
134     typename = std::enable_if_t<is_jsi_type_converter_defined<T>>
135   >
136   void defineProperty(jni::alias_ref<jstring> name, T value, int options) {
137     jsi::Runtime &jsRuntime = runtimeHolder.getJSRuntime();
138 
139     auto cName = name->toStdString();
140     jsi::Object descriptor = preparePropertyDescriptor(jsRuntime, options);
141     descriptor.setProperty(jsRuntime, "value", jsi_type_converter<T>::convert(jsRuntime, value));
142     JavaScriptObject::defineProperty(jsRuntime, jsObject.get(), cName, std::move(descriptor));
143   }
144 };
145 } // namespace expo
146