react-native-nitro-modules 0.1.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,10 +72,14 @@ public:
72
72
 
73
73
  public:
74
74
  void loadHybridMethods() override {
75
+ // Call base method to make sure we properly inherit `toString()` and `equals()`
76
+ HybridObject::loadHybridMethods();
75
77
  // Register all methods that need to be exposed to JS
76
- registerHybridGetter("number", &MyHybridObject::getNumber, this);
77
- registerHybridSetter("number", &MyHybridObject::setNumber, this);
78
- registerHybridMethod("add", &MyHybridObject::add, this);
78
+ registerHybrids(this, [](Prototype& prototype) {
79
+ prototype.registerHybridGetter("number", &MyHybridObject::getNumber);
80
+ prototype.registerHybridSetter("number", &MyHybridObject::setNumber);
81
+ prototype.registerHybridMethod("add", &MyHybridObject::add);
82
+ });
79
83
  }
80
84
 
81
85
  private:
@@ -149,6 +153,10 @@ The following C++ / JS types are supported out of the box:
149
153
  <td><code>Promise&lt;T&gt;</code></td>
150
154
  <td><code>std::future&lt;T&gt;</code></td>
151
155
  </tr>
156
+ <tr>
157
+ <td><code>(TArgs...) =&gt; void</code></td>
158
+ <td><code>std::function&lt;void (TArgs...)&gt;</code></td>
159
+ </tr>
152
160
  <tr>
153
161
  <td><code>(TArgs...) =&gt; TReturn</code></td>
154
162
  <td><code>std::function&lt;std::future&lt;TReturn&gt; (TArgs...)&gt;</code></td>
@@ -170,7 +178,7 @@ The following C++ / JS types are supported out of the box:
170
178
 
171
179
  Since the `JSIConverter<T>` is just a template, you can extend it with any other custom types by overloading the interface.
172
180
 
173
- For example, to add support for an enum, overload `JSIConverter<YourEnum>`:
181
+ For example, to add support for an enum, overload `JSIConverter<MyEnum>`:
174
182
 
175
183
  ```cpp
176
184
  #include <NitroModules/JSIConverter.hpp>
@@ -195,7 +203,9 @@ namespace margelo::nitro {
195
203
  }
196
204
  ```
197
205
 
198
- ..and on the JS side, you can implicitly cast the `number` to an enum as well:
206
+ Once the `JSIConverter<T>` for `MyEnum` is defined, you can use the type `MyEnum` in C++ methods, getters and setters of `HybridObject`s.
207
+
208
+ And on the JS side, you can simply treat the returned `number` (int) as a `MyEnum`:
199
209
 
200
210
  ```js
201
211
  enum MyEnum {
@@ -25,6 +25,7 @@ include_directories(
25
25
  ../cpp/jsi
26
26
  ../cpp/platform
27
27
  ../cpp/registry
28
+ ../cpp/prototype
28
29
  ../cpp/test-object
29
30
  ../cpp/templates
30
31
  ../cpp/threading
@@ -0,0 +1,167 @@
1
+ //
2
+ // HybridFunction.hpp
3
+ // NitroModules
4
+ //
5
+ // Created by Marc Rousavy on 07.08.24.
6
+ //
7
+
8
+ #pragma once
9
+
10
+ namespace margelo::nitro {
11
+ template <typename T, typename Enable>
12
+ struct JSIConverter;
13
+ } // namespace margelo::nitro
14
+
15
+ #include "CountTrailingOptionals.hpp"
16
+ #include "JSIConverter.hpp"
17
+ #include "TypeInfo.hpp"
18
+ #include <functional>
19
+ #include <jsi/jsi.h>
20
+ #include <memory>
21
+ #include <string>
22
+ #include <type_traits>
23
+
24
+ namespace margelo::nitro {
25
+
26
+ using namespace facebook;
27
+
28
+ /**
29
+ * Represents the type of a function - it can be either a normal function ("METHOD"),
30
+ * or a property ("GETTER" + "SETTER")
31
+ */
32
+ enum class FunctionType { METHOD, GETTER, SETTER };
33
+
34
+ /**
35
+ * Represents a Hybrid Function.
36
+ */
37
+ class HybridFunction final {
38
+ private:
39
+ std::string _name;
40
+ size_t _paramCount;
41
+ jsi::HostFunctionType _function;
42
+
43
+ public:
44
+ // getters
45
+ inline const std::string& getName() const {
46
+ return _name;
47
+ }
48
+ inline size_t getParamCount() const {
49
+ return _paramCount;
50
+ }
51
+ inline const jsi::HostFunctionType& getHostFunction() const {
52
+ return _function;
53
+ }
54
+
55
+ public:
56
+ // functions
57
+ inline jsi::Function toJSFunction(jsi::Runtime& runtime) const {
58
+ return jsi::Function::createFromHostFunction(runtime, jsi::PropNameID::forUtf8(runtime, _name), _paramCount, _function);
59
+ }
60
+
61
+ private:
62
+ HybridFunction(jsi::HostFunctionType&& function, size_t paramCount, const std::string& name)
63
+ : _function(std::move(function)), _paramCount(paramCount), _name(name) {}
64
+
65
+ public:
66
+ /**
67
+ * Create a new `HybridFunction` that can be called from JS.
68
+ * This performs proper JSI -> C++ conversion using `JSIConverter<T>`,
69
+ * and assumes that the object this is called on has a proper `this` configured.
70
+ * The object's `this` needs to be a `NativeState`.
71
+ */
72
+ template <typename Derived, typename ReturnType, typename... Args>
73
+ static inline HybridFunction createHybridFunction(const std::string& name, ReturnType (Derived::*method)(Args...), FunctionType type) {
74
+ jsi::HostFunctionType hostFunction = [name, method, type](/* JS Runtime */ jsi::Runtime& runtime,
75
+ /* HybridObject */ const jsi::Value& thisValue,
76
+ /* JS arguments */ const jsi::Value* args,
77
+ /* argument size */ size_t count) -> jsi::Value {
78
+ // 1. Get actual `HybridObject` instance from `thisValue` (it's stored as `NativeState`)
79
+ #if DEBUG
80
+ if (!thisValue.isObject()) [[unlikely]] {
81
+ throw jsi::JSError(runtime, "Cannot call hybrid function " + name + "(...) - `this` is not bound!");
82
+ }
83
+ #endif
84
+ jsi::Object thisObject = thisValue.getObject(runtime);
85
+
86
+ #if DEBUG
87
+ if (!thisObject.hasNativeState<Derived>(runtime)) [[unlikely]] {
88
+ if (thisObject.hasNativeState(runtime)) {
89
+ throw jsi::JSError(runtime, "Cannot call hybrid function " + name + "(...) - `this` has a NativeState, but it's the wrong type!");
90
+ } else {
91
+ throw jsi::JSError(runtime, "Cannot call hybrid function " + name + "(...) - `this` does not have a NativeState!");
92
+ }
93
+ }
94
+ #endif
95
+ std::shared_ptr<Derived> hybridInstance = thisObject.getNativeState<Derived>(runtime);
96
+
97
+ // 2. Make sure the given arguments match, either with a static size, or with potentially optional arguments size.
98
+ constexpr size_t optionalArgsCount = trailing_optionals_count_v<Args...>;
99
+ constexpr size_t maxArgsCount = sizeof...(Args);
100
+ constexpr size_t minArgsCount = maxArgsCount - optionalArgsCount;
101
+ bool isWithinArgsRange = (count >= minArgsCount && count <= maxArgsCount);
102
+ if (!isWithinArgsRange) [[unlikely]] {
103
+ // invalid amount of arguments passed!
104
+ std::string hybridObjectName = hybridInstance->getName();
105
+ if constexpr (minArgsCount == maxArgsCount) {
106
+ // min and max args length is the same, so we don't have any optional parameters. fixed count
107
+ throw jsi::JSError(runtime, hybridObjectName + "." + name + "(...) expected " + std::to_string(maxArgsCount) +
108
+ " arguments, but received " + std::to_string(count) + "!");
109
+ } else {
110
+ // min and max args length are different, so we have optional parameters - variable length arguments.
111
+ throw jsi::JSError(runtime, hybridObjectName + "." + name + "(...) expected between " + std::to_string(minArgsCount) + " and " +
112
+ std::to_string(maxArgsCount) + " arguments, but received " + std::to_string(count) + "!");
113
+ }
114
+ }
115
+
116
+ // 3. Actually call method - either raw JSI method, or by going through `JSIConverter<T>` first.
117
+ try {
118
+ if constexpr (std::is_same_v<ReturnType, jsi::Value>) {
119
+ // If the return type is a jsi::Value, we assume the user wants full JSI code control.
120
+ // The signature must be identical to jsi::HostFunction (jsi::Runtime&, jsi::Value& this, ...)
121
+ return (hybridInstance->*method)(runtime, thisValue, args, count);
122
+ } else {
123
+ // Call the actual method with JSI values as arguments and return a JSI value again.
124
+ // Internally, this method converts the JSI values to C++ values.
125
+ return callMethod(hybridInstance.get(), method, runtime, args, count, std::index_sequence_for<Args...>{});
126
+ }
127
+ } catch (const std::exception& exception) {
128
+ // Some exception was thrown - add method name information and re-throw as `JSError`.
129
+ std::string hybridObjectName = hybridInstance->getName();
130
+ std::string message = exception.what();
131
+ std::string suffix = type == FunctionType::METHOD ? "(...)" : "";
132
+ throw jsi::JSError(runtime, hybridObjectName + "." + name + suffix + ": " + message);
133
+ } catch (...) {
134
+ // Some unknown exception was thrown - add method name information and re-throw as `JSError`.
135
+ std::string hybridObjectName = hybridInstance->getName();
136
+ std::string errorName = TypeInfo::getCurrentExceptionName();
137
+ std::string suffix = type == FunctionType::METHOD ? "(...)" : "";
138
+ throw jsi::JSError(runtime, hybridObjectName + "." + name + suffix + " threw an unknown " + errorName + " error.");
139
+ }
140
+ };
141
+
142
+ return HybridFunction(std::move(hostFunction), sizeof...(Args), name);
143
+ }
144
+
145
+ private:
146
+ /**
147
+ * Calls the given method on the given instance with the given `jsi::Value` arguments by converting them to the desired target types.
148
+ * The given method's return value will be converted to a `jsi::Value` again.
149
+ */
150
+ template <typename Derived, typename ReturnType, typename... Args, size_t... Is>
151
+ static inline jsi::Value callMethod(Derived* obj, ReturnType (Derived::*method)(Args...), jsi::Runtime& runtime, const jsi::Value* args,
152
+ size_t argsSize, std::index_sequence<Is...>) {
153
+ jsi::Value defaultValue;
154
+
155
+ if constexpr (std::is_void_v<ReturnType>) {
156
+ // It's a void method.
157
+ (obj->*method)(JSIConverter<std::decay_t<Args>>::fromJSI(runtime, Is < argsSize ? args[Is] : defaultValue)...);
158
+ return jsi::Value::undefined();
159
+ } else {
160
+ // It's returning some C++ type, we need to convert that to a JSI value now.
161
+ ReturnType result = (obj->*method)(JSIConverter<std::decay_t<Args>>::fromJSI(runtime, Is < argsSize ? args[Is] : defaultValue)...);
162
+ return JSIConverter<std::decay_t<ReturnType>>::toJSI(runtime, std::move(result));
163
+ }
164
+ }
165
+ };
166
+
167
+ } // namespace margelo::nitro
@@ -3,11 +3,10 @@
3
3
  //
4
4
 
5
5
  #include "HybridObject.hpp"
6
- #include "HybridContext.hpp"
7
6
  #include "JSIConverter.hpp"
8
7
  #include "NitroLogger.hpp"
9
8
 
10
- #define LOG_MEMORY_ALLOCATIONS true
9
+ #define LOG_MEMORY_ALLOCATIONS false
11
10
 
12
11
  namespace margelo::nitro {
13
12
 
@@ -45,7 +44,7 @@ static int getId(const char* name) {
45
44
  }
46
45
  #endif
47
46
 
48
- HybridObject::HybridObject(const char* name) : _name(name), _mutex(std::make_unique<std::mutex>()) {
47
+ HybridObject::HybridObject(const char* name) : HybridObjectPrototype(), _name(name) {
49
48
  #if LOG_MEMORY_ALLOCATIONS
50
49
  _instanceId = getId(name);
51
50
  uint32_t alive = incrementAliveInstancesAndGet(_name);
@@ -62,55 +61,6 @@ HybridObject::~HybridObject() {
62
61
  Logger::log(TAG, "(MEMORY) ❌ Deleting %s (#%i)... (Total %s(s): %i | Total HybridObjects: %i) ", _name, _instanceId, _name, alive,
63
62
  totalObjects);
64
63
  #endif
65
- _functionCache.clear();
66
- }
67
-
68
- size_t HybridObject::getTotalExternalMemorySize() noexcept {
69
- static constexpr int STRING_KEY_AVERAGE_SIZE = 32; // good average for string keys
70
- static constexpr int CACHED_SIZE = STRING_KEY_AVERAGE_SIZE + sizeof(jsi::Function);
71
- static constexpr int METHOD_SIZE = STRING_KEY_AVERAGE_SIZE + sizeof(HybridFunction);
72
- static constexpr int GETTER_SIZE = STRING_KEY_AVERAGE_SIZE + sizeof(jsi::HostFunctionType);
73
- static constexpr int SETTER_SIZE = STRING_KEY_AVERAGE_SIZE + sizeof(jsi::HostFunctionType);
74
-
75
- size_t cachedFunctions = 0;
76
- for (const auto& cache : _functionCache) {
77
- cachedFunctions += cache.second.size();
78
- }
79
-
80
- size_t externalSize = getExternalMemorySize();
81
- return (_getters.size() * GETTER_SIZE) + (_setters.size() * SETTER_SIZE) + (_methods.size() * METHOD_SIZE) +
82
- (cachedFunctions * CACHED_SIZE) + sizeof(std::mutex) + externalSize;
83
- }
84
-
85
- jsi::Value HybridObject::toObject(jsi::Runtime& runtime) {
86
- // 1. Try to find existing jsi::Object from cache
87
- auto found = _jsObjects.find(&runtime);
88
- if (found != _jsObjects.end()) {
89
- // 1.1 We found an object in cache, let's see if it's still alive..
90
- BorrowingReference<jsi::Object> weak = found->second;
91
- OwningReference<jsi::Object> strong = weak.lock();
92
- if (strong) {
93
- // 1.2. It is alive (strong) - update memory size and return to JS!
94
- size_t memorySize = getTotalExternalMemorySize();
95
- strong->setExternalMemoryPressure(runtime, memorySize);
96
- // 1.3. It is alive (strong) - copy the JSI Pointer into a new jsi::Value, and return to JS
97
- return jsi::Value(runtime, *strong);
98
- }
99
- }
100
-
101
- // 2. There is either no value in cache, or it's already dead. Create a new one
102
- auto cache = JSICache::getOrCreateCache(runtime);
103
- // 3. Create a new jsi::Object from this HybridObject/jsi::HostObject
104
- std::shared_ptr<HybridObject> shared = shared_from_this();
105
- jsi::Object object = jsi::Object::createFromHostObject(runtime, shared);
106
- // 4. HybridObjects expose their external memory size, so inform JS GC about it!
107
- size_t memorySize = getTotalExternalMemorySize();
108
- object.setExternalMemoryPressure(runtime, memorySize);
109
- // 5. Make it a global (weak) reference
110
- auto global = cache.makeGlobal<jsi::Object>(std::move(object));
111
- _jsObjects[&runtime] = global.weak();
112
- // 6. Return a jsi::Value copy again to the caller.
113
- return jsi::Value(runtime, *global);
114
64
  }
115
65
 
116
66
  std::string HybridObject::toString() {
@@ -126,95 +76,53 @@ bool HybridObject::equals(std::shared_ptr<HybridObject> other) {
126
76
  }
127
77
 
128
78
  void HybridObject::loadHybridMethods() {
129
- registerHybridGetter("name", &HybridObject::getName, this);
130
- registerHybridMethod("toString", &HybridObject::toString, this);
131
- registerHybridMethod("equals", &HybridObject::equals, this);
79
+ registerHybrids(this, [](Prototype& prototype) {
80
+ prototype.registerHybridGetter("name", &HybridObject::getName);
81
+ prototype.registerHybridMethod("toString", &HybridObject::toString);
82
+ prototype.registerHybridMethod("equals", &HybridObject::equals);
83
+ });
132
84
  }
133
85
 
134
- std::vector<jsi::PropNameID> HybridObject::getPropertyNames(facebook::jsi::Runtime& runtime) {
135
- std::unique_lock lock(*_mutex);
136
- ensureInitialized(runtime);
137
-
138
- std::vector<jsi::PropNameID> result;
139
- size_t totalSize = _methods.size() + _getters.size() + _setters.size() + 1;
140
- result.reserve(totalSize);
141
-
142
- for (const auto& item : _methods) {
143
- result.push_back(jsi::PropNameID::forUtf8(runtime, item.first));
144
- }
145
- for (const auto& item : _getters) {
146
- result.push_back(jsi::PropNameID::forUtf8(runtime, item.first));
147
- }
148
- for (const auto& item : _setters) {
149
- result.push_back(jsi::PropNameID::forUtf8(runtime, item.first));
150
- }
151
- return result;
152
- }
153
-
154
- jsi::Value HybridObject::get(facebook::jsi::Runtime& runtime, const facebook::jsi::PropNameID& propName) {
155
- std::unique_lock lock(*_mutex);
156
- ensureInitialized(runtime);
157
-
158
- std::string name = propName.utf8(runtime);
159
-
160
- auto& functionCache = _functionCache[&runtime];
161
-
162
- if (functionCache.contains(name)) [[likely]] {
163
- // cache hit - let's see if the function is still alive..
164
- OwningReference<jsi::Function> function = functionCache[name];
165
- if (function) [[likely]] {
166
- // function is still alive, we can use it.
167
- return jsi::Value(runtime, *function);
86
+ jsi::Value HybridObject::toObject(jsi::Runtime& runtime) {
87
+ // 1. Check if we have a jsi::WeakObject in cache that we can use to avoid re-creating the object each time
88
+ auto cachedObject = _objectCache.find(&runtime);
89
+ if (cachedObject != _objectCache.end()) {
90
+ // 1.1. We have a WeakObject, try to see if it is still alive
91
+ OwningLock<jsi::WeakObject> lock = cachedObject->second.lock();
92
+ jsi::Value object = cachedObject->second->lock(runtime);
93
+ if (!object.isUndefined()) {
94
+ // 1.2. It is still alive - we can use it instead of creating a new one!
95
+ return object;
168
96
  }
169
97
  }
170
98
 
171
- if (_getters.contains(name)) {
172
- // it's a property getter. call it directly
173
- return _getters[name](runtime, jsi::Value::undefined(), nullptr, 0);
174
- }
99
+ // 2. Get the object's base prototype (global & shared)
100
+ jsi::Value prototype = getPrototype(runtime);
175
101
 
176
- if (_methods.contains(name)) {
177
- // it's a function. we now need to wrap it in a jsi::Function, store it in cache, then return it.
178
- HybridFunction& hybridFunction = _methods.at(name);
179
- // get (or create) a runtime-specific function cache
180
- auto runtimeCache = JSICache::getOrCreateCache(runtime);
181
- // create the jsi::Function
182
- jsi::Function function = jsi::Function::createFromHostFunction(runtime, jsi::PropNameID::forUtf8(runtime, name),
183
- hybridFunction.parameterCount, hybridFunction.function);
184
- // throw it into the cache for next time
185
- OwningReference<jsi::Function> globalFunction = runtimeCache.makeGlobal<jsi::Function>(std::move(function));
186
- functionCache[name] = globalFunction;
187
- // copy the reference & return it to JS
188
- return jsi::Value(runtime, *globalFunction);
189
- }
102
+ // 3. Get the global JS Object.create(...) constructor so we can create an object from the given prototype
103
+ jsi::Object objectConstructor = runtime.global().getPropertyAsObject(runtime, "Object");
104
+ jsi::Function create = objectConstructor.getPropertyAsFunction(runtime, "create");
190
105
 
191
- // this property does not exist. Return undefined
192
- return jsi::Value::undefined();
193
- }
106
+ // 4. Create the object using Object.create(...)
107
+ jsi::Object object = create.call(runtime, prototype).asObject(runtime);
194
108
 
195
- void HybridObject::set(facebook::jsi::Runtime& runtime, const facebook::jsi::PropNameID& propName, const facebook::jsi::Value& value) {
196
- std::unique_lock lock(*_mutex);
197
- ensureInitialized(runtime);
109
+ // 5. Assign NativeState to the object so the prototype can resolve the native methods
110
+ object.setNativeState(runtime, shared_from_this());
198
111
 
199
- std::string name = propName.utf8(runtime);
112
+ // 6. Set memory size so Hermes GC knows about actual memory
113
+ object.setExternalMemoryPressure(runtime, getExternalMemorySize());
200
114
 
201
- if (_setters.contains(name)) {
202
- // Call setter
203
- _setters[name](runtime, jsi::Value::undefined(), &value, 1);
204
- return;
205
- }
115
+ #if DEBUG
116
+ // 7. Assign a private __type property for debugging - this will be used so users know it's not just an empty object.
117
+ object.setProperty(runtime, "__type", jsi::String::createFromUtf8(runtime, "NativeState<" + std::string(_name) + ">"));
118
+ #endif
206
119
 
207
- // this property does not exist, and cannot be set. Throw and error!
208
- throw std::runtime_error("Cannot set property \"" + name + "\" - " + std::string(_name) + " does not have a setter for \"" + name +
209
- "\"!");
210
- }
120
+ // 8. Throw a jsi::WeakObject pointing to our object into cache so subsequent calls can use it from cache
121
+ JSICacheReference cache = JSICache::getOrCreateCache(runtime);
122
+ _objectCache.emplace(&runtime, cache.makeShared(jsi::WeakObject(runtime, object)));
211
123
 
212
- void HybridObject::ensureInitialized(facebook::jsi::Runtime& runtime) {
213
- if (!_didLoadMethods) [[unlikely]] {
214
- // lazy-load all exposed methods
215
- loadHybridMethods();
216
- _didLoadMethods = true;
217
- }
124
+ // 9. Return it!
125
+ return object;
218
126
  }
219
127
 
220
128
  } // namespace margelo::nitro
@@ -4,21 +4,14 @@
4
4
 
5
5
  #pragma once
6
6
 
7
- #include "CountTrailingOptionals.hpp"
8
- #include "JSIConverter.hpp"
9
- #include "OwningReference.hpp"
10
- #include "TypeInfo.hpp"
11
- #include <functional>
7
+ #include "HybridObjectPrototype.hpp"
8
+
12
9
  #include <jsi/jsi.h>
13
10
  #include <memory>
14
- #include <mutex>
15
11
  #include <type_traits>
16
- #include <unordered_map>
17
12
 
18
13
  namespace margelo::nitro {
19
14
 
20
- enum class MethodType { METHOD, GETTER, SETTER };
21
-
22
15
  using namespace facebook;
23
16
 
24
17
  /**
@@ -30,13 +23,7 @@ using namespace facebook;
30
23
  *
31
24
  * The new class can then be passed to JS using the `JSIConverter<HybridObject>`.
32
25
  */
33
- class HybridObject : public jsi::HostObject, public std::enable_shared_from_this<HybridObject> {
34
- public:
35
- struct HybridFunction {
36
- jsi::HostFunctionType function;
37
- size_t parameterCount;
38
- };
39
-
26
+ class HybridObject : public jsi::NativeState, public HybridObjectPrototype, public std::enable_shared_from_this<HybridObject> {
40
27
  public:
41
28
  /**
42
29
  * Create a new instance of a `HybridObject`.
@@ -58,9 +45,13 @@ public:
58
45
  HybridObject(HybridObject&& move) = delete;
59
46
 
60
47
  public:
61
- void set(jsi::Runtime&, const jsi::PropNameID& name, const jsi::Value& value) override;
62
- jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& propName) override;
63
- std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime& runtime) override;
48
+ /**
49
+ * Return the `jsi::Object` that holds this `HybridObject`. (boxed in a `jsi::Value`)
50
+ * This properly assigns (or creates) the base prototype for this type,
51
+ * and assigns it's NativeState.
52
+ * Additionally, this sets the external memory pressure for proper GC memory management.
53
+ */
54
+ jsi::Value toObject(jsi::Runtime& runtime);
64
55
 
65
56
  public:
66
57
  /**
@@ -72,22 +63,6 @@ public:
72
63
  return std::static_pointer_cast<Derived>(shared_from_this());
73
64
  }
74
65
 
75
- public:
76
- /**
77
- * Get the total size of any external (heap) allocations this `HybridObject` (or
78
- * a subclass of it) has made.
79
- * This includes any base allocations (such as function cache), as well as
80
- * overridden extra memory size.
81
- */
82
- size_t getTotalExternalMemorySize() noexcept;
83
-
84
- /**
85
- * Return the `jsi::Object` that holds this `HybridObject` (boxed in a `jsi::Value`)
86
- * Compared to other `jsi::HostObject`s, the `HybridObject` actually
87
- * caches the created `jsi::Object` instances for safer memory management.
88
- */
89
- jsi::Value toObject(jsi::Runtime& runtime);
90
-
91
66
  public:
92
67
  /**
93
68
  * Get the HybridObject's name
@@ -128,128 +103,17 @@ protected:
128
103
  *
129
104
  * void User::loadHybridMethods() {
130
105
  * HybridObject::loadHybridMethods();
131
- * registerHybridMethod("getAge", &User::getAge, this);
106
+ * registerHybridMethod("getAge", &User::getAge);
132
107
  * }
133
108
  * ```
134
109
  */
135
- virtual void loadHybridMethods();
110
+ virtual void loadHybridMethods() override;
136
111
 
137
112
  private:
138
113
  static constexpr auto TAG = "HybridObject";
139
114
  const char* _name = TAG;
140
115
  int _instanceId = 1;
141
- bool _didLoadMethods = false;
142
- std::unique_ptr<std::mutex> _mutex;
143
- std::unordered_map<std::string, HybridFunction> _methods;
144
- std::unordered_map<std::string, jsi::HostFunctionType> _getters;
145
- std::unordered_map<std::string, jsi::HostFunctionType> _setters;
146
- std::unordered_map<jsi::Runtime*, std::unordered_map<std::string, OwningReference<jsi::Function>>> _functionCache;
147
- std::unordered_map<jsi::Runtime*, BorrowingReference<jsi::Object>> _jsObjects;
148
-
149
- private:
150
- inline void ensureInitialized(jsi::Runtime& runtime);
151
-
152
- private:
153
- template <typename Derived, typename ReturnType, typename... Args, size_t... Is>
154
- static inline jsi::Value callMethod(Derived* obj, ReturnType (Derived::*method)(Args...), jsi::Runtime& runtime, const jsi::Value* args,
155
- size_t argsSize, std::index_sequence<Is...>) {
156
- jsi::Value defaultValue;
157
-
158
- if constexpr (std::is_void_v<ReturnType>) {
159
- // It's a void method.
160
- (obj->*method)(JSIConverter<std::decay_t<Args>>::fromJSI(runtime, Is < argsSize ? args[Is] : defaultValue)...);
161
- return jsi::Value::undefined();
162
- } else {
163
- // It's returning some C++ type, we need to convert that to a JSI value now.
164
- ReturnType result = (obj->*method)(JSIConverter<std::decay_t<Args>>::fromJSI(runtime, Is < argsSize ? args[Is] : defaultValue)...);
165
- return JSIConverter<std::decay_t<ReturnType>>::toJSI(runtime, std::move(result));
166
- }
167
- }
168
-
169
- template <typename Derived, typename ReturnType, typename... Args>
170
- static inline jsi::HostFunctionType createHybridMethod(std::string name, ReturnType (Derived::*method)(Args...), Derived* derivedInstance,
171
- MethodType type) {
172
- return [name, derivedInstance, method, type](jsi::Runtime& runtime, const jsi::Value& thisVal, const jsi::Value* args,
173
- size_t count) -> jsi::Value {
174
- constexpr size_t optionalArgsCount = trailing_optionals_count_v<Args...>;
175
- constexpr size_t maxArgsCount = sizeof...(Args);
176
- constexpr size_t minArgsCount = maxArgsCount - optionalArgsCount;
177
- bool isWithinArgsRange = (count >= minArgsCount && count <= maxArgsCount);
178
- if (!isWithinArgsRange) [[unlikely]] {
179
- // invalid amount of arguments passed!
180
- std::string hybridObjectName = derivedInstance->_name;
181
- if constexpr (minArgsCount == maxArgsCount) {
182
- // min and max args length is the same, so we don't have any optional parameters. fixed count
183
- throw jsi::JSError(runtime, hybridObjectName + "." + name + "(...) expected " + std::to_string(maxArgsCount) +
184
- " arguments, but received " + std::to_string(count) + "!");
185
- } else {
186
- // min and max args length are different, so we have optional parameters - variable length arguments.
187
- throw jsi::JSError(runtime, hybridObjectName + "." + name + "(...) expected between " + std::to_string(minArgsCount) + " and " +
188
- std::to_string(maxArgsCount) + " arguments, but received " + std::to_string(count) + "!");
189
- }
190
- }
191
-
192
- try {
193
- if constexpr (std::is_same_v<ReturnType, jsi::Value>) {
194
- // If the return type is a jsi::Value, we assume the user wants full JSI code control.
195
- // The signature must be identical to jsi::HostFunction (jsi::Runtime&, jsi::Value& this, ...)
196
- return (derivedInstance->*method)(runtime, thisVal, args, count);
197
- } else {
198
- // Call the actual method with JSI values as arguments and return a JSI value again.
199
- // Internally, this method converts the JSI values to C++ values.
200
- return callMethod(derivedInstance, method, runtime, args, count, std::index_sequence_for<Args...>{});
201
- }
202
- } catch (const std::exception& exception) {
203
- std::string hybridObjectName = derivedInstance->_name;
204
- std::string message = exception.what();
205
- std::string suffix = type == MethodType::METHOD ? "(...)" : "";
206
- throw jsi::JSError(runtime, hybridObjectName + "." + name + suffix + ": " + message);
207
- } catch (...) {
208
- std::string hybridObjectName = derivedInstance->_name;
209
- std::string errorName = TypeInfo::getCurrentExceptionName();
210
- std::string suffix = type == MethodType::METHOD ? "(...)" : "";
211
- throw jsi::JSError(runtime, hybridObjectName + "." + name + suffix + " threw an unknown " + errorName + " error.");
212
- }
213
- };
214
- }
215
-
216
- protected:
217
- template <typename Derived, typename ReturnType, typename... Args>
218
- inline void registerHybridMethod(std::string name, ReturnType (Derived::*method)(Args...), Derived* derivedInstance) {
219
- if (_getters.contains(name) || _setters.contains(name)) [[unlikely]] {
220
- throw std::runtime_error("Cannot add Hybrid Method \"" + name + "\" - a property with that name already exists!");
221
- }
222
- if (_methods.contains(name)) [[unlikely]] {
223
- throw std::runtime_error("Cannot add Hybrid Method \"" + name + "\" - a method with that name already exists!");
224
- }
225
-
226
- _methods[name] = HybridFunction{.function = createHybridMethod(name, method, derivedInstance, MethodType::METHOD),
227
- .parameterCount = sizeof...(Args)};
228
- }
229
-
230
- template <typename Derived, typename ReturnType>
231
- inline void registerHybridGetter(std::string name, ReturnType (Derived::*method)(), Derived* derivedInstance) {
232
- if (_getters.contains(name)) [[unlikely]] {
233
- throw std::runtime_error("Cannot add Hybrid Property Getter \"" + name + "\" - a getter with that name already exists!");
234
- }
235
- if (_methods.contains(name)) [[unlikely]] {
236
- throw std::runtime_error("Cannot add Hybrid Property Getter \"" + name + "\" - a method with that name already exists!");
237
- }
238
-
239
- _getters[name] = createHybridMethod(name, method, derivedInstance, MethodType::GETTER);
240
- }
241
-
242
- template <typename Derived, typename ValueType>
243
- inline void registerHybridSetter(std::string name, void (Derived::*method)(ValueType), Derived* derivedInstance) {
244
- if (_setters.contains(name)) [[unlikely]] {
245
- throw std::runtime_error("Cannot add Hybrid Property Setter \"" + name + "\" - a setter with that name already exists!");
246
- }
247
- if (_methods.contains(name)) [[unlikely]] {
248
- throw std::runtime_error("Cannot add Hybrid Property Setter \"" + name + "\" - a method with that name already exists!");
249
- }
250
-
251
- _setters[name] = createHybridMethod(name, method, derivedInstance, MethodType::SETTER);
252
- }
116
+ std::unordered_map<jsi::Runtime*, OwningReference<jsi::WeakObject>> _objectCache;
253
117
  };
254
118
 
255
119
  } // namespace margelo::nitro