| // Copyright (c) 2022, 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. |
| |
| // ignore_for_file: unused_local_variable |
| |
| // Objective C support is only available on mac. |
| @TestOn('mac-os') |
| import 'dart:async'; |
| import 'dart:ffi'; |
| import 'dart:io'; |
| import 'dart:isolate'; |
| |
| import 'package:ffi/ffi.dart'; |
| import 'package:objective_c/objective_c.dart'; |
| import 'package:objective_c/src/internal.dart' |
| as internal_for_testing |
| show blockHasRegisteredClosure; |
| import 'package:path/path.dart' as path; |
| import 'package:test/test.dart'; |
| |
| import '../test_utils.dart'; |
| import 'block_bindings.dart'; |
| import 'util.dart'; |
| |
| typedef IntBlock = ObjCBlock_Int32_Int32; |
| typedef VoidBlock = ObjCBlock_ffiVoid; |
| typedef ListenerBlock = ObjCBlock_ffiVoid_IntBlock; |
| typedef FloatBlock = ObjCBlock_ffiFloat_ffiFloat; |
| typedef DoubleBlock = ObjCBlock_ffiDouble_ffiDouble; |
| typedef Vec4Block = ObjCBlock_Vec4_Vec4; |
| typedef SelectorBlock = ObjCBlock_ffiVoid_objcObjCSelector; |
| typedef ObjectBlock = ObjCBlock_DummyObject_DummyObject; |
| typedef NullableObjectBlock = ObjCBlock_DummyObject_DummyObject$1; |
| typedef NullableStringBlock = ObjCBlock_NSString_NSString; |
| typedef ObjectListenerBlock = ObjCBlock_ffiVoid_DummyObject; |
| typedef NullableListenerBlock = ObjCBlock_ffiVoid_DummyObject$1; |
| typedef StructListenerBlock = ObjCBlock_ffiVoid_Vec2_Vec4_NSObject; |
| typedef NSStringListenerBlock = ObjCBlock_ffiVoid_NSString; |
| typedef NoTrampolineListenerBlock = ObjCBlock_ffiVoid_Int32_Vec4_ffiChar; |
| typedef BlockBlock = ObjCBlock_IntBlock_IntBlock; |
| typedef IntPtrBlock = ObjCBlock_ffiVoid_Int32$1; |
| typedef ResultBlock = ObjCBlock_ffiVoid_Int32; |
| |
| bool get hasIsolateOwnershipApi => |
| DynamicLibrary.process().providesSymbol('Dart_SetCurrentThreadOwnsIsolate'); |
| void setCurrentThreadOwnsIsolate() => |
| DynamicLibrary.process().lookupFunction<Void Function(), void Function()>( |
| 'Dart_SetCurrentThreadOwnsIsolate', |
| )(); |
| |
| void main() { |
| late final BlockTestObjCLibrary lib; |
| |
| group('Blocks', () { |
| setUpAll(() { |
| // TODO(https://github.com/dart-lang/native/issues/1068): Remove this. |
| DynamicLibrary.open( |
| path.join( |
| packagePathForTests, |
| '..', |
| 'objective_c', |
| 'test', |
| 'objective_c.dylib', |
| ), |
| ); |
| final dylib = File( |
| path.join( |
| packagePathForTests, |
| 'test', |
| 'native_objc_test', |
| 'objc_test.dylib', |
| ), |
| ); |
| verifySetupFile(dylib); |
| lib = BlockTestObjCLibrary(DynamicLibrary.open(dylib.absolute.path)); |
| |
| generateBindingsForCoverage('block'); |
| |
| BlockTester.setup(NativeApi.initializeApiDLData); |
| }); |
| |
| test('BlockTester is working', () { |
| // This doesn't test any Block functionality, just that the BlockTester |
| // itself is working correctly. |
| final blockTester = BlockTester.newFromMultiplier(10); |
| expect(blockTester.call(123), 1230); |
| final intBlock = blockTester.getBlock(); |
| final blockTester2 = BlockTester.newFromBlock(intBlock); |
| blockTester2.pokeBlock(); |
| expect(blockTester2.call(456), 4560); |
| }); |
| |
| test('Block from function pointer', () { |
| final block = IntBlock.fromFunctionPointer( |
| Pointer.fromFunction(_add100, 999), |
| ); |
| final blockTester = BlockTester.newFromBlock(block); |
| blockTester.pokeBlock(); |
| expect(blockTester.call(123), 223); |
| expect(block(123), 223); |
| }); |
| |
| int Function(int) makeAdder(int addTo) { |
| return (int x) => addTo + x; |
| } |
| |
| test('Block from function', () { |
| final block = IntBlock.fromFunction(makeAdder(4000)); |
| final blockTester = BlockTester.newFromBlock(block); |
| blockTester.pokeBlock(); |
| expect(blockTester.call(123), 4123); |
| expect(block(123), 4123); |
| }); |
| |
| test('Listener block same thread', () async { |
| final hasRun = Completer<void>(); |
| int value = 0; |
| final block = VoidBlock.listener(() { |
| value = 123; |
| hasRun.complete(); |
| }); |
| |
| BlockTester.callOnSameThread(block); |
| |
| await hasRun.future; |
| expect(value, 123); |
| }); |
| |
| test('Listener block new thread', () async { |
| final hasRun = Completer<void>(); |
| int value = 0; |
| final block = VoidBlock.listener(() { |
| value = 123; |
| hasRun.complete(); |
| }); |
| |
| final thread = BlockTester.callOnNewThread(block); |
| thread.start(); |
| |
| await hasRun.future; |
| expect(value, 123); |
| }); |
| |
| void waitSync(Duration d) { |
| final t = Stopwatch(); |
| t.start(); |
| while (t.elapsed < d) { |
| // Waiting... |
| } |
| } |
| |
| test('Blocking block same thread', () { |
| int value = 0; |
| final block = VoidBlock.blocking(() { |
| waitSync(Duration(milliseconds: 100)); |
| value = 123; |
| }); |
| BlockTester.callOnSameThread(block); |
| expect(value, 123); |
| }); |
| |
| test('Blocking block new thread', () async { |
| final block = IntPtrBlock.blocking((Pointer<Int32> result) { |
| waitSync(Duration(milliseconds: 100)); |
| result.value = 123456; |
| }); |
| final resultCompleter = Completer<int>(); |
| final resultBlock = ResultBlock.listener((int result) { |
| resultCompleter.complete(result); |
| }); |
| BlockTester.blockingBlockTest(block, resultBlock: resultBlock); |
| expect(await resultCompleter.future, 123456); |
| }); |
| |
| test('Blocking block same thread throws', () { |
| int value = 0; |
| final block = VoidBlock.blocking(() { |
| value = 123; |
| throw "Hello"; |
| }); |
| BlockTester.callOnSameThread(block); |
| expect(value, 123); |
| }); |
| |
| test('Blocking block new thread throws', () async { |
| final block = IntPtrBlock.blocking((Pointer<Int32> result) { |
| result.value = 123456; |
| throw "Hello"; |
| }); |
| final resultCompleter = Completer<int>(); |
| final resultBlock = ResultBlock.listener((int result) { |
| resultCompleter.complete(result); |
| }); |
| BlockTester.blockingBlockTest(block, resultBlock: resultBlock); |
| expect(await resultCompleter.future, 123456); |
| }); |
| |
| test('Blocking block manual invocation', () { |
| int value = 0; |
| final block = VoidBlock.blocking(() { |
| waitSync(Duration(milliseconds: 100)); |
| value = 123; |
| }); |
| block(); |
| expect(value, 123); |
| }); |
| |
| test('Float block', () { |
| final block = FloatBlock.fromFunction((double x) { |
| return x + 4.56; |
| }); |
| expect(block(1.23), closeTo(5.79, 1e-6)); |
| expect(BlockTester.callFloatBlock(block), closeTo(5.79, 1e-6)); |
| }); |
| |
| test('Double block', () { |
| final block = DoubleBlock.fromFunction((double x) { |
| return x + 4.56; |
| }); |
| expect(block(1.23), closeTo(5.79, 1e-6)); |
| expect(BlockTester.callDoubleBlock(block), closeTo(5.79, 1e-6)); |
| }); |
| |
| test('Struct block', () { |
| using((Arena arena) { |
| final inputPtr = arena<Vec4>(); |
| final input = inputPtr.ref; |
| input.x = 1.2; |
| input.y = 3.4; |
| input.z = 5.6; |
| input.w = 7.8; |
| |
| final tempPtr = arena<Vec4>(); |
| final temp = tempPtr.ref; |
| final block = Vec4Block.fromFunction((Vec4 v) { |
| // Twiddle the Vec4 components. |
| temp.x = v.y; |
| temp.y = v.z; |
| temp.z = v.w; |
| temp.w = v.x; |
| return temp; |
| }); |
| |
| final result1 = block(input); |
| expect(result1.x, 3.4); |
| expect(result1.y, 5.6); |
| expect(result1.z, 7.8); |
| expect(result1.w, 1.2); |
| |
| final result2 = BlockTester.callVec4Block(block); |
| expect(result2.x, 3.4); |
| expect(result2.y, 5.6); |
| expect(result2.z, 7.8); |
| expect(result2.w, 1.2); |
| }); |
| }); |
| |
| test('Selector block', () { |
| late String sel; |
| final block = SelectorBlock.fromFunction((Pointer<ObjCSelector> x) { |
| sel = x.toDartString(); |
| }); |
| |
| block('Hello'.toSelector()); |
| expect(sel, 'Hello'); |
| |
| BlockTester.callSelectorBlock(block); |
| expect(sel, 'Select'); |
| }); |
| |
| test('Object block', () { |
| bool isCalled = false; |
| final block = ObjectBlock.fromFunction((DummyObject x) { |
| isCalled = true; |
| return x; |
| }); |
| |
| final obj = DummyObject(); |
| final result1 = block(obj); |
| expect(result1, obj); |
| expect(isCalled, isTrue); |
| |
| isCalled = false; |
| final result2 = BlockTester.callObjectBlock(block); |
| expect(result2, isNot(obj)); |
| expect(result2.ref.pointer, isNot(nullptr)); |
| expect(isCalled, isTrue); |
| }); |
| |
| test('Nullable object block', () { |
| bool isCalled = false; |
| final block = NullableObjectBlock.fromFunction((DummyObject? x) { |
| isCalled = true; |
| return x; |
| }); |
| |
| final obj = DummyObject(); |
| final result1 = block(obj); |
| expect(result1, obj); |
| expect(isCalled, isTrue); |
| |
| isCalled = false; |
| final result2 = block(null); |
| expect(result2, isNull); |
| expect(isCalled, isTrue); |
| |
| isCalled = false; |
| final result3 = BlockTester.callNullableObjectBlock(block); |
| expect(result3, isNull); |
| expect(isCalled, isTrue); |
| }); |
| |
| test('Nullable string block', () { |
| // Regression test for https://github.com/dart-lang/native/issues/1537. |
| final block = NullableStringBlock.fromFunction( |
| (NSString? x) => '${x?.toDartString()} Cat'.toNSString(), |
| ); |
| |
| final result1 = block('Dog'.toNSString()); |
| expect(result1?.toDartString(), 'Dog Cat'); |
| |
| final result2 = block(null); |
| expect(result2?.toDartString(), 'null Cat'); |
| |
| final result3 = BlockTester.callNullableStringBlock(block); |
| expect(result3?.toDartString(), 'Lizard Cat'); |
| }); |
| |
| test('Object listener block', () async { |
| final hasRun = Completer<void>(); |
| final block = ObjectListenerBlock.listener((DummyObject x) { |
| expect(x, isNotNull); |
| hasRun.complete(); |
| }); |
| |
| BlockTester.callObjectListener(block); |
| await hasRun.future; |
| }); |
| |
| test('Nullable listener block', () async { |
| final hasRun = Completer<void>(); |
| final block = NullableListenerBlock.listener((DummyObject? x) { |
| expect(x, isNull); |
| hasRun.complete(); |
| }); |
| |
| BlockTester.callNullableListener(block); |
| await hasRun.future; |
| }); |
| |
| test('Struct listener block', () async { |
| final hasRun = Completer<void>(); |
| final block = StructListenerBlock.listener(( |
| Vec2 vec2, |
| Vec4 vec4, |
| NSObject dummy, |
| ) { |
| expect(vec2.x, 100); |
| expect(vec2.y, 200); |
| |
| expect(vec4.x, 1.2); |
| expect(vec4.y, 3.4); |
| expect(vec4.z, 5.6); |
| expect(vec4.w, 7.8); |
| |
| expect(dummy, isNotNull); |
| |
| hasRun.complete(); |
| }); |
| |
| BlockTester.callStructListener(block); |
| await hasRun.future; |
| }); |
| |
| test('NSString listener block', () async { |
| final hasRun = Completer<void>(); |
| final block = NSStringListenerBlock.listener((NSString s) { |
| expect(s.toDartString(), "Foo 123"); |
| hasRun.complete(); |
| }); |
| |
| BlockTester.callNSStringListener(block, x: 123); |
| await hasRun.future; |
| }); |
| |
| test('No trampoline listener block', () async { |
| final hasRun = Completer<void>(); |
| final block = NoTrampolineListenerBlock.listener(( |
| int x, |
| Vec4 vec4, |
| Pointer<Char> charPtr, |
| ) { |
| expect(x, 123); |
| |
| expect(vec4.x, 1.2); |
| expect(vec4.y, 3.4); |
| expect(vec4.z, 5.6); |
| expect(vec4.w, 7.8); |
| |
| expect(charPtr.cast<Utf8>().toDartString(), "Hello World"); |
| |
| hasRun.complete(); |
| }); |
| |
| BlockTester.callNoTrampolineListener(block); |
| await hasRun.future; |
| }); |
| |
| test('Block block', () { |
| final blockBlock = BlockBlock.fromFunction(( |
| ObjCBlock<Int32 Function(Int32)> intBlock, |
| ) { |
| return IntBlock.fromFunction((int x) { |
| return 3 * intBlock(x); |
| }); |
| }); |
| |
| final intBlock = IntBlock.fromFunction((int x) { |
| return 5 * x; |
| }); |
| final result1 = blockBlock(intBlock); |
| expect(result1(1), 15); |
| |
| final result2 = BlockTester.newBlock(blockBlock, withMult: 2); |
| expect(result2(1), 6); |
| }); |
| |
| test('Native block block', () { |
| final blockBlock = BlockTester.newBlockBlock(7); |
| |
| final intBlock = IntBlock.fromFunction((int x) { |
| return 5 * x; |
| }); |
| final result1 = blockBlock(intBlock); |
| expect(result1(1), 35); |
| |
| final result2 = BlockTester.newBlock(blockBlock, withMult: 2); |
| expect(result2(1), 14); |
| }); |
| |
| Pointer<ObjCBlockImpl> funcPointerBlockRefCountTest() { |
| final block = IntBlock.fromFunctionPointer( |
| Pointer.fromFunction(_add100, 999), |
| ); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure(block.ref.pointer), |
| false, |
| ); |
| expect(blockRetainCount(block.ref.pointer), 1); |
| return block.ref.pointer; |
| } |
| |
| test('Function pointer block ref counting', () { |
| final rawBlock = funcPointerBlockRefCountTest(); |
| doGC(); |
| expect(blockRetainCount(rawBlock), 0); |
| }, skip: !canDoGC); |
| |
| Pointer<ObjCBlockImpl> funcBlockRefCountTest() { |
| final block = IntBlock.fromFunction(makeAdder(4000)); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure(block.ref.pointer), |
| true, |
| ); |
| expect(blockRetainCount(block.ref.pointer), 1); |
| return block.ref.pointer; |
| } |
| |
| test('Function block ref counting', () async { |
| final rawBlock = funcBlockRefCountTest(); |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. |
| expect(blockRetainCount(rawBlock), 0); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure(rawBlock.cast()), |
| false, |
| ); |
| }, skip: !canDoGC); |
| |
| Pointer<ObjCBlockImpl> blockManualRetainRefCountTest() { |
| final block = IntBlock.fromFunction(makeAdder(4000)); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure(block.ref.pointer), |
| true, |
| ); |
| expect(blockRetainCount(block.ref.pointer), 1); |
| final rawBlock = block.ref.retainAndReturnPointer(); |
| expect(blockRetainCount(rawBlock), 2); |
| return rawBlock; |
| } |
| |
| int blockManualRetainRefCountTest2(Pointer<ObjCBlockImpl> rawBlock) { |
| final block = IntBlock.castFromPointer( |
| rawBlock.cast(), |
| retain: false, |
| release: true, |
| ); |
| return blockRetainCount(block.ref.pointer); |
| } |
| |
| test('Block ref counting with manual retain and release', () async { |
| final rawBlock = blockManualRetainRefCountTest(); |
| doGC(); |
| expect(blockRetainCount(rawBlock), 1); |
| expect(blockManualRetainRefCountTest2(rawBlock), 1); |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. |
| expect(blockRetainCount(rawBlock), 0); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure(rawBlock.cast()), |
| false, |
| ); |
| }, skip: !canDoGC); |
| |
| (Pointer<ObjCBlockImpl>, Pointer<ObjCBlockImpl>, Pointer<ObjCBlockImpl>) |
| blockBlockDartCallRefCountTest() { |
| final pool = lib.objc_autoreleasePoolPush(); |
| final inputBlock = IntBlock.fromFunction((int x) { |
| return 5 * x; |
| }); |
| final blockBlock = BlockBlock.fromFunction(( |
| ObjCBlock<Int32 Function(Int32)> intBlock, |
| ) { |
| return IntBlock.fromFunction((int x) { |
| return 3 * intBlock(x); |
| }); |
| }); |
| final outputBlock = blockBlock(inputBlock); |
| expect(outputBlock(1), 15); |
| lib.objc_autoreleasePoolPop(pool); |
| doGC(); |
| |
| // One reference held by inputBlock object, another bound to the |
| // outputBlock lambda. |
| expect(blockRetainCount(inputBlock.ref.pointer), 2); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure( |
| inputBlock.ref.pointer.cast(), |
| ), |
| true, |
| ); |
| |
| expect(blockRetainCount(blockBlock.ref.pointer), 1); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure( |
| blockBlock.ref.pointer.cast(), |
| ), |
| true, |
| ); |
| expect(blockRetainCount(outputBlock.ref.pointer), 1); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure( |
| outputBlock.ref.pointer.cast(), |
| ), |
| true, |
| ); |
| return ( |
| inputBlock.ref.pointer, |
| blockBlock.ref.pointer, |
| outputBlock.ref.pointer, |
| ); |
| } |
| |
| test('Calling a block block from Dart has correct ref counting', () async { |
| final (inputBlock, blockBlock, outputBlock) = |
| blockBlockDartCallRefCountTest(); |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. |
| |
| expect(blockRetainCount(inputBlock), 0); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure(inputBlock.cast()), |
| false, |
| ); |
| expect(blockRetainCount(blockBlock), 0); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure(blockBlock.cast()), |
| false, |
| ); |
| expect(blockRetainCount(outputBlock), 0); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure(outputBlock.cast()), |
| false, |
| ); |
| }, skip: !canDoGC); |
| |
| (Pointer<ObjCBlockImpl>, Pointer<ObjCBlockImpl>, Pointer<ObjCBlockImpl>) |
| blockBlockObjCCallRefCountTest() { |
| final pool = lib.objc_autoreleasePoolPush(); |
| late Pointer<ObjCBlockImpl> inputBlock; |
| final blockBlock = BlockBlock.fromFunction(( |
| ObjCBlock<Int32 Function(Int32)> intBlock, |
| ) { |
| inputBlock = intBlock.ref.pointer; |
| return IntBlock.fromFunction((int x) { |
| return 3 * intBlock(x); |
| }); |
| }); |
| final outputBlock = BlockTester.newBlock(blockBlock, withMult: 2); |
| expect(outputBlock(1), 6); |
| lib.objc_autoreleasePoolPop(pool); |
| doGC(); |
| |
| expect(blockRetainCount(inputBlock), 1); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure(inputBlock.cast()), |
| false, |
| ); |
| expect(blockRetainCount(blockBlock.ref.pointer), 1); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure( |
| blockBlock.ref.pointer.cast(), |
| ), |
| true, |
| ); |
| expect(blockRetainCount(outputBlock.ref.pointer), 1); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure( |
| outputBlock.ref.pointer.cast(), |
| ), |
| true, |
| ); |
| return (inputBlock, blockBlock.ref.pointer, outputBlock.ref.pointer); |
| } |
| |
| test('Calling a block block from ObjC has correct ref counting', () async { |
| final (inputBlock, blockBlock, outputBlock) = |
| blockBlockObjCCallRefCountTest(); |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. |
| |
| expect(blockRetainCount(inputBlock), 0); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure(inputBlock.cast()), |
| false, |
| ); |
| expect(blockRetainCount(blockBlock), 0); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure(blockBlock.cast()), |
| false, |
| ); |
| expect(blockRetainCount(outputBlock), 0); |
| expect( |
| internal_for_testing.blockHasRegisteredClosure(outputBlock.cast()), |
| false, |
| ); |
| }, skip: !canDoGC); |
| |
| (Pointer<ObjCBlockImpl>, Pointer<ObjCBlockImpl>, Pointer<ObjCBlockImpl>) |
| nativeBlockBlockDartCallRefCountTest() { |
| final pool = lib.objc_autoreleasePoolPush(); |
| final inputBlock = IntBlock.fromFunction((int x) { |
| return 5 * x; |
| }); |
| final blockBlock = BlockTester.newBlockBlock(7); |
| final outputBlock = blockBlock(inputBlock); |
| expect(outputBlock(1), 35); |
| lib.objc_autoreleasePoolPop(pool); |
| doGC(); |
| |
| // One reference held by inputBlock object, another held internally by the |
| // ObjC implementation of the blockBlock. |
| expect(blockRetainCount(inputBlock.ref.pointer), 2); |
| |
| expect(blockRetainCount(blockBlock.ref.pointer), 1); |
| expect(blockRetainCount(outputBlock.ref.pointer), 1); |
| return ( |
| inputBlock.ref.pointer, |
| blockBlock.ref.pointer, |
| outputBlock.ref.pointer, |
| ); |
| } |
| |
| test( |
| 'Calling a native block block from Dart has correct ref counting', |
| () { |
| final (inputBlock, blockBlock, outputBlock) = |
| nativeBlockBlockDartCallRefCountTest(); |
| doGC(); |
| expect(blockRetainCount(inputBlock), 0); |
| expect(blockRetainCount(blockBlock), 0); |
| expect(blockRetainCount(outputBlock), 0); |
| }, |
| skip: !canDoGC, |
| ); |
| |
| (Pointer<ObjCBlockImpl>, Pointer<ObjCBlockImpl>) |
| nativeBlockBlockObjCCallRefCountTest() { |
| final blockBlock = BlockTester.newBlockBlock(7); |
| final outputBlock = BlockTester.newBlock(blockBlock, withMult: 2); |
| expect(outputBlock(1), 14); |
| doGC(); |
| |
| expect(blockRetainCount(blockBlock.ref.pointer), 1); |
| expect(blockRetainCount(outputBlock.ref.pointer), 1); |
| return (blockBlock.ref.pointer, outputBlock.ref.pointer); |
| } |
| |
| test( |
| 'Calling a native block block from ObjC has correct ref counting', |
| () { |
| final (blockBlock, outputBlock) = |
| nativeBlockBlockObjCCallRefCountTest(); |
| doGC(); |
| expect(blockRetainCount(blockBlock), 0); |
| expect(blockRetainCount(outputBlock), 0); |
| }, |
| skip: !canDoGC, |
| ); |
| |
| (Pointer<Int32>, Pointer<Int32>) objectBlockRefCountTest(Allocator alloc) { |
| final pool = lib.objc_autoreleasePoolPush(); |
| final inputCounter = alloc<Int32>(); |
| final outputCounter = alloc<Int32>(); |
| inputCounter.value = 0; |
| outputCounter.value = 0; |
| |
| final block = ObjectBlock.fromFunction((DummyObject x) { |
| return DummyObject.newWithCounter(outputCounter); |
| }); |
| |
| final inputObj = DummyObject.newWithCounter(inputCounter); |
| final outputObj = block(inputObj); |
| expect(inputCounter.value, 1); |
| expect(outputCounter.value, 1); |
| |
| lib.objc_autoreleasePoolPop(pool); |
| return (inputCounter, outputCounter); |
| } |
| |
| test( |
| 'Objects received and returned by blocks have correct ref counts', |
| () { |
| using((Arena arena) { |
| final (inputCounter, outputCounter) = objectBlockRefCountTest(arena); |
| doGC(); |
| expect(inputCounter.value, 0); |
| expect(outputCounter.value, 0); |
| }); |
| }, |
| skip: !canDoGC, |
| ); |
| |
| (Pointer<Int32>, Pointer<Int32>) objectNativeBlockRefCountTest( |
| Allocator alloc, |
| ) { |
| final pool = lib.objc_autoreleasePoolPush(); |
| final inputCounter = alloc<Int32>(); |
| final outputCounter = alloc<Int32>(); |
| inputCounter.value = 0; |
| outputCounter.value = 0; |
| |
| final block = ObjectBlock.fromFunction((DummyObject x) { |
| x.setCounter(inputCounter); |
| return DummyObject.newWithCounter(outputCounter); |
| }); |
| |
| final outputObj = BlockTester.callObjectBlock(block); |
| expect(inputCounter.value, 1); |
| expect(outputCounter.value, 1); |
| |
| lib.objc_autoreleasePoolPop(pool); |
| return (inputCounter, outputCounter); |
| } |
| |
| test( |
| 'Objects received and returned by native blocks have correct ref counts', |
| () { |
| using((Arena arena) async { |
| final (inputCounter, outputCounter) = objectNativeBlockRefCountTest( |
| arena, |
| ); |
| doGC(); |
| await Future<void>.delayed( |
| Duration.zero, |
| ); // Let dispose message arrive |
| doGC(); |
| |
| expect(inputCounter.value, 0); |
| expect(outputCounter.value, 0); |
| }); |
| }, |
| skip: !canDoGC, |
| ); |
| |
| Future<(Pointer<ObjCBlockImpl>, Pointer<ObjCBlockImpl>)> |
| listenerBlockArgumentRetentionTest() async { |
| final hasRun = Completer<void>(); |
| late ObjCBlock<Int32 Function(Int32)> inputBlock; |
| final blockBlock = ListenerBlock.listener(( |
| ObjCBlock<Int32 Function(Int32)> intBlock, |
| ) { |
| expect(blockRetainCount(intBlock.ref.pointer), greaterThan(0)); |
| inputBlock = intBlock; |
| hasRun.complete(); |
| }); |
| |
| final thread = BlockTester.callWithBlockOnNewThread(blockBlock); |
| thread.start(); |
| |
| await hasRun.future; |
| expect(inputBlock(123), 12300); |
| thread.ref.release(); |
| doGC(); |
| |
| expect(blockRetainCount(inputBlock.ref.pointer), 1); |
| expect(blockRetainCount(blockBlock.ref.pointer), 1); |
| return (inputBlock.ref.pointer, blockBlock.ref.pointer); |
| } |
| |
| test('Listener block arguments are not prematurely destroyed', () async { |
| // https://github.com/dart-lang/native/issues/835 |
| final (inputBlock, blockBlock) = |
| await listenerBlockArgumentRetentionTest(); |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. |
| doGC(); |
| |
| expect(blockRetainCount(inputBlock), 0); |
| expect(blockRetainCount(blockBlock), 0); |
| }, skip: !canDoGC); |
| |
| test('Blocking block ref counting same thread', () async { |
| DummyObject? dummyObject = DummyObject(); |
| DartObjectListenerBlock? block = ObjectListenerBlock.blocking(( |
| DummyObject obj, |
| ) { |
| // Object passed as argument. |
| expect(objectRetainCount(obj.ref.pointer), greaterThan(0)); |
| |
| // Object bound in block's lambda. |
| expect(dummyObject, isNotNull); |
| }); |
| |
| final tester = BlockTester.newFromListener(block); |
| final rawBlock = block!.ref.pointer; |
| expect(blockRetainCount(rawBlock), 2); |
| |
| final rawDummyObject = dummyObject!.ref.pointer; |
| expect(objectRetainCount(rawDummyObject), 1); |
| |
| dummyObject = null; |
| block = null; |
| tester.invokeAndReleaseListener(null); |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. |
| doGC(); |
| |
| expect(blockRetainCount(rawBlock), 0); |
| expect(objectRetainCount(rawDummyObject), 0); |
| }, skip: !canDoGC); |
| |
| test('Blocking block ref counting new thread', () async { |
| final completer = Completer<void>(); |
| DummyObject? dummyObject = DummyObject(); |
| DartObjectListenerBlock? block = ObjectListenerBlock.blocking(( |
| DummyObject obj, |
| ) { |
| // Object passed as argument. |
| expect(objectRetainCount(obj.ref.pointer), greaterThan(0)); |
| |
| // Object bound in block's lambda. |
| expect(dummyObject, isNotNull); |
| |
| completer.complete(); |
| }); |
| |
| final tester = BlockTester.newFromListener(block); |
| final rawBlock = block!.ref.pointer; |
| expect(blockRetainCount(rawBlock), 2); |
| |
| final rawDummyObject = dummyObject!.ref.pointer; |
| expect(objectRetainCount(rawDummyObject), 1); |
| |
| tester.invokeAndReleaseListenerOnNewThread(); |
| await completer.future; |
| dummyObject = null; |
| block = null; |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. |
| doGC(); |
| |
| expect(blockRetainCount(rawBlock), 0); |
| expect(objectRetainCount(rawDummyObject), 0); |
| }, skip: !canDoGC); |
| |
| test('Block fields have sensible values', () { |
| final block = IntBlock.fromFunction(makeAdder(4000)); |
| final blockPtr = block.ref.pointer; |
| expect(blockPtr.ref.isa, isNot(0)); |
| expect(blockPtr.ref.flags, isNot(0)); // Set by Block_copy. |
| expect(blockPtr.ref.reserved, 0); |
| expect(blockPtr.ref.invoke, isNot(0)); |
| expect(blockPtr.ref.target, isNot(0)); |
| final descPtr = blockPtr.ref.descriptor; |
| expect(descPtr.ref.reserved, 0); |
| expect(descPtr.ref.size, isNot(0)); |
| expect(descPtr.ref.copy_helper, nullptr); |
| expect(descPtr.ref.dispose_helper, isNot(nullptr)); |
| expect(descPtr.ref.signature, nullptr); |
| }); |
| |
| test('Block trampoline args converted to id', () { |
| final objCBindings = File( |
| path.join( |
| packagePathForTests, |
| 'test', |
| 'native_objc_test', |
| 'block_bindings.m', |
| ), |
| ).readAsStringSync(); |
| |
| // Objects are converted to id. |
| expect(objCBindings, isNot(contains('NSObject'))); |
| expect(objCBindings, isNot(contains('NSString'))); |
| expect(objCBindings, contains('id')); |
| |
| // Blocks are also converted to id. Note: (^) is part of a block type. |
| expect(objCBindings, isNot(contains('(^)'))); |
| |
| // Other types, like structs, are still there. |
| expect(objCBindings, contains('Vec2')); |
| expect(objCBindings, contains('Vec4')); |
| }); |
| |
| (BlockTester, Pointer<ObjCBlockImpl>, Pointer<ObjCObject>) regress1571Inner( |
| Completer<void> completer, |
| ) { |
| final dummyObject = DummyObject(); |
| DartObjectListenerBlock? block = ObjectListenerBlock.listener(( |
| DummyObject obj, |
| ) { |
| expect(objectRetainCount(obj.ref.pointer), greaterThan(0)); |
| completer.complete(); |
| expect(dummyObject, isNotNull); |
| }); |
| final tester = BlockTester.newFromListener(block); |
| expect(blockRetainCount(block.ref.pointer), 2); |
| expect(objectRetainCount(dummyObject.ref.pointer), 1); |
| return (tester, block.ref.pointer, dummyObject.ref.pointer); |
| } |
| |
| test( |
| 'Regression test for https://github.com/dart-lang/native/issues/1571', |
| () async { |
| // Pass a listener block to an ObjC API that retains a reference to the |
| // block, and release the Dart-side reference. Then, on a different |
| // thread, invoke the block and immediately release the ObjC-side |
| // reference. Before the fix, the dtor message could arrive before the |
| // invoke message. This was a flaky error, so try a few times. |
| for (int i = 0; i < 10; ++i) { |
| final completer = Completer<void>(); |
| final (tester, blockPtr, objectPtr) = regress1571Inner(completer); |
| |
| await flutterDoGC(); |
| expect(blockRetainCount(blockPtr), 1); |
| expect(objectRetainCount(objectPtr), 1); |
| |
| tester.invokeAndReleaseListenerOnNewThread(); |
| await completer.future; |
| |
| await flutterDoGC(); |
| expect(blockRetainCount(blockPtr), 0); |
| expect(objectRetainCount(objectPtr), 0); |
| } |
| }, |
| ); |
| |
| test('Block.fromFunction, keepIsolateAlive', () async { |
| final isolateSendPort = Completer<SendPort>(); |
| final blocksCreated = Completer<void>(); |
| final blkKeepAliveDestroyed = Completer<void>(); |
| final receivePort = RawReceivePort((msg) { |
| if (msg is SendPort) { |
| isolateSendPort.complete(msg); |
| } else if (msg == 'Blocks created') { |
| blocksCreated.complete(); |
| } else if (msg == 'blkKeepAlive destroyed') { |
| blkKeepAliveDestroyed.complete(); |
| } |
| }); |
| |
| final isExited = Completer<void>(); |
| late final RawReceivePort exitPort; |
| exitPort = RawReceivePort((_) { |
| isExited.complete(); |
| exitPort.close(); |
| }); |
| |
| final isolate = Isolate.spawn( |
| (sendPort) { |
| final blkKeepAlive = VoidBlock.fromFunction( |
| () {}, |
| keepIsolateAlive: true, |
| ); |
| final blkDontKeepAlive = VoidBlock.fromFunction( |
| () {}, |
| keepIsolateAlive: false, |
| ); |
| sendPort.send('Blocks created'); |
| |
| final isolatePort = RawReceivePort((msg) { |
| if (msg == 'Destroy blkKeepAlive') { |
| blkKeepAlive.ref.release(); |
| sendPort.send('blkKeepAlive destroyed'); |
| } |
| })..keepIsolateAlive = false; |
| |
| sendPort.send(isolatePort.sendPort); |
| }, |
| receivePort.sendPort, |
| onExit: exitPort.sendPort, |
| ); |
| |
| await blocksCreated.future; |
| |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let exit message arrive. |
| |
| // Both blocks are still alive. |
| expect(isExited.isCompleted, isFalse); |
| |
| (await isolateSendPort.future).send('Destroy blkKeepAlive'); |
| await blkKeepAliveDestroyed.future; |
| |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let exit message arrive. |
| |
| // Only blkDontKeepAlive is alive. |
| await isExited; |
| |
| receivePort.close(); |
| }, skip: !canDoGC); |
| |
| test('Block.listener, keepIsolateAlive', () async { |
| final isolateSendPort = Completer<SendPort>(); |
| final blocksCreated = Completer<void>(); |
| final blkKeepAliveDestroyed = Completer<void>(); |
| final receivePort = RawReceivePort((msg) { |
| if (msg is SendPort) { |
| isolateSendPort.complete(msg); |
| } else if (msg == 'Blocks created') { |
| blocksCreated.complete(); |
| } else if (msg == 'blkKeepAlive destroyed') { |
| blkKeepAliveDestroyed.complete(); |
| } |
| }); |
| |
| final isExited = Completer<void>(); |
| late final RawReceivePort exitPort; |
| exitPort = RawReceivePort((_) { |
| isExited.complete(); |
| exitPort.close(); |
| }); |
| |
| final isolate = Isolate.spawn( |
| (sendPort) { |
| final blkKeepAlive = VoidBlock.listener( |
| () {}, |
| keepIsolateAlive: true, |
| ); |
| final blkDontKeepAlive = VoidBlock.listener( |
| () {}, |
| keepIsolateAlive: false, |
| ); |
| sendPort.send('Blocks created'); |
| |
| final isolatePort = RawReceivePort((msg) { |
| if (msg == 'Destroy blkKeepAlive') { |
| blkKeepAlive.ref.release(); |
| sendPort.send('blkKeepAlive destroyed'); |
| } |
| })..keepIsolateAlive = false; |
| |
| sendPort.send(isolatePort.sendPort); |
| }, |
| receivePort.sendPort, |
| onExit: exitPort.sendPort, |
| ); |
| |
| await blocksCreated.future; |
| |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let exit message arrive. |
| |
| // Both blocks are still alive. |
| expect(isExited.isCompleted, isFalse); |
| |
| (await isolateSendPort.future).send('Destroy blkKeepAlive'); |
| await blkKeepAliveDestroyed.future; |
| |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let exit message arrive. |
| |
| // Only blkDontKeepAlive is alive. |
| await isExited; |
| |
| receivePort.close(); |
| }, skip: !canDoGC); |
| |
| test('Block.blocking, keepIsolateAlive', () async { |
| final isolateSendPort = Completer<SendPort>(); |
| final blocksCreated = Completer<void>(); |
| final blkKeepAliveDestroyed = Completer<void>(); |
| final receivePort = RawReceivePort((msg) { |
| if (msg is SendPort) { |
| isolateSendPort.complete(msg); |
| } else if (msg == 'Blocks created') { |
| blocksCreated.complete(); |
| } else if (msg == 'blkKeepAlive destroyed') { |
| blkKeepAliveDestroyed.complete(); |
| } |
| }); |
| |
| final isExited = Completer<void>(); |
| late final RawReceivePort exitPort; |
| exitPort = RawReceivePort((_) { |
| isExited.complete(); |
| exitPort.close(); |
| }); |
| |
| final isolate = Isolate.spawn( |
| (sendPort) { |
| final blkKeepAlive = VoidBlock.blocking( |
| () {}, |
| keepIsolateAlive: true, |
| ); |
| final blkDontKeepAlive = VoidBlock.blocking( |
| () {}, |
| keepIsolateAlive: false, |
| ); |
| sendPort.send('Blocks created'); |
| |
| final isolatePort = RawReceivePort((msg) { |
| if (msg == 'Destroy blkKeepAlive') { |
| blkKeepAlive.ref.release(); |
| sendPort.send('blkKeepAlive destroyed'); |
| } |
| })..keepIsolateAlive = false; |
| |
| sendPort.send(isolatePort.sendPort); |
| }, |
| receivePort.sendPort, |
| onExit: exitPort.sendPort, |
| ); |
| |
| await blocksCreated.future; |
| |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let exit message arrive. |
| |
| // Both blocks are still alive. |
| expect(isExited.isCompleted, isFalse); |
| |
| (await isolateSendPort.future).send('Destroy blkKeepAlive'); |
| await blkKeepAliveDestroyed.future; |
| |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. |
| doGC(); |
| await Future<void>.delayed(Duration.zero); // Let exit message arrive. |
| |
| // Only blkDontKeepAlive is alive. |
| await isExited; |
| |
| receivePort.close(); |
| }, skip: !canDoGC); |
| |
| test('Blocking block deadlock', () async { |
| // Regression test for https://github.com/dart-lang/native/issues/1967 |
| // Run the test on a new isolate so we can safely claim ownership of it. |
| final value = await Isolate.run(() { |
| setCurrentThreadOwnsIsolate(); |
| |
| var innerValue = 0; |
| final block = VoidBlock.blocking(() { |
| innerValue = 123; |
| }); |
| BlockTester.callOnSameThreadOutsideIsolate(block); |
| return innerValue; |
| }); |
| expect(value, 123); |
| }, skip: !hasIsolateOwnershipApi); |
| }); |
| } |
| |
| int _add100(int x) { |
| return x + 100; |
| } |