Skip to content

Commit 2bc02c0

Browse files
committed
A ton of improvements around jsi::Runtime safe code.
1 parent 3e89cf8 commit 2bc02c0

File tree

13 files changed

+279
-155
lines changed

13 files changed

+279
-155
lines changed

packages/react-native-nitro-modules/cpp/core/Promise.cpp

Lines changed: 0 additions & 42 deletions
This file was deleted.

packages/react-native-nitro-modules/cpp/core/Promise.hpp

Lines changed: 0 additions & 33 deletions
This file was deleted.

packages/react-native-nitro-modules/cpp/core/PromiseFactory.cpp

Lines changed: 0 additions & 22 deletions
This file was deleted.

packages/react-native-nitro-modules/cpp/core/PromiseFactory.hpp

Lines changed: 0 additions & 34 deletions
This file was deleted.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//
2+
// FunctionCache.cpp
3+
// NitroModules
4+
//
5+
// Created by Marc Rousavy on 20.06.24.
6+
//
7+
8+
#include "FunctionCache.hpp"
9+
#include "NitroLogger.hpp"
10+
#include "JSIUtils.h"
11+
12+
namespace margelo {
13+
14+
static constexpr auto CACHE_PROP_NAME = "__nitroModulesFunctionCache";
15+
16+
FunctionCache::FunctionCache(jsi::Runtime* runtime): _runtime(runtime) {}
17+
18+
std::weak_ptr<FunctionCache> FunctionCache::getOrCreateCache(jsi::Runtime &runtime) {
19+
if (runtime.global().hasProperty(runtime, CACHE_PROP_NAME)) {
20+
// A cache already exists for the given runtime - get it and return a weak ref to it.
21+
Logger::log(TAG, "Runtime %i already has a cache, getting it..", getRuntimeId(runtime));
22+
auto cache = runtime.global().getPropertyAsObject(runtime, CACHE_PROP_NAME);
23+
auto nativeState = cache.getNativeState<FunctionCache>(runtime);
24+
return std::weak_ptr(nativeState);
25+
}
26+
27+
// Cache doesn't exist yet - create one, inject it into global, and return a weak ref.
28+
Logger::log(TAG, "Creating new FunctionCache for runtime %i..", getRuntimeId(runtime));
29+
auto nativeState = std::make_shared<FunctionCache>(&runtime);
30+
jsi::Object cache(runtime);
31+
cache.setNativeState(runtime, nativeState);
32+
runtime.global().setProperty(runtime, CACHE_PROP_NAME, std::move(cache));
33+
return std::weak_ptr(nativeState);
34+
}
35+
36+
std::weak_ptr<jsi::Function> FunctionCache::makeGlobal(jsi::Function&& function) {
37+
auto shared = std::make_shared<jsi::Function>(function);
38+
_cache.push_back(shared);
39+
return std::weak_ptr(shared);
40+
}
41+
42+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// FunctionCache.hpp
3+
// NitroModules
4+
//
5+
// Created by Marc Rousavy on 20.06.24.
6+
//
7+
8+
#pragma once
9+
10+
#include <jsi/jsi.h>
11+
#include <memory>
12+
#include <vector>
13+
14+
namespace margelo {
15+
16+
using namespace facebook;
17+
18+
/**
19+
Safely holds `jsi::Function` instances, managed by a `jsi::Runtime`.
20+
*/
21+
class FunctionCache: public jsi::NativeState {
22+
public:
23+
explicit FunctionCache(jsi::Runtime* runtime);
24+
25+
public:
26+
/**
27+
Gets or creates a `FunctionCache` for the given `jsi::Runtime`.
28+
To access the cache, try to `.lock()` the returned `weak_ptr`.
29+
If it can be locked, you can access data in the cache. Otherwise the Runtime has already been deleted.
30+
Do not hold the returned `shared_ptr` in memory, only use it in the calling function's scope.
31+
*/
32+
static std::weak_ptr<FunctionCache> getOrCreateCache(jsi::Runtime& runtime);
33+
34+
public:
35+
/**
36+
Creates a reference to a `jsi::Function` that can be stored in memory and accessed later.
37+
The `jsi::Function` will be managed by the `jsi::Runtime`, if the `jsi::Runtime` gets the destroyed,
38+
so will the `jsi::Function`.
39+
40+
To access the `jsi::Function`, try to `.lock()` the `weak_ptr`.
41+
If it can be locked, it is still valid, otherwise the Runtime has already been deleted.
42+
Do not hold the returned `shared_ptr` in memory, only use it in the calling function's scope.
43+
Note: By design, this is not thread-safe, the returned `weak_ptr` must only be locked on the same thread as it was created on.
44+
*/
45+
std::weak_ptr<jsi::Function> makeGlobal(jsi::Function&& function);
46+
47+
private:
48+
jsi::Runtime* _runtime;
49+
std::vector<std::shared_ptr<jsi::Function>> _cache;
50+
static constexpr auto TAG = "FunctionCache";
51+
};
52+
53+
54+
} // namespace margelo

packages/react-native-nitro-modules/cpp/core/JSIConverter.hpp renamed to packages/react-native-nitro-modules/cpp/jsi/JSIConverter.hpp

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "Promise.hpp"
1010
#include "PromiseFactory.hpp"
1111
#include "Dispatcher.hpp"
12+
#include "FunctionCache.hpp"
1213
#include <array>
1314
#include <future>
1415
#include <jsi/jsi.h>
@@ -22,6 +23,14 @@
2223

2324
namespace margelo {
2425

26+
/**
27+
The JSIConverter<T> class can convert any type from and to a jsi::Value.
28+
It uses templates to statically create fromJSI/toJSI methods, and will throw compile-time errors
29+
if a given type is not convertable.
30+
Value types, custom types (HostObjects), and even functions with any number of arguments/types are supported.
31+
This type can be extended by just creating a new template for JSIConverter in a header.
32+
*/
33+
2534
using namespace facebook;
2635

2736
// Unknown type (error)
@@ -149,12 +158,13 @@ template <typename TResult> struct JSIConverter<std::future<TResult>> {
149158
}
150159
static jsi::Value toJSI(jsi::Runtime& runtime, std::future<TResult>&& arg) {
151160
auto sharedFuture = std::make_shared<std::future<TResult>>(std::move(arg));
152-
return PromiseFactory::createPromise(runtime, [sharedFuture = std::move(sharedFuture)](jsi::Runtime& runtime,
153-
std::shared_ptr<Promise> promise,
154-
std::shared_ptr<Dispatcher> dispatcher) {
155-
// Spawn new async thread to wait for the result
156-
std::thread waiterThread([promise, &runtime, dispatcher, sharedFuture = std::move(sharedFuture)]() {
157-
// wait until the future completes. we are running on a background task here.
161+
auto dispatcher = Dispatcher::getRuntimeGlobalDispatcher(runtime);
162+
163+
return Promise::createPromise(runtime, [sharedFuture, dispatcher](jsi::Runtime& runtime,
164+
std::shared_ptr<Promise> promise) {
165+
// Spawn new async thread to synchronously wait for the `future<T>` to complete
166+
std::thread waiterThread([promise, &runtime, dispatcher, sharedFuture]() {
167+
// synchronously wait until the `future<T>` completes. we are running on a background task here.
158168
sharedFuture->wait();
159169

160170
// the async function completed successfully, resolve the promise on JS Thread
@@ -163,27 +173,27 @@ template <typename TResult> struct JSIConverter<std::future<TResult>> {
163173
if constexpr (std::is_same_v<TResult, void>) {
164174
// it's returning void, just return undefined to JS
165175
sharedFuture->get();
166-
promise->resolve(jsi::Value::undefined());
176+
promise->resolve(runtime, jsi::Value::undefined());
167177
} else {
168178
// it's returning a custom type, convert it to a jsi::Value
169179
TResult result = sharedFuture->get();
170180
jsi::Value jsResult = JSIConverter<TResult>::toJSI(runtime, result);
171-
promise->resolve(std::move(jsResult));
181+
promise->resolve(runtime, std::move(jsResult));
172182
}
173183
} catch (const std::exception& exception) {
174184
// the async function threw an error, reject the promise on JS Thread
175185
std::string what = exception.what();
176-
promise->reject(what);
186+
promise->reject(runtime, what);
177187
} catch (...) {
178188
// the async function threw a non-std error, try getting it
179189
#if __has_include(<cxxabi.h>)
180190
std::string name = __cxxabiv1::__cxa_current_exception_type()->name();
181191
#else
182-
std::string name = "<unknown>";
192+
std::string name = "<unknown>";
183193
#endif
184-
promise->reject("Unknown non-std exception: " + name);
194+
promise->reject(runtime, "Unknown non-std exception: " + name);
185195
}
186-
196+
187197
// This lambda owns the promise shared pointer, and we need to call its
188198
// destructor on the correct thread here - otherwise it might be called
189199
// from the waiterThread.
@@ -198,17 +208,31 @@ template <typename TResult> struct JSIConverter<std::future<TResult>> {
198208
// [](Args...) -> T {} <> (Args...) => T
199209
template <typename ReturnType, typename... Args> struct JSIConverter<std::function<ReturnType(Args...)>> {
200210
static std::function<ReturnType(Args...)> fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
211+
// Make function global - it'll be managed by the Runtime's memory, and we only have a weak_ref to it.
212+
auto cache = FunctionCache::getOrCreateCache(runtime).lock();
201213
jsi::Function function = arg.getObject(runtime).getFunction(runtime);
202-
203-
// TODO: Weakify this using a RuntimeWatch so it is safely managed by the Runtime, not by us.
204-
auto sharedFunction = std::make_shared<jsi::Function>(std::move(function));
214+
auto sharedFunction = cache->makeGlobal(std::move(function));
215+
216+
// Create a C++ function that can be called by the consumer.
217+
// This will call the jsi::Function if it is still alive.
205218
return [&runtime, sharedFunction](Args... args) -> ReturnType {
206-
jsi::Value result = sharedFunction->call(runtime, JSIConverter<std::decay_t<Args>>::toJSI(runtime, args)...);
207219
if constexpr (std::is_same_v<ReturnType, void>) {
208220
// it is a void function (returns undefined)
221+
auto function = sharedFunction.lock();
222+
if (!function) {
223+
// runtime has already been deleted. since this returns void, we can just ignore it being deleted.
224+
return;
225+
}
226+
function->call(runtime, JSIConverter<std::decay_t<Args>>::toJSI(runtime, args)...);
209227
return;
210228
} else {
211229
// it returns a custom type, parse it from the JSI value.
230+
auto function = sharedFunction.lock();
231+
if (!function) {
232+
// runtime has already been deleted. since we expect a return value here, we need to throw.
233+
throw std::runtime_error("Cannot call the given Function - the Runtime has already been destroyed!");
234+
}
235+
jsi::Value result = function->call(runtime, JSIConverter<std::decay_t<Args>>::toJSI(runtime, args)...);
212236
return JSIConverter<ReturnType>::fromJSI(runtime, std::move(result));
213237
}
214238
};
@@ -294,9 +318,7 @@ template <typename ValueType> struct JSIConverter<std::unordered_map<std::string
294318

295319
// HybridObject <> {}
296320
template <typename T> struct is_shared_ptr_to_host_object : std::false_type {};
297-
298321
template <typename T> struct is_shared_ptr_to_host_object<std::shared_ptr<T>> : std::is_base_of<jsi::HostObject, T> {};
299-
300322
template <typename T> struct JSIConverter<T, std::enable_if_t<is_shared_ptr_to_host_object<T>::value>> {
301323
using TPointee = typename T::element_type;
302324

@@ -354,9 +376,7 @@ template <typename T> struct JSIConverter<T, std::enable_if_t<is_shared_ptr_to_h
354376

355377
// NativeState <> {}
356378
template <typename T> struct is_shared_ptr_to_native_state : std::false_type {};
357-
358379
template <typename T> struct is_shared_ptr_to_native_state<std::shared_ptr<T>> : std::is_base_of<jsi::NativeState, T> {};
359-
360380
template <typename T> struct JSIConverter<T, std::enable_if_t<is_shared_ptr_to_native_state<T>::value>> {
361381
using TPointee = typename T::element_type;
362382

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// JSIUtils.h
3+
// NitroModules
4+
//
5+
// Created by Marc Rousavy on 20.06.24.
6+
//
7+
8+
#pragma once
9+
10+
#include <jsi/jsi.h>
11+
12+
namespace margelo {
13+
14+
static inline std::string getRuntimeId(jsi::Runtime& runtime) {
15+
return runtime.description();
16+
17+
// TODO: Do we wanna use address instead of description?
18+
// uint64_t address = reinterpret_cast<uint64_t>(&runtime);
19+
// return std::to_string(address);
20+
}
21+
22+
}

0 commit comments

Comments
 (0)