| // 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') | |
| import 'dart:async'; | |
| import 'dart:ffi'; | |
| import 'dart:io'; | |
| import 'dart:isolate'; | |
| import 'package:ffi/ffi.dart'; | |
| import 'package:objective_c/objective_c.dart' hide ObjCProtocolImpl, getClass; | |
| import 'package:objective_c/src/internal.dart' show getProtocol; | |
| import 'package:path/path.dart' as path; | |
| import 'package:test/test.dart'; | |
| import '../test_utils.dart'; | |
| import 'protocol_test_bindings.dart'; | |
| import 'util.dart'; | |
| typedef InstanceMethodBlock = ObjCBlock_NSString_ffiVoid_NSString_ffiDouble; | |
| typedef OptionalMethodBlock = ObjCBlock_Int32_ffiVoid_SomeStruct; | |
| typedef VoidMethodBlock = ObjCBlock_ffiVoid_ffiVoid_Int32; | |
| typedef OtherMethodBlock = ObjCBlock_Int32_ffiVoid_Int32_Int32_Int32_Int32; | |
| void main() { | |
| group('protocol', () { | |
| group('ObjC implementation', () { | |
| test('Method implementation', () { | |
| final protocolImpl = ObjCProtocolImpl(); | |
| final MyProtocol asMyProtocol = protocolImpl; | |
| final SecondaryProtocol asSecondaryProtocol = protocolImpl; | |
| final consumer = ProtocolConsumer(); | |
| // Required instance method. | |
| final result = consumer.callInstanceMethod(asMyProtocol); | |
| expect( | |
| result.toDartString(), | |
| 'ObjCProtocolImpl: Hello from ObjC: 3.14', | |
| ); | |
| // Optional instance method. | |
| final intResult = consumer.callOptionalMethod(asMyProtocol); | |
| expect(intResult, 579); | |
| // Required instance method from secondary protocol. | |
| final otherIntResult = consumer.callOtherMethod(asSecondaryProtocol); | |
| expect(otherIntResult, 10); | |
| // Method using two protocols. | |
| final twoMethodResult = consumer.callTwoMethods(asMyProtocol); | |
| expect(twoMethodResult, 579); | |
| }); | |
| test('Method implementation, invoke from Dart', () { | |
| final protocolImpl = ObjCProtocolImpl(); | |
| // Required instance method. | |
| final result = protocolImpl.instanceMethod( | |
| "abc".toNSString(), | |
| withDouble: 123, | |
| ); | |
| expect(result.toDartString(), 'ObjCProtocolImpl: abc: 123.00'); | |
| // Optional instance method. | |
| final structPtr = calloc<SomeStruct>(); | |
| structPtr.ref.x = 12; | |
| structPtr.ref.y = 34; | |
| final intResult = protocolImpl.optionalMethod(structPtr.ref); | |
| expect(intResult, 46); | |
| calloc.free(structPtr); | |
| // Required instance method from secondary protocol. | |
| final otherIntResult = protocolImpl.otherMethod(2, b: 4, c: 6, d: 8); | |
| expect(otherIntResult, 20); | |
| // Method from a protocol that isn't included by the filters. | |
| expect(protocolImpl.fooMethod(), 2468); | |
| // Class methods. | |
| expect(ObjCProtocolImpl.requiredClassMethod(), 9876); | |
| expect(ObjCProtocolImpl.optionalClassMethod(), 5432); | |
| }); | |
| test('Unimplemented method', () { | |
| final protocolImpl = ObjCProtocolImplMissingMethod(); | |
| final consumer = ProtocolConsumer(); | |
| // Optional instance method, not implemented. | |
| final intResult = consumer.callOptionalMethod(protocolImpl); | |
| expect(intResult, -999); | |
| }); | |
| test('Unimplemented method, invoke from Dart', () { | |
| final protocolImpl = ObjCProtocolImplMissingMethod(); | |
| // Optional instance method, not implemented. | |
| final structPtr = calloc<SomeStruct>(); | |
| structPtr.ref.x = 12; | |
| structPtr.ref.y = 34; | |
| expect( | |
| () => protocolImpl.optionalMethod(structPtr.ref), | |
| throwsA(isA<UnimplementedOptionalMethodException>()), | |
| ); | |
| calloc.free(structPtr); | |
| expect( | |
| () => ObjCProtocolImpl.unimplementedOtionalClassMethod(), | |
| throwsA(isA<UnimplementedOptionalMethodException>()), | |
| ); | |
| }); | |
| }); | |
| group('Dart implementation using helpers', () { | |
| test('Method implementation', () { | |
| final consumer = ProtocolConsumer(); | |
| final MyProtocol myProtocol = MyProtocol$Builder.implement( | |
| instanceMethod_withDouble_: (NSString s, double x) { | |
| return 'MyProtocol: ${s.toDartString()}: $x'.toNSString(); | |
| }, | |
| optionalMethod_: (SomeStruct s) { | |
| return s.y - s.x; | |
| }, | |
| ); | |
| expect(MyProtocol.conformsTo(myProtocol), isTrue); | |
| expect(SecondaryProtocol.conformsTo(myProtocol), isFalse); | |
| // Required instance method. | |
| final result = consumer.callInstanceMethod(myProtocol); | |
| expect(result.toDartString(), 'MyProtocol: Hello from ObjC: 3.14'); | |
| // Optional instance method. | |
| final intResult = consumer.callOptionalMethod(myProtocol); | |
| expect(intResult, 333); | |
| }); | |
| test('Multiple protocol implementation', () { | |
| final consumer = ProtocolConsumer(); | |
| final protocolBuilder = ObjCProtocolBuilder(); | |
| MyProtocol$Builder.addToBuilder( | |
| protocolBuilder, | |
| instanceMethod_withDouble_: (NSString s, double x) { | |
| return 'ProtocolBuilder: ${s.toDartString()}: $x'.toNSString(); | |
| }, | |
| optionalMethod_: (SomeStruct s) { | |
| return s.y - s.x; | |
| }, | |
| ); | |
| SecondaryProtocol$Builder.addToBuilder( | |
| protocolBuilder, | |
| otherMethod_b_c_d_: (int a, int b, int c, int d) { | |
| return a * b * c * d; | |
| }, | |
| ); | |
| final protocolImpl = protocolBuilder.build(); | |
| final MyProtocol asMyProtocol = MyProtocol.as(protocolImpl); | |
| final SecondaryProtocol asSecondaryProtocol = SecondaryProtocol.as( | |
| protocolImpl, | |
| ); | |
| expect(MyProtocol.conformsTo(protocolImpl), isTrue); | |
| expect(SecondaryProtocol.conformsTo(protocolImpl), isTrue); | |
| // Required instance method. | |
| final result = consumer.callInstanceMethod(asMyProtocol); | |
| expect(result.toDartString(), 'ProtocolBuilder: Hello from ObjC: 3.14'); | |
| // Required instance method from secondary protocol. | |
| final otherIntResult = consumer.callOtherMethod(asSecondaryProtocol); | |
| expect(otherIntResult, 24); | |
| // Method using two protocols. | |
| final twoMethodResult = consumer.callTwoMethods(asMyProtocol); | |
| expect(twoMethodResult, 222000); | |
| }); | |
| test('Multiple protocol implementation using method fields', () { | |
| final consumer = ProtocolConsumer(); | |
| final protocolBuilder = ObjCProtocolBuilder(); | |
| MyProtocol$Builder.instanceMethod_withDouble_.implement( | |
| protocolBuilder, | |
| (NSString s, double x) { | |
| return 'ProtocolBuilder: ${s.toDartString()}: $x'.toNSString(); | |
| }, | |
| ); | |
| SecondaryProtocol$Builder.otherMethod_b_c_d_.implement( | |
| protocolBuilder, | |
| (int a, int b, int c, int d) { | |
| return a * b * c * d; | |
| }, | |
| ); | |
| final protocolImpl = protocolBuilder.build(); | |
| final MyProtocol asMyProtocol = MyProtocol.as(protocolImpl); | |
| final SecondaryProtocol asSecondaryProtocol = SecondaryProtocol.as( | |
| protocolImpl, | |
| ); | |
| // Required instance method. | |
| final result = consumer.callInstanceMethod(asMyProtocol); | |
| expect(result.toDartString(), 'ProtocolBuilder: Hello from ObjC: 3.14'); | |
| // Required instance method from secondary protocol. | |
| final otherIntResult = consumer.callOtherMethod(asSecondaryProtocol); | |
| expect(otherIntResult, 24); | |
| }); | |
| test('Unimplemented method', () { | |
| final consumer = ProtocolConsumer(); | |
| final MyProtocol myProtocol = MyProtocol$Builder.implement( | |
| instanceMethod_withDouble_: (NSString s, double x) { | |
| throw UnimplementedError(); | |
| }, | |
| ); | |
| // Optional instance method, not implemented. | |
| final intResult = consumer.callOptionalMethod(myProtocol); | |
| expect(intResult, -999); | |
| }); | |
| test('Method implementation as listener', () async { | |
| final consumer = ProtocolConsumer(); | |
| final listenerCompleter = Completer<int>(); | |
| final MyProtocol myProtocol = MyProtocol$Builder.implementAsListener( | |
| instanceMethod_withDouble_: (NSString s, double x) { | |
| return 'MyProtocol: ${s.toDartString()}: $x'.toNSString(); | |
| }, | |
| optionalMethod_: (SomeStruct s) { | |
| return s.y - s.x; | |
| }, | |
| voidMethod_: (int x) { | |
| listenerCompleter.complete(x); | |
| }, | |
| ); | |
| // Required instance method. | |
| final result = consumer.callInstanceMethod(myProtocol); | |
| expect(result.toDartString(), 'MyProtocol: Hello from ObjC: 3.14'); | |
| // Optional instance method. | |
| final intResult = consumer.callOptionalMethod(myProtocol); | |
| expect(intResult, 333); | |
| // Listener method. | |
| consumer.callMethodOnRandomThread(myProtocol); | |
| expect(await listenerCompleter.future, 123); | |
| }); | |
| test('Multiple protocol implementation as listener', () async { | |
| final consumer = ProtocolConsumer(); | |
| final listenerCompleter = Completer<int>(); | |
| final protocolBuilder = ObjCProtocolBuilder(); | |
| MyProtocol$Builder.addToBuilderAsListener( | |
| protocolBuilder, | |
| instanceMethod_withDouble_: (NSString s, double x) { | |
| return 'ProtocolBuilder: ${s.toDartString()}: $x'.toNSString(); | |
| }, | |
| voidMethod_: (int x) { | |
| listenerCompleter.complete(x); | |
| }, | |
| ); | |
| SecondaryProtocol$Builder.addToBuilder( | |
| protocolBuilder, | |
| otherMethod_b_c_d_: (int a, int b, int c, int d) { | |
| return a * b * c * d; | |
| }, | |
| ); | |
| final protocolImpl = protocolBuilder.build(); | |
| final MyProtocol asMyProtocol = MyProtocol.as(protocolImpl); | |
| final SecondaryProtocol asSecondaryProtocol = SecondaryProtocol.as( | |
| protocolImpl, | |
| ); | |
| // Required instance method. | |
| final result = consumer.callInstanceMethod(asMyProtocol); | |
| expect(result.toDartString(), 'ProtocolBuilder: Hello from ObjC: 3.14'); | |
| // Required instance method from secondary protocol. | |
| final otherIntResult = consumer.callOtherMethod(asSecondaryProtocol); | |
| expect(otherIntResult, 24); | |
| // Listener method. | |
| consumer.callMethodOnRandomThread(asMyProtocol); | |
| expect(await listenerCompleter.future, 123); | |
| }); | |
| void waitSync(Duration d) { | |
| final t = Stopwatch(); | |
| t.start(); | |
| while (t.elapsed < d) { | |
| // Waiting... | |
| } | |
| } | |
| test('Method implementation as blocking', () async { | |
| final consumer = ProtocolConsumer(); | |
| final listenerCompleter = Completer<int>(); | |
| final MyProtocol myProtocol = MyProtocol$Builder.implementAsBlocking( | |
| instanceMethod_withDouble_: (NSString s, double x) { | |
| throw UnimplementedError(); | |
| }, | |
| voidMethod_: (int x) { | |
| listenerCompleter.complete(x); | |
| }, | |
| intPtrMethod_: (Pointer<Int32> ptr) { | |
| waitSync(Duration(milliseconds: 100)); | |
| ptr.value = 123456; | |
| }, | |
| ); | |
| // Blocking method. | |
| consumer.callBlockingMethodOnRandomThread(myProtocol); | |
| expect(await listenerCompleter.future, 123456); | |
| }); | |
| test('Multiple protocol implementation as blocking', () async { | |
| final consumer = ProtocolConsumer(); | |
| final listenerCompleter = Completer<int>(); | |
| final protocolBuilder = ObjCProtocolBuilder(); | |
| MyProtocol$Builder.addToBuilderAsBlocking( | |
| protocolBuilder, | |
| instanceMethod_withDouble_: (NSString s, double x) { | |
| throw UnimplementedError(); | |
| }, | |
| voidMethod_: (int x) { | |
| listenerCompleter.complete(x); | |
| }, | |
| intPtrMethod_: (Pointer<Int32> ptr) { | |
| waitSync(Duration(milliseconds: 100)); | |
| ptr.value = 98765; | |
| }, | |
| ); | |
| SecondaryProtocol$Builder.addToBuilder( | |
| protocolBuilder, | |
| otherMethod_b_c_d_: (int a, int b, int c, int d) { | |
| return a * b * c * d; | |
| }, | |
| ); | |
| final protocolImpl = protocolBuilder.build(); | |
| final MyProtocol asMyProtocol = MyProtocol.as(protocolImpl); | |
| final SecondaryProtocol asSecondaryProtocol = SecondaryProtocol.as( | |
| protocolImpl, | |
| ); | |
| // Required instance method from secondary protocol. | |
| final otherIntResult = consumer.callOtherMethod(asSecondaryProtocol); | |
| expect(otherIntResult, 24); | |
| // Blocking method. | |
| consumer.callBlockingMethodOnRandomThread(asMyProtocol); | |
| expect(await listenerCompleter.future, 98765); | |
| }); | |
| test('Direct method implementation using block', () async { | |
| final consumer = ProtocolConsumer(); | |
| final builder = ObjCProtocolBuilder(); | |
| MyProtocol$Builder.instanceMethod_withDouble_.implementWithBlock( | |
| builder, | |
| ObjCBlock_NSString_ffiVoid_NSString_ffiDouble.fromFunction( | |
| (Pointer<Void> _, NSString s, double x) => | |
| 'DirectImpl: ${s.toDartString()}: $x'.toNSString(), | |
| ), | |
| ); | |
| final myProtocol = MyProtocol.as(builder.build()); | |
| // Required instance method. | |
| final result = consumer.callInstanceMethod(myProtocol); | |
| expect(result.toDartString(), 'DirectImpl: Hello from ObjC: 3.14'); | |
| }); | |
| }); | |
| test('Filters', () { | |
| // SuperProtocol and FilteredProtocol's methods are included in the | |
| // bindings, but there should only be stub bindings for the protocols | |
| // themselves, because they're not included by the config. | |
| // FilteredUnusedProtocol shouldn't appear at all. | |
| final bindings = File( | |
| path.join( | |
| packagePathForTests, | |
| 'test', | |
| 'native_objc_test', | |
| 'protocol_test_bindings.dart', | |
| ), | |
| ).readAsStringSync(); | |
| expect(bindings, contains('instanceMethod_withDouble_')); | |
| expect(bindings, contains('fooMethod')); | |
| expect(bindings, contains('EmptyProtocol')); | |
| expect(bindings, isNot(contains('EmptyProtocol is a stub'))); | |
| expect(bindings, contains('MyProtocol')); | |
| expect(bindings, isNot(contains('MyProtocol is a stub'))); | |
| expect(bindings, contains('SecondaryProtocol')); | |
| expect(bindings, isNot(contains('SecondaryProtocol is a stub'))); | |
| expect(bindings, contains('SuperProtocol is a stub')); | |
| expect(bindings, contains('FilteredProtocol is a stub')); | |
| expect(bindings, isNot(contains('FilteredUnusedProtocol'))); | |
| expect(bindings, isNot(contains('filteredUnusedProtocolMethod'))); | |
| }); | |
| test('Unused protocol', () { | |
| // Regression test for https://github.com/dart-lang/native/issues/1672. | |
| final proto = UnusedProtocol$Builder.implement(someMethod: () => 123); | |
| expect(proto, isNotNull); | |
| }); | |
| test('Disabled method', () { | |
| // Regression test for https://github.com/dart-lang/native/issues/1702. | |
| expect(MyProtocol$Builder.instanceMethod_withDouble_.isAvailable, isTrue); | |
| expect(MyProtocol$Builder.optionalMethod_.isAvailable, isTrue); | |
| expect(MyProtocol$Builder.disabledMethod.isAvailable, isFalse); | |
| expect( | |
| () => MyProtocol$Builder.disabledMethod.implement( | |
| ObjCProtocolBuilder(), | |
| () => 123, | |
| ), | |
| throwsA(isA<FailedToLoadProtocolMethodException>()), | |
| ); | |
| }); | |
| test('conformsTo', () { | |
| final inst = ObjCProtocolImpl(); | |
| expect(NSObjectProtocol.conformsTo(inst), isTrue); | |
| expect(MyProtocol.conformsTo(inst), isTrue); | |
| expect(SecondaryProtocol.conformsTo(inst), isTrue); | |
| expect(EmptyProtocol.conformsTo(inst), isFalse); | |
| expect(UnusedProtocol.conformsTo(inst), isFalse); | |
| }); | |
| test('Threading stress test', () async { | |
| final consumer = ProtocolConsumer(); | |
| final completer = Completer<void>(); | |
| int count = 0; | |
| final protocolBuilder = ObjCProtocolBuilder(); | |
| MyProtocol$Builder.voidMethod_.implementAsListener(protocolBuilder, ( | |
| int x, | |
| ) { | |
| expect(x, 123); | |
| ++count; | |
| if (count == 1000) completer.complete(); | |
| }); | |
| final protocol = protocolBuilder.build(); | |
| final MyProtocol asMyProtocol = MyProtocol.as(protocol); | |
| for (int i = 0; i < 1000; ++i) { | |
| consumer.callMethodOnRandomThread(asMyProtocol); | |
| } | |
| await completer.future; | |
| expect(count, 1000); | |
| }); | |
| @pragma('vm:never-inline') | |
| (NSObject, ReferenceTracker) blockRefCountTestInner(Arena arena) { | |
| final protocolBuilder = ObjCProtocolBuilder(); | |
| final block = InstanceMethodBlock.fromFunction( | |
| (Pointer<Void> p, NSString s, double x) => 'Hello'.toNSString(), | |
| ); | |
| final blockTracker = ReferenceTracker(arena); | |
| blockTracker.trackBlock(block); | |
| MyProtocol$Builder.instanceMethod_withDouble_.implementWithBlock( | |
| protocolBuilder, | |
| block, | |
| ); | |
| final protocol = protocolBuilder.build(); | |
| // There are 2 references to the block. One owned by the Dart wrapper | |
| // object, and the other owned by the protocol. | |
| doGC(); | |
| expect(blockTracker.isAlive, true); | |
| return (protocol, blockTracker); | |
| } | |
| @pragma('vm:never-inline') | |
| ReferenceTracker blockRefCountTest(Arena arena) { | |
| final (protocol, blockTracker) = blockRefCountTestInner(arena); | |
| // The Dart side block wrapper has gone out of scope, but the protocol | |
| // still owns a reference to it. | |
| doGC(); | |
| expect(blockTracker.isAlive, true); | |
| expect(protocol, isNotNull); // Force protocol to stay in scope. | |
| return blockTracker; | |
| } | |
| test('Block ref counting', () async { | |
| await using((arena) async { | |
| final blockTracker = blockRefCountTest(arena); | |
| // The protocol object has gone out of scope, so it should be cleaned up. | |
| // So should the block. | |
| doGC(); | |
| await Future<void>.delayed(Duration.zero); | |
| doGC(); | |
| expect(blockTracker.isAlive, false); | |
| }); | |
| }, skip: !canDoGC); | |
| test('keepIsolateAlive', () async { | |
| final isolateSendPort = Completer<SendPort>(); | |
| final protosCreated = Completer<void>(); | |
| final protoKeepAliveDestroyed = Completer<void>(); | |
| final receivePort = RawReceivePort((msg) { | |
| if (msg is SendPort) { | |
| isolateSendPort.complete(msg); | |
| } else if (msg == 'Protocols created') { | |
| protosCreated.complete(); | |
| } else if (msg == 'protoKeepAlive destroyed') { | |
| protoKeepAliveDestroyed.complete(); | |
| } | |
| }); | |
| final isExited = Completer<void>(); | |
| late final RawReceivePort exitPort; | |
| exitPort = RawReceivePort((_) { | |
| isExited.complete(); | |
| exitPort.close(); | |
| }); | |
| final isolate = Isolate.spawn( | |
| (sendPort) { | |
| final protoKeepAlive = ObjCProtocolBuilder().build( | |
| keepIsolateAlive: true, | |
| ); | |
| final protoDontKeepAlive = ObjCProtocolBuilder().build( | |
| keepIsolateAlive: false, | |
| ); | |
| sendPort.send('Protocols created'); | |
| final isolatePort = RawReceivePort((msg) { | |
| if (msg == 'Destroy protoKeepAlive') { | |
| protoKeepAlive.ref.release(); | |
| sendPort.send('protoKeepAlive destroyed'); | |
| } | |
| })..keepIsolateAlive = false; | |
| sendPort.send(isolatePort.sendPort); | |
| }, | |
| receivePort.sendPort, | |
| onExit: exitPort.sendPort, | |
| ); | |
| await protosCreated.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 protoKeepAlive'); | |
| await protoKeepAliveDestroyed.future; | |
| doGC(); | |
| await Future<void>.delayed(Duration.zero); // Let dispose message arrive. | |
| doGC(); | |
| await Future<void>.delayed(Duration.zero); // Let exit message arrive. | |
| // Only protoDontKeepAlive is alive. | |
| await isExited; | |
| receivePort.close(); | |
| }, skip: !canDoGC); | |
| test('class disposal, builder first', () { | |
| ObjCProtocolBuilder? protocolBuilder = ObjCProtocolBuilder( | |
| debugName: 'Foo', | |
| ); | |
| NSObject? protocol = protocolBuilder.build(); | |
| final clazz = getClass(protocol); | |
| expect( | |
| getClassName(clazz).cast<Utf8>().toDartString(), | |
| startsWith('Foo'), | |
| ); | |
| expect(isValidClass(clazz), isTrue); | |
| protocolBuilder = null; | |
| doGC(); | |
| expect(isValidClass(clazz), isTrue); | |
| expect(protocol, isNotNull); | |
| protocol = null; | |
| doGC(); | |
| expect(isValidClass(clazz), isFalse); | |
| }, skip: !canDoGC); | |
| test('class disposal, instance first', () { | |
| ObjCProtocolBuilder? protocolBuilder = ObjCProtocolBuilder( | |
| debugName: 'Foo', | |
| ); | |
| NSObject? protocol = protocolBuilder.build(); | |
| final clazz = getClass(protocol); | |
| expect( | |
| getClassName(clazz).cast<Utf8>().toDartString(), | |
| startsWith('Foo'), | |
| ); | |
| expect(isValidClass(clazz), isTrue); | |
| protocolBuilder = null; | |
| doGC(); | |
| expect(isValidClass(clazz), isTrue); | |
| expect(protocol, isNotNull); | |
| protocol = null; | |
| doGC(); | |
| expect(isValidClass(clazz), isFalse); | |
| }, skip: !canDoGC); | |
| test('adding more methods after build', () { | |
| final protocolBuilder = ObjCProtocolBuilder(); | |
| MyProtocol$Builder.addToBuilder( | |
| protocolBuilder, | |
| instanceMethod_withDouble_: (NSString s, double x) { | |
| return 'ProtocolBuilder: ${s.toDartString()}: $x'.toNSString(); | |
| }, | |
| optionalMethod_: (SomeStruct s) { | |
| return s.y - s.x; | |
| }, | |
| ); | |
| final protocolImpl = protocolBuilder.build(); | |
| expect( | |
| () => SecondaryProtocol$Builder.addToBuilder( | |
| protocolBuilder, | |
| otherMethod_b_c_d_: (int a, int b, int c, int d) { | |
| return a * b * c * d; | |
| }, | |
| ), | |
| throwsA(isA<StateError>()), | |
| ); | |
| }); | |
| test('calling methods on a protocol instance', () { | |
| final protoImpl = ObjCProtocolImpl(); | |
| final MyProtocol myProto = protoImpl.returnsMyProtocol(); | |
| final result = myProto.instanceMethod( | |
| "abc".toNSString(), | |
| withDouble: 123, | |
| ); | |
| expect(result.toDartString(), 'ObjCProtocolImpl: abc: 123.00'); | |
| }); | |
| group('Isolate destruction callbacks', () { | |
| test('Listener - successful callback flow', () async { | |
| final protoPort = ReceivePort(); | |
| final callbackPort = ReceivePort(); | |
| final exitPort = ReceivePort(); | |
| final targetIsolate = await Isolate.spawn(_protocolTargetIsolateEntry, ( | |
| protoPort.sendPort, | |
| callbackPort.sendPort, | |
| true, | |
| true, | |
| false, | |
| ), onExit: exitPort.sendPort); | |
| final proto = await protoPort.first as NSObject; | |
| await using((arena) async { | |
| final tracker = ReferenceTracker(arena); | |
| NSObject? arg = NSObject(); | |
| tracker.track(arg); | |
| expect(tracker.isAlive, isTrue); | |
| final myProto = MyProtocol.as(proto); | |
| myProto.objectMethod(arg); | |
| final callbackResult = await callbackPort.first; | |
| expect(callbackResult, 'callback_executed'); | |
| targetIsolate.kill(priority: Isolate.immediate); | |
| await exitPort.first; | |
| arg = null; | |
| doGC(); | |
| expect(tracker.isAlive, isFalse); | |
| }); | |
| }); | |
| test('Listener - target isolate destroyed before invocation', () async { | |
| final protoPort = ReceivePort(); | |
| final callbackPort = ReceivePort(); | |
| final exitPort = ReceivePort(); | |
| final targetIsolate = await Isolate.spawn(_protocolTargetIsolateEntry, ( | |
| protoPort.sendPort, | |
| callbackPort.sendPort, | |
| true, | |
| false, | |
| false, | |
| ), onExit: exitPort.sendPort); | |
| final proto = await protoPort.first as NSObject; | |
| targetIsolate.kill(priority: Isolate.immediate); | |
| await exitPort.first; | |
| await using((arena) async { | |
| final tracker = ReferenceTracker(arena); | |
| NSObject? arg = NSObject(); | |
| tracker.track(arg); | |
| expect(tracker.isAlive, isTrue); | |
| final myProto = MyProtocol.as(proto); | |
| myProto.objectMethod(arg); | |
| expect( | |
| callbackPort.first.timeout(const Duration(milliseconds: 200)), | |
| throwsA(isA<TimeoutException>()), | |
| ); | |
| arg = null; | |
| doGC(); | |
| expect(tracker.isAlive, isFalse); | |
| }); | |
| }); | |
| test('Listener - target isolate destroyed after invocation' | |
| ' but before handling', () async { | |
| final protoPort = ReceivePort(); | |
| final callbackPort = ReceivePort(); | |
| final exitPort = ReceivePort(); | |
| final targetIsolate = await Isolate.spawn(_protocolTargetIsolateEntry, ( | |
| protoPort.sendPort, | |
| callbackPort.sendPort, | |
| true, | |
| false, | |
| true, | |
| ), onExit: exitPort.sendPort); | |
| final proto = await protoPort.first as NSObject; | |
| await using((arena) async { | |
| final tracker = ReferenceTracker(arena); | |
| NSObject? arg = NSObject(); | |
| tracker.track(arg); | |
| expect(tracker.isAlive, isTrue); | |
| final myProto = MyProtocol.as(proto); | |
| myProto.objectMethod(arg); | |
| targetIsolate.kill(priority: Isolate.immediate); | |
| await exitPort.first; | |
| expect( | |
| callbackPort.first.timeout(const Duration(milliseconds: 200)), | |
| throwsA(isA<TimeoutException>()), | |
| ); | |
| arg = null; | |
| doGC(); | |
| expect(tracker.isAlive, isFalse); | |
| }); | |
| }); | |
| test('Blocking - successful callback flow', () async { | |
| final protoPort = ReceivePort(); | |
| final callbackPort = ReceivePort(); | |
| final exitPort = ReceivePort(); | |
| final targetIsolate = await Isolate.spawn(_protocolTargetIsolateEntry, ( | |
| protoPort.sendPort, | |
| callbackPort.sendPort, | |
| false, | |
| true, | |
| false, | |
| ), onExit: exitPort.sendPort); | |
| final proto = await protoPort.first as NSObject; | |
| await using((arena) async { | |
| final tracker = ReferenceTracker(arena); | |
| NSObject? arg = NSObject(); | |
| tracker.track(arg); | |
| expect(tracker.isAlive, isTrue); | |
| final myProto = MyProtocol.as(proto); | |
| myProto.objectMethod(arg); | |
| final callbackResult = await callbackPort.first; | |
| expect(callbackResult, 'callback_executed'); | |
| targetIsolate.kill(priority: Isolate.immediate); | |
| await exitPort.first; | |
| arg = null; | |
| doGC(); | |
| expect(tracker.isAlive, isFalse); | |
| }); | |
| }); | |
| test('Blocking - target isolate destroyed before invocation', () async { | |
| final protoPort = ReceivePort(); | |
| final callbackPort = ReceivePort(); | |
| final exitPort = ReceivePort(); | |
| final targetIsolate = await Isolate.spawn(_protocolTargetIsolateEntry, ( | |
| protoPort.sendPort, | |
| callbackPort.sendPort, | |
| false, | |
| false, | |
| false, | |
| ), onExit: exitPort.sendPort); | |
| final proto = await protoPort.first as NSObject; | |
| targetIsolate.kill(priority: Isolate.immediate); | |
| await exitPort.first; | |
| await using((arena) async { | |
| final tracker = ReferenceTracker(arena); | |
| NSObject? arg = NSObject(); | |
| tracker.track(arg); | |
| expect(tracker.isAlive, isTrue); | |
| final myProto = MyProtocol.as(proto); | |
| myProto.objectMethod(arg); | |
| expect( | |
| callbackPort.first.timeout(const Duration(milliseconds: 200)), | |
| throwsA(isA<TimeoutException>()), | |
| ); | |
| arg = null; | |
| doGC(); | |
| expect(tracker.isAlive, isFalse); | |
| }); | |
| }); | |
| test('Blocking - target isolate destroyed after invocation' | |
| ' but before handling', () async { | |
| final protoPort = ReceivePort(); | |
| final callbackPort = ReceivePort(); | |
| final exitPort = ReceivePort(); | |
| final targetIsolate = await Isolate.spawn(_protocolTargetIsolateEntry, ( | |
| protoPort.sendPort, | |
| callbackPort.sendPort, | |
| false, | |
| false, | |
| true, | |
| ), onExit: exitPort.sendPort); | |
| final proto = await protoPort.first as NSObject; | |
| // Spawn a killer isolate to kill the target isolate after 200ms. | |
| await Isolate.spawn((args) async { | |
| final (targetIsolate, exitPort) = args; | |
| await Future<void>.delayed(const Duration(milliseconds: 200)); | |
| targetIsolate.kill(priority: Isolate.immediate); | |
| }, (targetIsolate, exitPort.sendPort)); | |
| await using((arena) async { | |
| final tracker = ReferenceTracker(arena); | |
| NSObject? arg = NSObject(); | |
| tracker.track(arg); | |
| expect(tracker.isAlive, isTrue); | |
| final myProto = MyProtocol.as(proto); | |
| myProto.objectMethod(arg); | |
| await exitPort.first; | |
| expect( | |
| callbackPort.first.timeout(const Duration(milliseconds: 200)), | |
| throwsA(isA<TimeoutException>()), | |
| ); | |
| arg = null; | |
| doGC(); | |
| expect(tracker.isAlive, isFalse); | |
| }); | |
| }); | |
| }, skip: !canDoGC); | |
| }); | |
| } | |
| void _protocolTargetIsolateEntry( | |
| ( | |
| SendPort protoPort, | |
| SendPort callbackPort, | |
| bool isListener, | |
| bool sleepBeforeExit, | |
| bool busyWaitBeforeExit, | |
| ) | |
| args, | |
| ) async { | |
| final ( | |
| protoPort, | |
| callbackPort, | |
| isListener, | |
| sleepBeforeExit, | |
| busyWaitBeforeExit, | |
| ) = args; | |
| final builder = ObjCProtocolBuilder(); | |
| if (isListener) { | |
| MyProtocol$Builder.objectMethod_.implementAsListener(builder, ( | |
| NSObject obj, | |
| ) { | |
| callbackPort.send('callback_executed'); | |
| }); | |
| } else { | |
| MyProtocol$Builder.objectMethod_.implementAsBlocking(builder, ( | |
| NSObject obj, | |
| ) { | |
| callbackPort.send('callback_executed'); | |
| }); | |
| } | |
| MyProtocol$Builder.instanceMethod_withDouble_.implement(builder, ( | |
| NSString s, | |
| double x, | |
| ) { | |
| return 'unused'.toNSString(); | |
| }); | |
| final proto = builder.build(); | |
| protoPort.send(proto); | |
| if (sleepBeforeExit) { | |
| await Future<void>.delayed(const Duration(seconds: 10)); | |
| } else if (busyWaitBeforeExit) { | |
| final stopwatch = Stopwatch()..start(); | |
| while (stopwatch.elapsed.inSeconds < 10) { | |
| // Wait 10 seconds, but don't use Future.delayed. We specifically don't | |
| // want to allow messages to arrive while we're waiting. | |
| } | |
| } | |
| } |