Skip to content

Commit f7d006e

Browse files
sammy-SCfacebook-github-bot
authored andcommitted
Fix memory leak convertJSIFunctionToCallback
Summary: changelog: [internal] ### When does leak happen? Leak happens anytime a callback isn't executed inside native module, it will never get cleaned up. Imagine a native module with method that takes onSuccess and onFail callbacks. Only one of them will be called at any time and the other one will leak. ### Why does it leak? It leaks because when `CallbackWrapper` is created using `CallbackWrapper::createWeak`. Inside `CallbackWrapper::createWeak`, the newly created object is inserted into `LongLivedObjectCollection`. This object collection will keep it alive until `CallbackWrapper::destroy` is called, which isn't called in case closure isn't executed. ### Solution Introduce class RCTBlockGuard which ties cleanup of resources to lifetime of the block. Reviewed By: RSNara Differential Revision: D26664173 fbshipit-source-id: 9348f7c39eb317cf1e8e5d59e77a378e5e04f3eb
1 parent 9f120ef commit f7d006e

File tree

5 files changed

+82
-1
lines changed

5 files changed

+82
-1
lines changed

React/Base/RCTBridge.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ RCT_EXTERN void RCTEnableTurboModuleEagerInit(BOOL enabled);
160160
RCT_EXTERN BOOL RCTTurboModuleSharedMutexInitEnabled(void);
161161
RCT_EXTERN void RCTEnableTurboModuleSharedMutexInit(BOOL enabled);
162162

163+
// Turn on TurboModule shared mutex initialization
164+
RCT_EXTERN BOOL RCTTurboModuleBlockGuardEnabled(void);
165+
RCT_EXTERN void RCTEnableTurboModuleBlockGuard(BOOL enabled);
166+
163167
/**
164168
* Async batched bridge used to communicate with the JavaScript application.
165169
*/

React/Base/RCTBridge.m

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,17 @@ void RCTEnableTurboModuleSharedMutexInit(BOOL enabled)
135135
turboModuleSharedMutexInitEnabled = enabled;
136136
}
137137

138+
static BOOL turboModuleBlockGuardEnabled = NO;
139+
BOOL RCTTurboModuleBlockGuardEnabled(void)
140+
{
141+
return turboModuleBlockGuardEnabled;
142+
}
143+
144+
void RCTEnableTurboModuleBlockGuard(BOOL enabled)
145+
{
146+
turboModuleBlockGuardEnabled = enabled;
147+
}
148+
138149
@interface RCTBridge () <RCTReloadListener>
139150
@end
140151

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <Foundation/Foundation.h>
9+
10+
NS_ASSUME_NONNULL_BEGIN
11+
12+
/**
13+
* RCTBlockGuard is designed to be used with obj-c blocks to assist with manual deallocation of C++ resources
14+
* tied to lifetime of a block. If C++ resources needs to be manually released at the end of block or when the block
15+
* is deallocated, place the clean up code inside constructor and make sure the instace of the class is references in
16+
* the block.
17+
*/
18+
@interface RCTBlockGuard : NSObject
19+
20+
- (instancetype)initWithCleanup:(void (^)(void))cleanup;
21+
22+
@end
23+
24+
NS_ASSUME_NONNULL_END
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "RCTBlockGuard.h"
9+
10+
@implementation RCTBlockGuard {
11+
void (^_cleanup)(void);
12+
}
13+
14+
- (instancetype)initWithCleanup:(void (^)(void))cleanup
15+
{
16+
if (self = [super init]) {
17+
_cleanup = cleanup;
18+
}
19+
20+
return self;
21+
}
22+
23+
- (void)dealloc
24+
{
25+
_cleanup();
26+
}
27+
28+
@end

ReactCommon/react/nativemodule/core/platform/ios/RCTTurboModule.mm

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
#import "RCTTurboModule.h"
9+
#import "RCTBlockGuard.h"
910

1011
#import <objc/message.h>
1112
#import <objc/runtime.h>
@@ -170,6 +171,16 @@ static int32_t getUniqueId()
170171
convertJSIFunctionToCallback(jsi::Runtime &runtime, const jsi::Function &value, std::shared_ptr<CallInvoker> jsInvoker)
171172
{
172173
auto weakWrapper = CallbackWrapper::createWeak(value.getFunction(runtime), runtime, jsInvoker);
174+
RCTBlockGuard *blockGuard;
175+
if (RCTTurboModuleBlockGuardEnabled()) {
176+
blockGuard = [[RCTBlockGuard alloc] initWithCleanup:^() {
177+
auto strongWrapper = weakWrapper.lock();
178+
if (strongWrapper) {
179+
strongWrapper->destroy();
180+
}
181+
}];
182+
}
183+
173184
BOOL __block wrapperWasCalled = NO;
174185
RCTResponseSenderBlock callback = ^(NSArray *responses) {
175186
if (wrapperWasCalled) {
@@ -181,7 +192,7 @@ static int32_t getUniqueId()
181192
return;
182193
}
183194

184-
strongWrapper->jsInvoker().invokeAsync([weakWrapper, responses]() {
195+
strongWrapper->jsInvoker().invokeAsync([weakWrapper, responses, blockGuard]() {
185196
auto strongWrapper2 = weakWrapper.lock();
186197
if (!strongWrapper2) {
187198
return;
@@ -190,6 +201,9 @@ static int32_t getUniqueId()
190201
std::vector<jsi::Value> args = convertNSArrayToStdVector(strongWrapper2->runtime(), responses);
191202
strongWrapper2->callback().call(strongWrapper2->runtime(), (const jsi::Value *)args.data(), args.size());
192203
strongWrapper2->destroy();
204+
205+
// Delete the CallbackWrapper when the block gets dealloced without being invoked.
206+
(void)blockGuard;
193207
});
194208

195209
wrapperWasCalled = YES;

0 commit comments

Comments
 (0)