[pigeon] Fix C++ generator's handling of non-class host APIs (#2270)

diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
index f784aa0..27ad31c 100644
--- a/packages/pigeon/CHANGELOG.md
+++ b/packages/pigeon/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 3.2.4
+
+* [c++] Fixes most non-class arguments and return values in host APIs. The
+  types of arguments and return values have changed, so this may require updates
+  to existing code.
+
 ## 3.2.3
 
 * Adds `unnecessary_import` to linter ignore list in generated dart tests.
diff --git a/packages/pigeon/lib/cpp_generator.dart b/packages/pigeon/lib/cpp_generator.dart
index 8cf965c..ad4758a 100644
--- a/packages/pigeon/lib/cpp_generator.dart
+++ b/packages/pigeon/lib/cpp_generator.dart
@@ -57,8 +57,8 @@
 
 String _getCodecName(Api api) => '${api.name}CodecSerializer';
 
-String _pointerPrefix = 'pointer';
-String _encodablePrefix = 'encodable';
+const String _pointerPrefix = 'pointer';
+const String _encodablePrefix = 'encodable';
 
 void _writeCodecHeader(Indent indent, Api api, Root root) {
   final String codecName = _getCodecName(api);
@@ -134,39 +134,43 @@
   }
 }
 
-void _writeErrorOr(Indent indent) {
+void _writeErrorOr(Indent indent,
+    {Iterable<String> friends = const <String>[]}) {
+  final String friendLines = friends
+      .map((String className) => '\tfriend class $className;')
+      .join('\n');
   indent.format('''
 class FlutterError {
  public:
-\tFlutterError();
 \tFlutterError(const std::string& arg_code)
-\t\t: code(arg_code) {};
+\t\t: code(arg_code) {}
 \tFlutterError(const std::string& arg_code, const std::string& arg_message)
-\t\t: code(arg_code), message(arg_message) {};
+\t\t: code(arg_code), message(arg_message) {}
 \tFlutterError(const std::string& arg_code, const std::string& arg_message, const flutter::EncodableValue& arg_details)
-\t\t: code(arg_code), message(arg_message), details(arg_details) {};
+\t\t: code(arg_code), message(arg_message), details(arg_details) {}
 \tstd::string code;
 \tstd::string message;
 \tflutter::EncodableValue details;
 };
+
 template<class T> class ErrorOr {
-\tstd::variant<std::unique_ptr<T>, T, FlutterError> v;
+\tstd::variant<T, FlutterError> v;
  public:
 \tErrorOr(const T& rhs) { new(&v) T(rhs); }
+\tErrorOr(const T&& rhs) { v = std::move(rhs); }
 \tErrorOr(const FlutterError& rhs) {
 \t\tnew(&v) FlutterError(rhs);
 \t}
-\tstatic ErrorOr<std::unique_ptr<T>> MakeWithUniquePtr(std::unique_ptr<T> rhs) {
-\t\tErrorOr<std::unique_ptr<T>> ret = ErrorOr<std::unique_ptr<T>>();
-\t\tret.v = std::move(rhs);
-\t\treturn ret;
-\t}
+\tErrorOr(const FlutterError&& rhs) { v = std::move(rhs); }
+
 \tbool hasError() const { return std::holds_alternative<FlutterError>(v); }
 \tconst T& value() const { return std::get<T>(v); };
 \tconst FlutterError& error() const { return std::get<FlutterError>(v); };
+
  private:
+$friendLines
 \tErrorOr() = default;
-\tfriend class ErrorOr;
+\tT TakeValue() && { return std::get<T>(std::move(v)); }
 };
 ''');
 }
@@ -185,11 +189,11 @@
     indent.scoped(' public:', '', () {
       indent.writeln('${klass.name}();');
       for (final NamedType field in klass.fields) {
-        final HostDatatype baseDatatype = getHostDatatype(
+        final HostDatatype baseDatatype = getFieldHostDatatype(
             field,
             root.classes,
             root.enums,
-            (NamedType x) => _baseCppTypeForBuiltinDartType(x.type));
+            (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
         indent.writeln(
             '${_getterReturnType(baseDatatype)} ${_makeGetterName(field)}() const;');
         indent.writeln(
@@ -227,11 +231,11 @@
       }
 
       for (final NamedType field in klass.fields) {
-        final HostDatatype hostDatatype = getHostDatatype(
+        final HostDatatype hostDatatype = getFieldHostDatatype(
             field,
             root.classes,
             root.enums,
-            (NamedType x) => _baseCppTypeForBuiltinDartType(x.type));
+            (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
         indent.writeln(
             '${_valueType(hostDatatype)} ${_makeInstanceVariableName(field)};');
       }
@@ -256,8 +260,8 @@
 
   // Getters and setters.
   for (final NamedType field in klass.fields) {
-    final HostDatatype hostDatatype = getHostDatatype(field, root.classes,
-        root.enums, (NamedType x) => _baseCppTypeForBuiltinDartType(x.type));
+    final HostDatatype hostDatatype = getFieldHostDatatype(field, root.classes,
+        root.enums, (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
     final String instanceVariableName = _makeInstanceVariableName(field);
     final String qualifiedGetterName =
         '${klass.name}::${_makeGetterName(field)}';
@@ -296,11 +300,11 @@
   indent.scoped('{', '}', () {
     indent.scoped('return flutter::EncodableMap{', '};', () {
       for (final NamedType field in klass.fields) {
-        final HostDatatype hostDatatype = getHostDatatype(
+        final HostDatatype hostDatatype = getFieldHostDatatype(
             field,
             root.classes,
             root.enums,
-            (NamedType x) => _baseCppTypeForBuiltinDartType(x.type));
+            (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
 
         final String instanceVariable = _makeInstanceVariableName(field);
 
@@ -346,16 +350,16 @@
       final String encodableFieldName =
           '${_encodablePrefix}_${_makeVariableName(field)}';
       indent.writeln(
-          'auto $encodableFieldName = map.at(flutter::EncodableValue("${field.name}"));');
+          'auto& $encodableFieldName = map.at(flutter::EncodableValue("${field.name}"));');
       if (rootEnumNameSet.contains(field.type.baseName)) {
         indent.writeln(
             'if (const int32_t* $pointerFieldName = std::get_if<int32_t>(&$encodableFieldName))\t$instanceVariableName = (${field.type.baseName})*$pointerFieldName;');
       } else {
-        final HostDatatype hostDatatype = getHostDatatype(
+        final HostDatatype hostDatatype = getFieldHostDatatype(
             field,
             root.classes,
             root.enums,
-            (NamedType x) => _baseCppTypeForBuiltinDartType(x.type));
+            (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
         if (field.type.baseName == 'int') {
           indent.format('''
 if (const int32_t* $pointerFieldName = std::get_if<int32_t>(&$encodableFieldName))
@@ -385,7 +389,7 @@
   indent.addln('');
 }
 
-void _writeHostApiHeader(Indent indent, Api api) {
+void _writeHostApiHeader(Indent indent, Api api, Root root) {
   assert(api.location == ApiLocation.host);
 
   indent.writeln(
@@ -397,14 +401,24 @@
       indent.writeln('${api.name}& operator=(const ${api.name}&) = delete;');
       indent.writeln('virtual ~${api.name}() { };');
       for (final Method method in api.methods) {
-        final String returnTypeName = method.returnType.isVoid
-            ? 'std::optional<FlutterError>'
-            : 'ErrorOr<${_nullSafeCppTypeForDartType(method.returnType, considerReference: false)}>';
+        final HostDatatype returnType = getHostDatatype(
+            method.returnType,
+            root.classes,
+            root.enums,
+            (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
+        final String returnTypeName = _apiReturnType(returnType);
 
         final List<String> argSignature = <String>[];
         if (method.arguments.isNotEmpty) {
-          final Iterable<String> argTypes = method.arguments
-              .map((NamedType e) => _nullSafeCppTypeForDartType(e.type));
+          final Iterable<String> argTypes =
+              method.arguments.map((NamedType arg) {
+            final HostDatatype hostType = getFieldHostDatatype(
+                arg,
+                root.classes,
+                root.enums,
+                (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
+            return _hostApiArgumentType(hostType);
+          });
           final Iterable<String> argNames =
               method.arguments.map((NamedType e) => _makeVariableName(e));
           argSignature.addAll(
@@ -439,7 +453,7 @@
   }, nestCount: 0);
 }
 
-void _writeHostApiSource(Indent indent, Api api) {
+void _writeHostApiSource(Indent indent, Api api, Root root) {
   assert(api.location == ApiLocation.host);
 
   final String codecName = _getCodecName(api);
@@ -470,67 +484,136 @@
           indent.write(
               'channel->SetMessageHandler([api](const flutter::EncodableValue& message, const flutter::MessageReply<flutter::EncodableValue>& reply) ');
           indent.scoped('{', '});', () {
-            final String returnTypeName = method.returnType.isVoid
-                ? 'std::optional<FlutterError>'
-                : 'ErrorOr<${_nullSafeCppTypeForDartType(method.returnType, considerReference: false)}>';
             indent.writeln('flutter::EncodableMap wrapped;');
             indent.write('try ');
             indent.scoped('{', '}', () {
               final List<String> methodArgument = <String>[];
               if (method.arguments.isNotEmpty) {
                 indent.writeln(
-                    'auto args = std::get<flutter::EncodableList>(message);');
+                    'const auto& args = std::get<flutter::EncodableList>(message);');
+
+                // Writes the code to declare and populate a variable called
+                // [argName] to use as a parameter to an API method call from
+                // an existing EncodablValue variable called [encodableArgName]
+                // which corresponds to [arg] in the API definition.
+                void extractEncodedArgument(
+                    String argName,
+                    String encodableArgName,
+                    NamedType arg,
+                    HostDatatype hostType) {
+                  if (arg.type.isNullable) {
+                    // Nullable arguments are always pointers, with nullptr
+                    // corresponding to null.
+                    if (hostType.datatype == 'int64_t') {
+                      // The EncodableValue will either be an int32_t or an
+                      // int64_t depending on the value, but the generated API
+                      // requires an int64_t so that it can handle any case.
+                      // Create a local variable for the 64-bit value...
+                      final String valueVarName = '${argName}_value';
+                      indent.writeln(
+                          'const int64_t $valueVarName = $encodableArgName.IsNull() ? 0 : $encodableArgName.LongValue();');
+                      // ... then declare the arg as a reference to that local.
+                      indent.writeln(
+                          'const auto* $argName = $encodableArgName.IsNull() ? nullptr : &$valueVarName;');
+                    } else if (hostType.isBuiltin) {
+                      indent.writeln(
+                          'const auto* $argName = std::get_if<${hostType.datatype}>(&$encodableArgName);');
+                    } else {
+                      indent.writeln(
+                          'const auto* $argName = &(std::any_cast<const ${hostType.datatype}&>(std::get<flutter::CustomEncodableValue>($encodableArgName)));');
+                    }
+                  } else {
+                    // Non-nullable arguments are either passed by value or
+                    // reference, but the extraction doesn't need to distinguish
+                    // since those are the same at the call site.
+                    if (hostType.datatype == 'int64_t') {
+                      // The EncodableValue will either be an int32_t or an
+                      // int64_t depending on the value, but the generated API
+                      // requires an int64_t so that it can handle any case.
+                      indent.writeln(
+                          'const int64_t $argName = $encodableArgName.LongValue();');
+                    } else if (hostType.isBuiltin) {
+                      indent.writeln(
+                          'const auto& $argName = std::get<${hostType.datatype}>($encodableArgName);');
+                    } else {
+                      indent.writeln(
+                          'const auto& $argName = std::any_cast<const ${hostType.datatype}&>(std::get<flutter::CustomEncodableValue>($encodableArgName));');
+                    }
+                  }
+                }
+
                 enumerate(method.arguments, (int index, NamedType arg) {
-                  final String argType = _nullSafeCppTypeForDartType(arg.type);
+                  final HostDatatype hostType = getHostDatatype(
+                      arg.type,
+                      root.classes,
+                      root.enums,
+                      (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
                   final String argName = _getSafeArgumentName(index, arg);
 
                   final String encodableArgName =
                       '${_encodablePrefix}_$argName';
-                  indent.writeln('auto $encodableArgName = args.at($index);');
+                  indent.writeln(
+                      'const auto& $encodableArgName = args.at($index);');
                   if (!arg.type.isNullable) {
                     indent.write('if ($encodableArgName.IsNull()) ');
                     indent.scoped('{', '}', () {
                       indent.writeln(
-                          'wrapped.insert(std::make_pair(flutter::EncodableValue("${Keys.error}"), WrapError("$argName unexpectedly null.")));');
+                          'wrapped.emplace(flutter::EncodableValue("${Keys.error}"), WrapError("$argName unexpectedly null."));');
                       indent.writeln('reply(wrapped);');
                       indent.writeln('return;');
                     });
                   }
-                  indent.writeln(
-                      '$argType $argName = std::any_cast<$argType>(std::get<flutter::CustomEncodableValue>($encodableArgName));');
+                  extractEncodedArgument(
+                      argName, encodableArgName, arg, hostType);
                   methodArgument.add(argName);
                 });
               }
 
               String _wrapResponse(String reply, TypeDeclaration returnType) {
-                final bool isReferenceReturnType = _isReferenceType(
-                    _baseCppTypeForDartType(method.returnType));
                 String elseBody = '';
                 final String ifCondition;
                 final String errorGetter;
                 final String prefix = (reply != '') ? '\t' : '';
+
+                const String resultKey =
+                    'flutter::EncodableValue("${Keys.result}")';
+                const String errorKey =
+                    'flutter::EncodableValue("${Keys.error}")';
+                const String nullValue = 'flutter::EncodableValue()';
                 if (returnType.isVoid) {
                   elseBody =
-                      '$prefix\twrapped.insert(std::make_pair(flutter::EncodableValue("${Keys.result}"), flutter::EncodableValue()));${indent.newline}';
+                      '$prefix\twrapped.emplace($resultKey, $nullValue);${indent.newline}';
                   ifCondition = 'output.has_value()';
                   errorGetter = 'value';
                 } else {
-                  if (isReferenceReturnType && !returnType.isNullable) {
+                  final HostDatatype hostType = getHostDatatype(
+                      returnType,
+                      root.classes,
+                      root.enums,
+                      (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
+                  const String extractedValue = 'std::move(output).TakeValue()';
+                  final String wrapperType = hostType.isBuiltin
+                      ? 'flutter::EncodableValue'
+                      : 'flutter::CustomEncodableValue';
+                  if (returnType.isNullable) {
+                    // The value is a std::optional, so needs an extra layer of
+                    // handling.
                     elseBody = '''
-$prefix\tif (!output.value()) {
-$prefix\t\twrapped.insert(std::make_pair(flutter::EncodableValue("${Keys.error}"), WrapError("output is unexpectedly null.")));
+$prefix\tauto output_optional = $extractedValue;
+$prefix\tif (output_optional) {
+$prefix\t\twrapped.emplace($resultKey, $wrapperType(std::move(output_optional).value()));
 $prefix\t} else {
-$prefix\t\twrapped.insert(std::make_pair(flutter::EncodableValue("${Keys.result}"), flutter::CustomEncodableValue(*output.value().get())));
+$prefix\t\twrapped.emplace($resultKey, $nullValue);
 $prefix\t}${indent.newline}''';
                   } else {
                     elseBody =
-                        '$prefix\twrapped.insert(std::make_pair(flutter::EncodableValue("${Keys.result}"), flutter::CustomEncodableValue(output.value())));${indent.newline}';
+                        '$prefix\twrapped.emplace($resultKey, $wrapperType($extractedValue));${indent.newline}';
                   }
                   ifCondition = 'output.hasError()';
                   errorGetter = 'error';
                 }
                 return '${prefix}if ($ifCondition) {${indent.newline}'
-                    '$prefix\twrapped.insert(std::make_pair(flutter::EncodableValue("${Keys.error}"), WrapError(output.$errorGetter())));${indent.newline}'
+                    '$prefix\twrapped.emplace($errorKey, WrapError(output.$errorGetter()));${indent.newline}'
                     '$prefix$reply'
                     '$prefix} else {${indent.newline}'
                     '$elseBody'
@@ -538,9 +621,15 @@
                     '$prefix}';
               }
 
+              final HostDatatype returnType = getHostDatatype(
+                  method.returnType,
+                  root.classes,
+                  root.enums,
+                  (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x));
+              final String returnTypeName = _apiReturnType(returnType);
               if (method.isAsynchronous) {
                 methodArgument.add(
-                  '[&wrapped, &reply]($returnTypeName output) {${indent.newline}'
+                  '[&wrapped, &reply]($returnTypeName&& output) {${indent.newline}'
                   '${_wrapResponse('\treply(wrapped);${indent.newline}', method.returnType)}'
                   '}',
                 );
@@ -557,7 +646,7 @@
             indent.write('catch (const std::exception& exception) ');
             indent.scoped('{', '}', () {
               indent.writeln(
-                  'wrapped.insert(std::make_pair(flutter::EncodableValue("${Keys.error}"), WrapError(exception.what())));');
+                  'wrapped.emplace(flutter::EncodableValue("${Keys.error}"), WrapError(exception.what()));');
               if (method.isAsynchronous) {
                 indent.writeln('reply(wrapped);');
               }
@@ -685,7 +774,7 @@
           } else {
             indent.writeln('$returnTypeName $output{};');
           }
-          final String pointerVariable = '${_pointerPrefix}_$output';
+          const String pointerVariable = '${_pointerPrefix}_$output';
           if (func.returnType.baseName == 'int') {
             indent.format('''
 if (const int32_t* $pointerVariable = std::get_if<int32_t>(&args))
@@ -767,6 +856,7 @@
 
 String? _baseCppTypeForBuiltinDartType(TypeDeclaration type) {
   const Map<String, String> cppTypeForDartTypeMap = <String, String>{
+    'void': 'void',
     'bool': 'bool',
     'int': 'int64_t',
     'String': 'std::string',
@@ -809,6 +899,18 @@
   return type.isNullable ? 'const $baseType*' : 'const $baseType&';
 }
 
+/// Returns the C++ type to use for arguments to a host API. This is slightly
+/// different from [_unownedArgumentType] since passing `std::string_view*` in
+/// to the host API implementation when the actual type is `std::string*` is
+/// needlessly complicated, so it uses `std::string` directly.
+String _hostApiArgumentType(HostDatatype type) {
+  final String baseType = type.datatype;
+  if (_isPodType(type)) {
+    return type.isNullable ? 'const $baseType*' : baseType;
+  }
+  return type.isNullable ? 'const $baseType*' : 'const $baseType&';
+}
+
 /// Returns the C++ type to use for the return of a getter for a field of type
 /// [type].
 String _getterReturnType(HostDatatype type) {
@@ -822,6 +924,19 @@
   return type.isNullable ? 'const $baseType*' : 'const $baseType&';
 }
 
+/// Returns the C++ type to use for the return of an API method retutrning
+/// [type].
+String _apiReturnType(HostDatatype type) {
+  if (type.datatype == 'void') {
+    return 'std::optional<FlutterError>';
+  }
+  String valueType = type.datatype;
+  if (type.isNullable) {
+    valueType = 'std::optional<$valueType>';
+  }
+  return 'ErrorOr<$valueType>';
+}
+
 // TODO(stuartmorgan): Audit all uses of this and convert them to context-based
 // methods like those above. Code still using this method may well have bugs.
 String _nullSafeCppTypeForDartType(TypeDeclaration type,
@@ -919,7 +1034,7 @@
 
   indent.addln('');
 
-  _writeErrorOr(indent);
+  _writeErrorOr(indent, friends: root.apis.map((Api api) => api.name));
 
   for (final Class klass in root.classes) {
     _writeDataClassDeclaration(indent, klass, root,
@@ -932,7 +1047,7 @@
     _writeCodecHeader(indent, api, root);
     indent.addln('');
     if (api.location == ApiLocation.host) {
-      _writeHostApiHeader(indent, api);
+      _writeHostApiHeader(indent, api, root);
     } else if (api.location == ApiLocation.flutter) {
       _writeFlutterApiHeader(indent, api);
     }
@@ -986,13 +1101,13 @@
     _writeCodecSource(indent, api, root);
     indent.addln('');
     if (api.location == ApiLocation.host) {
-      _writeHostApiSource(indent, api);
+      _writeHostApiSource(indent, api, root);
 
       indent.addln('');
       indent.format('''
 flutter::EncodableMap ${api.name}::WrapError(std::string_view error_message) {
 \treturn flutter::EncodableMap({
-\t\t{flutter::EncodableValue("${Keys.errorMessage}"), flutter::EncodableValue(std::string(error_message).data())},
+\t\t{flutter::EncodableValue("${Keys.errorMessage}"), flutter::EncodableValue(std::string(error_message))},
 \t\t{flutter::EncodableValue("${Keys.errorCode}"), flutter::EncodableValue("Error")},
 \t\t{flutter::EncodableValue("${Keys.errorDetails}"), flutter::EncodableValue()}
 \t});
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
index 63f63d7..e935fb1 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 = '3.2.3';
+const String pigeonVersion = '3.2.4';
 
 /// Read all the content from [stdin] to a String.
 String readStdin() {
@@ -172,39 +172,58 @@
   final bool isNullable;
 }
 
-/// Calculates the [HostDatatype] for the provided [NamedType].  It will check
-/// the field against [classes], the list of custom classes, to check if it is a
-/// builtin type. [builtinResolver] will return the host datatype for the Dart
-/// datatype for builtin types.  [customResolver] can modify the datatype of
-/// custom types.
-HostDatatype getHostDatatype(NamedType field, List<Class> classes,
-    List<Enum> enums, String? Function(NamedType) builtinResolver,
+/// Calculates the [HostDatatype] for the provided [NamedType].
+///
+/// It will check the field against [classes], the list of custom classes, to
+/// check if it is a builtin type. [builtinResolver] will return the host
+/// datatype for the Dart datatype for builtin types.
+///
+/// [customResolver] can modify the datatype of custom types.
+HostDatatype getFieldHostDatatype(NamedType field, List<Class> classes,
+    List<Enum> enums, String? Function(TypeDeclaration) builtinResolver,
     {String Function(String)? customResolver}) {
-  final String? datatype = builtinResolver(field);
+  return _getHostDatatype(field.type, classes, enums, builtinResolver,
+      customResolver: customResolver, fieldName: field.name);
+}
+
+/// Calculates the [HostDatatype] for the provided [TypeDeclaration].
+///
+/// It will check the field against [classes], the list of custom classes, to
+/// check if it is a builtin type. [builtinResolver] will return the host
+/// datatype for the Dart datatype for builtin types.
+///
+/// [customResolver] can modify the datatype of custom types.
+HostDatatype getHostDatatype(TypeDeclaration type, List<Class> classes,
+    List<Enum> enums, String? Function(TypeDeclaration) builtinResolver,
+    {String Function(String)? customResolver}) {
+  return _getHostDatatype(type, classes, enums, builtinResolver,
+      customResolver: customResolver);
+}
+
+HostDatatype _getHostDatatype(TypeDeclaration type, List<Class> classes,
+    List<Enum> enums, String? Function(TypeDeclaration) builtinResolver,
+    {String Function(String)? customResolver, String? fieldName}) {
+  final String? datatype = builtinResolver(type);
   if (datatype == null) {
-    if (classes.map((Class x) => x.name).contains(field.type.baseName)) {
+    if (classes.map((Class x) => x.name).contains(type.baseName)) {
       final String customName = customResolver != null
-          ? customResolver(field.type.baseName)
-          : field.type.baseName;
+          ? customResolver(type.baseName)
+          : type.baseName;
       return HostDatatype(
-          datatype: customName,
-          isBuiltin: false,
-          isNullable: field.type.isNullable);
-    } else if (enums.map((Enum x) => x.name).contains(field.type.baseName)) {
+          datatype: customName, isBuiltin: false, isNullable: type.isNullable);
+    } else if (enums.map((Enum x) => x.name).contains(type.baseName)) {
       final String customName = customResolver != null
-          ? customResolver(field.type.baseName)
-          : field.type.baseName;
+          ? customResolver(type.baseName)
+          : type.baseName;
       return HostDatatype(
-          datatype: customName,
-          isBuiltin: false,
-          isNullable: field.type.isNullable);
+          datatype: customName, isBuiltin: false, isNullable: type.isNullable);
     } else {
       throw Exception(
-          'unrecognized datatype for field:"${field.name}" of type:"${field.type.baseName}"');
+          'unrecognized datatype ${fieldName == null ? '' : 'for field:"$fieldName" '}of type:"${type.baseName}"');
     }
   } else {
     return HostDatatype(
-        datatype: datatype, isBuiltin: true, isNullable: field.type.isNullable);
+        datatype: datatype, isBuiltin: true, isNullable: type.isNullable);
   }
 }
 
diff --git a/packages/pigeon/lib/java_generator.dart b/packages/pigeon/lib/java_generator.dart
index 7b6d741..c155482 100644
--- a/packages/pigeon/lib/java_generator.dart
+++ b/packages/pigeon/lib/java_generator.dart
@@ -451,8 +451,8 @@
 /// object.
 String _castObject(
     NamedType field, List<Class> classes, List<Enum> enums, String varName) {
-  final HostDatatype hostDatatype = getHostDatatype(field, classes, enums,
-      (NamedType x) => _javaTypeForBuiltinDartType(x.type));
+  final HostDatatype hostDatatype = getFieldHostDatatype(field, classes, enums,
+      (TypeDeclaration x) => _javaTypeForBuiltinDartType(x));
   if (field.type.baseName == 'int') {
     return '($varName == null) ? null : (($varName instanceof Integer) ? (Integer)$varName : (${hostDatatype.datatype})$varName)';
   } else if (!hostDatatype.isBuiltin &&
@@ -521,8 +521,11 @@
 
   void writeDataClass(Class klass) {
     void writeField(NamedType field) {
-      final HostDatatype hostDatatype = getHostDatatype(field, root.classes,
-          root.enums, (NamedType x) => _javaTypeForBuiltinDartType(x.type));
+      final HostDatatype hostDatatype = getFieldHostDatatype(
+          field,
+          root.classes,
+          root.enums,
+          (TypeDeclaration x) => _javaTypeForBuiltinDartType(x));
       final String nullability =
           field.type.isNullable ? '@Nullable' : '@NonNull';
       indent.writeln(
@@ -547,8 +550,11 @@
       indent.scoped('{', '}', () {
         indent.writeln('Map<String, Object> toMapResult = new HashMap<>();');
         for (final NamedType field in klass.fields) {
-          final HostDatatype hostDatatype = getHostDatatype(field, root.classes,
-              root.enums, (NamedType x) => _javaTypeForBuiltinDartType(x.type));
+          final HostDatatype hostDatatype = getFieldHostDatatype(
+              field,
+              root.classes,
+              root.enums,
+              (TypeDeclaration x) => _javaTypeForBuiltinDartType(x));
           String toWriteValue = '';
           final String fieldName = field.name;
           if (!hostDatatype.isBuiltin &&
@@ -592,8 +598,11 @@
       indent.write('public static final class Builder ');
       indent.scoped('{', '}', () {
         for (final NamedType field in klass.fields) {
-          final HostDatatype hostDatatype = getHostDatatype(field, root.classes,
-              root.enums, (NamedType x) => _javaTypeForBuiltinDartType(x.type));
+          final HostDatatype hostDatatype = getFieldHostDatatype(
+              field,
+              root.classes,
+              root.enums,
+              (TypeDeclaration x) => _javaTypeForBuiltinDartType(x));
           final String nullability =
               field.type.isNullable ? '@Nullable' : '@NonNull';
           indent.writeln(
diff --git a/packages/pigeon/lib/objc_generator.dart b/packages/pigeon/lib/objc_generator.dart
index dc1661b..84dc5cd 100644
--- a/packages/pigeon/lib/objc_generator.dart
+++ b/packages/pigeon/lib/objc_generator.dart
@@ -107,9 +107,10 @@
   return result;
 }
 
-String? _objcTypePtrForPrimitiveDartType(String? classPrefix, NamedType field) {
-  return _objcTypeForDartTypeMap.containsKey(field.type.baseName)
-      ? _objcTypeForDartType(classPrefix, field.type).ptr
+String? _objcTypePtrForPrimitiveDartType(
+    String? classPrefix, TypeDeclaration type) {
+  return _objcTypeForDartTypeMap.containsKey(type.baseName)
+      ? _objcTypeForDartType(classPrefix, type).ptr
       : null;
 }
 
@@ -170,8 +171,11 @@
               indent.write(x);
             };
       isFirst = false;
-      final HostDatatype hostDatatype = getHostDatatype(field, classes, enums,
-          (NamedType x) => _objcTypePtrForPrimitiveDartType(prefix, x),
+      final HostDatatype hostDatatype = getFieldHostDatatype(
+          field,
+          classes,
+          enums,
+          (TypeDeclaration x) => _objcTypePtrForPrimitiveDartType(prefix, x),
           customResolver: enumNames.contains(field.type.baseName)
               ? (String x) => _className(prefix, x)
               : (String x) => '${_className(prefix, x)} *');
@@ -205,8 +209,11 @@
       indent.addln(';');
     }
     for (final NamedType field in klass.fields) {
-      final HostDatatype hostDatatype = getHostDatatype(field, classes, enums,
-          (NamedType x) => _objcTypePtrForPrimitiveDartType(prefix, x),
+      final HostDatatype hostDatatype = getFieldHostDatatype(
+          field,
+          classes,
+          enums,
+          (TypeDeclaration x) => _objcTypePtrForPrimitiveDartType(prefix, x),
           customResolver: enumNames.contains(field.type.baseName)
               ? (String x) => _className(prefix, x)
               : (String x) => '${_className(prefix, x)} *');
diff --git a/packages/pigeon/lib/swift_generator.dart b/packages/pigeon/lib/swift_generator.dart
index 8a7d6ec..8be4278 100644
--- a/packages/pigeon/lib/swift_generator.dart
+++ b/packages/pigeon/lib/swift_generator.dart
@@ -408,8 +408,8 @@
   final Indent indent = Indent(sink);
 
   HostDatatype _getHostDatatype(NamedType field) {
-    return getHostDatatype(field, root.classes, root.enums,
-        (NamedType x) => _swiftTypeForBuiltinDartType(x.type));
+    return getFieldHostDatatype(field, root.classes, root.enums,
+        (TypeDeclaration x) => _swiftTypeForBuiltinDartType(x));
   }
 
   void writeHeader() {
diff --git a/packages/pigeon/platform_tests/windows_unit_tests/windows/CMakeLists.txt b/packages/pigeon/platform_tests/windows_unit_tests/windows/CMakeLists.txt
index 73e559a..db289d0 100644
--- a/packages/pigeon/platform_tests/windows_unit_tests/windows/CMakeLists.txt
+++ b/packages/pigeon/platform_tests/windows_unit_tests/windows/CMakeLists.txt
@@ -52,9 +52,14 @@
 # The plugin's C API is not very useful for unit testing, so build the sources
 # directly into the test binary rather than using the DLL.
 add_executable(${TEST_RUNNER}
+  # Tests.
+  test/multiple_arity_test.cpp
   test/non_null_fields_test.cpp
+  test/nullable_returns_test.cpp
   test/null_fields_test.cpp
   test/pigeon_test.cpp
+  test/primitive_test.cpp
+  # Generated sources.
   test/all_datatypes.g.cpp
   test/all_datatypes.g.h
   test/all_void.g.cpp
@@ -87,6 +92,11 @@
   test/voidflutter.g.h
   test/voidhost.g.cpp
   test/voidhost.g.h
+  # Test utilities.
+  test/utils/echo_messenger.cpp
+  test/utils/echo_messenger.h
+  test/utils/fake_host_messenger.cpp
+  test/utils/fake_host_messenger.h
 
   ${PLUGIN_SOURCES}
 )
diff --git a/packages/pigeon/platform_tests/windows_unit_tests/windows/test/multiple_arity_test.cpp b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/multiple_arity_test.cpp
new file mode 100644
index 0000000..8434164
--- /dev/null
+++ b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/multiple_arity_test.cpp
@@ -0,0 +1,52 @@
+// 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.
+
+#include <gtest/gtest.h>
+
+#include "test/multiple_arity.g.h"
+#include "test/utils/fake_host_messenger.h"
+
+namespace multiple_arity_pigeontest {
+
+namespace {
+using flutter::EncodableList;
+using flutter::EncodableMap;
+using flutter::EncodableValue;
+using testing::FakeHostMessenger;
+
+class TestHostApi : public MultipleArityHostApi {
+ public:
+  TestHostApi() {}
+  virtual ~TestHostApi() {}
+
+ protected:
+  ErrorOr<int64_t> Subtract(int64_t x, int64_t y) override { return x - y; }
+};
+
+const EncodableValue& GetResult(const EncodableValue& pigeon_response) {
+  return std::get<EncodableMap>(pigeon_response).at(EncodableValue("result"));
+}
+}  // namespace
+
+TEST(MultipleArity, HostSimple) {
+  FakeHostMessenger messenger(&MultipleArityHostApi::GetCodec());
+  TestHostApi api;
+  MultipleArityHostApi::SetUp(&messenger, &api);
+
+  int64_t result = 0;
+  messenger.SendHostMessage("dev.flutter.pigeon.MultipleArityHostApi.subtract",
+                            EncodableValue(EncodableList({
+                                EncodableValue(30),
+                                EncodableValue(10),
+                            })),
+                            [&result](const EncodableValue& reply) {
+                              result = GetResult(reply).LongValue();
+                            });
+
+  EXPECT_EQ(result, 20);
+}
+
+// TODO(stuartmorgan): Add a FlutterApi version of the test.
+
+}  // namespace multiple_arity_pigeontest
diff --git a/packages/pigeon/platform_tests/windows_unit_tests/windows/test/nullable_returns_test.cpp b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/nullable_returns_test.cpp
new file mode 100644
index 0000000..4804e1a
--- /dev/null
+++ b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/nullable_returns_test.cpp
@@ -0,0 +1,112 @@
+// 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.
+
+#include <gtest/gtest.h>
+
+#include <optional>
+
+#include "test/nullable_returns.g.h"
+#include "test/utils/fake_host_messenger.h"
+
+namespace nullable_returns_pigeontest {
+
+namespace {
+using flutter::EncodableList;
+using flutter::EncodableMap;
+using flutter::EncodableValue;
+using testing::FakeHostMessenger;
+
+class TestNullableArgHostApi : public NullableArgHostApi {
+ public:
+  TestNullableArgHostApi() {}
+  virtual ~TestNullableArgHostApi() {}
+
+ protected:
+  ErrorOr<int64_t> Doit(const int64_t* x) override {
+    return x == nullptr ? 42 : *x;
+  }
+};
+
+class TestNullableReturnHostApi : public NullableReturnHostApi {
+ public:
+  TestNullableReturnHostApi(std::optional<int64_t> return_value)
+      : value_(return_value) {}
+  virtual ~TestNullableReturnHostApi() {}
+
+ protected:
+  ErrorOr<std::optional<int64_t>> Doit() override { return value_; }
+
+ private:
+  std::optional<int64_t> value_;
+};
+
+const EncodableValue& GetResult(const EncodableValue& pigeon_response) {
+  return std::get<EncodableMap>(pigeon_response).at(EncodableValue("result"));
+}
+}  // namespace
+
+TEST(NullableReturns, HostNullableArgNull) {
+  FakeHostMessenger messenger(&NullableArgHostApi::GetCodec());
+  TestNullableArgHostApi api;
+  NullableArgHostApi::SetUp(&messenger, &api);
+
+  int64_t result = 0;
+  messenger.SendHostMessage("dev.flutter.pigeon.NullableArgHostApi.doit",
+                            EncodableValue(EncodableList({EncodableValue()})),
+                            [&result](const EncodableValue& reply) {
+                              result = GetResult(reply).LongValue();
+                            });
+
+  EXPECT_EQ(result, 42);
+}
+
+TEST(NullableReturns, HostNullableArgNonNull) {
+  FakeHostMessenger messenger(&NullableArgHostApi::GetCodec());
+  TestNullableArgHostApi api;
+  NullableArgHostApi::SetUp(&messenger, &api);
+
+  int64_t result = 0;
+  messenger.SendHostMessage("dev.flutter.pigeon.NullableArgHostApi.doit",
+                            EncodableValue(EncodableList({EncodableValue(7)})),
+                            [&result](const EncodableValue& reply) {
+                              result = GetResult(reply).LongValue();
+                            });
+
+  EXPECT_EQ(result, 7);
+}
+
+TEST(NullableReturns, HostNullableReturnNull) {
+  FakeHostMessenger messenger(&NullableReturnHostApi::GetCodec());
+  TestNullableReturnHostApi api(std::nullopt);
+  NullableReturnHostApi::SetUp(&messenger, &api);
+
+  // Initialize to a non-null value to ensure that it's actually set to null,
+  // rather than just never set.
+  EncodableValue result(true);
+  messenger.SendHostMessage(
+      "dev.flutter.pigeon.NullableReturnHostApi.doit",
+      EncodableValue(EncodableList({})),
+      [&result](const EncodableValue& reply) { result = GetResult(reply); });
+
+  EXPECT_TRUE(result.IsNull());
+}
+
+TEST(NullableReturns, HostNullableReturnNonNull) {
+  FakeHostMessenger messenger(&NullableReturnHostApi::GetCodec());
+  TestNullableReturnHostApi api(42);
+  NullableReturnHostApi::SetUp(&messenger, &api);
+
+  EncodableValue result;
+  messenger.SendHostMessage(
+      "dev.flutter.pigeon.NullableReturnHostApi.doit",
+      EncodableValue(EncodableList({})),
+      [&result](const EncodableValue& reply) { result = GetResult(reply); });
+
+  EXPECT_FALSE(result.IsNull());
+  EXPECT_EQ(result.LongValue(), 42);
+}
+
+// TODO(stuartmorgan): Add FlutterApi versions of the tests.
+
+}  // namespace nullable_returns_pigeontest
diff --git a/packages/pigeon/platform_tests/windows_unit_tests/windows/test/pigeon_test.cpp b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/pigeon_test.cpp
index 558d9d6..60c1f1f 100644
--- a/packages/pigeon/platform_tests/windows_unit_tests/windows/test/pigeon_test.cpp
+++ b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/pigeon_test.cpp
@@ -59,7 +59,7 @@
   ~MockApi() = default;
 
   MOCK_METHOD(std::optional<FlutterError>, Initialize, (), (override));
-  MOCK_METHOD(ErrorOr<std::unique_ptr<MessageSearchReply>>, Search,
+  MOCK_METHOD(ErrorOr<MessageSearchReply>, Search,
               (const MessageSearchRequest&), (override));
 };
 
@@ -130,8 +130,8 @@
       .Times(1)
       .WillOnce(testing::SaveArg<1>(&handler));
   EXPECT_CALL(mock_api, Search(testing::_))
-      .WillOnce(Return(ByMove(ErrorOr<MessageSearchReply>::MakeWithUniquePtr(
-          std::make_unique<MessageSearchReply>()))));
+      .WillOnce(
+          Return(ByMove(ErrorOr<MessageSearchReply>(MessageSearchReply()))));
   MessageApi::SetUp(&mock_messenger, &mock_api);
   bool did_call_reply = false;
   flutter::BinaryReply reply = [&did_call_reply](const uint8_t* data,
diff --git a/packages/pigeon/platform_tests/windows_unit_tests/windows/test/primitive_test.cpp b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/primitive_test.cpp
new file mode 100644
index 0000000..68b4dfe
--- /dev/null
+++ b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/primitive_test.cpp
@@ -0,0 +1,158 @@
+// 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.
+
+#include <gtest/gtest.h>
+
+#include "test/primitive.g.h"
+#include "test/utils/fake_host_messenger.h"
+
+namespace primitive_pigeontest {
+
+namespace {
+using flutter::EncodableList;
+using flutter::EncodableMap;
+using flutter::EncodableValue;
+using testing::FakeHostMessenger;
+
+class TestHostApi : public PrimitiveHostApi {
+ public:
+  TestHostApi() {}
+  virtual ~TestHostApi() {}
+
+ protected:
+  ErrorOr<int64_t> AnInt(int64_t value) override { return value; }
+  ErrorOr<bool> ABool(bool value) override { return value; }
+  ErrorOr<std::string> AString(const std::string& value) override {
+    return std::string(value);
+  }
+  ErrorOr<double> ADouble(double value) override { return value; }
+  ErrorOr<flutter::EncodableMap> AMap(
+      const flutter::EncodableMap& value) override {
+    return value;
+  }
+  ErrorOr<flutter::EncodableList> AList(
+      const flutter::EncodableList& value) override {
+    return value;
+  }
+  ErrorOr<std::vector<int32_t>> AnInt32List(
+      const std::vector<int32_t>& value) override {
+    return value;
+  }
+  ErrorOr<flutter::EncodableList> ABoolList(
+      const flutter::EncodableList& value) override {
+    return value;
+  }
+  ErrorOr<flutter::EncodableMap> AStringIntMap(
+      const flutter::EncodableMap& value) override {
+    return value;
+  }
+};
+
+const EncodableValue& GetResult(const EncodableValue& pigeon_response) {
+  return std::get<EncodableMap>(pigeon_response).at(EncodableValue("result"));
+}
+}  // namespace
+
+TEST(Primitive, HostInt) {
+  FakeHostMessenger messenger(&PrimitiveHostApi::GetCodec());
+  TestHostApi api;
+  PrimitiveHostApi::SetUp(&messenger, &api);
+
+  int64_t result = 0;
+  messenger.SendHostMessage("dev.flutter.pigeon.PrimitiveHostApi.anInt",
+                            EncodableValue(EncodableList({EncodableValue(7)})),
+                            [&result](const EncodableValue& reply) {
+                              result = GetResult(reply).LongValue();
+                            });
+
+  EXPECT_EQ(result, 7);
+}
+
+TEST(Primitive, HostBool) {
+  FakeHostMessenger messenger(&PrimitiveHostApi::GetCodec());
+  TestHostApi api;
+  PrimitiveHostApi::SetUp(&messenger, &api);
+
+  bool result = false;
+  messenger.SendHostMessage(
+      "dev.flutter.pigeon.PrimitiveHostApi.aBool",
+      EncodableValue(EncodableList({EncodableValue(true)})),
+      [&result](const EncodableValue& reply) {
+        result = std::get<bool>(GetResult(reply));
+      });
+
+  EXPECT_EQ(result, true);
+}
+
+TEST(Primitive, HostDouble) {
+  FakeHostMessenger messenger(&PrimitiveHostApi::GetCodec());
+  TestHostApi api;
+  PrimitiveHostApi::SetUp(&messenger, &api);
+
+  double result = 0.0;
+  messenger.SendHostMessage(
+      "dev.flutter.pigeon.PrimitiveHostApi.aDouble",
+      EncodableValue(EncodableList({EncodableValue(3.0)})),
+      [&result](const EncodableValue& reply) {
+        result = std::get<double>(GetResult(reply));
+      });
+
+  EXPECT_EQ(result, 3.0);
+}
+
+TEST(Primitive, HostString) {
+  FakeHostMessenger messenger(&PrimitiveHostApi::GetCodec());
+  TestHostApi api;
+  PrimitiveHostApi::SetUp(&messenger, &api);
+
+  std::string result;
+  messenger.SendHostMessage(
+      "dev.flutter.pigeon.PrimitiveHostApi.aString",
+      EncodableValue(EncodableList({EncodableValue("hello")})),
+      [&result](const EncodableValue& reply) {
+        result = std::get<std::string>(GetResult(reply));
+      });
+
+  EXPECT_EQ(result, "hello");
+}
+
+TEST(Primitive, HostList) {
+  FakeHostMessenger messenger(&PrimitiveHostApi::GetCodec());
+  TestHostApi api;
+  PrimitiveHostApi::SetUp(&messenger, &api);
+
+  EncodableList result;
+  messenger.SendHostMessage(
+      "dev.flutter.pigeon.PrimitiveHostApi.aList",
+      EncodableValue(EncodableList({EncodableValue(EncodableList({1, 2, 3}))})),
+      [&result](const EncodableValue& reply) {
+        result = std::get<EncodableList>(GetResult(reply));
+      });
+
+  EXPECT_EQ(result.size(), 3);
+  EXPECT_EQ(result[2].LongValue(), 3);
+}
+
+TEST(Primitive, HostMap) {
+  FakeHostMessenger messenger(&PrimitiveHostApi::GetCodec());
+  TestHostApi api;
+  PrimitiveHostApi::SetUp(&messenger, &api);
+
+  EncodableMap result;
+  messenger.SendHostMessage(
+      "dev.flutter.pigeon.PrimitiveHostApi.aMap",
+      EncodableValue(EncodableList({EncodableValue(EncodableMap({
+          {EncodableValue("foo"), EncodableValue("bar")},
+      }))})),
+      [&result](const EncodableValue& reply) {
+        result = std::get<EncodableMap>(GetResult(reply));
+      });
+
+  EXPECT_EQ(result.size(), 1);
+  EXPECT_EQ(result[EncodableValue("foo")], EncodableValue("bar"));
+}
+
+// TODO(stuartmorgan): Add FlutterApi versions of the tests.
+
+}  // namespace primitive_pigeontest
diff --git a/packages/pigeon/platform_tests/windows_unit_tests/windows/test/utils/echo_messenger.cpp b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/utils/echo_messenger.cpp
new file mode 100644
index 0000000..18da6c9
--- /dev/null
+++ b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/utils/echo_messenger.cpp
@@ -0,0 +1,32 @@
+// 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.
+
+#include "echo_messenger.h"
+
+#include <flutter/encodable_value.h>
+#include <flutter/message_codec.h>
+
+namespace testing {
+
+EchoMessenger::EchoMessenger(
+    const flutter::MessageCodec<flutter::EncodableValue>* codec)
+    : codec_(codec) {}
+EchoMessenger::~EchoMessenger() {}
+
+// flutter::BinaryMessenger:
+void EchoMessenger::Send(const std::string& channel, const uint8_t* message,
+                         size_t message_size,
+                         flutter::BinaryReply reply) const {
+  std::unique_ptr<flutter::EncodableValue> arg_value =
+      codec_->DecodeMessage(message, message_size);
+  const auto& args = std::get<flutter::EncodableList>(*arg_value);
+  std::unique_ptr<std::vector<uint8_t>> reply_data =
+      codec_->EncodeMessage(args[0]);
+  reply(reply_data->data(), reply_data->size());
+}
+
+void EchoMessenger::SetMessageHandler(const std::string& channel,
+                                      flutter::BinaryMessageHandler handler) {}
+
+}  // namespace testing
diff --git a/packages/pigeon/platform_tests/windows_unit_tests/windows/test/utils/echo_messenger.h b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/utils/echo_messenger.h
new file mode 100644
index 0000000..7bc4625
--- /dev/null
+++ b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/utils/echo_messenger.h
@@ -0,0 +1,35 @@
+// 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.
+
+#ifndef PLATFORM_TESTS_WINDOWS_UNIT_TESTS_WINDOWS_TEST_UTILS_ECHO_MESSENGER_H_
+#define PLATFORM_TESTS_WINDOWS_UNIT_TESTS_WINDOWS_TEST_UTILS_ECHO_MESSENGER_H_
+
+#include <flutter/binary_messenger.h>
+#include <flutter/encodable_value.h>
+#include <flutter/message_codec.h>
+
+namespace testing {
+
+// A BinaryMessenger that replies with the first argument sent to it.
+class EchoMessenger : public flutter::BinaryMessenger {
+ public:
+  // Creates an echo messenger that expects MessageCalls encoded with the given
+  // codec.
+  EchoMessenger(const flutter::MessageCodec<flutter::EncodableValue>* codec);
+  virtual ~EchoMessenger();
+
+  // flutter::BinaryMessenger:
+  void Send(const std::string& channel, const uint8_t* message,
+            size_t message_size,
+            flutter::BinaryReply reply = nullptr) const override;
+  void SetMessageHandler(const std::string& channel,
+                         flutter::BinaryMessageHandler handler) override;
+
+ private:
+  const flutter::MessageCodec<flutter::EncodableValue>* codec_;
+};
+
+}  // namespace testing
+
+#endif  // PLATFORM_TESTS_WINDOWS_UNIT_TESTS_WINDOWS_TEST_UTILS_ECHO_MESSENGER_H_
diff --git a/packages/pigeon/platform_tests/windows_unit_tests/windows/test/utils/fake_host_messenger.cpp b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/utils/fake_host_messenger.cpp
new file mode 100644
index 0000000..a2b10b5
--- /dev/null
+++ b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/utils/fake_host_messenger.cpp
@@ -0,0 +1,46 @@
+// 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.
+
+#include "fake_host_messenger.h"
+
+#include <flutter/encodable_value.h>
+#include <flutter/message_codec.h>
+
+#include <memory>
+#include <vector>
+
+namespace testing {
+
+FakeHostMessenger::FakeHostMessenger(
+    const flutter::MessageCodec<flutter::EncodableValue>* codec)
+    : codec_(codec) {}
+FakeHostMessenger::~FakeHostMessenger() {}
+
+void FakeHostMessenger::SendHostMessage(const std::string& channel,
+
+                                        const flutter::EncodableValue& message,
+                                        HostMessageReply reply_handler) {
+  const auto* codec = codec_;
+  flutter::BinaryReply binary_handler = [reply_handler, codec, channel](
+                                            const uint8_t* reply_data,
+                                            size_t reply_size) {
+    std::unique_ptr<flutter::EncodableValue> reply =
+        codec->DecodeMessage(reply_data, reply_size);
+    reply_handler(*reply);
+  };
+
+  std::unique_ptr<std::vector<uint8_t>> data = codec_->EncodeMessage(message);
+  handlers_[channel](data->data(), data->size(), std::move(binary_handler));
+}
+
+void FakeHostMessenger::Send(const std::string& channel, const uint8_t* message,
+                             size_t message_size,
+                             flutter::BinaryReply reply) const {}
+
+void FakeHostMessenger::SetMessageHandler(
+    const std::string& channel, flutter::BinaryMessageHandler handler) {
+  handlers_[channel] = std::move(handler);
+}
+
+}  // namespace testing
diff --git a/packages/pigeon/platform_tests/windows_unit_tests/windows/test/utils/fake_host_messenger.h b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/utils/fake_host_messenger.h
new file mode 100644
index 0000000..fd31426
--- /dev/null
+++ b/packages/pigeon/platform_tests/windows_unit_tests/windows/test/utils/fake_host_messenger.h
@@ -0,0 +1,51 @@
+// 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.
+
+#ifndef PLATFORM_TESTS_WINDOWS_UNIT_TESTS_WINDOWS_TEST_UTILS_FAKE_HOST_MESSENGER_H_
+#define PLATFORM_TESTS_WINDOWS_UNIT_TESTS_WINDOWS_TEST_UTILS_FAKE_HOST_MESSENGER_H_
+
+#include <flutter/binary_messenger.h>
+#include <flutter/encodable_value.h>
+#include <flutter/message_codec.h>
+
+#include <map>
+
+namespace testing {
+
+typedef std::function<void(const flutter::EncodableValue& reply)>
+    HostMessageReply;
+
+// A BinaryMessenger that allows tests to act as the engine to call host APIs.
+class FakeHostMessenger : public flutter::BinaryMessenger {
+ public:
+  // Creates an messenger that can send and receive responses with the given
+  // codec.
+  FakeHostMessenger(
+      const flutter::MessageCodec<flutter::EncodableValue>* codec);
+  virtual ~FakeHostMessenger();
+
+  // Calls the registered handler for the given channel, and calls reply_handler
+  // with the response.
+  //
+  // This allows a test to simulate a message from the Dart side, exercising the
+  // encoding and decoding logic generated for a host API.
+  void SendHostMessage(const std::string& channel,
+                       const flutter::EncodableValue& message,
+                       HostMessageReply reply_handler);
+
+  // flutter::BinaryMessenger:
+  void Send(const std::string& channel, const uint8_t* message,
+            size_t message_size,
+            flutter::BinaryReply reply = nullptr) const override;
+  void SetMessageHandler(const std::string& channel,
+                         flutter::BinaryMessageHandler handler) override;
+
+ private:
+  const flutter::MessageCodec<flutter::EncodableValue>* codec_;
+  std::map<std::string, flutter::BinaryMessageHandler> handlers_;
+};
+
+}  // namespace testing
+
+#endif  // PLATFORM_TESTS_WINDOWS_UNIT_TESTS_WINDOWS_TEST_UTILS_FAKE_HOST_MESSENGER_H_
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
index 4826d6c..6d81137 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: 3.2.3 # This must match the version in lib/generator_tools.dart
+version: 3.2.4 # This must match the version in lib/generator_tools.dart
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/pigeon/test/cpp_generator_test.dart b/packages/pigeon/test/cpp_generator_test.dart
index 0b2c93c..c720503 100644
--- a/packages/pigeon/test/cpp_generator_test.dart
+++ b/packages/pigeon/test/cpp_generator_test.dart
@@ -525,6 +525,482 @@
     }
   });
 
+  test('host nullable return types map correctly', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+          name: 'returnNullableBool',
+          arguments: <NamedType>[],
+          returnType: const TypeDeclaration(
+            baseName: 'bool',
+            isNullable: true,
+          ),
+          isAsynchronous: false,
+        ),
+        Method(
+          name: 'returnNullableInt',
+          arguments: <NamedType>[],
+          returnType: const TypeDeclaration(
+            baseName: 'int',
+            isNullable: true,
+          ),
+          isAsynchronous: false,
+        ),
+        Method(
+          name: 'returnNullableString',
+          arguments: <NamedType>[],
+          returnType: const TypeDeclaration(
+            baseName: 'String',
+            isNullable: true,
+          ),
+          isAsynchronous: false,
+        ),
+        Method(
+          name: 'returnNullableList',
+          arguments: <NamedType>[],
+          returnType: const TypeDeclaration(
+            baseName: 'List',
+            typeArguments: <TypeDeclaration>[
+              TypeDeclaration(
+                baseName: 'String',
+                isNullable: true,
+              )
+            ],
+            isNullable: true,
+          ),
+          isAsynchronous: false,
+        ),
+        Method(
+          name: 'returnNullableMap',
+          arguments: <NamedType>[],
+          returnType: const TypeDeclaration(
+            baseName: 'Map',
+            typeArguments: <TypeDeclaration>[
+              TypeDeclaration(
+                baseName: 'String',
+                isNullable: true,
+              ),
+              TypeDeclaration(
+                baseName: 'String',
+                isNullable: true,
+              )
+            ],
+            isNullable: true,
+          ),
+          isAsynchronous: false,
+        ),
+        Method(
+          name: 'returnNullableDataClass',
+          arguments: <NamedType>[],
+          returnType: const TypeDeclaration(
+            baseName: 'ReturnData',
+            isNullable: true,
+          ),
+          isAsynchronous: false,
+        ),
+      ])
+    ], classes: <Class>[
+      Class(name: 'ReturnData', fields: <NamedType>[
+        NamedType(
+            type: const TypeDeclaration(
+              baseName: 'bool',
+              isNullable: false,
+            ),
+            name: 'aValue',
+            offset: null),
+      ]),
+    ], enums: <Enum>[]);
+    {
+      final StringBuffer sink = StringBuffer();
+      generateCppHeader('', const CppOptions(), root, sink);
+      final String code = sink.toString();
+      expect(
+          code, contains('ErrorOr<std::optional<bool>> ReturnNullableBool()'));
+      expect(code,
+          contains('ErrorOr<std::optional<int64_t>> ReturnNullableInt()'));
+      expect(
+          code,
+          contains(
+              'ErrorOr<std::optional<std::string>> ReturnNullableString()'));
+      expect(
+          code,
+          contains(
+              'ErrorOr<std::optional<flutter::EncodableList>> ReturnNullableList()'));
+      expect(
+          code,
+          contains(
+              'ErrorOr<std::optional<flutter::EncodableMap>> ReturnNullableMap()'));
+      expect(
+          code,
+          contains(
+              'ErrorOr<std::optional<ReturnData>> ReturnNullableDataClass()'));
+    }
+  });
+
+  test('host non-nullable return types map correctly', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+          name: 'returnBool',
+          arguments: <NamedType>[],
+          returnType: const TypeDeclaration(
+            baseName: 'bool',
+            isNullable: false,
+          ),
+          isAsynchronous: false,
+        ),
+        Method(
+          name: 'returnInt',
+          arguments: <NamedType>[],
+          returnType: const TypeDeclaration(
+            baseName: 'int',
+            isNullable: false,
+          ),
+          isAsynchronous: false,
+        ),
+        Method(
+          name: 'returnString',
+          arguments: <NamedType>[],
+          returnType: const TypeDeclaration(
+            baseName: 'String',
+            isNullable: false,
+          ),
+          isAsynchronous: false,
+        ),
+        Method(
+          name: 'returnList',
+          arguments: <NamedType>[],
+          returnType: const TypeDeclaration(
+            baseName: 'List',
+            typeArguments: <TypeDeclaration>[
+              TypeDeclaration(
+                baseName: 'String',
+                isNullable: true,
+              )
+            ],
+            isNullable: false,
+          ),
+          isAsynchronous: false,
+        ),
+        Method(
+          name: 'returnMap',
+          arguments: <NamedType>[],
+          returnType: const TypeDeclaration(
+            baseName: 'Map',
+            typeArguments: <TypeDeclaration>[
+              TypeDeclaration(
+                baseName: 'String',
+                isNullable: true,
+              ),
+              TypeDeclaration(
+                baseName: 'String',
+                isNullable: true,
+              )
+            ],
+            isNullable: false,
+          ),
+          isAsynchronous: false,
+        ),
+        Method(
+          name: 'returnDataClass',
+          arguments: <NamedType>[],
+          returnType: const TypeDeclaration(
+            baseName: 'ReturnData',
+            isNullable: false,
+          ),
+          isAsynchronous: false,
+        ),
+      ])
+    ], classes: <Class>[
+      Class(name: 'ReturnData', fields: <NamedType>[
+        NamedType(
+            type: const TypeDeclaration(
+              baseName: 'bool',
+              isNullable: false,
+            ),
+            name: 'aValue',
+            offset: null),
+      ]),
+    ], enums: <Enum>[]);
+    {
+      final StringBuffer sink = StringBuffer();
+      generateCppHeader('', const CppOptions(), root, sink);
+      final String code = sink.toString();
+      expect(code, contains('ErrorOr<bool> ReturnBool()'));
+      expect(code, contains('ErrorOr<int64_t> ReturnInt()'));
+      expect(code, contains('ErrorOr<std::string> ReturnString()'));
+      expect(code, contains('ErrorOr<flutter::EncodableList> ReturnList()'));
+      expect(code, contains('ErrorOr<flutter::EncodableMap> ReturnMap()'));
+      expect(code, contains('ErrorOr<ReturnData> ReturnDataClass()'));
+    }
+  });
+
+  test('host nullable arguments map correctly', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          arguments: <NamedType>[
+            NamedType(
+                name: 'aBool',
+                type: const TypeDeclaration(
+                  baseName: 'bool',
+                  isNullable: true,
+                )),
+            NamedType(
+                name: 'anInt',
+                type: const TypeDeclaration(
+                  baseName: 'int',
+                  isNullable: true,
+                )),
+            NamedType(
+                name: 'aString',
+                type: const TypeDeclaration(
+                  baseName: 'String',
+                  isNullable: true,
+                )),
+            NamedType(
+                name: 'aList',
+                type: const TypeDeclaration(
+                  baseName: 'List',
+                  typeArguments: <TypeDeclaration>[
+                    TypeDeclaration(baseName: 'Object', isNullable: true)
+                  ],
+                  isNullable: true,
+                )),
+            NamedType(
+                name: 'aMap',
+                type: const TypeDeclaration(
+                  baseName: 'Map',
+                  typeArguments: <TypeDeclaration>[
+                    TypeDeclaration(baseName: 'String', isNullable: true),
+                    TypeDeclaration(baseName: 'Object', isNullable: true),
+                  ],
+                  isNullable: true,
+                )),
+            NamedType(
+                name: 'anObject',
+                type: const TypeDeclaration(
+                  baseName: 'ParameterObject',
+                  isNullable: true,
+                )),
+          ],
+          returnType: const TypeDeclaration.voidDeclaration(),
+          isAsynchronous: false,
+        ),
+      ])
+    ], classes: <Class>[
+      Class(name: 'ParameterObject', fields: <NamedType>[
+        NamedType(
+            type: const TypeDeclaration(
+              baseName: 'bool',
+              isNullable: false,
+            ),
+            name: 'aValue',
+            offset: null),
+      ]),
+    ], enums: <Enum>[]);
+    {
+      final StringBuffer sink = StringBuffer();
+      generateCppHeader('', const CppOptions(), root, sink);
+      final String code = sink.toString();
+      expect(
+          code,
+          contains('DoSomething(const bool* a_bool, '
+              'const int64_t* an_int, '
+              'const std::string* a_string, '
+              'const flutter::EncodableList* a_list, '
+              'const flutter::EncodableMap* a_map, '
+              'const ParameterObject* an_object)'));
+    }
+    {
+      final StringBuffer sink = StringBuffer();
+      generateCppSource(const CppOptions(), root, sink);
+      final String code = sink.toString();
+      // Most types should just use get_if, since the parameter is a pointer,
+      // and get_if will automatically handle null values (since a null
+      // EncodableValue will not match the queried type, so get_if will return
+      // nullptr).
+      expect(
+          code,
+          contains(
+              'const auto* a_bool_arg = std::get_if<bool>(&encodable_a_bool_arg);'));
+      expect(
+          code,
+          contains(
+              'const auto* a_string_arg = std::get_if<std::string>(&encodable_a_string_arg);'));
+      expect(
+          code,
+          contains(
+              'const auto* a_list_arg = std::get_if<flutter::EncodableList>(&encodable_a_list_arg);'));
+      expect(
+          code,
+          contains(
+              'const auto* a_map_arg = std::get_if<flutter::EncodableMap>(&encodable_a_map_arg);'));
+      // Ints are complicated since there are two possible pointer types, but
+      // the paramter always needs an int64_t*.
+      expect(
+          code,
+          contains(
+              'const int64_t an_int_arg_value = encodable_an_int_arg.IsNull() ? 0 : encodable_an_int_arg.LongValue();'));
+      expect(
+          code,
+          contains(
+              'const auto* an_int_arg = encodable_an_int_arg.IsNull() ? nullptr : &an_int_arg_value;'));
+      // Custom class types require an extra layer of extraction.
+      expect(
+          code,
+          contains(
+              'const auto* an_object_arg = &(std::any_cast<const ParameterObject&>(std::get<flutter::CustomEncodableValue>(encodable_an_object_arg)));'));
+    }
+  });
+
+  test('host non-nullable arguments map correctly', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          arguments: <NamedType>[
+            NamedType(
+                name: 'aBool',
+                type: const TypeDeclaration(
+                  baseName: 'bool',
+                  isNullable: false,
+                )),
+            NamedType(
+                name: 'anInt',
+                type: const TypeDeclaration(
+                  baseName: 'int',
+                  isNullable: false,
+                )),
+            NamedType(
+                name: 'aString',
+                type: const TypeDeclaration(
+                  baseName: 'String',
+                  isNullable: false,
+                )),
+            NamedType(
+                name: 'aList',
+                type: const TypeDeclaration(
+                  baseName: 'List',
+                  typeArguments: <TypeDeclaration>[
+                    TypeDeclaration(baseName: 'Object', isNullable: true)
+                  ],
+                  isNullable: false,
+                )),
+            NamedType(
+                name: 'aMap',
+                type: const TypeDeclaration(
+                  baseName: 'Map',
+                  typeArguments: <TypeDeclaration>[
+                    TypeDeclaration(baseName: 'String', isNullable: true),
+                    TypeDeclaration(baseName: 'Object', isNullable: true),
+                  ],
+                  isNullable: false,
+                )),
+            NamedType(
+                name: 'anObject',
+                type: const TypeDeclaration(
+                  baseName: 'ParameterObject',
+                  isNullable: false,
+                )),
+          ],
+          returnType: const TypeDeclaration.voidDeclaration(),
+          isAsynchronous: false,
+        ),
+      ])
+    ], classes: <Class>[
+      Class(name: 'ParameterObject', fields: <NamedType>[
+        NamedType(
+            type: const TypeDeclaration(
+              baseName: 'bool',
+              isNullable: false,
+            ),
+            name: 'aValue',
+            offset: null),
+      ]),
+    ], enums: <Enum>[]);
+    {
+      final StringBuffer sink = StringBuffer();
+      generateCppHeader('', const CppOptions(), root, sink);
+      final String code = sink.toString();
+      expect(
+          code,
+          contains('DoSomething(bool a_bool, '
+              'int64_t an_int, '
+              'const std::string& a_string, '
+              'const flutter::EncodableList& a_list, '
+              'const flutter::EncodableMap& a_map, '
+              'const ParameterObject& an_object)'));
+    }
+    {
+      final StringBuffer sink = StringBuffer();
+      generateCppSource(const CppOptions(), root, sink);
+      final String code = sink.toString();
+      // Most types should extract references. Since the type is non-nullable,
+      // there's only one possible type.
+      expect(
+          code,
+          contains(
+              'const auto& a_bool_arg = std::get<bool>(encodable_a_bool_arg);'));
+      expect(
+          code,
+          contains(
+              'const auto& a_string_arg = std::get<std::string>(encodable_a_string_arg);'));
+      expect(
+          code,
+          contains(
+              'const auto& a_list_arg = std::get<flutter::EncodableList>(encodable_a_list_arg);'));
+      expect(
+          code,
+          contains(
+              'const auto& a_map_arg = std::get<flutter::EncodableMap>(encodable_a_map_arg);'));
+      // Ints use a copy since there are two possible reference types, but
+      // the paramter always needs an int64_t.
+      expect(
+          code,
+          contains(
+            'const int64_t an_int_arg = encodable_an_int_arg.LongValue();',
+          ));
+      // Custom class types require an extra layer of extraction.
+      expect(
+          code,
+          contains(
+              'const auto& an_object_arg = std::any_cast<const ParameterObject&>(std::get<flutter::CustomEncodableValue>(encodable_an_object_arg));'));
+    }
+  });
+
+  test('host API argument extraction uses references', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          arguments: <NamedType>[
+            NamedType(
+                name: 'anArg',
+                type: const TypeDeclaration(
+                  baseName: 'int',
+                  isNullable: false,
+                )),
+          ],
+          returnType: const TypeDeclaration.voidDeclaration(),
+          isAsynchronous: false,
+        ),
+      ])
+    ], classes: <Class>[], enums: <Enum>[]);
+
+    final StringBuffer sink = StringBuffer();
+    generateCppSource(const CppOptions(), root, sink);
+    final String code = sink.toString();
+    // A bare 'auto' here would create a copy, not a reference, which is
+    // ineffecient.
+    expect(
+        code,
+        contains(
+            'const auto& args = std::get<flutter::EncodableList>(message);'));
+    expect(code, contains('const auto& encodable_an_arg_arg = args.at(0);'));
+  });
+
   test('enum argument', () {
     final Root root = Root(
       apis: <Api>[