Skip to content

Commit 6e61465

Browse files
committed
fix: resolve memory management issues in JNI bindings, JSI lambdas, and delegate wrappers
1 parent 36f2432 commit 6e61465

File tree

5 files changed

+102
-67
lines changed

5 files changed

+102
-67
lines changed

packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxReactNativeDelegate.kt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class SandboxReactNativeDelegate(
5959
private var jsiStateHandle: Long = 0
6060
private var sandboxReactContext: ReactContext? = null
6161
private var ownsReactHost = false
62+
private var instanceEventListener: ReactInstanceEventListener? = null
6263

6364
@OptIn(UnstableReactNativeAPI::class)
6465
fun loadReactNativeView(
@@ -86,8 +87,7 @@ class SandboxReactNativeDelegate(
8687
ownsReactHost = false
8788
Log.d(TAG, "Reusing shared ReactHost for origin '$origin' (refCount=${shared.refCount})")
8889
} else {
89-
val sandboxId = System.identityHashCode(this).toString(16)
90-
sandboxContext = SandboxContextWrapper(context, sandboxId)
90+
sandboxContext = SandboxContextWrapper(context, origin)
9191

9292
val packages: List<ReactPackage> =
9393
listOf(
@@ -132,7 +132,7 @@ class SandboxReactNativeDelegate(
132132

133133
reactHost = host
134134

135-
host.addReactInstanceEventListener(
135+
val listener =
136136
object : ReactInstanceEventListener {
137137
override fun onReactContextInitialized(reactContext: ReactContext) {
138138
sandboxReactContext = reactContext
@@ -142,8 +142,9 @@ class SandboxReactNativeDelegate(
142142
}
143143
}
144144
}
145-
},
146-
)
145+
}
146+
instanceEventListener = listener
147+
host.addReactInstanceEventListener(listener)
147148

148149
val surface = host.createSurface(sandboxContext, componentName, initialProperties)
149150
reactSurface = surface
@@ -304,6 +305,10 @@ class SandboxReactNativeDelegate(
304305
reactSurface = null
305306

306307
val host = reactHost
308+
instanceEventListener?.let { listener ->
309+
host?.removeReactInstanceEventListener(listener)
310+
}
311+
instanceEventListener = null
307312
if (host != null) {
308313
if (origin.isNotEmpty()) {
309314
val shared = sharedHosts[origin]

packages/react-native-sandbox/android/src/main/jni/SandboxJSIInstaller.cpp

Lines changed: 60 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,46 @@ struct SandboxJSIState {
3333
static std::mutex gRegistryMutex;
3434
static std::unordered_map<jlong, std::shared_ptr<SandboxJSIState>> gStates;
3535

36+
static JNIEnv* getJNIEnv() {
37+
JNIEnv* env = nullptr;
38+
if (gJavaVM) {
39+
gJavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
40+
if (!env) {
41+
gJavaVM->AttachCurrentThread(&env, nullptr);
42+
}
43+
}
44+
return env;
45+
}
46+
3647
/**
3748
* ISandboxDelegate that dispatches postMessage through the Kotlin delegate
3849
* via JNI. The Kotlin postMessage uses runOnJSQueueThread to safely access
3950
* the JSI runtime on the correct thread.
51+
*
52+
* Holds its own JNI global reference which must be released via invalidate().
4053
*/
4154
class JNISandboxDelegate : public rnsandbox::ISandboxDelegate {
4255
public:
43-
JNISandboxDelegate(jobject globalDelegateRef)
44-
: globalDelegateRef_(globalDelegateRef) {}
56+
explicit JNISandboxDelegate(JNIEnv* env, jobject delegateRef)
57+
: globalDelegateRef_(env->NewGlobalRef(delegateRef)) {}
58+
59+
~JNISandboxDelegate() override {
60+
invalidate();
61+
}
62+
63+
void invalidate() {
64+
std::lock_guard<std::mutex> lock(mutex_);
65+
if (globalDelegateRef_) {
66+
JNIEnv* env = getJNIEnv();
67+
if (env) {
68+
env->DeleteGlobalRef(globalDelegateRef_);
69+
}
70+
globalDelegateRef_ = nullptr;
71+
}
72+
}
4573

4674
void postMessage(const std::string& message) override {
75+
std::lock_guard<std::mutex> lock(mutex_);
4776
JNIEnv* env = getJNIEnv();
4877
if (!env || !globalDelegateRef_)
4978
return;
@@ -74,30 +103,9 @@ class JNISandboxDelegate : public rnsandbox::ISandboxDelegate {
74103

75104
private:
76105
jobject globalDelegateRef_;
77-
78-
static JNIEnv* getJNIEnv() {
79-
JNIEnv* env = nullptr;
80-
if (gJavaVM) {
81-
gJavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
82-
if (!env) {
83-
gJavaVM->AttachCurrentThread(&env, nullptr);
84-
}
85-
}
86-
return env;
87-
}
106+
std::mutex mutex_;
88107
};
89108

90-
static JNIEnv* getJNIEnv() {
91-
JNIEnv* env = nullptr;
92-
if (gJavaVM) {
93-
gJavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
94-
if (!env) {
95-
gJavaVM->AttachCurrentThread(&env, nullptr);
96-
}
97-
}
98-
return env;
99-
}
100-
101109
static std::string safeGetStringProperty(
102110
jsi::Runtime& rt,
103111
const jsi::Object& obj,
@@ -124,7 +132,7 @@ stubJsiFunction(jsi::Runtime& runtime, jsi::Object& object, const char* name) {
124132

125133
static void setupErrorHandler(
126134
jsi::Runtime& runtime,
127-
jobject globalDelegateRef) {
135+
std::weak_ptr<SandboxJSIState> stateWeak) {
128136
jsi::Object global = runtime.global();
129137
jsi::Value errorUtilsVal = global.getProperty(runtime, "ErrorUtils");
130138
if (!errorUtilsVal.isObject())
@@ -142,23 +150,27 @@ static void setupErrorHandler(
142150
runtime,
143151
jsi::PropNameID::forAscii(runtime, "sandboxGlobalErrorHandler"),
144152
2,
145-
[globalDelegateRef, originalHandler = std::move(originalHandler)](
153+
[stateWeak, originalHandler = std::move(originalHandler)](
146154
jsi::Runtime& rt,
147155
const jsi::Value&,
148156
const jsi::Value* args,
149157
size_t count) -> jsi::Value {
150158
if (count < 2)
151159
return jsi::Value::undefined();
152160

161+
auto state = stateWeak.lock();
162+
if (!state || !state->delegateRef)
163+
return jsi::Value::undefined();
164+
153165
JNIEnv* jniEnv = getJNIEnv();
154166
if (!jniEnv)
155167
return jsi::Value::undefined();
156168

157-
jclass cls = jniEnv->GetObjectClass(globalDelegateRef);
169+
jclass cls = jniEnv->GetObjectClass(state->delegateRef);
158170
jfieldID hasHandlerField =
159171
jniEnv->GetFieldID(cls, "hasOnErrorHandler", "Z");
160172
jboolean hasHandler =
161-
jniEnv->GetBooleanField(globalDelegateRef, hasHandlerField);
173+
jniEnv->GetBooleanField(state->delegateRef, hasHandlerField);
162174

163175
if (hasHandler) {
164176
const jsi::Object& error = args[0].asObject(rt);
@@ -175,7 +187,7 @@ static void setupErrorHandler(
175187
jstring jMsg = jniEnv->NewStringUTF(message.c_str());
176188
jstring jStack = jniEnv->NewStringUTF(stack.c_str());
177189
jniEnv->CallVoidMethod(
178-
globalDelegateRef,
190+
state->delegateRef,
179191
emitMethod,
180192
jName,
181193
jMsg,
@@ -223,7 +235,7 @@ jlong installSandboxJSIBindings(
223235
runtime,
224236
jsi::PropNameID::forAscii(runtime, "postMessage"),
225237
2,
226-
[globalDelegateRef](
238+
[stateWeak = std::weak_ptr<SandboxJSIState>(state)](
227239
jsi::Runtime& rt,
228240
const jsi::Value&,
229241
const jsi::Value* args,
@@ -238,6 +250,10 @@ jlong installSandboxJSIBindings(
238250
rt, "postMessage: first argument must be an object");
239251
}
240252

253+
auto statePtr = stateWeak.lock();
254+
if (!statePtr || !statePtr->delegateRef)
255+
return jsi::Value::undefined();
256+
241257
jsi::Object jsonObj = rt.global().getPropertyAsObject(rt, "JSON");
242258
jsi::Function stringify =
243259
jsonObj.getPropertyAsFunction(rt, "stringify");
@@ -255,7 +271,6 @@ jlong installSandboxJSIBindings(
255271
}
256272
std::string targetOrigin = args[1].getString(rt).utf8(rt);
257273

258-
// Fan out to all delegates registered for the target origin
259274
auto& registry = rnsandbox::SandboxRegistry::getInstance();
260275
auto targets = registry.findAll(targetOrigin);
261276
if (!targets.empty()) {
@@ -266,11 +281,11 @@ jlong installSandboxJSIBindings(
266281
LOGW("postMessage: target '%s' not found", targetOrigin.c_str());
267282
}
268283
} else {
269-
jclass cls = jniEnv->GetObjectClass(globalDelegateRef);
284+
jclass cls = jniEnv->GetObjectClass(statePtr->delegateRef);
270285
jmethodID mid = jniEnv->GetMethodID(
271286
cls, "emitOnMessageFromJS", "(Ljava/lang/String;)V");
272287
jstring jMsg = jniEnv->NewStringUTF(messageJson.c_str());
273-
jniEnv->CallVoidMethod(globalDelegateRef, mid, jMsg);
288+
jniEnv->CallVoidMethod(statePtr->delegateRef, mid, jMsg);
274289
jniEnv->DeleteLocalRef(jMsg);
275290
jniEnv->DeleteLocalRef(cls);
276291
}
@@ -356,7 +371,7 @@ jlong installSandboxJSIBindings(
356371
makePropDesc(std::move(setOnMessageFn)));
357372

358373
try {
359-
setupErrorHandler(runtime, globalDelegateRef);
374+
setupErrorHandler(runtime, std::weak_ptr<SandboxJSIState>(state));
360375
} catch (const std::exception& e) {
361376
LOGW("Failed to setup error handler: %s", e.what());
362377
}
@@ -379,7 +394,7 @@ jlong installSandboxJSIBindings(
379394
if (!origin.empty()) {
380395
state->origin = origin;
381396
auto delegate =
382-
std::make_shared<JNISandboxDelegate>(globalDelegateRef);
397+
std::make_shared<JNISandboxDelegate>(jniEnv, globalDelegateRef);
383398
state->registryDelegate = delegate;
384399
rnsandbox::SandboxRegistry::getInstance().registerSandbox(
385400
origin, delegate, std::set<std::string>());
@@ -481,7 +496,7 @@ Java_io_callstack_rnsandbox_SandboxJSIInstaller_nativeInstallErrorHandler(
481496
return;
482497

483498
try {
484-
setupErrorHandler(*state->runtime, state->delegateRef);
499+
setupErrorHandler(*state->runtime, std::weak_ptr<SandboxJSIState>(state));
485500
} catch (const std::exception& e) {
486501
LOGW("Failed to setup error handler post-bundle: %s", e.what());
487502
}
@@ -497,19 +512,28 @@ Java_io_callstack_rnsandbox_SandboxJSIInstaller_nativeDestroy(
497512
if (it != gStates.end()) {
498513
std::string origin;
499514
std::shared_ptr<rnsandbox::ISandboxDelegate> delegate;
515+
jobject delegateRef = nullptr;
500516
{
501517
std::lock_guard<std::mutex> stateLock(it->second->mutex);
502518
origin = it->second->origin;
503519
delegate = it->second->registryDelegate;
520+
delegateRef = it->second->delegateRef;
504521
it->second->onMessageCallback.reset();
505522
it->second->pendingMessages.clear();
506523
it->second->runtime = nullptr;
507524
it->second->registryDelegate.reset();
525+
it->second->delegateRef = nullptr;
508526
}
509527
if (!origin.empty() && delegate) {
510528
rnsandbox::SandboxRegistry::getInstance().unregisterDelegate(
511529
origin, delegate);
512530
}
531+
if (delegateRef) {
532+
JNIEnv* env = getJNIEnv();
533+
if (env) {
534+
env->DeleteGlobalRef(delegateRef);
535+
}
536+
}
513537
gStates.erase(it);
514538
}
515539
}

packages/react-native-sandbox/cxx/SandboxDelegateWrapper.h

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
#pragma once
22

3-
#include <memory>
43
#include <string>
5-
#include <vector>
64
#include "ISandboxDelegate.h"
75

8-
// Forward declaration to avoid including Objective-C headers in C++ interface
96
#ifdef __OBJC__
107
@class SandboxReactNativeDelegate;
118
#else
@@ -18,21 +15,20 @@ namespace rnsandbox {
1815
* C++ wrapper for SandboxReactNativeDelegate that implements ISandboxDelegate.
1916
* This allows the C++ registry to work with Objective-C++ objects through
2017
* a proper C++ interface.
18+
*
19+
* The wrapped delegate pointer is non-owning. Call invalidate() before the
20+
* delegate is deallocated to prevent dangling pointer access.
2121
*/
2222
class SandboxDelegateWrapper : public ISandboxDelegate {
2323
public:
24-
/**
25-
* Creates a wrapper around the given Objective-C++ delegate.
26-
* @param delegate The Objective-C++ delegate to wrap (must not be null)
27-
*/
2824
explicit SandboxDelegateWrapper(SandboxReactNativeDelegate* delegate);
2925

30-
/**
31-
* Destructor - does not delete the wrapped delegate
32-
*/
3326
~SandboxDelegateWrapper() override = default;
3427

35-
// ISandboxDelegate implementation
28+
void invalidate() {
29+
delegate_ = nullptr;
30+
}
31+
3632
void postMessage(const std::string& message) override;
3733
bool routeMessage(const std::string& message, const std::string& targetId)
3834
override;
@@ -41,7 +37,6 @@ class SandboxDelegateWrapper : public ISandboxDelegate {
4137
void setAllowedTurboModules(const std::set<std::string>& modules) override;
4238

4339
private:
44-
// Non-owning pointer to the Objective-C++ delegate
4540
SandboxReactNativeDelegate* delegate_;
4641
};
4742

packages/react-native-sandbox/cxx/StubTurboModuleCxx.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ facebook::jsi::Function StubTurboModuleCxx::createStubFunction(
3434
runtime,
3535
facebook::jsi::PropNameID::forAscii(runtime, methodName.c_str()),
3636
0,
37-
[this, methodName](
38-
facebook::jsi::Runtime& rt,
39-
const facebook::jsi::Value& thisVal,
40-
const facebook::jsi::Value* args,
41-
size_t count) -> facebook::jsi::Value {
37+
[moduleName = moduleName_, methodName](
38+
facebook::jsi::Runtime&,
39+
const facebook::jsi::Value&,
40+
const facebook::jsi::Value*,
41+
size_t) -> facebook::jsi::Value {
4242
SANDBOX_LOG_WARN(
4343
"[StubTurboModuleCxx] Method call '%s' blocked on module '%s'.",
4444
methodName.c_str(),
45-
this->moduleName_.c_str());
45+
moduleName.c_str());
4646
return facebook::jsi::Value::undefined();
4747
});
4848
}

0 commit comments

Comments
 (0)