Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
4 changes: 2 additions & 2 deletions NativeScript/runtime/android/jsi/EngineHost.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <memory>
#include <mutex>
Expand Down Expand Up @@ -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<EngineHost> Adopt(::facebook::jsi::Runtime &runtime);
#else
static std::shared_ptr<EngineHost> Create();
Expand Down
33 changes: 20 additions & 13 deletions NativeScript/runtime/android/jsi/Runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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> 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);
Expand Down Expand Up @@ -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> 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);
Expand Down
9 changes: 4 additions & 5 deletions NativeScript/runtime/android/jsi/Runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
6 changes: 3 additions & 3 deletions NativeScript/runtime/android/jsi/workers/WorkerWrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -422,10 +422,10 @@ void WorkerWrapper::BackgroundLooper(std::shared_ptr<WorkerWrapper> 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();
}

Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion NativeScript/runtime/android/napi/Runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
{
Expand Down
2 changes: 1 addition & 1 deletion NativeScript/runtime/android/napi/Runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
4 changes: 2 additions & 2 deletions NativeScript/runtime/android/napi/workers/WorkerWrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ void WorkerWrapper::BackgroundLooper(std::shared_ptr<WorkerWrapper> 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;
}

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*
* <p>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 {
Expand All @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -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) {
Expand All @@ -104,7 +130,7 @@ public boolean installWorkletRuntime(com.facebook.react.bridge.ReadableMap runti

@Override
public boolean isInstalled() {
return nativeIsInstalled();
return attachedRuntime != null;
}

/**
Expand Down Expand Up @@ -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()) {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
Loading