// 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 'dart:ffi'; 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'; @Native)>( isLeaf: true, symbol: 'getBlockRetainCount', ) external int _getBlockRetainCount(Pointer block); // 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 _getBlockRetainCount(ptr.cast()) > 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? 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).', ); } } }); }); }