From 6e130e0f1516e5fdadfd3fa92e2c1be265cd4c8d Mon Sep 17 00:00:00 2001 From: Aaron Clarke Date: Thu, 4 Aug 2022 16:26:06 -0700 Subject: [PATCH 1/2] Started handling messages from background isolates. --- .../channels/ios/Runner/AppDelegate.m | 13 ++- dev/integration_tests/channels/lib/main.dart | 3 + .../channels/lib/src/basic_messaging.dart | 33 ++++++++ .../channels/lib/src/test_step.dart | 2 + .../lib/src/services/platform_channel.dart | 83 +++++++++++++++++-- 5 files changed, 128 insertions(+), 6 deletions(-) diff --git a/dev/integration_tests/channels/ios/Runner/AppDelegate.m b/dev/integration_tests/channels/ios/Runner/AppDelegate.m index 719abd3a48835..8fc53c3005ce7 100644 --- a/dev/integration_tests/channels/ios/Runner/AppDelegate.m +++ b/dev/integration_tests/channels/ios/Runner/AppDelegate.m @@ -114,7 +114,18 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [FlutterMethodChannel methodChannelWithName:@"std-method" binaryMessenger:flutterController codec:[FlutterStandardMethodCodec codecWithReaderWriter:extendedReaderWriter]]]; - return [super application:application didFinishLaunchingWithOptions:launchOptions]; + + [[FlutterBasicMessageChannel + messageChannelWithName:@"std-echo" + binaryMessenger:flutterController + codec:[FlutterStandardMessageCodec + codecWithReaderWriter:extendedReaderWriter]] + setMessageHandler:^(id message, FlutterReply reply) { + reply(message); + }]; + + return [super application:application + didFinishLaunchingWithOptions:launchOptions]; } - (void)setupMessagingHandshakeOnChannel:(FlutterBasicMessageChannel*)channel { diff --git a/dev/integration_tests/channels/lib/main.dart b/dev/integration_tests/channels/lib/main.dart index fdd1a25c583d5..70bfec9d4254d 100644 --- a/dev/integration_tests/channels/lib/main.dart +++ b/dev/integration_tests/channels/lib/main.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io' show Platform; import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -166,6 +167,8 @@ class _TestAppState extends State { () => basicStringMessageToUnknownChannel(), () => basicJsonMessageToUnknownChannel(), () => basicStandardMessageToUnknownChannel(), + if (Platform.isIOS) + () => basicBackgroundStandardEcho(123), ]; Future? _result; int _step = 0; diff --git a/dev/integration_tests/channels/lib/src/basic_messaging.dart b/dev/integration_tests/channels/lib/src/basic_messaging.dart index 08392fc5d4912..0d46edd1ec6dd 100644 --- a/dev/integration_tests/channels/lib/src/basic_messaging.dart +++ b/dev/integration_tests/channels/lib/src/basic_messaging.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:isolate'; import 'package:flutter/services.dart'; @@ -78,6 +79,38 @@ Future basicStandardHandshake(dynamic message) async { 'Standard >${toString(message)}<', channel, message); } +Future _basicBackgroundStandardEchoMain(List args) async { + final SendPort sendPort = args[2] as SendPort; + final Object message = args[1]; + final String name = 'Background Echo >${toString(message)}<'; + const String description = + 'Uses a platform channel from a background isolate.'; + try { + BackgroundIsolateBinding.initializeBackgroundIsolate( + args[0] as BackgroundIsolateBinding); + const BasicMessageChannel channel = BasicMessageChannel( + 'std-echo', + ExtendedStandardMessageCodec(), + ); + final Object response = await channel.send(message) as Object; + + final TestStatus testStatus = TestStepResult.deepEquals(message, response) + ? TestStatus.ok + : TestStatus.failed; + sendPort.send(TestStepResult(name, description, testStatus)); + } catch (ex) { + sendPort.send(TestStepResult(name, description, TestStatus.failed, + error: ex.toString())); + } +} + +Future basicBackgroundStandardEcho(Object message) async { + final BackgroundIsolateBinding binding = BackgroundIsolateBinding.initializeRootIsolate(); + final ReceivePort receivePort = ReceivePort(); + Isolate.spawn(_basicBackgroundStandardEchoMain, [binding, message, receivePort.sendPort]); + return await receivePort.first as TestStepResult; +} + Future basicBinaryMessageToUnknownChannel() async { const BasicMessageChannel channel = BasicMessageChannel( diff --git a/dev/integration_tests/channels/lib/src/test_step.dart b/dev/integration_tests/channels/lib/src/test_step.dart index 6702560ebff5b..b449099f92c5d 100644 --- a/dev/integration_tests/channels/lib/src/test_step.dart +++ b/dev/integration_tests/channels/lib/src/test_step.dart @@ -90,6 +90,8 @@ class TestStepResult { ], ); } + + static bool deepEquals(dynamic a, dynamic b) => _deepEquals(a, b); } Future resultOfHandshake( diff --git a/packages/flutter/lib/src/services/platform_channel.dart b/packages/flutter/lib/src/services/platform_channel.dart index 47b609b3de821..a56f4bf8ae090 100644 --- a/packages/flutter/lib/src/services/platform_channel.dart +++ b/packages/flutter/lib/src/services/platform_channel.dart @@ -4,6 +4,8 @@ import 'dart:async'; import 'dart:developer'; +import 'dart:isolate' show ReceivePort; +import 'dart:ui' as ui show PlatformDispatcher, PlatformMessageResponseCallback; import 'package:flutter/foundation.dart'; @@ -123,6 +125,72 @@ void _debugRecordDownStream(String channelTypeName, String name, _debugLaunchProfilePlatformChannels(); } +BinaryMessenger? _backgroundIsolateBinaryMessenger; + +class BackgroundIsolateBinding { + BackgroundIsolateBinding._() : _rootIsolateId = ui.PlatformDispatcher.instance.registerRootIsolate(); + + final int _rootIsolateId; + + static BackgroundIsolateBinding initializeRootIsolate() { + return BackgroundIsolateBinding._(); + } + + static void initializeBackgroundIsolate(BackgroundIsolateBinding binding) { + if (_backgroundIsolateBinaryMessenger == null) { + ui.PlatformDispatcher.instance.registerBackgroundIsolate(binding._rootIsolateId); + final _PortBinaryMessenger portBinaryMessenger = _PortBinaryMessenger(); + _backgroundIsolateBinaryMessenger = portBinaryMessenger; + portBinaryMessenger.receivePort.listen((dynamic message) { + try { + final List args = message as List; + final int identifier = args[0] as int; + final Uint8List bytes = args[1] as Uint8List; + final ByteData byteData = ByteData.sublistView(bytes); + final Completer completer = + portBinaryMessenger._completers[identifier]!; + portBinaryMessenger._completers.remove(identifier); + completer.complete(byteData); + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'services library', + context: + ErrorDescription('during a platform message response callback'), + )); + } + }); + } + } +} + +class _PortBinaryMessenger extends BinaryMessenger { + final ReceivePort receivePort = ReceivePort(); + final Map> _completers = >{}; + int _messageCount = 0; + + @override + Future handlePlatformMessage(String channel, ByteData? data, ui.PlatformMessageResponseCallback? callback) { + throw UnimplementedError(); + } + + @override + Future? send(String channel, ByteData? message) { + final Completer completer = Completer(); + final int messageIdentifier = ++_messageCount; + _completers[messageIdentifier] = completer; + ui.PlatformDispatcher.instance.sendPortPlatformMessage( + channel, message, messageIdentifier, receivePort.sendPort); + return completer.future; + } + + @override + void setMessageHandler(String channel, MessageHandler? handler) { + throw UnimplementedError(); + } +} + /// A named channel for communicating with platform plugins using asynchronous /// message passing. /// @@ -162,8 +230,9 @@ class BasicMessageChannel { /// The messenger which sends the bytes for this channel, not null. BinaryMessenger get binaryMessenger { - final BinaryMessenger result = - _binaryMessenger ?? ServicesBinding.instance.defaultBinaryMessenger; + final BinaryMessenger result = _binaryMessenger ?? + _backgroundIsolateBinaryMessenger ?? + ServicesBinding.instance.defaultBinaryMessenger; return !kReleaseMode && debugProfilePlatformChannels ? _debugBinaryMessengers[this] ??= _ProfiledBinaryMessenger( // ignore: no_runtimetype_tostring @@ -250,8 +319,9 @@ class MethodChannel { /// /// The messenger may not be null. BinaryMessenger get binaryMessenger { - final BinaryMessenger result = - _binaryMessenger ?? ServicesBinding.instance.defaultBinaryMessenger; + final BinaryMessenger result = _binaryMessenger ?? + _backgroundIsolateBinaryMessenger ?? + ServicesBinding.instance.defaultBinaryMessenger; return !kReleaseMode && debugProfilePlatformChannels ? _debugBinaryMessengers[this] ??= _ProfiledBinaryMessenger( // ignore: no_runtimetype_tostring @@ -601,7 +671,10 @@ class EventChannel { final MethodCodec codec; /// The messenger used by this channel to send platform messages, not null. - BinaryMessenger get binaryMessenger => _binaryMessenger ?? ServicesBinding.instance.defaultBinaryMessenger; + BinaryMessenger get binaryMessenger => + _binaryMessenger ?? + _backgroundIsolateBinaryMessenger ?? + ServicesBinding.instance.defaultBinaryMessenger; final BinaryMessenger? _binaryMessenger; /// Sets up a broadcast stream for receiving events on this channel. From 04194fd1ada2a94125b16fc2a9d21e2eed715ebc Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 18 Aug 2022 11:35:14 -0700 Subject: [PATCH 2/2] alternative api proposal --- .../channels/lib/src/basic_messaging.dart | 18 +- .../flutter/lib/src/services/binding.dart | 9 + .../lib/src/services/platform_channel.dart | 194 +++++++++++------- 3 files changed, 140 insertions(+), 81 deletions(-) diff --git a/dev/integration_tests/channels/lib/src/basic_messaging.dart b/dev/integration_tests/channels/lib/src/basic_messaging.dart index 0d46edd1ec6dd..03c24e1567621 100644 --- a/dev/integration_tests/channels/lib/src/basic_messaging.dart +++ b/dev/integration_tests/channels/lib/src/basic_messaging.dart @@ -83,14 +83,13 @@ Future _basicBackgroundStandardEchoMain(List args) async { final SendPort sendPort = args[2] as SendPort; final Object message = args[1]; final String name = 'Background Echo >${toString(message)}<'; - const String description = - 'Uses a platform channel from a background isolate.'; + const String description = 'Uses a platform channel from a background isolate.'; try { - BackgroundIsolateBinding.initializeBackgroundIsolate( - args[0] as BackgroundIsolateBinding); - const BasicMessageChannel channel = BasicMessageChannel( + BackgroundIsolateBinaryMessenger.ensureInitialized(args[0] as RootIsolateToken); + final BasicMessageChannel channel = BasicMessageChannel( 'std-echo', - ExtendedStandardMessageCodec(), + const ExtendedStandardMessageCodec(), + binaryMessenger: BackgroundIsolateBinaryMessenger.instance, ); final Object response = await channel.send(message) as Object; @@ -105,9 +104,12 @@ Future _basicBackgroundStandardEchoMain(List args) async { } Future basicBackgroundStandardEcho(Object message) async { - final BackgroundIsolateBinding binding = BackgroundIsolateBinding.initializeRootIsolate(); final ReceivePort receivePort = ReceivePort(); - Isolate.spawn(_basicBackgroundStandardEchoMain, [binding, message, receivePort.sendPort]); + Isolate.spawn(_basicBackgroundStandardEchoMain, [ + ServicesBinding.instance.rootIsolateToken, + message, + receivePort.sendPort, + ]); return await receivePort.first as TestStepResult; } diff --git a/packages/flutter/lib/src/services/binding.dart b/packages/flutter/lib/src/services/binding.dart index 5fd8436b32930..22354e321233c 100644 --- a/packages/flutter/lib/src/services/binding.dart +++ b/packages/flutter/lib/src/services/binding.dart @@ -34,6 +34,7 @@ mixin ServicesBinding on BindingBase, SchedulerBinding { @override void initInstances() { super.initInstances(); + assert(platformDispatcher.isRootIsolate, 'Bindings are only meaningful on the root isolate.'); _instance = this; _defaultBinaryMessenger = createBinaryMessenger(); _restorationManager = createRestorationManager(); @@ -82,6 +83,14 @@ mixin ServicesBinding on BindingBase, SchedulerBinding { BinaryMessenger get defaultBinaryMessenger => _defaultBinaryMessenger; late final BinaryMessenger _defaultBinaryMessenger; + /// A token that represents the root isolate, used for coordinating with background + /// isolates. + /// + /// This property is primarily intended for use with + /// [BackgroundIsolateBinaryMessenger.ensureInitialized], which takes a + /// [RootIsolateToken] as its argument. + late RootIsolateToken rootIsolateToken = platformDispatcher.registerRootIsolate(); + /// The low level buffering and dispatch mechanism for messages sent by /// plugins on the engine side to their corresponding plugin code on /// the framework side. diff --git a/packages/flutter/lib/src/services/platform_channel.dart b/packages/flutter/lib/src/services/platform_channel.dart index a56f4bf8ae090..648cd8b35477c 100644 --- a/packages/flutter/lib/src/services/platform_channel.dart +++ b/packages/flutter/lib/src/services/platform_channel.dart @@ -125,70 +125,114 @@ void _debugRecordDownStream(String channelTypeName, String name, _debugLaunchProfilePlatformChannels(); } -BinaryMessenger? _backgroundIsolateBinaryMessenger; - -class BackgroundIsolateBinding { - BackgroundIsolateBinding._() : _rootIsolateId = ui.PlatformDispatcher.instance.registerRootIsolate(); - - final int _rootIsolateId; - - static BackgroundIsolateBinding initializeRootIsolate() { - return BackgroundIsolateBinding._(); +/// The transport for platform messages on background isolates. +/// +/// To obtain an instance of this class, call +/// [BackgroundIsolateBinaryMessenger.ensureInitialized] using the token obtained +/// from the root isolate's [ServicesBinding.rootIsolateToken]. +/// +/// Background isolates do not support [BinaryMessenger.setMessageHandler], they +/// can only handle replies (one reply per message sent using [send]). +class BackgroundIsolateBinaryMessenger extends BinaryMessenger { + BackgroundIsolateBinaryMessenger._(RootIsolateToken rootIsolate) { + ui.PlatformDispatcher.instance.registerBackgroundIsolate(rootIsolate); + _receivePort.listen(_receive); } - static void initializeBackgroundIsolate(BackgroundIsolateBinding binding) { - if (_backgroundIsolateBinaryMessenger == null) { - ui.PlatformDispatcher.instance.registerBackgroundIsolate(binding._rootIsolateId); - final _PortBinaryMessenger portBinaryMessenger = _PortBinaryMessenger(); - _backgroundIsolateBinaryMessenger = portBinaryMessenger; - portBinaryMessenger.receivePort.listen((dynamic message) { - try { - final List args = message as List; - final int identifier = args[0] as int; - final Uint8List bytes = args[1] as Uint8List; - final ByteData byteData = ByteData.sublistView(bytes); - final Completer completer = - portBinaryMessenger._completers[identifier]!; - portBinaryMessenger._completers.remove(identifier); - completer.complete(byteData); - } catch (exception, stack) { - FlutterError.reportError(FlutterErrorDetails( - exception: exception, - stack: stack, - library: 'services library', - context: - ErrorDescription('during a platform message response callback'), - )); - } - }); - } + /// The existing instance of this class, if any. + /// + /// Throws if [ensureInitialized] has not been called at least once. + static BackgroundIsolateBinaryMessenger get instance => _instance ?? (throw StateError( + 'BackgroundIsolateBinaryMessenger.instance must be initialized using BackgroundIsolateBinaryMessenger.ensureInitialized' + )); + static BackgroundIsolateBinaryMessenger? _instance; + /// Ensures that [BackgroundIsolateBinaryMessenger.instance] has been initialized. + /// + /// The argument should be the value obtained from [ServicesBinding.rootIsolateToken] + /// on the root isolate. + /// + /// This function is idempotent (calling it multiple times is harmless but has no effect). + static BackgroundIsolateBinaryMessenger ensureInitialized(RootIsolateToken rootIsolate) { + return _instance ??= BackgroundIsolateBinaryMessenger._(rootIsolate); } -} -class _PortBinaryMessenger extends BinaryMessenger { - final ReceivePort receivePort = ReceivePort(); + final ReceivePort _receivePort = ReceivePort(); final Map> _completers = >{}; int _messageCount = 0; @override Future handlePlatformMessage(String channel, ByteData? data, ui.PlatformMessageResponseCallback? callback) { - throw UnimplementedError(); + throw UnimplementedError('handlePlatformMessage is deprecated.'); } @override Future? send(String channel, ByteData? message) { final Completer completer = Completer(); - final int messageIdentifier = ++_messageCount; + _messageCount += 1; + final int messageIdentifier = _messageCount; _completers[messageIdentifier] = completer; ui.PlatformDispatcher.instance.sendPortPlatformMessage( - channel, message, messageIdentifier, receivePort.sendPort); + channel, + message, + messageIdentifier, + _receivePort.sendPort, + ); return completer.future; } + void _receive(Object? message) { + try { + final List args = message! as List; + final int identifier = args[0]! as int; + final Uint8List bytes = args[1]! as Uint8List; + final ByteData byteData = ByteData.sublistView(bytes); + _completers.remove(identifier)!.complete(byteData); + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'services library', + context: ErrorDescription('during a background isolate platform message response callback'), + )); + } + } + @override void setMessageHandler(String channel, MessageHandler? handler) { - throw UnimplementedError(); + throw UnsupportedError('Background isolates do not support setMessageHandler(). Messages from the host platform always go to the root isolate.'); + } +} + +BinaryMessenger _findBinaryMessenger(BinaryMessenger? explicitBinaryMessenger, Object client, Object codec) { + BinaryMessenger result; + if (explicitBinaryMessenger != null) { + result = explicitBinaryMessenger; + } else { + assert(() { + if (!ui.PlatformDispatcher.instance.isRootIsolate) { + throw ArgumentError.value( + explicitBinaryMessenger, + 'binaryMessenger', + 'On background isolates, the "binaryMessenger" argument to ${client.runtimeType} constructors ' + 'must be an explicit BinaryMessenger, not null. Typically a BackgroundIsolateBinaryMessenger ' + 'is used, as obtained from BackgroundIsolateBinaryMessenger.ensureInitialized().', + ); + } + return true; + }()); + result = ServicesBinding.instance.defaultBinaryMessenger; } + assert(() { + if (debugProfilePlatformChannels) { + result = _debugBinaryMessengers[client] ??= _ProfiledBinaryMessenger( + result, + client.runtimeType.toString(), + codec.runtimeType.toString(), + ); + } + return true; + }()); + return result; } /// A named channel for communicating with platform plugins using asynchronous @@ -215,8 +259,10 @@ class _PortBinaryMessenger extends BinaryMessenger { class BasicMessageChannel { /// Creates a [BasicMessageChannel] with the specified [name], [codec] and [binaryMessenger]. /// - /// The [name] and [codec] arguments cannot be null. The default [ServicesBinding.defaultBinaryMessenger] - /// instance is used if [binaryMessenger] is null. + /// The [name] and [codec] arguments cannot be null. The default + /// [ServicesBinding.defaultBinaryMessenger] instance is used if + /// [binaryMessenger] is null on the root isolate; on other isolates it must + /// not be null. const BasicMessageChannel(this.name, this.codec, { BinaryMessenger? binaryMessenger }) : assert(name != null), assert(codec != null), @@ -229,16 +275,14 @@ class BasicMessageChannel { final MessageCodec codec; /// The messenger which sends the bytes for this channel, not null. - BinaryMessenger get binaryMessenger { - final BinaryMessenger result = _binaryMessenger ?? - _backgroundIsolateBinaryMessenger ?? - ServicesBinding.instance.defaultBinaryMessenger; - return !kReleaseMode && debugProfilePlatformChannels - ? _debugBinaryMessengers[this] ??= _ProfiledBinaryMessenger( - // ignore: no_runtimetype_tostring - result, runtimeType.toString(), codec.runtimeType.toString()) - : result; - } + /// + /// On the root isolate, this defaults to the + /// [ServicesBinding.defaultBinaryMessenger]. + /// + /// On other isolates, a [BinaryMessenger] must be specified in the + /// [BasicMessageChannel] constructor. Typically this is the transport + /// obtained using [BackgroundIsolateBinaryMessenger.ensureInitialized]. + BinaryMessenger get binaryMessenger => _findBinaryMessenger(_binaryMessenger, this, codec); final BinaryMessenger? _binaryMessenger; /// Sends the specified [message] to the platform plugins on this channel. @@ -302,8 +346,10 @@ class MethodChannel { /// The [codec] used will be [StandardMethodCodec], unless otherwise /// specified. /// - /// The [name] and [codec] arguments cannot be null. The default [ServicesBinding.defaultBinaryMessenger] - /// instance is used if [binaryMessenger] is null. + /// The [name] and [codec] arguments cannot be null. The default + /// [ServicesBinding.defaultBinaryMessenger] instance is used if + /// [binaryMessenger] is null on the root isolate; on other isolates it must + /// not be null. const MethodChannel(this.name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger ]) : assert(name != null), assert(codec != null), @@ -317,17 +363,13 @@ class MethodChannel { /// The messenger used by this channel to send platform messages. /// - /// The messenger may not be null. - BinaryMessenger get binaryMessenger { - final BinaryMessenger result = _binaryMessenger ?? - _backgroundIsolateBinaryMessenger ?? - ServicesBinding.instance.defaultBinaryMessenger; - return !kReleaseMode && debugProfilePlatformChannels - ? _debugBinaryMessengers[this] ??= _ProfiledBinaryMessenger( - // ignore: no_runtimetype_tostring - result, runtimeType.toString(), codec.runtimeType.toString()) - : result; - } + /// On the root isolate, this defaults to the + /// [ServicesBinding.defaultBinaryMessenger]. + /// + /// On other isolates, a [BinaryMessenger] must be specified in the + /// [MethodChannel] constructor. Typically this is the transport + /// obtained using [BackgroundIsolateBinaryMessenger.ensureInitialized]. + BinaryMessenger get binaryMessenger => _findBinaryMessenger(_binaryMessenger, this, codec); final BinaryMessenger? _binaryMessenger; /// Backend implementation of [invokeMethod]. @@ -657,8 +699,10 @@ class EventChannel { /// The [codec] used will be [StandardMethodCodec], unless otherwise /// specified. /// - /// Neither [name] nor [codec] may be null. The default [ServicesBinding.defaultBinaryMessenger] - /// instance is used if [binaryMessenger] is null. + /// Neither [name] nor [codec] may be null. The default + /// [ServicesBinding.defaultBinaryMessenger] instance is used if + /// [binaryMessenger] is null on the root isolate; on other isolates it must + /// not be null. const EventChannel(this.name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger]) : assert(name != null), assert(codec != null), @@ -671,10 +715,14 @@ class EventChannel { final MethodCodec codec; /// The messenger used by this channel to send platform messages, not null. - BinaryMessenger get binaryMessenger => - _binaryMessenger ?? - _backgroundIsolateBinaryMessenger ?? - ServicesBinding.instance.defaultBinaryMessenger; + /// + /// On the root isolate, this defaults to the + /// [ServicesBinding.defaultBinaryMessenger]. + /// + /// On other isolates, a [BinaryMessenger] must be specified in the + /// [EventChannel] constructor. Typically this is the transport + /// obtained using [BackgroundIsolateBinaryMessenger.ensureInitialized]. + BinaryMessenger get binaryMessenger => _findBinaryMessenger(_binaryMessenger, this, codec); final BinaryMessenger? _binaryMessenger; /// Sets up a broadcast stream for receiving events on this channel.