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