Skip to content

Commit bc7c85f

Browse files
RSNarafacebook-github-bot
authored andcommitted
Delete jsi::Functions before jsi::Runtime gets deleted
Summary: ## The Problem 1. `CatalystInstanceImpl` indirectly holds on to the `jsi::Runtime`. When you destroy `CatalystInstanceImpl`, you destroy the `jsi::Runtime`. As a part of reloading React Native, we destroy and re-create `CatalystInstanceImpl`, which destroys and re-creates the `jsi::Runtime`. 2. When JS passes in a callback to a TurboModule method, we take that callback (a `jsi::Function`) and wrap it in a Java `Callback` (implemented by `JCxxCallbackImpl`). This Java `Callback`, when executed, schedules the `jsi::Function` to be invoked on a Java thread at a later point in time. **Note:** The Java NativeModule can hold on to the Java `Callback` (and, by transitivity, the `jsi::Function`) for potentially forever. 3. It is a requirement of `jsi::Runtime` that all objects associated with the Runtime (ex: `jsi::Function`) must be destroyed before the Runtime itself is destroyed. See: https://fburl.com/m3mqk6wt ### jsi.h ``` /// .................................................... In addition, to /// make shutdown safe, destruction of objects associated with the Runtime /// must be destroyed before the Runtime is destroyed, or from the /// destructor of a managed HostObject or HostFunction. Informally, this /// means that the main source of unsafe behavior is to hold a jsi object /// in a non-Runtime-managed object, and not clean it up before the Runtime /// is shut down. If your lifecycle is such that avoiding this is hard, /// you will probably need to do use your own locks. class Runtime { public: virtual ~Runtime(); ``` Therefore, when you delete `CatalystInstanceImpl`, you could end up with a situation where the `jsi::Runtime` is destroyed before all `jsi::Function`s are destroyed. In dev, this leads the program to crash when you reload the app after having used a TurboModule method that uses callbacks. ## The Solution If the only reference to a `HostObject` or a `HostFunction` is in the JS Heap, then the `HostObject` and `HostFunction` destructors can destroy JSI objects. The TurboModule cache is the only thing, aside from the JS Heap, that holds a reference to all C++ TurboModules. But that cache (and the entire native side of `TurboModuleManager`) is destroyed when we call `mHybridData.resetNative()` in `TurboModuleManager.onCatalystInstanceDestroy()` in D16552730. (I verified this by commenting out `mHybridData.resetNative()` and placing a breakpoint in the destructor of `JavaTurboModule`). So, when we're cleaning up `TurboModuleManager`, the only reference to a Java TurboModule is the JS Heap. Therefore, it's safe and correct for us to destroy all `jsi::Function`s created by the Java TurboModule in `~JavaTurboModule`. So, in this diff, I keep a set of all `CallbackWrappers`, and explicitly call `destroy()` on them in the `JavaTurboModule` destructor. Note that since `~JavaTurboModule` accesses `callbackWrappers_`, it must be executed on the JS Thread, since `createJavaCallbackFromJSIFunction` also accesses `callbackWrappers_` on the JS Thread. For additional safety, I also eagerly destroyed the `jsi::Function` after it's been invoked once. I'm not yet sure if we only want JS callbacks to only ever be invoked once. So, I've created a Task to document this work: T48128233. Reviewed By: mdvacca Differential Revision: D16623340 fbshipit-source-id: 3a4c3efc70b9b3c8d329f19fdf4b4423c489695b
1 parent 7c0b735 commit bc7c85f

File tree

7 files changed

+378
-148
lines changed

7 files changed

+378
-148
lines changed

ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec;
2424
import com.facebook.react.common.ReactConstants;
2525
import com.facebook.react.common.annotations.VisibleForTesting;
26+
import com.facebook.react.config.ReactFeatureFlags;
2627
import com.facebook.react.module.annotations.ReactModule;
2728
import com.facebook.react.turbomodule.core.JSCallInvokerHolderImpl;
2829
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
@@ -359,24 +360,56 @@ public void run() {
359360
listener.onBridgeDestroyed();
360361
}
361362
}
362-
AsyncTask.execute(
363-
new Runnable() {
364-
@Override
365-
public void run() {
366-
// Kill non-UI threads from neutral third party
367-
// potentially expensive, so don't run on UI thread
368-
369-
// contextHolder is used as a lock to guard against other users of the JS VM
370-
// having
371-
// the VM destroyed underneath them, so notify them before we resetNative
372-
mJavaScriptContextHolder.clear();
373-
374-
mHybridData.resetNative();
375-
getReactQueueConfiguration().destroy();
376-
Log.d(ReactConstants.TAG, "CatalystInstanceImpl.destroy() end");
377-
ReactMarker.logMarker(ReactMarkerConstants.DESTROY_CATALYST_INSTANCE_END);
378-
}
379-
});
363+
364+
final JSIModule turboModuleManager =
365+
ReactFeatureFlags.useTurboModules
366+
? mJSIModuleRegistry.getModule(JSIModuleType.TurboModuleManager)
367+
: null;
368+
369+
getReactQueueConfiguration()
370+
.getJSQueueThread()
371+
.runOnQueue(
372+
new Runnable() {
373+
@Override
374+
public void run() {
375+
// We need to destroy the TurboModuleManager on the JS Thread
376+
if (turboModuleManager != null) {
377+
turboModuleManager.onCatalystInstanceDestroy();
378+
}
379+
380+
getReactQueueConfiguration()
381+
.getUIQueueThread()
382+
.runOnQueue(
383+
new Runnable() {
384+
@Override
385+
public void run() {
386+
// AsyncTask.execute must be executed from the UI Thread
387+
AsyncTask.execute(
388+
new Runnable() {
389+
@Override
390+
public void run() {
391+
// Kill non-UI threads from neutral third party
392+
// potentially expensive, so don't run on UI thread
393+
394+
// contextHolder is used as a lock to guard against
395+
// other users of the JS VM having the VM destroyed
396+
// underneath them, so notify them before we reset
397+
// Native
398+
mJavaScriptContextHolder.clear();
399+
400+
mHybridData.resetNative();
401+
getReactQueueConfiguration().destroy();
402+
Log.d(
403+
ReactConstants.TAG,
404+
"CatalystInstanceImpl.destroy() end");
405+
ReactMarker.logMarker(
406+
ReactMarkerConstants.DESTROY_CATALYST_INSTANCE_END);
407+
}
408+
});
409+
}
410+
});
411+
}
412+
});
380413
}
381414
});
382415

ReactAndroid/src/main/java/com/facebook/react/bridge/JSIModuleRegistry.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,15 @@ public void registerModules(List<JSIModuleSpec> jsiModules) {
3232
}
3333

3434
public void notifyJSInstanceDestroy() {
35-
for (JSIModuleHolder moduleHolder : mModules.values()) {
35+
for (Map.Entry<JSIModuleType, JSIModuleHolder> entry : mModules.entrySet()) {
36+
JSIModuleType moduleType = entry.getKey();
37+
38+
// Don't call TurboModuleManager.onCatalystInstanceDestroy
39+
if (moduleType == JSIModuleType.TurboModuleManager) {
40+
continue;
41+
}
42+
43+
JSIModuleHolder moduleHolder = entry.getValue();
3644
moduleHolder.notifyJSInstanceDestroy();
3745
}
3846
}

ReactCommon/turbomodule/core/TurboCxxModule.cpp

Lines changed: 71 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,35 +22,49 @@ static CxxModule::Callback makeTurboCxxModuleCallback(
2222
jsi::Runtime &runtime,
2323
std::shared_ptr<CallbackWrapper> callbackWrapper) {
2424
return [callbackWrapper](std::vector<folly::dynamic> args) {
25-
callbackWrapper->jsInvoker->invokeAsync([callbackWrapper, args]() {
25+
callbackWrapper->jsInvoker().invokeAsync([callbackWrapper, args]() {
2626
std::vector<jsi::Value> innerArgs;
2727
for (auto &a : args) {
28-
innerArgs.push_back(jsi::valueFromDynamic(callbackWrapper->runtime, a));
28+
innerArgs.push_back(
29+
jsi::valueFromDynamic(callbackWrapper->runtime(), a));
2930
}
30-
callbackWrapper->callback.call(callbackWrapper->runtime, (const jsi::Value *)innerArgs.data(), innerArgs.size());
31+
callbackWrapper->callback().call(
32+
callbackWrapper->runtime(),
33+
(const jsi::Value *)innerArgs.data(),
34+
innerArgs.size());
3135
});
3236
};
3337
}
3438

35-
TurboCxxModule::TurboCxxModule(std::unique_ptr<CxxModule> cxxModule, std::shared_ptr<JSCallInvoker> jsInvoker)
36-
: TurboModule(cxxModule->getName(), jsInvoker),
37-
cxxMethods_(cxxModule->getMethods()),
38-
cxxModule_(std::move(cxxModule)) {}
39+
TurboCxxModule::TurboCxxModule(
40+
std::unique_ptr<CxxModule> cxxModule,
41+
std::shared_ptr<JSCallInvoker> jsInvoker)
42+
: TurboModule(cxxModule->getName(), jsInvoker),
43+
cxxMethods_(cxxModule->getMethods()),
44+
cxxModule_(std::move(cxxModule)) {}
3945

40-
jsi::Value TurboCxxModule::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) {
46+
jsi::Value TurboCxxModule::get(
47+
jsi::Runtime &runtime,
48+
const jsi::PropNameID &propName) {
4149
std::string propNameUtf8 = propName.utf8(runtime);
4250

4351
if (propNameUtf8 == "getConstants") {
44-
// This is special cased because `getConstants()` is already a part of CxxModule.
52+
// This is special cased because `getConstants()` is already a part of
53+
// CxxModule.
4554
return jsi::Function::createFromHostFunction(
4655
runtime,
4756
propName,
4857
0,
49-
[this](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) {
58+
[this](
59+
jsi::Runtime &rt,
60+
const jsi::Value &thisVal,
61+
const jsi::Value *args,
62+
size_t count) {
5063
jsi::Object result(rt);
5164
auto constants = cxxModule_->getConstants();
5265
for (auto &pair : constants) {
53-
result.setProperty(rt, pair.first.c_str(), jsi::valueFromDynamic(rt, pair.second));
66+
result.setProperty(
67+
rt, pair.first.c_str(), jsi::valueFromDynamic(rt, pair.second));
5468
}
5569
return result;
5670
});
@@ -62,7 +76,11 @@ jsi::Value TurboCxxModule::get(jsi::Runtime& runtime, const jsi::PropNameID& pro
6276
runtime,
6377
propName,
6478
0,
65-
[this, propNameUtf8](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) {
79+
[this, propNameUtf8](
80+
jsi::Runtime &rt,
81+
const jsi::Value &thisVal,
82+
const jsi::Value *args,
83+
size_t count) {
6684
return invokeMethod(rt, VoidKind, propNameUtf8, args, count);
6785
});
6886
}
@@ -77,7 +95,6 @@ jsi::Value TurboCxxModule::invokeMethod(
7795
const std::string &methodName,
7896
const jsi::Value *args,
7997
size_t count) {
80-
8198
auto it = cxxMethods_.begin();
8299
for (; it != cxxMethods_.end(); it++) {
83100
auto method = *it;
@@ -87,7 +104,8 @@ jsi::Value TurboCxxModule::invokeMethod(
87104
}
88105

89106
if (it == cxxMethods_.end()) {
90-
throw std::runtime_error("Function '" + methodName + "' cannot be found on cxxmodule: " + name_);
107+
throw std::runtime_error(
108+
"Function '" + methodName + "' cannot be found on cxxmodule: " + name_);
91109
}
92110

93111
auto method = *it;
@@ -97,23 +115,37 @@ jsi::Value TurboCxxModule::invokeMethod(
97115
for (size_t i = 0; i < count; i++) {
98116
innerArgs.push_back(jsi::dynamicFromValue(runtime, args[i]));
99117
}
100-
return jsi::valueFromDynamic(runtime, method.syncFunc(std::move(innerArgs)));
118+
return jsi::valueFromDynamic(
119+
runtime, method.syncFunc(std::move(innerArgs)));
101120
} else if (method.func && !method.isPromise) {
102121
// Async method.
103122
CxxModule::Callback first;
104123
CxxModule::Callback second;
105124

106125
if (count < method.callbacks) {
107-
throw std::invalid_argument(folly::to<std::string>("Expected ", method.callbacks,
108-
" callbacks, but only ", count, " parameters provided"));
126+
throw std::invalid_argument(folly::to<std::string>(
127+
"Expected ",
128+
method.callbacks,
129+
" callbacks, but only ",
130+
count,
131+
" parameters provided"));
109132
}
110133

111134
if (method.callbacks == 1) {
112-
auto wrapper = std::make_shared<CallbackWrapper>(args[count - 1].getObject(runtime).getFunction(runtime), runtime, jsInvoker_);
135+
auto wrapper = std::make_shared<CallbackWrapper>(
136+
args[count - 1].getObject(runtime).getFunction(runtime),
137+
runtime,
138+
jsInvoker_);
113139
first = makeTurboCxxModuleCallback(runtime, wrapper);
114140
} else if (method.callbacks == 2) {
115-
auto wrapper1 = std::make_shared<CallbackWrapper>(args[count - 2].getObject(runtime).getFunction(runtime), runtime, jsInvoker_);
116-
auto wrapper2 = std::make_shared<CallbackWrapper>(args[count - 1].getObject(runtime).getFunction(runtime), runtime, jsInvoker_);
141+
auto wrapper1 = std::make_shared<CallbackWrapper>(
142+
args[count - 2].getObject(runtime).getFunction(runtime),
143+
runtime,
144+
jsInvoker_);
145+
auto wrapper2 = std::make_shared<CallbackWrapper>(
146+
args[count - 1].getObject(runtime).getFunction(runtime),
147+
runtime,
148+
jsInvoker_);
117149
first = makeTurboCxxModuleCallback(runtime, wrapper1);
118150
second = makeTurboCxxModuleCallback(runtime, wrapper2);
119151
}
@@ -125,19 +157,26 @@ jsi::Value TurboCxxModule::invokeMethod(
125157

126158
method.func(std::move(innerArgs), first, second);
127159
} else if (method.isPromise) {
128-
return createPromiseAsJSIValue(runtime, [method, args, count, this](jsi::Runtime &rt, std::shared_ptr<Promise> promise) {
129-
auto resolveWrapper = std::make_shared<CallbackWrapper>(promise->resolve_.getFunction(rt), rt, jsInvoker_);
130-
auto rejectWrapper = std::make_shared<CallbackWrapper>(promise->reject_.getFunction(rt), rt, jsInvoker_);
131-
CxxModule::Callback resolve = makeTurboCxxModuleCallback(rt, resolveWrapper);
132-
CxxModule::Callback reject = makeTurboCxxModuleCallback(rt, rejectWrapper);
133-
134-
auto innerArgs = folly::dynamic::array();
135-
for (size_t i = 0; i < count; i++) {
136-
innerArgs.push_back(jsi::dynamicFromValue(rt, args[i]));
137-
}
160+
return createPromiseAsJSIValue(
161+
runtime,
162+
[method, args, count, this](
163+
jsi::Runtime &rt, std::shared_ptr<Promise> promise) {
164+
auto resolveWrapper = std::make_shared<CallbackWrapper>(
165+
promise->resolve_.getFunction(rt), rt, jsInvoker_);
166+
auto rejectWrapper = std::make_shared<CallbackWrapper>(
167+
promise->reject_.getFunction(rt), rt, jsInvoker_);
168+
CxxModule::Callback resolve =
169+
makeTurboCxxModuleCallback(rt, resolveWrapper);
170+
CxxModule::Callback reject =
171+
makeTurboCxxModuleCallback(rt, rejectWrapper);
172+
173+
auto innerArgs = folly::dynamic::array();
174+
for (size_t i = 0; i < count; i++) {
175+
innerArgs.push_back(jsi::dynamicFromValue(rt, args[i]));
176+
}
138177

139-
method.func(std::move(innerArgs), resolve, reject);
140-
});
178+
method.func(std::move(innerArgs), resolve, reject);
179+
});
141180
}
142181

143182
return jsi::Value::undefined();

ReactCommon/turbomodule/core/TurboModuleUtils.h

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77

88
#pragma once
99

10+
#include <cassert>
1011
#include <string>
1112

13+
#include <folly/Optional.h>
1214
#include <jsi/jsi.h>
1315

1416
#include <ReactCommon/JSCallInvoker.h>
@@ -32,18 +34,61 @@ struct Promise {
3234
jsi::Function reject_;
3335
};
3436

35-
using PromiseSetupFunctionType = std::function<void(jsi::Runtime &rt, std::shared_ptr<Promise>)>;
36-
jsi::Value createPromiseAsJSIValue(jsi::Runtime &rt, const PromiseSetupFunctionType func);
37+
using PromiseSetupFunctionType =
38+
std::function<void(jsi::Runtime &rt, std::shared_ptr<Promise>)>;
39+
jsi::Value createPromiseAsJSIValue(
40+
jsi::Runtime &rt,
41+
const PromiseSetupFunctionType func);
3742

3843
// Helper for passing jsi::Function arg to other methods.
39-
struct CallbackWrapper {
40-
CallbackWrapper(jsi::Function callback, jsi::Runtime &runtime, std::shared_ptr<react::JSCallInvoker> jsInvoker)
41-
: callback(std::move(callback)),
42-
runtime(runtime),
43-
jsInvoker(jsInvoker) {}
44-
jsi::Function callback;
45-
jsi::Runtime &runtime;
46-
std::shared_ptr<react::JSCallInvoker> jsInvoker;
44+
class CallbackWrapper {
45+
private:
46+
struct Data {
47+
Data(
48+
jsi::Function callback,
49+
jsi::Runtime &runtime,
50+
std::shared_ptr<react::JSCallInvoker> jsInvoker)
51+
: callback(std::move(callback)),
52+
runtime(runtime),
53+
jsInvoker(std::move(jsInvoker)) {}
54+
55+
jsi::Function callback;
56+
jsi::Runtime &runtime;
57+
std::shared_ptr<react::JSCallInvoker> jsInvoker;
58+
};
59+
60+
folly::Optional<Data> data_;
61+
62+
public:
63+
CallbackWrapper(
64+
jsi::Function callback,
65+
jsi::Runtime &runtime,
66+
std::shared_ptr<react::JSCallInvoker> jsInvoker)
67+
: data_(Data{std::move(callback), runtime, jsInvoker}) {}
68+
69+
// Delete the enclosed jsi::Function
70+
void destroy() {
71+
data_ = folly::none;
72+
}
73+
74+
bool isDestroyed() {
75+
return !data_.hasValue();
76+
}
77+
78+
jsi::Function &callback() {
79+
assert(!isDestroyed());
80+
return data_->callback;
81+
}
82+
83+
jsi::Runtime &runtime() {
84+
assert(!isDestroyed());
85+
return data_->runtime;
86+
}
87+
88+
react::JSCallInvoker &jsInvoker() {
89+
assert(!isDestroyed());
90+
return *(data_->jsInvoker);
91+
}
4792
};
4893

4994
} // namespace react

0 commit comments

Comments
 (0)