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
This commit is contained in:
Ramanpreet Nara
2019-08-02 17:08:19 -07:00
committed by Facebook Github Bot
parent 7c0b73501e
commit bc7c85f153
7 changed files with 375 additions and 145 deletions
@@ -23,6 +23,7 @@ import com.facebook.react.bridge.queue.ReactQueueConfigurationImpl;
import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.turbomodule.core.JSCallInvokerHolderImpl;
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
@@ -359,24 +360,56 @@ public class CatalystInstanceImpl implements CatalystInstance {
listener.onBridgeDestroyed();
}
}
AsyncTask.execute(
new Runnable() {
@Override
public void run() {
// Kill non-UI threads from neutral third party
// potentially expensive, so don't run on UI thread
// contextHolder is used as a lock to guard against other users of the JS VM
// having
// the VM destroyed underneath them, so notify them before we resetNative
mJavaScriptContextHolder.clear();
final JSIModule turboModuleManager =
ReactFeatureFlags.useTurboModules
? mJSIModuleRegistry.getModule(JSIModuleType.TurboModuleManager)
: null;
mHybridData.resetNative();
getReactQueueConfiguration().destroy();
Log.d(ReactConstants.TAG, "CatalystInstanceImpl.destroy() end");
ReactMarker.logMarker(ReactMarkerConstants.DESTROY_CATALYST_INSTANCE_END);
}
});
getReactQueueConfiguration()
.getJSQueueThread()
.runOnQueue(
new Runnable() {
@Override
public void run() {
// We need to destroy the TurboModuleManager on the JS Thread
if (turboModuleManager != null) {
turboModuleManager.onCatalystInstanceDestroy();
}
getReactQueueConfiguration()
.getUIQueueThread()
.runOnQueue(
new Runnable() {
@Override
public void run() {
// AsyncTask.execute must be executed from the UI Thread
AsyncTask.execute(
new Runnable() {
@Override
public void run() {
// Kill non-UI threads from neutral third party
// potentially expensive, so don't run on UI thread
// contextHolder is used as a lock to guard against
// other users of the JS VM having the VM destroyed
// underneath them, so notify them before we reset
// Native
mJavaScriptContextHolder.clear();
mHybridData.resetNative();
getReactQueueConfiguration().destroy();
Log.d(
ReactConstants.TAG,
"CatalystInstanceImpl.destroy() end");
ReactMarker.logMarker(
ReactMarkerConstants.DESTROY_CATALYST_INSTANCE_END);
}
});
}
});
}
});
}
});
@@ -32,7 +32,15 @@ public class JSIModuleRegistry {
}
public void notifyJSInstanceDestroy() {
for (JSIModuleHolder moduleHolder : mModules.values()) {
for (Map.Entry<JSIModuleType, JSIModuleHolder> entry : mModules.entrySet()) {
JSIModuleType moduleType = entry.getKey();
// Don't call TurboModuleManager.onCatalystInstanceDestroy
if (moduleType == JSIModuleType.TurboModuleManager) {
continue;
}
JSIModuleHolder moduleHolder = entry.getValue();
moduleHolder.notifyJSInstanceDestroy();
}
}