Skip to content

Commit a6d7332

Browse files
authored
fix: prevent JS function to native block leak (#223)
* fix: prevent JS function to native block leak * fix: validate if isolate is alive
1 parent 0c4b819 commit a6d7332

File tree

4 files changed

+65
-2
lines changed

4 files changed

+65
-2
lines changed

NativeScript/runtime/Interop.mm

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
.dispose = [](JSBlock* block) {
3838
if (block->descriptor == &JSBlock::kJSBlockDescriptor) {
3939
MethodCallbackWrapper* wrapper = static_cast<MethodCallbackWrapper*>(block->userData);
40-
if (wrapper->isolateWrapper_.IsValid()) {
40+
if (Runtime::IsAlive(wrapper->isolateWrapper_.Isolate()) && wrapper->isolateWrapper_.IsValid()) {
4141
Isolate* isolate = wrapper->isolateWrapper_.Isolate();
4242
v8::Locker locker(isolate);
4343
Isolate::Scope isolate_scope(isolate);
@@ -80,6 +80,8 @@
8080

8181
*blockPointer = {
8282
.isa = nullptr,
83+
// this block is created with a refcount of 1
84+
// this means we "own" it and need to free it
8385
.flags = JSBlock::BLOCK_HAS_COPY_DISPOSE | JSBlock::BLOCK_NEEDS_FREE | (1 /* ref count */ << 1),
8486
.reserved = 0,
8587
.invoke = (void*)result.first,
@@ -90,7 +92,8 @@
9092

9193
object_setClass((__bridge id)blockPointer, objc_getClass("__NSMallocBlock__"));
9294

93-
return blockPointer;
95+
// transfer ownership back to ARC (see refcount comment above)
96+
return CFBridgingRelease(blockPointer);
9497
}
9598

9699
Local<Value> Interop::CallFunction(CMethodCall& methodCall) {

TestFixtures/Marshalling/TNSObjCTypes.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ CFTypeRef TNSFunctionWithCreateCFTypeRefReturn() CF_RETURNS_RETAINED;
1212
typedef int (^NumberReturner)(int, int, int);
1313

1414
@interface TNSObjCTypes : NSObject
15+
@property (nonatomic, copy) void (^retainedBlock)(void);
1516
+ (void)methodWithComplexBlock:(id (^)(int, id, SEL, NSObject*, TNSOStruct))block;
1617
+ (id)methodWithObject:(id)x;
1718

@@ -22,6 +23,10 @@ typedef int (^NumberReturner)(int, int, int);
2223
- (void)methodWithSimpleBlock:(void (^)(void))block;
2324
- (void)methodWithComplexBlock:(id (^)(int, id, SEL, NSObject*, TNSOStruct))block;
2425

26+
- (void)methodRetainingBlock:(void (^)(void))block;
27+
- (void)methodCallRetainingBlock;
28+
- (void)methodReleaseRetainingBlock;
29+
2530
- (NumberReturner)methodWithBlockScope:(int)number;
2631
- (id)methodReturningBlockAsId:(int)number;
2732

TestFixtures/Marshalling/TNSObjCTypes.m

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ - (void)methodWithSimpleBlock:(void (^)(void))block {
4848
block();
4949
}
5050

51+
- (void)methodRetainingBlock:(void (^)(void))block {
52+
_retainedBlock = block;
53+
}
54+
- (void)methodCallRetainingBlock {
55+
_retainedBlock();
56+
}
57+
- (void)methodReleaseRetainingBlock {
58+
_retainedBlock = NULL;
59+
}
60+
5161
- (void)methodWithComplexBlock:(id (^)(int, id, SEL, NSObject*, TNSOStruct))block {
5262
TNSOStruct str = { 5, 6, 7 };
5363
id result = block(1, @2, @selector(init), @[@3, @4], str);

TestRunner/app/tests/Marshalling/ObjCTypesTests.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,51 @@ describe(module.id, function () {
6565
expect(actual).toBe("simple block called");
6666
});
6767

68+
it("Block releases after call", function (done) {
69+
const functionRef = new WeakRef(function () {
70+
TNSLog('simple block called');
71+
});
72+
TNSObjCTypes.alloc().init().methodWithSimpleBlock(functionRef.deref());
73+
74+
var actual = TNSGetOutput();
75+
expect(actual).toBe("simple block called");
76+
gc();
77+
setTimeout(() => {
78+
gc();
79+
expect(!!functionRef.deref()).toBe(false);
80+
done();
81+
});
82+
});
83+
84+
it("Block retains and releases", function (done) {
85+
const functionRef = new WeakRef(function () {
86+
TNSLog('simple block called');
87+
});
88+
const instance = TNSObjCTypes.alloc().init();
89+
instance.methodRetainingBlock(functionRef.deref());
90+
function verifyBlockCall() {
91+
instance.methodCallRetainingBlock();
92+
var actual = TNSGetOutput();
93+
expect(actual).toBe("simple block called");
94+
TNSClearOutput();
95+
}
96+
verifyBlockCall();
97+
98+
gc();
99+
setTimeout(() => {
100+
gc();
101+
expect(!!functionRef.deref()).toBe(true);
102+
verifyBlockCall();
103+
instance.methodReleaseRetainingBlock();
104+
gc();
105+
setTimeout(() => {
106+
gc();
107+
expect(!!functionRef.deref()).toBe(false);
108+
done();
109+
})
110+
});
111+
});
112+
68113
it("InstanceComplexBlock", function () {
69114
function block(i, id, sel, obj, str) {
70115
expect(i).toBe(1);

0 commit comments

Comments
 (0)