diff --git a/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp b/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp index 6187a585d..12f85ad54 100644 --- a/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp +++ b/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp @@ -2582,6 +2582,11 @@ void MetadataNode::SetMissingBaseMethods( } void MetadataNode::BuildMetadata(const std::string &filesPath) { + // Once per process: the static node caches point into this tree, and it + // outlives any one main runtime. + if (s_metadataReader.GetRoot() != nullptr) { + return; + } s_metadataReader = MetadataBuilder::BuildMetadata(filesPath); } diff --git a/NativeScript/runtime/android/jsi/EngineHost.h b/NativeScript/runtime/android/jsi/EngineHost.h index 31dffa0be..9a6016cdf 100644 --- a/NativeScript/runtime/android/jsi/EngineHost.h +++ b/NativeScript/runtime/android/jsi/EngineHost.h @@ -11,7 +11,7 @@ // EngineHost is held by shared_ptr and JSScope keeps a strong reference for its // lifetime. That is what makes teardown safe by construction: the VM, and the // recursive mutex the scope holds, cannot be freed while a scope is still -// unwinding over them. See Runtime::DisposeWorkerRuntime. +// unwinding over them. See Runtime::DisposeRuntime. #include #include @@ -58,7 +58,7 @@ class EngineHost { #if defined(NS_JSI_HOST_RUNTIME) // Adopts a runtime the embedder owns. The reference must outlive this host: // React Native tears its runtime down only after the TurboModule that holds - // us has been invalidated, which is what ReleaseEngineState is wired to. + // us has been invalidated, which is where Runtime::DisposeRuntime runs. static std::shared_ptr Adopt(::facebook::jsi::Runtime &runtime); #else static std::shared_ptr Create(); diff --git a/NativeScript/runtime/android/jsi/Runtime.cpp b/NativeScript/runtime/android/jsi/Runtime.cpp index 7991157d2..dd4306f25 100644 --- a/NativeScript/runtime/android/jsi/Runtime.cpp +++ b/NativeScript/runtime/android/jsi/Runtime.cpp @@ -546,6 +546,13 @@ Runtime::~Runtime() { if (m_mainLooper_fd[1] != -1) { close(m_mainLooper_fd[1]); } + + // A guest's main runtime is deleted when its host runtime goes away, and + // the next one attached may run on a different thread. + m_mainLooper = nullptr; + m_mainLooper_fd[0] = m_mainLooper_fd[1] = -1; + s_main_rt = nullptr; + s_mainThreadInitialized = false; } } @@ -605,6 +612,19 @@ void Runtime::DestroyRuntime() { // and ~EngineHost does the teardown once nothing is standing on it. } +void Runtime::DisposeRuntime(Runtime *runtime) { + // `engineHost` is referenced by the JSEnterScope macro below. + std::shared_ptr engineHost = runtime->GetEngineHost(); + { + JSEnterScope + runtime->DestroyRuntime(); + } + // Both this scope's reference and the runtime's are gone by the end of the + // next line, and whichever drops last runs ~EngineHost. Nothing frees the VM + // while the scope above is still unwinding over it. + delete runtime; +} + bool Runtime::NotifyGC(JNIEnv *jEnv, jobject obj, jintArray object_ids) { if (this->is_destroying) return true; m_objectManager->OnGarbageCollected(jEnv, object_ids); @@ -673,19 +693,6 @@ void Runtime::RunWorker(const std::string &filePath) { m_module.LoadWorker(engineHost->GetRuntime(), filePath); } -void Runtime::DisposeWorkerRuntime(Runtime *runtime) { - // `engineHost` is referenced by the JSEnterScope macro below. - std::shared_ptr engineHost = runtime->GetEngineHost(); - { - JSEnterScope - runtime->DestroyRuntime(); - } - // Both this scope's reference and the runtime's are gone by the end of the - // next line, and whichever drops last runs ~EngineHost. Nothing frees the VM - // while the scope above is still unwinding over it. - delete runtime; -} - jobject Runtime::RunScript(JNIEnv *_env, jobject obj, jstring scriptFile) { auto filename = ArgConverter::jstringToString(scriptFile); auto sourceUrl = ModuleInternal::EnsureFileProtocol(filename); diff --git a/NativeScript/runtime/android/jsi/Runtime.h b/NativeScript/runtime/android/jsi/Runtime.h index 3b41c83a4..5088e1dc7 100644 --- a/NativeScript/runtime/android/jsi/Runtime.h +++ b/NativeScript/runtime/android/jsi/Runtime.h @@ -104,11 +104,10 @@ namespace tns { void RunWorker(const std::string &filePath); #endif - // Tears down a worker's Runtime (engine scope + engine state release) and - // deletes it. Called from the native worker thread during shutdown. -#if !defined(NS_JSI_HOST_RUNTIME) - static void DisposeWorkerRuntime(Runtime *runtime); -#endif + // Tears down a Runtime inside its engine scope and deletes it. The engine + // must still be alive with no JS running on it, and nothing may reach + // `runtime` from another thread once this begins. + static void DisposeRuntime(Runtime *runtime); jobject RunScript(JNIEnv *_env, jobject obj, jstring scriptFile); diff --git a/NativeScript/runtime/android/jsi/workers/WorkerWrapper.cpp b/NativeScript/runtime/android/jsi/workers/WorkerWrapper.cpp index 5bd5b3b7f..86c26c576 100644 --- a/NativeScript/runtime/android/jsi/workers/WorkerWrapper.cpp +++ b/NativeScript/runtime/android/jsi/workers/WorkerWrapper.cpp @@ -422,10 +422,10 @@ void WorkerWrapper::BackgroundLooper(std::shared_ptr self) { workerRt_.store(nullptr); // Enters the engine scope, tears the runtime down and deletes it. - Runtime::DisposeWorkerRuntime(runtime_); + Runtime::DisposeRuntime(runtime_); runtime_ = nullptr; // The last reference this thread holds to the worker VM, and it is - // dropped only after DisposeWorkerRuntime's scope has fully unwound. + // dropped only after DisposeRuntime's scope has fully unwound. workerHost_.reset(); } @@ -529,7 +529,7 @@ void WorkerWrapper::EnsureJniCached() { env.GetStaticMethodID(RUNTIME_CLASS, "initWorkerRuntime", "(ILjava/lang/String;)I"); RUN_WORKER_LOOP_METHOD_ID = env.GetStaticMethodID(RUNTIME_CLASS, "runWorkerLoop", "()V"); DETACH_WORKER_RUNTIME_METHOD_ID = - env.GetStaticMethodID(RUNTIME_CLASS, "detachWorkerRuntime", "(I)V"); + env.GetStaticMethodID(RUNTIME_CLASS, "detachRuntime", "(I)V"); LOOPER_CLASS = env.FindClass("android/os/Looper"); assert(LOOPER_CLASS != nullptr); diff --git a/NativeScript/runtime/android/napi/Runtime.cpp b/NativeScript/runtime/android/napi/Runtime.cpp index 49fc0455f..40d3dd124 100644 --- a/NativeScript/runtime/android/napi/Runtime.cpp +++ b/NativeScript/runtime/android/napi/Runtime.cpp @@ -596,7 +596,7 @@ void Runtime::RunWorker(const std::string &filePath) { m_module.LoadWorker(env, filePath); } -void Runtime::DisposeWorkerRuntime(Runtime *runtime) { +void Runtime::DisposeRuntime(Runtime *runtime) { // `env` is referenced by the (engine-specific) JSEnterScope macro below. napi_env env = runtime->env; { diff --git a/NativeScript/runtime/android/napi/Runtime.h b/NativeScript/runtime/android/napi/Runtime.h index e3c78be88..83901f80e 100644 --- a/NativeScript/runtime/android/napi/Runtime.h +++ b/NativeScript/runtime/android/napi/Runtime.h @@ -84,7 +84,7 @@ namespace tns { // Tears down a worker's Runtime (engine scope + env free) and deletes // it. Called from the native worker thread during shutdown. - static void DisposeWorkerRuntime(Runtime *runtime); + static void DisposeRuntime(Runtime *runtime); jobject RunScript(JNIEnv *_env, jobject obj, jstring scriptFile); diff --git a/NativeScript/runtime/android/napi/workers/WorkerWrapper.cpp b/NativeScript/runtime/android/napi/workers/WorkerWrapper.cpp index 21fa72e02..c16efaa1e 100644 --- a/NativeScript/runtime/android/napi/workers/WorkerWrapper.cpp +++ b/NativeScript/runtime/android/napi/workers/WorkerWrapper.cpp @@ -502,7 +502,7 @@ void WorkerWrapper::BackgroundLooper(std::shared_ptr self) { // Enters the engine scope, tears down the runtime and frees the env, // then deletes the Runtime (which frees the underlying engine runtime). - Runtime::DisposeWorkerRuntime(runtime_); + Runtime::DisposeRuntime(runtime_); runtime_ = nullptr; } @@ -604,7 +604,7 @@ void WorkerWrapper::EnsureJniCached() { env.GetStaticMethodID(RUNTIME_CLASS, "initWorkerRuntime", "(ILjava/lang/String;)I"); RUN_WORKER_LOOP_METHOD_ID = env.GetStaticMethodID(RUNTIME_CLASS, "runWorkerLoop", "()V"); DETACH_WORKER_RUNTIME_METHOD_ID = - env.GetStaticMethodID(RUNTIME_CLASS, "detachWorkerRuntime", "(I)V"); + env.GetStaticMethodID(RUNTIME_CLASS, "detachRuntime", "(I)V"); LOOPER_CLASS = env.FindClass("android/os/Looper"); assert(LOOPER_CLASS != nullptr); diff --git a/packages/react-native/android/src/main/cpp/NativeScriptRnInstall.cpp b/packages/react-native/android/src/main/cpp/NativeScriptRnInstall.cpp index fcf76578f..f08e3c5f2 100644 --- a/packages/react-native/android/src/main/cpp/NativeScriptRnInstall.cpp +++ b/packages/react-native/android/src/main/cpp/NativeScriptRnInstall.cpp @@ -18,15 +18,6 @@ using namespace tns; -namespace { - -// Set once the first attach succeeds. React Native can reload the JS bundle, -// which builds a fresh jsi::Runtime; the module checks this before installing -// so a reload rebinds rather than stacking a second runtime on the first. -bool g_attached = false; - -} // namespace - extern "C" JNIEXPORT void JNICALL Java_com_tns_Runtime_attachNativeScript( JNIEnv* env, jobject obj, jint runtimeId, jstring metadataPath, jlong hostRuntimePtr, jboolean verboseLoggingEnabled, jint maxLogcatObjectSize, jboolean forceLog, @@ -58,8 +49,6 @@ extern "C" JNIEXPORT void JNICALL Java_com_tns_Runtime_attachNativeScript( runtime->GetEngineHost()->ExecuteScript(source, "internal/ts_helpers.js"); } } - - g_attached = true; } catch (NativeScriptException& e) { e.ReThrowToJava(nullptr); } catch (std::exception& e) { @@ -71,9 +60,19 @@ extern "C" JNIEXPORT void JNICALL Java_com_tns_Runtime_attachNativeScript( } } -extern "C" JNIEXPORT jboolean JNICALL -Java_org_nativescript_reactnative_NativeScriptNativeApiModule_nativeIsInstalled(JNIEnv*, jobject) { - return g_attached ? JNI_TRUE : JNI_FALSE; +extern "C" JNIEXPORT void JNICALL Java_com_tns_Runtime_detachNativeScript(JNIEnv*, jobject, + jint runtimeId) { + try { + Runtime::DisposeRuntime(Runtime::GetRuntime(runtimeId)); + } catch (NativeScriptException& e) { + e.ReThrowToJava(nullptr); + } catch (std::exception& e) { + NativeScriptException nsEx(std::string("Error: c++ exception: ") + e.what()); + nsEx.ReThrowToJava(nullptr); + } catch (...) { + NativeScriptException nsEx(std::string("Error: c++ exception!")); + nsEx.ReThrowToJava(nullptr); + } } extern "C" JNIEXPORT jstring JNICALL diff --git a/packages/react-native/android/src/main/java/org/nativescript/reactnative/NativeScriptNativeApiModule.java b/packages/react-native/android/src/main/java/org/nativescript/reactnative/NativeScriptNativeApiModule.java index d07d099b8..3b5e5508e 100644 --- a/packages/react-native/android/src/main/java/org/nativescript/reactnative/NativeScriptNativeApiModule.java +++ b/packages/react-native/android/src/main/java/org/nativescript/reactnative/NativeScriptNativeApiModule.java @@ -3,6 +3,7 @@ import android.content.Context; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.queue.MessageQueueThread; import com.facebook.react.module.annotations.ReactModule; import com.tns.Logger; @@ -23,6 +24,11 @@ * thread -- it is a synchronous TurboModule method, so React Native already * guarantees that. Everything it does happens against the runtime React Native * created; nothing here starts a JavaScript engine. + * + *

React Native creates one instance of this module per JavaScript runtime and + * invalidates it before destroying that runtime, which is where NativeScript + * lets go of it. A reloaded bundle installs into its new runtime through a new + * instance. */ @ReactModule(name = NativeScriptNativeApiModule.NAME) public class NativeScriptNativeApiModule extends NativeScriptNativeApiSpec { @@ -44,6 +50,8 @@ public class NativeScriptNativeApiModule extends NativeScriptNativeApiSpec { private final ReactApplicationContext reactContext; + private volatile Runtime attachedRuntime; + public NativeScriptNativeApiModule(ReactApplicationContext reactContext) { super(reactContext); this.reactContext = reactContext; @@ -54,13 +62,11 @@ public String getName() { return NAME; } - private native boolean nativeIsInstalled(); - private native String nativeRuntimeBackend(); @Override public boolean install(String metadataPath) { - if (nativeIsInstalled()) { + if (attachedRuntime != null) { return true; } @@ -88,10 +94,30 @@ public boolean install(String metadataPath) { + "JSI runtime and cannot run over the legacy bridge."); } - attachRuntime(context, runtimePtr, resolvedPath, readAsset(context, ASSET_TS_HELPERS)); + attachedRuntime = attachRuntime(context, runtimePtr, resolvedPath, + readAsset(context, ASSET_TS_HELPERS)); return true; } + @Override + public void invalidate() { + Runtime runtime = attachedRuntime; + attachedRuntime = null; + if (runtime != null) { + // Detaching releases handles into the jsi::Runtime, so it has to run + // before React Native destroys that runtime and never alongside JS. + // Bridgeless has already stopped the JS thread here, so the post is + // refused and this thread is safe. The bridge can invalidate from + // another thread while JS still runs, and queues the runtime's + // destruction on the JS thread behind this. + MessageQueueThread jsThread = reactContext.getJSMessageQueueThread(); + if (jsThread == null || jsThread.isOnThread() || !jsThread.runOnQueue(runtime::detach)) { + runtime.detach(); + } + } + super.invalidate(); + } + @Override public boolean installWorkletRuntime(com.facebook.react.bridge.ReadableMap runtimeHolder, String metadataPath) { @@ -104,7 +130,7 @@ public boolean installWorkletRuntime(com.facebook.react.bridge.ReadableMap runti @Override public boolean isInstalled() { - return nativeIsInstalled(); + return attachedRuntime != null; } /** @@ -138,8 +164,8 @@ public boolean __writeTestMarker(String content) { } } - private static void attachRuntime(Context context, long runtimePtr, String metadataPath, - String bootstrapScript) { + private static Runtime attachRuntime(Context context, long runtimePtr, String metadataPath, + String bootstrapScript) { File filesDir = context.getFilesDir(); File dexDir = new File(filesDir, "nativescript-dex"); if (!dexDir.exists() && !dexDir.mkdirs()) { @@ -148,7 +174,7 @@ private static void attachRuntime(Context context, long runtimePtr, String metad Logger logger = new NativeScriptLogger(isDebuggable(context)); - Runtime.attachToHostRuntime( + return Runtime.attachToHostRuntime( logger, context.getPackageName(), context.getApplicationInfo().nativeLibraryDir, diff --git a/platforms/android/test-app/runtime/src/main/java/com/tns/Runtime.java b/platforms/android/test-app/runtime/src/main/java/com/tns/Runtime.java index 739c10824..f08f4d87d 100644 --- a/platforms/android/test-app/runtime/src/main/java/com/tns/Runtime.java +++ b/platforms/android/test-app/runtime/src/main/java/com/tns/Runtime.java @@ -53,6 +53,9 @@ private native void attachNativeScript(int runtimeId, String metadataPath, long boolean verboseLoggingEnabled, int maxLogcatObjectSize, boolean forceLog, String bootstrapScript); + // Implemented alongside attachNativeScript. + private native void detachNativeScript(int runtimeId); + private native void runModule(int runtimeId, String filePath) throws NativeScriptException; private native Object runScript(int runtimeId, String filePath) throws NativeScriptException; @@ -520,6 +523,15 @@ private void attach(long hostRuntimePtr, String metadataPath, String bootstrapSc initialized = true; } + /* + Unbinds from the embedder's runtime and frees what NativeScript holds in + it. Must run while that runtime is still alive and not running JS. + */ + public void detach() { + detachRuntime(runtimeId); + detachNativeScript(runtimeId); + } + /* Called via JNI from a native-spawned worker thread (WorkerWrapper), after the thread has attached to the JVM and before the worker script runs. @@ -562,11 +574,11 @@ public static void runWorkerLoop() { } /* - Called via JNI from the native worker thread during shutdown, before the - worker's runtime is disposed. + Called before a runtime is disposed: via JNI from the native worker + thread during shutdown, and by detach(). */ @RuntimeCallable - public static void detachWorkerRuntime(int runtimeId) { + public static void detachRuntime(int runtimeId) { Runtime runtime = runtimeCache.remove(runtimeId); if (runtime != null) { GcListener.unsubscribe(runtime);