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