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