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