[pigeon] Add initial FlutterApi integration tests (#2901)

* Add trivial async test as a baseline for the scaffolding

* Kotlin generator async void fix

* Add initial round-trip tests

* Windows fixes

* Cleanup

* Version bump for bug fix

* Fix analysis

* Version bump in generator
diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
index 9085b70..11332f6 100644
--- a/packages/pigeon/CHANGELOG.md
+++ b/packages/pigeon/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 4.2.9
+
+* [kotlin] Fixes a bug with some methods that return `void`.
+
 ## 4.2.8
 
 * Adds the ability to use `runWithOptions` entrypoint to allow external libraries to use the pigeon easier.
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
index 156cfa9..9774303 100644
--- a/packages/pigeon/lib/generator_tools.dart
+++ b/packages/pigeon/lib/generator_tools.dart
@@ -9,7 +9,7 @@
 import 'ast.dart';
 
 /// The current version of pigeon. This must match the version in pubspec.yaml.
-const String pigeonVersion = '4.2.8';
+const String pigeonVersion = '4.2.9';
 
 /// Read all the content from [stdin] to a String.
 String readStdin() {
diff --git a/packages/pigeon/lib/kotlin_generator.dart b/packages/pigeon/lib/kotlin_generator.dart
index 700c100..d4ed272 100644
--- a/packages/pigeon/lib/kotlin_generator.dart
+++ b/packages/pigeon/lib/kotlin_generator.dart
@@ -233,15 +233,11 @@
                       'api.${method.name}(${methodArgument.join(', ')})';
                   if (method.isAsynchronous) {
                     indent.write('$call ');
-                    if (method.returnType.isVoid) {
-                      indent.scoped('{', '}', () {
-                        indent.writeln('reply.reply(null)');
-                      });
-                    } else {
-                      indent.scoped('{', '}', () {
-                        indent.writeln('reply.reply(wrapResult(it))');
-                      });
-                    }
+                    final String resultValue =
+                        method.returnType.isVoid ? 'null' : 'it';
+                    indent.scoped('{', '}', () {
+                      indent.writeln('reply.reply(wrapResult($resultValue))');
+                    });
                   } else if (method.returnType.isVoid) {
                     indent.writeln(call);
                     indent.writeln('wrapped["${Keys.result}"] = null');
diff --git a/packages/pigeon/pigeons/core_tests.dart b/packages/pigeon/pigeons/core_tests.dart
index ca56f90..3adf890 100644
--- a/packages/pigeon/pigeons/core_tests.dart
+++ b/packages/pigeon/pigeons/core_tests.dart
@@ -33,6 +33,8 @@
 /// platform_test integration tests.
 @HostApi()
 abstract class HostIntegrationCoreApi {
+  // ========== Syncronous method tests ==========
+
   /// A no-op function taking no arguments and returning no value, to sanity
   /// test basic calling.
   void noop();
@@ -54,13 +56,31 @@
   @ObjCSelector('createNestedObjectWithString:')
   AllTypesWrapper createNestedString(String string);
 
-  // TODO(stuartmorgan): Add wrapper methods to trigger calls back into
-  // FlutterIntegrationCore methods, to allow Dart-driven integration testing
-  // of host->Dart calls. Each wrapper would be implemented by calling the
-  // corresponding FlutterIntegrationCore method, passing arguments and return
-  // values along unchanged. Since these will need to be async, we also need
-  // async host API tests here, so that failures in Dart->host async calling
-  // don't only show up here.
+  // ========== Asyncronous method tests ==========
+
+  /// A no-op function taking no arguments and returning no value, to sanity
+  /// test basic asynchronous calling.
+  @async
+  void noopAsync();
+
+  /// Returns the passed string asynchronously.
+  @async
+  @ObjCSelector('echoAsyncString:')
+  String echoAsyncString(String aString);
+
+  // ========== Flutter API test wrappers ==========
+
+  @async
+  void callFlutterNoop();
+
+  @async
+  @ObjCSelector('callFlutterEchoString:')
+  String callFlutterEchoString(String aString);
+
+  // TODO(stuartmorgan): Add callFlutterEchoString and the associated test once
+  // either https://github.com/flutter/flutter/issues/116117 is fixed, or the
+  // problematic type is moved out of AllTypes and into its own test, since
+  // the type mismatch breaks the second `encode` round.
 }
 
 /// The core interface that the Dart platform_test code implements for host
@@ -74,6 +94,10 @@
   /// Returns the passed object, to test serialization and deserialization.
   @ObjCSelector('echoAllTypes:')
   AllTypes echoAllTypes(AllTypes everything);
+
+  /// Returns the passed string, to test serialization and deserialization.
+  @ObjCSelector('echoString:')
+  String echoString(String aString);
 }
 
 /// An API that can be implemented for minimal, compile-only tests.
diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/AlternateLanguageTestPlugin.java b/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/AlternateLanguageTestPlugin.java
index 3677e7d..a4057b7 100644
--- a/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/AlternateLanguageTestPlugin.java
+++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/AlternateLanguageTestPlugin.java
@@ -8,14 +8,19 @@
 import androidx.annotation.Nullable;
 import com.example.alternate_language_test_plugin.CoreTests.AllTypes;
 import com.example.alternate_language_test_plugin.CoreTests.AllTypesWrapper;
+import com.example.alternate_language_test_plugin.CoreTests.FlutterIntegrationCoreApi;
 import com.example.alternate_language_test_plugin.CoreTests.HostIntegrationCoreApi;
+import com.example.alternate_language_test_plugin.CoreTests.Result;
 import io.flutter.embedding.engine.plugins.FlutterPlugin;
 
 /** This plugin handles the native side of the integration tests in example/integration_test/. */
 public class AlternateLanguageTestPlugin implements FlutterPlugin, HostIntegrationCoreApi {
+  @Nullable FlutterIntegrationCoreApi flutterApi = null;
+
   @Override
   public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
     HostIntegrationCoreApi.setup(binding.getBinaryMessenger(), this);
+    flutterApi = new FlutterIntegrationCoreApi(binding.getBinaryMessenger());
   }
 
   @Override
@@ -46,4 +51,35 @@
     AllTypes innerObject = new AllTypes.Builder().setAString(string).build();
     return new AllTypesWrapper.Builder().setValues(innerObject).build();
   }
+
+  @Override
+  public void noopAsync(Result<Void> result) {
+    result.success(null);
+  }
+
+  @Override
+  public void echoAsyncString(@NonNull String aString, Result<String> result) {
+    result.success(aString);
+  }
+
+  @Override
+  public void callFlutterNoop(Result<Void> result) {
+    flutterApi.noop(
+        new FlutterIntegrationCoreApi.Reply<Void>() {
+          public void reply(Void value) {
+            result.success(value);
+          }
+        });
+  }
+
+  @Override
+  public void callFlutterEchoString(@NonNull String aString, Result<String> result) {
+    flutterApi.echoString(
+        aString,
+        new FlutterIntegrationCoreApi.Reply<String>() {
+          public void reply(String value) {
+            result.success(value);
+          }
+        });
+  }
 }
diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/Classes/AlternateLanguageTestPlugin.m b/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/Classes/AlternateLanguageTestPlugin.m
index 6e670ca..4f22b7d 100644
--- a/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/Classes/AlternateLanguageTestPlugin.m
+++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/Classes/AlternateLanguageTestPlugin.m
@@ -6,14 +6,19 @@
 
 #import "CoreTests.gen.h"
 
+@interface AlternateLanguageTestPlugin ()
+@property(nonatomic) FlutterIntegrationCoreApi *flutterAPI;
+@end
+
 /**
- * This plugin is currently a no-op since only unit tests have been set up.
- * In the future, this will register Pigeon APIs used in integration tests.
+ * This plugin handles the native side of the integration tests in example/integration_test/.
  */
 @implementation AlternateLanguageTestPlugin
 + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
   AlternateLanguageTestPlugin *plugin = [[AlternateLanguageTestPlugin alloc] init];
-  HostIntegrationCoreApiSetup(registrar.messenger, plugin);
+  HostIntegrationCoreApiSetup([registrar messenger], plugin);
+  plugin.flutterAPI =
+      [[FlutterIntegrationCoreApi alloc] initWithBinaryMessenger:[registrar messenger]];
 }
 
 #pragma mark HostIntegrationCoreApi implementation
@@ -43,4 +48,27 @@
   return [AllTypesWrapper makeWithValues:innerObject];
 }
 
+- (void)noopAsyncWithCompletion:(void (^)(FlutterError *_Nullable))completion {
+  completion(nil);
+}
+
+- (void)echoAsyncString:(NSString *)aString
+             completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion {
+  completion(aString, nil);
+}
+
+- (void)callFlutterNoopWithCompletion:(void (^)(FlutterError *_Nullable))completion {
+  [self.flutterAPI noopWithCompletion:^(NSError *error) {
+    completion(error);
+  }];
+}
+
+- (void)callFlutterEchoString:(NSString *)aString
+                   completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion {
+  [self.flutterAPI echoString:aString
+                   completion:^(NSString *value, NSError *error) {
+                     completion(value, error);
+                   }];
+}
+
 @end
diff --git a/packages/pigeon/platform_tests/shared_test_plugin_code/lib/integration_tests.dart b/packages/pigeon/platform_tests/shared_test_plugin_code/lib/integration_tests.dart
index 5c369ed..08a9930 100644
--- a/packages/pigeon/platform_tests/shared_test_plugin_code/lib/integration_tests.dart
+++ b/packages/pigeon/platform_tests/shared_test_plugin_code/lib/integration_tests.dart
@@ -35,7 +35,7 @@
 void runPigeonIntegrationTests(TargetGenerator targetGenerator) {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
 
-  group('Host API tests', () {
+  group('Host sync API tests', () {
     testWidgets('basic void->void call works', (WidgetTester _) async {
       final HostIntegrationCoreApi api = HostIntegrationCoreApi();
 
@@ -125,8 +125,65 @@
     });
   });
 
-  group('Flutter API tests', () {
-    // TODO(stuartmorgan): Add Flutter API tests, driven by wrapper host APIs
-    // that forward the arguments and return values in the opposite direction.
+  group('Host async API tests', () {
+    testWidgets('basic void->void call works', (WidgetTester _) async {
+      final HostIntegrationCoreApi api = HostIntegrationCoreApi();
+
+      expect(api.noopAsync(), completes);
+    });
+
+    testWidgets('strings serialize and deserialize correctly',
+        (WidgetTester _) async {
+      final HostIntegrationCoreApi api = HostIntegrationCoreApi();
+
+      const String sentObject = 'Hello, asyncronously!';
+
+      final String echoObject = await api.echoAsyncString(sentObject);
+      expect(echoObject, sentObject);
+    });
   });
+
+  // These tests rely on the ansync Dart->host calls to work correctly, since
+  // the host->Dart call is wrapped in a driving Dart->host call, so any test
+  // added to this group should have coverage of the relevant arguments and
+  // return value in the "Host async API tests" group.
+  group('Flutter API tests', () {
+    setUp(() {
+      FlutterIntegrationCoreApi.setup(_FlutterApiTestImplementation());
+    });
+
+    testWidgets('basic void->void call works', (WidgetTester _) async {
+      final HostIntegrationCoreApi api = HostIntegrationCoreApi();
+
+      expect(api.callFlutterNoop(), completes);
+    });
+
+    testWidgets('strings serialize and deserialize correctly',
+        (WidgetTester _) async {
+      final HostIntegrationCoreApi api = HostIntegrationCoreApi();
+
+      const String sentObject = 'Hello Dart!';
+
+      final String echoObject = await api.callFlutterEchoString(sentObject);
+      expect(echoObject, sentObject);
+    });
+  },
+      // TODO(stuartmorgan): Enable when FlutterApi generation is fixed for
+      // C++. See https://github.com/flutter/flutter/issues/108682.
+      skip: targetGenerator == TargetGenerator.cpp);
+}
+
+class _FlutterApiTestImplementation implements FlutterIntegrationCoreApi {
+  @override
+  AllTypes echoAllTypes(AllTypes everything) {
+    return everything;
+  }
+
+  @override
+  String echoString(String aString) {
+    return aString;
+  }
+
+  @override
+  void noop() {}
 }
diff --git a/packages/pigeon/platform_tests/shared_test_plugin_code/lib/src/generated/core_tests.gen.dart b/packages/pigeon/platform_tests/shared_test_plugin_code/lib/src/generated/core_tests.gen.dart
index 42a39d6..1b027d8 100644
--- a/packages/pigeon/platform_tests/shared_test_plugin_code/lib/src/generated/core_tests.gen.dart
+++ b/packages/pigeon/platform_tests/shared_test_plugin_code/lib/src/generated/core_tests.gen.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v4.2.8), do not edit directly.
+// Autogenerated from Pigeon (v4.2.9), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
 import 'dart:async';
@@ -284,6 +284,116 @@
       return (replyMap['result'] as AllTypesWrapper?)!;
     }
   }
+
+  /// A no-op function taking no arguments and returning no value, to sanity
+  /// test basic asynchronous calling.
+  Future<void> noopAsync() async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.HostIntegrationCoreApi.noopAsync', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(null) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return;
+    }
+  }
+
+  /// Returns the passed string asynchronously.
+  Future<String> echoAsyncString(String arg_aString) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncString', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(<Object?>[arg_aString]) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return (replyMap['result'] as String?)!;
+    }
+  }
+
+  Future<void> callFlutterNoop() async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterNoop', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(null) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return;
+    }
+  }
+
+  Future<String> callFlutterEchoString(String arg_aString) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoString',
+        codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(<Object?>[arg_aString]) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return (replyMap['result'] as String?)!;
+    }
+  }
 }
 
 class _FlutterIntegrationCoreApiCodec extends StandardMessageCodec {
@@ -321,6 +431,9 @@
 
   /// Returns the passed object, to test serialization and deserialization.
   AllTypes echoAllTypes(AllTypes everything);
+
+  /// Returns the passed string, to test serialization and deserialization.
+  String echoString(String aString);
   static void setup(FlutterIntegrationCoreApi? api,
       {BinaryMessenger? binaryMessenger}) {
     {
@@ -356,6 +469,25 @@
         });
       }
     }
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.FlutterIntegrationCoreApi.echoString', codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMessageHandler(null);
+      } else {
+        channel.setMessageHandler((Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.FlutterIntegrationCoreApi.echoString was null.');
+          final List<Object?> args = (message as List<Object?>?)!;
+          final String? arg_aString = (args[0] as String?);
+          assert(arg_aString != null,
+              'Argument for dev.flutter.pigeon.FlutterIntegrationCoreApi.echoString was null, expected non-null String.');
+          final String output = api.echoString(arg_aString!);
+          return output;
+        });
+      }
+    }
   }
 }
 
diff --git a/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/TestPlugin.kt b/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/TestPlugin.kt
index 761fe27..9a2961b 100644
--- a/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/TestPlugin.kt
+++ b/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/TestPlugin.kt
@@ -17,8 +17,11 @@
  * example/integration_test/.
  */
 class TestPlugin: FlutterPlugin, HostIntegrationCoreApi {
+  var flutterApi: FlutterIntegrationCoreApi? = null
+
   override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
     HostIntegrationCoreApi.setUp(binding.getBinaryMessenger(), this)
+    flutterApi = FlutterIntegrationCoreApi(binding.getBinaryMessenger())
   }
 
   override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
@@ -44,4 +47,20 @@
   override fun createNestedString(string: String): AllTypesWrapper {
     return AllTypesWrapper(AllTypes(aString = string))
   }
+
+  override fun noopAsync(callback: () -> Unit) {
+    callback()
+  }
+
+  override fun echoAsyncString(aString: String, callback: (String) -> Unit) {
+    callback(aString)
+  }
+
+  override fun callFlutterNoop(callback: () -> Unit) {
+    flutterApi!!.noop() { callback() }
+  }
+
+  override fun callFlutterEchoString(aString: String, callback: (String) -> Unit) {
+    flutterApi!!.echoString(aString) { flutterString -> callback(flutterString) }
+  }
 }
diff --git a/packages/pigeon/platform_tests/test_plugin/ios/Classes/TestPlugin.swift b/packages/pigeon/platform_tests/test_plugin/ios/Classes/TestPlugin.swift
index 121e5d8..73fc349 100644
--- a/packages/pigeon/platform_tests/test_plugin/ios/Classes/TestPlugin.swift
+++ b/packages/pigeon/platform_tests/test_plugin/ios/Classes/TestPlugin.swift
@@ -10,11 +10,17 @@
  * example/integration_test/.
  */
 public class TestPlugin: NSObject, FlutterPlugin, HostIntegrationCoreApi {
+  var flutterAPI: FlutterIntegrationCoreApi
+
   public static func register(with registrar: FlutterPluginRegistrar) {
-    let plugin = TestPlugin()
+    let plugin = TestPlugin(binaryMessenger: registrar.messenger())
     HostIntegrationCoreApiSetup.setUp(binaryMessenger: registrar.messenger(), api: plugin)
   }
 
+  init(binaryMessenger: FlutterBinaryMessenger) {
+    flutterAPI = FlutterIntegrationCoreApi(binaryMessenger: binaryMessenger)
+  }
+
   // MARK: HostIntegrationCoreApi implementation
 
   func noop() {
@@ -36,4 +42,24 @@
   func createNestedString(string: String) -> AllTypesWrapper {
     return AllTypesWrapper(values: AllTypes(aString: string))
   }
+
+  func noopAsync(completion: @escaping () -> Void) {
+    completion()
+  }
+
+  func echoAsyncString(aString: String, completion: @escaping (String) -> Void) {
+    completion(aString)
+  }
+
+  func callFlutterNoop(completion: @escaping () -> Void) {
+    flutterAPI.noop() {
+      completion()
+    }
+  }
+
+  func callFlutterEchoString(aString: String, completion: @escaping (String) -> Void) {
+    flutterAPI.echoString(aString: aString) { flutterString in
+      completion(flutterString)
+    }
+  }
 }
diff --git a/packages/pigeon/platform_tests/test_plugin/macos/Classes/TestPlugin.swift b/packages/pigeon/platform_tests/test_plugin/macos/Classes/TestPlugin.swift
index 08f0eee..a9da20c 100644
--- a/packages/pigeon/platform_tests/test_plugin/macos/Classes/TestPlugin.swift
+++ b/packages/pigeon/platform_tests/test_plugin/macos/Classes/TestPlugin.swift
@@ -10,11 +10,17 @@
  * example/integration_test/.
  */
 public class TestPlugin: NSObject, FlutterPlugin, HostIntegrationCoreApi {
+  var flutterAPI: FlutterIntegrationCoreApi
+
   public static func register(with registrar: FlutterPluginRegistrar) {
-    let plugin = TestPlugin()
+    let plugin = TestPlugin(binaryMessenger: registrar.messenger)
     HostIntegrationCoreApiSetup.setUp(binaryMessenger: registrar.messenger, api: plugin)
   }
 
+  init(binaryMessenger: FlutterBinaryMessenger) {
+    flutterAPI = FlutterIntegrationCoreApi(binaryMessenger: binaryMessenger)
+  }
+
   // MARK: HostIntegrationCoreApi implementation
 
   func noop() {
@@ -36,4 +42,24 @@
   func createNestedString(string: String) -> AllTypesWrapper {
     return AllTypesWrapper(values: AllTypes(aString: string))
   }
+
+  func noopAsync(completion: @escaping () -> Void) {
+    completion()
+  }
+
+  func echoAsyncString(aString: String, completion: @escaping (String) -> Void) {
+    completion(aString)
+  }
+
+  func callFlutterNoop(completion: @escaping () -> Void) {
+    flutterAPI.noop() {
+      completion()
+    }
+  }
+
+  func callFlutterEchoString(aString: String, completion: @escaping (String) -> Void) {
+    flutterAPI.echoString(aString: aString) { flutterString in
+      completion(flutterString)
+    }
+  }
 }
diff --git a/packages/pigeon/platform_tests/test_plugin/windows/test_plugin.cpp b/packages/pigeon/platform_tests/test_plugin/windows/test_plugin.cpp
index fd75ece..43dbd0b 100644
--- a/packages/pigeon/platform_tests/test_plugin/windows/test_plugin.cpp
+++ b/packages/pigeon/platform_tests/test_plugin/windows/test_plugin.cpp
@@ -9,6 +9,8 @@
 #include <windows.h>
 
 #include <memory>
+#include <optional>
+#include <string>
 
 #include "pigeon/core_tests.gen.h"
 
@@ -18,19 +20,22 @@
 using core_tests_pigeontest::AllTypesWrapper;
 using core_tests_pigeontest::ErrorOr;
 using core_tests_pigeontest::FlutterError;
+using core_tests_pigeontest::FlutterIntegrationCoreApi;
 using core_tests_pigeontest::HostIntegrationCoreApi;
 
 // static
 void TestPlugin::RegisterWithRegistrar(
     flutter::PluginRegistrarWindows* registrar) {
-  auto plugin = std::make_unique<TestPlugin>();
+  auto plugin = std::make_unique<TestPlugin>(registrar->messenger());
 
   HostIntegrationCoreApi::SetUp(registrar->messenger(), plugin.get());
 
   registrar->AddPlugin(std::move(plugin));
 }
 
-TestPlugin::TestPlugin() {}
+TestPlugin::TestPlugin(flutter::BinaryMessenger* binary_messenger)
+    : flutter_api_(
+          std::make_unique<FlutterIntegrationCoreApi>(binary_messenger)) {}
 
 TestPlugin::~TestPlugin() {}
 
@@ -60,4 +65,28 @@
   return wrapper;
 }
 
+void TestPlugin::NoopAsync(
+    std::function<void(std::optional<FlutterError> reply)> result) {
+  result(std::nullopt);
+}
+
+void TestPlugin::EchoAsyncString(
+    const std::string& a_string,
+    std::function<void(ErrorOr<std::string> reply)> result) {
+  result(a_string);
+}
+
+void TestPlugin::CallFlutterNoop(
+    std::function<void(std::optional<FlutterError> reply)> result) {
+  flutter_api_->noop([result]() { result(std::nullopt); });
+}
+
+void TestPlugin::CallFlutterEchoString(
+    const std::string& a_string,
+    std::function<void(ErrorOr<std::string> reply)> result) {
+  flutter_api_->echoString(
+      a_string,
+      [result](const std::string& flutter_string) { result(flutter_string); });
+}
+
 }  // namespace test_plugin
diff --git a/packages/pigeon/platform_tests/test_plugin/windows/test_plugin.h b/packages/pigeon/platform_tests/test_plugin/windows/test_plugin.h
index 7e10836..0b162d0 100644
--- a/packages/pigeon/platform_tests/test_plugin/windows/test_plugin.h
+++ b/packages/pigeon/platform_tests/test_plugin/windows/test_plugin.h
@@ -9,6 +9,8 @@
 #include <flutter/plugin_registrar_windows.h>
 
 #include <memory>
+#include <optional>
+#include <string>
 
 #include "pigeon/core_tests.gen.h"
 
@@ -21,7 +23,7 @@
  public:
   static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar);
 
-  TestPlugin();
+  TestPlugin(flutter::BinaryMessenger* binary_messenger);
 
   virtual ~TestPlugin();
 
@@ -39,6 +41,25 @@
       const core_tests_pigeontest::AllTypesWrapper& wrapper) override;
   core_tests_pigeontest::ErrorOr<core_tests_pigeontest::AllTypesWrapper>
   CreateNestedString(const std::string& string) override;
+  void NoopAsync(std::function<
+                 void(std::optional<core_tests_pigeontest::FlutterError> reply)>
+                     result) override;
+  void EchoAsyncString(
+      const std::string& a_string,
+      std::function<void(core_tests_pigeontest::ErrorOr<std::string> reply)>
+          result) override;
+  void CallFlutterNoop(
+      std::function<
+          void(std::optional<core_tests_pigeontest::FlutterError> reply)>
+          result) override;
+  void CallFlutterEchoString(
+      const std::string& a_string,
+      std::function<void(core_tests_pigeontest::ErrorOr<std::string> reply)>
+          result) override;
+
+ private:
+  std::unique_ptr<core_tests_pigeontest::FlutterIntegrationCoreApi>
+      flutter_api_;
 };
 
 }  // namespace test_plugin
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
index 54cb878..45832df 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: 4.2.8 # This must match the version in lib/generator_tools.dart
+version: 4.2.9 # This must match the version in lib/generator_tools.dart
 
 environment:
   sdk: ">=2.12.0 <3.0.0"