blob: b3ab7513c2500a253c288c6bd2e116094933cdd8 [file] [edit]
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// Objective C support is only available on mac.
@TestOn('mac-os')
library;
import 'package:objective_c/objective_c.dart';
import 'package:objective_c/src/objective_c_bindings_generated.dart'
show ObjCBlock_ffiVoid_ffiVoid_NSStream_NSStreamEvent;
import 'package:test/test.dart';
import 'util.dart';
// Directly exercises the blockRef extraction pattern from the production fix.
// Extract block.ref into a local `blockRef` (static type ObjCBlockRef, which
// transitively implements Finalizable). The Finalizable contract keeps blockRef
// live in the GC stack map across the non-leaf FFI safepoint below.
//
// Intentionally never-inlined so the JIT performs its own liveness analysis.
@pragma('vm:never-inline')
bool _gcAndCheckBlock() {
final block = ObjCBlock_ffiVoid_ffiVoid_NSStream_NSStreamEvent.fromFunction(
(_, stream, event) {},
keepIsolateAlive: false,
);
final blockRef = block.ref;
final ptr = blockRef.pointer;
// Use callGCNowFromNative (non-leaf safepoint) to trigger GC, then check
// the retain count via blockRetainCount, which reads the block ABI flags
// field — the correct location for block retain counts on all architectures.
callGCNowFromNative();
return blockRetainCount(ptr) > 0;
}
void main() {
group('block wrapper not freed at GC safepoints', () {
setUpAll(() {
initGCInject();
installGCInjectSwizzle();
});
tearDownAll(removeGCInjectSwizzle);
// Diagnostic: verify that gc-now from native code actually triggers GC.
// If this test fails, the GC-injection swizzle is a no-op and the
// reproduction test below is not meaningful.
test('gc-now from native code collects unreachable objects', () {
if (!canDoGC) {
markTestSkipped(
'Dart_ExecuteInternalCommand unavailable — GC injection is a no-op.',
);
return;
}
expect(gcNowAvailableFromNative(), isTrue);
WeakReference<Object>? weakRef;
(() {
final obj = Object();
weakRef = WeakReference(obj);
})();
callGCNowFromNative();
expect(weakRef!.target, isNull);
});
// Swizzle injects gc-now before ObjC retains the block. Without the
// blockRef extraction fix, the optimizer marks `block` dead after its raw
// pointer is extracted and GC drops the retain count to 0.
// Run 1000 iterations to trigger JIT optimisation of implementMethod.
test('block survives GC injected inside implementMethod '
'(fails without blockRef extraction)', () {
const kIterations = 1000;
for (var i = 0; i < kIterations; i++) {
final builder = ObjCProtocolBuilder();
setGCInjectActive(true);
NSStreamDelegate$Builder.stream_handleEvent_.implement(
builder,
(stream, event) {},
);
setGCInjectActive(false);
// wasBlockFreedBeforeRetain() is sticky: stays true once set.
if (wasBlockFreedBeforeRetain()) break;
}
expect(
wasBlockFreedBeforeRetain(),
isFalse,
reason:
'Block was prematurely released by GC before ObjC retained it. '
'blockRef extraction in implementMethod is required (issue #3209).',
);
});
test('block local NOT freed at non-leaf FFI safepoint', () {
// Guaranteed to reproduce on iteration 1 with:
// dart --optimization-counter-threshold=0 test ...
if (!canDoGC) {
markTestSkipped(
'Dart_ExecuteInternalCommand unavailable — gc-now is a no-op, '
'test would pass vacuously.',
);
return;
}
const kIterations = 1000;
for (var i = 0; i < kIterations; i++) {
final survived = _gcAndCheckBlock();
if (!survived) {
fail(
'Block wrapper was GC-collected at FFI safepoint on iteration $i. '
'blockRef extraction in implementMethod required (issue #3209).',
);
}
}
});
});
}