[pigeon] implements background platform channels support (#1022)
* [pigeon] added support for background platform channels
* added objc support
* updated the docstring
* added java support
* added error if taskqueues are used with flutter apis
* added compilation test for java
* added ios compilation test
* updated readme and version
* stuart feedback
diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
index 31676bd..2e8c814 100644
--- a/packages/pigeon/CHANGELOG.md
+++ b/packages/pigeon/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.0.1
+
+* Adds support for TaskQueues for serial background execution.
+
## 2.0.0
* Implements nullable parameters.
diff --git a/packages/pigeon/README.md b/packages/pigeon/README.md
index e773591..149e2c1 100644
--- a/packages/pigeon/README.md
+++ b/packages/pigeon/README.md
@@ -165,6 +165,22 @@
}
```
+### TaskQueues
+
+When targeting a Flutter version that supports the
+[TaskQueue API](https://docs.flutter.dev/development/platform-integration/platform-channels?tab=type-mappings-kotlin-tab#channels-and-platform-threading)
+the threading model for handling HostApi methods can be selected with the
+`TaskQueue` annotation:
+
+```dart
+@HostApi()
+abstract class Api2Host {
+ @TaskQueue(type: TaskQueueType.serialBackgroundThread)
+ int add(int x, int y);
+}
+```
+
+
## Feedback
File an issue in [flutter/flutter](https://github.com/flutter/flutter) with the
diff --git a/packages/pigeon/lib/ast.dart b/packages/pigeon/lib/ast.dart
index c9d284c..faffc7b 100644
--- a/packages/pigeon/lib/ast.dart
+++ b/packages/pigeon/lib/ast.dart
@@ -4,6 +4,7 @@
import 'package:collection/collection.dart' show ListEquality;
import 'package:meta/meta.dart';
+import 'pigeon_lib.dart';
final Function _listEquals = const ListEquality<dynamic>().equals;
@@ -29,6 +30,7 @@
this.isAsynchronous = false,
this.offset,
this.objcSelector = '',
+ this.taskQueueType = TaskQueueType.serial,
});
/// The name of the method.
@@ -49,6 +51,9 @@
/// An override for the generated objc selector (ex. "divideNumber:by:").
String objcSelector;
+ /// Specifies how handlers are dispatched with respect to threading.
+ TaskQueueType taskQueueType;
+
@override
String toString() {
final String objcSelectorStr =
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
index 948c197..107f035 100644
--- a/packages/pigeon/lib/generator_tools.dart
+++ b/packages/pigeon/lib/generator_tools.dart
@@ -8,7 +8,7 @@
import 'ast.dart';
/// The current version of pigeon. This must match the version in pubspec.yaml.
-const String pigeonVersion = '2.0.0';
+const String pigeonVersion = '2.0.1';
/// Read all the content from [stdin] to a String.
String readStdin() {
diff --git a/packages/pigeon/lib/java_generator.dart b/packages/pigeon/lib/java_generator.dart
index 9e97488..efa210c 100644
--- a/packages/pigeon/lib/java_generator.dart
+++ b/packages/pigeon/lib/java_generator.dart
@@ -2,10 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'package:pigeon/functional.dart';
-
import 'ast.dart';
+import 'functional.dart';
import 'generator_tools.dart';
+import 'pigeon_lib.dart';
/// Options that control how Java code will be generated.
class JavaOptions {
@@ -152,11 +152,22 @@
final String channelName = makeChannelName(api, method);
indent.write('');
indent.scoped('{', '}', () {
+ String? taskQueue;
+ if (method.taskQueueType != TaskQueueType.serial) {
+ taskQueue = 'taskQueue';
+ indent.writeln(
+ 'BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue();');
+ }
indent.writeln('BasicMessageChannel<Object> channel =');
indent.inc();
indent.inc();
- indent.writeln(
- 'new BasicMessageChannel<>(binaryMessenger, "$channelName", getCodec());');
+ indent.write(
+ 'new BasicMessageChannel<>(binaryMessenger, "$channelName", getCodec()');
+ if (taskQueue != null) {
+ indent.addln(', $taskQueue);');
+ } else {
+ indent.addln(');');
+ }
indent.dec();
indent.dec();
indent.write('if (api != null) ');
diff --git a/packages/pigeon/lib/objc_generator.dart b/packages/pigeon/lib/objc_generator.dart
index 7839b38..df900d5 100644
--- a/packages/pigeon/lib/objc_generator.dart
+++ b/packages/pigeon/lib/objc_generator.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:pigeon/functional.dart';
+import 'package:pigeon/pigeon_lib.dart';
import 'ast.dart';
import 'generator_tools.dart';
@@ -588,15 +589,20 @@
assert(api.location == ApiLocation.host);
final String apiName = _className(options.prefix, api.name);
- void writeChannelAllocation(Method func, String varName) {
+ void writeChannelAllocation(Method func, String varName, String? taskQueue) {
indent.writeln('FlutterBasicMessageChannel *$varName =');
indent.inc();
- indent.writeln('[FlutterBasicMessageChannel');
+ indent.writeln('[[FlutterBasicMessageChannel alloc]');
indent.inc();
- indent.writeln('messageChannelWithName:@"${makeChannelName(api, func)}"');
+ indent.writeln('initWithName:@"${makeChannelName(api, func)}"');
indent.writeln('binaryMessenger:binaryMessenger');
- indent
- .writeln('codec:${_getCodecGetterName(options.prefix, api.name)}()];');
+ indent.write('codec:${_getCodecGetterName(options.prefix, api.name)}()');
+ if (taskQueue != null) {
+ indent.addln('');
+ indent.writeln('taskQueue:$taskQueue];');
+ } else {
+ indent.writeln('];');
+ }
indent.dec();
indent.dec();
}
@@ -698,7 +704,13 @@
for (final Method func in api.methods) {
indent.write('');
indent.scoped('{', '}', () {
- writeChannelAllocation(func, channelName);
+ String? taskQueue;
+ if (func.taskQueueType != TaskQueueType.serial) {
+ taskQueue = 'taskQueue';
+ indent.writeln(
+ 'NSObject<FlutterTaskQueue> *$taskQueue = [binaryMessenger makeBackgroundTaskQueue];');
+ }
+ writeChannelAllocation(func, channelName, taskQueue);
indent.write('if (api) ');
indent.scoped('{', '}', () {
writeChannelApiBinding(func, channelName);
diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart
index a53a637..85144d9 100644
--- a/packages/pigeon/lib/pigeon_lib.dart
+++ b/packages/pigeon/lib/pigeon_lib.dart
@@ -94,6 +94,33 @@
final String value;
}
+/// Type of TaskQueue which determines how handlers are dispatched for
+/// HostApi's.
+enum TaskQueueType {
+ /// Handlers are invoked serially on the default thread. This is the value if
+ /// unspecified.
+ serial,
+
+ /// Handlers are invoked serially on a background thread.
+ serialBackgroundThread,
+
+ // TODO(gaaclarke): Add support for concurrent task queues.
+ // /// Handlers are invoked concurrently on a background thread.
+ // concurrentBackgroundThread,
+}
+
+/// Metadata annotation to control how handlers are dispatched for HostApi's.
+/// Note that the TaskQueue API might not be available on the target version of
+/// Flutter, see also:
+/// https://docs.flutter.dev/development/platform-integration/platform-channels.
+class TaskQueue {
+ /// The constructor for a TaskQueue.
+ const TaskQueue({required this.type});
+
+ /// The type of the TaskQueue.
+ final TaskQueueType type;
+}
+
/// Represents an error as a result of parsing and generating code.
class Error {
/// Parametric constructor for Error.
@@ -507,6 +534,13 @@
));
}
}
+ if (method.taskQueueType != TaskQueueType.serial &&
+ api.location != ApiLocation.host) {
+ result.add(Error(
+ message: 'Unsupported TaskQueue specification on ${method.name}',
+ lineNumber: _calculateLineNumberNullable(source, method.offset),
+ ));
+ }
}
}
@@ -757,6 +791,18 @@
return null;
}
+ T? _stringToEnum<T>(List<T> values, String? str) {
+ if (str == null) {
+ return null;
+ }
+ for (final T value in values) {
+ if (value.toString() == str) {
+ return value;
+ }
+ }
+ return null;
+ }
+
@override
Object? visitMethodDeclaration(dart_ast.MethodDeclaration node) {
final dart_ast.FormalParameterList parameters = node.parameters!;
@@ -770,6 +816,17 @@
.asNullable<dart_ast.SimpleStringLiteral>()
?.value ??
'';
+ final dart_ast.ArgumentList? taskQueueArguments =
+ _findMetadata(node.metadata, 'TaskQueue')?.arguments;
+ final String? taskQueueTypeName = taskQueueArguments == null
+ ? null
+ : getFirstChildOfType<dart_ast.NamedExpression>(taskQueueArguments)
+ ?.expression
+ .asNullable<dart_ast.PrefixedIdentifier>()
+ ?.name;
+ final TaskQueueType taskQueueType =
+ _stringToEnum(TaskQueueType.values, taskQueueTypeName) ??
+ TaskQueueType.serial;
if (_currentApi != null) {
// Methods without named return types aren't supported.
final dart_ast.TypeAnnotation returnType = node.returnType!;
@@ -785,7 +842,8 @@
arguments: arguments,
isAsynchronous: isAsynchronous,
objcSelector: objcSelector,
- offset: node.offset));
+ offset: node.offset,
+ taskQueueType: taskQueueType));
} else if (_currentClass != null) {
_errors.add(Error(
message:
diff --git a/packages/pigeon/pigeons/background_platform_channels.dart b/packages/pigeon/pigeons/background_platform_channels.dart
new file mode 100644
index 0000000..c21be6c
--- /dev/null
+++ b/packages/pigeon/pigeons/background_platform_channels.dart
@@ -0,0 +1,11 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+@HostApi()
+abstract class Api2Host {
+ @TaskQueue(type: TaskQueueType.serialBackgroundThread)
+ int add(int x, int y);
+}
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
index aea750f..2abcbc5 100644
--- a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
@@ -10,6 +10,7 @@
0D02163D27BC7B48009BD76F /* nullable_returns.gen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D02163C27BC7B48009BD76F /* nullable_returns.gen.m */; };
0D36469D27C6BE3C0069B7BF /* NullableReturnsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D36469C27C6BE3C0069B7BF /* NullableReturnsTest.m */; };
0D3646A027C6DCEC0069B7BF /* MockBinaryMessenger.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D36469F27C6DCEC0069B7BF /* MockBinaryMessenger.m */; };
+ 0D21E59A27D0502D0051D07D /* background_platform_channels.gen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D21E59827D0502D0051D07D /* background_platform_channels.gen.m */; };
0D50127523FF75B100CD5B95 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D50127423FF75B100CD5B95 /* RunnerTests.m */; };
0D6FD3C526A76D400046D8BD /* primitive.gen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6FD3C426A76D400046D8BD /* primitive.gen.m */; };
0D6FD3C726A777C00046D8BD /* PrimitiveTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6FD3C626A777C00046D8BD /* PrimitiveTest.m */; };
@@ -72,6 +73,8 @@
0D36469C27C6BE3C0069B7BF /* NullableReturnsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NullableReturnsTest.m; sourceTree = "<group>"; };
0D36469E27C6DCEC0069B7BF /* MockBinaryMessenger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockBinaryMessenger.h; sourceTree = "<group>"; };
0D36469F27C6DCEC0069B7BF /* MockBinaryMessenger.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockBinaryMessenger.m; sourceTree = "<group>"; };
+ 0D21E59827D0502D0051D07D /* background_platform_channels.gen.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = background_platform_channels.gen.m; sourceTree = "<group>"; };
+ 0D21E59927D0502D0051D07D /* background_platform_channels.gen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = background_platform_channels.gen.h; sourceTree = "<group>"; };
0D50127223FF75B100CD5B95 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
0D50127423FF75B100CD5B95 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = "<group>"; };
0D50127623FF75B100CD5B95 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -204,6 +207,8 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
+ 0D21E59927D0502D0051D07D /* background_platform_channels.gen.h */,
+ 0D21E59827D0502D0051D07D /* background_platform_channels.gen.m */,
0D02163B27BC7B48009BD76F /* nullable_returns.gen.h */,
0D02163C27BC7B48009BD76F /* nullable_returns.gen.m */,
0DBD8C40279B741800E4FDBA /* non_null_fields.gen.h */,
@@ -424,6 +429,7 @@
0DD2E6BC2684031300A7D764 /* host2flutter.gen.m in Sources */,
0DA5DFD626CC39D600D2354B /* multiple_arity.gen.m in Sources */,
0DD2E6BE2684031300A7D764 /* message.gen.m in Sources */,
+ 0D21E59A27D0502D0051D07D /* background_platform_channels.gen.m in Sources */,
0DD2E6BA2684031300A7D764 /* void_arg_host.gen.m in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
0D02163D27BC7B48009BD76F /* nullable_returns.gen.m in Sources */,
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
index 12f922e..098cfac 100644
--- a/packages/pigeon/pubspec.yaml
+++ b/packages/pigeon/pubspec.yaml
@@ -2,7 +2,7 @@
description: Code generator tool to make communication between Flutter and the host platform type-safe and easier.
repository: https://github.com/flutter/packages/tree/main/packages/pigeon
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3Apigeon
-version: 2.0.0 # This must match the version in lib/generator_tools.dart
+version: 2.0.1 # This must match the version in lib/generator_tools.dart
environment:
sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh
index 60f3971..91d86ed 100755
--- a/packages/pigeon/run_tests.sh
+++ b/packages/pigeon/run_tests.sh
@@ -235,6 +235,7 @@
gen_ios_unittests_code ./pigeons/all_void.dart ""
gen_ios_unittests_code ./pigeons/all_datatypes.dart ""
gen_ios_unittests_code ./pigeons/async_handlers.dart ""
+ gen_ios_unittests_code ./pigeons/background_platform_channels.dart "BC"
gen_ios_unittests_code ./pigeons/enum.dart "AC"
gen_ios_unittests_code ./pigeons/host2flutter.dart ""
gen_ios_unittests_code ./pigeons/list.dart "LST"
@@ -294,6 +295,7 @@
gen_android_unittests_code ./pigeons/all_void.dart AllVoid
gen_android_unittests_code ./pigeons/android_unittests.dart Pigeon
gen_android_unittests_code ./pigeons/async_handlers.dart AsyncHandlers
+ gen_android_unittests_code ./pigeons/background_platform_channels.dart BackgroundPlatformChannels
gen_android_unittests_code ./pigeons/enum.dart Enum
gen_android_unittests_code ./pigeons/host2flutter.dart Host2Flutter
gen_android_unittests_code ./pigeons/java_double_host_api.dart JavaDoubleHostApi
diff --git a/packages/pigeon/test/java_generator_test.dart b/packages/pigeon/test/java_generator_test.dart
index 57c4ef9..f65fe58 100644
--- a/packages/pigeon/test/java_generator_test.dart
+++ b/packages/pigeon/test/java_generator_test.dart
@@ -4,6 +4,7 @@
import 'package:pigeon/ast.dart';
import 'package:pigeon/java_generator.dart';
+import 'package:pigeon/pigeon.dart';
import 'package:test/test.dart';
void main() {
@@ -983,4 +984,39 @@
contains(
'public void doit(@Nullable Long fooArg, Reply<Void> callback) {'));
});
+
+ test('background platform channel', () {
+ final Root root = Root(
+ apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doit',
+ returnType: const TypeDeclaration.voidDeclaration(),
+ arguments: <NamedType>[
+ NamedType(
+ name: 'foo',
+ type: const TypeDeclaration(
+ baseName: 'int',
+ isNullable: true,
+ )),
+ ],
+ taskQueueType: TaskQueueType.serialBackgroundThread)
+ ])
+ ],
+ classes: <Class>[],
+ enums: <Enum>[],
+ );
+ final StringBuffer sink = StringBuffer();
+ const JavaOptions javaOptions = JavaOptions(className: 'Messages');
+ generateJava(javaOptions, root, sink);
+ final String code = sink.toString();
+ expect(
+ code,
+ contains(
+ 'BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue();'));
+ expect(
+ code,
+ contains(
+ 'new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.Api.doit", getCodec(), taskQueue)'));
+ });
}
diff --git a/packages/pigeon/test/objc_generator_test.dart b/packages/pigeon/test/objc_generator_test.dart
index 7044ecf..590d710 100644
--- a/packages/pigeon/test/objc_generator_test.dart
+++ b/packages/pigeon/test/objc_generator_test.dart
@@ -4,6 +4,7 @@
import 'package:pigeon/ast.dart';
import 'package:pigeon/objc_generator.dart';
+import 'package:pigeon/pigeon_lib.dart';
import 'package:test/test.dart';
void main() {
@@ -1731,4 +1732,31 @@
expect(code, contains('- (void)doitFoo:(nullable NSNumber *)arg_foo'));
}
});
+
+ test('background platform channel', () {
+ final Root root = Root(
+ apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doit',
+ returnType: const TypeDeclaration(
+ baseName: 'int',
+ isNullable: true,
+ ),
+ arguments: <NamedType>[],
+ taskQueueType: TaskQueueType.serialBackgroundThread)
+ ])
+ ],
+ classes: <Class>[],
+ enums: <Enum>[],
+ );
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(const ObjcOptions(), root, sink);
+ final String code = sink.toString();
+ expect(
+ code,
+ contains(
+ 'NSObject<FlutterTaskQueue> *taskQueue = [binaryMessenger makeBackgroundTaskQueue];'));
+ expect(code, contains('taskQueue:taskQueue'));
+ });
}
diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart
index 9922bc5..6d6447c 100644
--- a/packages/pigeon/test/pigeon_lib_test.dart
+++ b/packages/pigeon/test/pigeon_lib_test.dart
@@ -1041,4 +1041,48 @@
expect(
results.root.apis[0].methods[0].arguments[0].type.isNullable, isTrue);
});
+
+ test('task queue specified', () {
+ const String code = '''
+@HostApi()
+abstract class Api {
+ @TaskQueue(type: TaskQueueType.serialBackgroundThread)
+ int? calc();
+}
+''';
+
+ final ParseResults results = _parseSource(code);
+ expect(results.errors.length, 0);
+ expect(results.root.apis[0].methods[0].taskQueueType,
+ equals(TaskQueueType.serialBackgroundThread));
+ });
+
+ test('task queue unspecified', () {
+ const String code = '''
+@HostApi()
+abstract class Api {
+ int? calc();
+}
+''';
+
+ final ParseResults results = _parseSource(code);
+ expect(results.errors.length, 0);
+ expect(results.root.apis[0].methods[0].taskQueueType,
+ equals(TaskQueueType.serial));
+ });
+
+ test('unsupported task queue on FlutterApi', () {
+ const String code = '''
+@FlutterApi()
+abstract class Api {
+ @TaskQueue(type: TaskQueueType.serialBackgroundThread)
+ int? calc();
+}
+''';
+
+ final ParseResults results = _parseSource(code);
+ expect(results.errors.length, 1);
+ expect(results.errors[0].message,
+ contains('Unsupported TaskQueue specification'));
+ });
}