| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'ast.dart'; |
| import 'functional.dart'; |
| import 'generator_tools.dart'; |
| import 'pigeon_lib.dart' show Error; |
| |
| /// General comment opening token. |
| const String _commentPrefix = '//'; |
| |
| /// Documentation comment spec. |
| const DocumentCommentSpecification _docCommentSpec = |
| DocumentCommentSpecification(_commentPrefix); |
| |
| /// The default serializer for Flutter. |
| const String _defaultCodecSerializer = 'flutter::StandardCodecSerializer'; |
| |
| /// Options that control how C++ code will be generated. |
| class CppOptions { |
| /// Creates a [CppOptions] object |
| const CppOptions({ |
| this.header, |
| this.namespace, |
| this.copyrightHeader, |
| }); |
| |
| /// The path to the header that will get placed in the source filed (example: |
| /// "foo.h"). |
| final String? header; |
| |
| /// The namespace where the generated class will live. |
| final String? namespace; |
| |
| /// A copyright header that will get prepended to generated code. |
| final Iterable<String>? copyrightHeader; |
| |
| /// Creates a [CppOptions] from a Map representation where: |
| /// `x = CppOptions.fromMap(x.toMap())`. |
| static CppOptions fromMap(Map<String, Object> map) { |
| return CppOptions( |
| header: map['header'] as String?, |
| namespace: map['namespace'] as String?, |
| copyrightHeader: map['copyrightHeader'] as Iterable<String>?, |
| ); |
| } |
| |
| /// Converts a [CppOptions] to a Map representation where: |
| /// `x = CppOptions.fromMap(x.toMap())`. |
| Map<String, Object> toMap() { |
| final Map<String, Object> result = <String, Object>{ |
| if (header != null) 'header': header!, |
| if (namespace != null) 'namespace': namespace!, |
| if (copyrightHeader != null) 'copyrightHeader': copyrightHeader!, |
| }; |
| return result; |
| } |
| |
| /// Overrides any non-null parameters from [options] into this to make a new |
| /// [CppOptions]. |
| CppOptions merge(CppOptions options) { |
| return CppOptions.fromMap(mergeMaps(toMap(), options.toMap())); |
| } |
| } |
| |
| String _getCodecSerializerName(Api api) => '${api.name}CodecSerializer'; |
| |
| const String _pointerPrefix = 'pointer'; |
| const String _encodablePrefix = 'encodable'; |
| |
| void _writeCodecHeader(Indent indent, Api api, Root root) { |
| assert(getCodecClasses(api, root).isNotEmpty); |
| final String codeSerializerName = _getCodecSerializerName(api); |
| indent.write('class $codeSerializerName : public $_defaultCodecSerializer '); |
| indent.scoped('{', '};', () { |
| indent.scoped(' public:', '', () { |
| indent.writeln(''); |
| indent.format(''' |
| inline static $codeSerializerName& GetInstance() { |
| \tstatic $codeSerializerName sInstance; |
| \treturn sInstance; |
| } |
| '''); |
| indent.writeln('$codeSerializerName();'); |
| }); |
| indent.writeScoped(' public:', '', () { |
| indent.writeln( |
| 'void WriteValue(const flutter::EncodableValue& value, flutter::ByteStreamWriter* stream) const override;'); |
| }); |
| indent.writeScoped(' protected:', '', () { |
| indent.writeln( |
| 'flutter::EncodableValue ReadValueOfType(uint8_t type, flutter::ByteStreamReader* stream) const override;'); |
| }); |
| }, nestCount: 0); |
| } |
| |
| void _writeCodecSource(Indent indent, Api api, Root root) { |
| assert(getCodecClasses(api, root).isNotEmpty); |
| final String codeSerializerName = _getCodecSerializerName(api); |
| indent.writeln('$codeSerializerName::$codeSerializerName() {}'); |
| indent.write( |
| 'flutter::EncodableValue $codeSerializerName::ReadValueOfType(uint8_t type, flutter::ByteStreamReader* stream) const '); |
| indent.scoped('{', '}', () { |
| indent.write('switch (type) '); |
| indent.scoped('{', '}', () { |
| for (final EnumeratedClass customClass in getCodecClasses(api, root)) { |
| indent.write('case ${customClass.enumeration}:'); |
| indent.writeScoped('', '', () { |
| indent.writeln( |
| 'return flutter::CustomEncodableValue(${customClass.name}(std::get<flutter::EncodableList>(ReadValue(stream))));'); |
| }); |
| } |
| indent.write('default:'); |
| indent.writeScoped('', '', () { |
| indent.writeln( |
| 'return $_defaultCodecSerializer::ReadValueOfType(type, stream);'); |
| }, addTrailingNewline: false); |
| }); |
| }); |
| indent.writeln(''); |
| indent.write( |
| 'void $codeSerializerName::WriteValue(const flutter::EncodableValue& value, flutter::ByteStreamWriter* stream) const '); |
| indent.writeScoped('{', '}', () { |
| indent.write( |
| 'if (const flutter::CustomEncodableValue* custom_value = std::get_if<flutter::CustomEncodableValue>(&value)) '); |
| indent.scoped('{', '}', () { |
| for (final EnumeratedClass customClass in getCodecClasses(api, root)) { |
| indent |
| .write('if (custom_value->type() == typeid(${customClass.name})) '); |
| indent.scoped('{', '}', () { |
| indent.writeln('stream->WriteByte(${customClass.enumeration});'); |
| indent.writeln( |
| 'WriteValue(flutter::EncodableValue(std::any_cast<${customClass.name}>(*custom_value).ToEncodableList()), stream);'); |
| indent.writeln('return;'); |
| }); |
| } |
| }); |
| indent.writeln('$_defaultCodecSerializer::WriteValue(value, stream);'); |
| }); |
| } |
| |
| 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: |
| \texplicit FlutterError(const std::string& code) |
| \t\t: code_(code) {} |
| \texplicit FlutterError(const std::string& code, const std::string& message) |
| \t\t: code_(code), message_(message) {} |
| \texplicit FlutterError(const std::string& code, const std::string& message, const flutter::EncodableValue& details) |
| \t\t: code_(code), message_(message), details_(details) {} |
| |
| \tconst std::string& code() const { return code_; } |
| \tconst std::string& message() const { return message_; } |
| \tconst flutter::EncodableValue& details() const { return details_; } |
| |
| private: |
| \tstd::string code_; |
| \tstd::string message_; |
| \tflutter::EncodableValue details_; |
| }; |
| |
| template<class T> class ErrorOr { |
| 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} |
| \tErrorOr(const FlutterError&& rhs) { v_ = std::move(rhs); } |
| |
| \tbool has_error() 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; |
| \tT TakeValue() && { return std::get<T>(std::move(v_)); } |
| |
| \tstd::variant<T, FlutterError> v_; |
| }; |
| '''); |
| } |
| |
| /// Writes the declaration for the custom class [klass]. |
| /// |
| /// See [_writeDataClassImplementation] for the corresponding declaration. |
| /// This is intended to be added to the header. |
| void _writeDataClassDeclaration(Indent indent, Class klass, Root root, |
| {String? testFriend}) { |
| indent.addln(''); |
| |
| const List<String> generatedMessages = <String>[ |
| ' Generated class from Pigeon that represents data sent in messages.' |
| ]; |
| |
| addDocumentationComments(indent, klass.documentationComments, _docCommentSpec, |
| generatorComments: generatedMessages); |
| |
| indent.write('class ${klass.name} '); |
| indent.scoped('{', '};', () { |
| indent.scoped(' public:', '', () { |
| indent.writeln('${klass.name}();'); |
| for (final NamedType field in getFieldsInSerializationOrder(klass)) { |
| addDocumentationComments( |
| indent, field.documentationComments, _docCommentSpec); |
| final HostDatatype baseDatatype = getFieldHostDatatype( |
| field, |
| root.classes, |
| root.enums, |
| (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x)); |
| indent.writeln( |
| '${_getterReturnType(baseDatatype)} ${_makeGetterName(field)}() const;'); |
| indent.writeln( |
| 'void ${_makeSetterName(field)}(${_unownedArgumentType(baseDatatype)} value_arg);'); |
| if (field.type.isNullable) { |
| // Add a second setter that takes the non-nullable version of the |
| // argument for convenience, since setting literal values with the |
| // pointer version is non-trivial. |
| final HostDatatype nonNullType = _nonNullableType(baseDatatype); |
| indent.writeln( |
| 'void ${_makeSetterName(field)}(${_unownedArgumentType(nonNullType)} value_arg);'); |
| } |
| indent.addln(''); |
| } |
| }); |
| |
| indent.scoped(' private:', '', () { |
| indent.writeln('${klass.name}(const flutter::EncodableList& list);'); |
| indent.writeln('flutter::EncodableList ToEncodableList() const;'); |
| for (final Class friend in root.classes) { |
| if (friend != klass && |
| friend.fields.any( |
| (NamedType element) => element.type.baseName == klass.name)) { |
| indent.writeln('friend class ${friend.name};'); |
| } |
| } |
| for (final Api api in root.apis) { |
| // TODO(gaaclarke): Find a way to be more precise with our |
| // friendships. |
| indent.writeln('friend class ${api.name};'); |
| indent.writeln('friend class ${_getCodecSerializerName(api)};'); |
| } |
| if (testFriend != null) { |
| indent.writeln('friend class $testFriend;'); |
| } |
| |
| for (final NamedType field in getFieldsInSerializationOrder(klass)) { |
| final HostDatatype hostDatatype = getFieldHostDatatype( |
| field, |
| root.classes, |
| root.enums, |
| (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x)); |
| indent.writeln( |
| '${_valueType(hostDatatype)} ${_makeInstanceVariableName(field)};'); |
| } |
| }); |
| }, nestCount: 0); |
| indent.writeln(''); |
| } |
| |
| /// Writes the implementation for the custom class [klass]. |
| /// |
| /// See [_writeDataClassDeclaration] for the corresponding declaration. |
| /// This is intended to be added to the implementation file. |
| void _writeDataClassImplementation(Indent indent, Class klass, Root root) { |
| final Set<String> rootClassNameSet = |
| root.classes.map((Class x) => x.name).toSet(); |
| final Set<String> rootEnumNameSet = |
| root.enums.map((Enum x) => x.name).toSet(); |
| |
| indent.addln(''); |
| indent.writeln('$_commentPrefix ${klass.name}'); |
| indent.addln(''); |
| |
| // Getters and setters. |
| for (final NamedType field in getFieldsInSerializationOrder(klass)) { |
| 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)}'; |
| final String qualifiedSetterName = |
| '${klass.name}::${_makeSetterName(field)}'; |
| final String returnExpression = hostDatatype.isNullable |
| ? '$instanceVariableName ? &(*$instanceVariableName) : nullptr' |
| : instanceVariableName; |
| |
| // Generates the string for a setter treating the type as [type], to allow |
| // generating multiple setter variants. |
| String makeSetter(HostDatatype type) { |
| const String setterArgumentName = 'value_arg'; |
| final String valueExpression = type.isNullable |
| ? '$setterArgumentName ? ${_valueType(type)}(*$setterArgumentName) : std::nullopt' |
| : setterArgumentName; |
| return 'void $qualifiedSetterName(${_unownedArgumentType(type)} $setterArgumentName) ' |
| '{ $instanceVariableName = $valueExpression; }'; |
| } |
| |
| indent.writeln( |
| '${_getterReturnType(hostDatatype)} $qualifiedGetterName() const ' |
| '{ return $returnExpression; }'); |
| indent.writeln(makeSetter(hostDatatype)); |
| if (hostDatatype.isNullable) { |
| // Write the non-nullable variant; see _writeDataClassDeclaration. |
| final HostDatatype nonNullType = _nonNullableType(hostDatatype); |
| indent.writeln(makeSetter(nonNullType)); |
| } |
| |
| indent.addln(''); |
| } |
| |
| // Serialization. |
| indent |
| .write('flutter::EncodableList ${klass.name}::ToEncodableList() const '); |
| indent.scoped('{', '}', () { |
| indent.scoped('return flutter::EncodableList{', '};', () { |
| for (final NamedType field in getFieldsInSerializationOrder(klass)) { |
| final HostDatatype hostDatatype = getFieldHostDatatype( |
| field, |
| root.classes, |
| root.enums, |
| (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x)); |
| |
| final String instanceVariable = _makeInstanceVariableName(field); |
| |
| String encodableValue = ''; |
| if (!hostDatatype.isBuiltin && |
| rootClassNameSet.contains(field.type.baseName)) { |
| final String operator = field.type.isNullable ? '->' : '.'; |
| encodableValue = |
| 'flutter::EncodableValue($instanceVariable${operator}ToEncodableList())'; |
| } else if (!hostDatatype.isBuiltin && |
| rootEnumNameSet.contains(field.type.baseName)) { |
| final String nonNullValue = |
| field.type.isNullable ? '(*$instanceVariable)' : instanceVariable; |
| encodableValue = 'flutter::EncodableValue((int)$nonNullValue)'; |
| } else { |
| final String operator = field.type.isNullable ? '*' : ''; |
| encodableValue = |
| 'flutter::EncodableValue($operator$instanceVariable)'; |
| } |
| |
| if (field.type.isNullable) { |
| encodableValue = |
| '$instanceVariable ? $encodableValue : flutter::EncodableValue()'; |
| } |
| |
| indent.writeln('$encodableValue,'); |
| } |
| }); |
| }); |
| indent.addln(''); |
| |
| // Default constructor. |
| indent.writeln('${klass.name}::${klass.name}() {}'); |
| indent.addln(''); |
| |
| // Deserialization. |
| indent.write( |
| '${klass.name}::${klass.name}(const flutter::EncodableList& list) '); |
| indent.scoped('{', '}', () { |
| enumerate(getFieldsInSerializationOrder(klass), |
| (int index, final NamedType field) { |
| final String instanceVariableName = _makeInstanceVariableName(field); |
| final String pointerFieldName = |
| '${_pointerPrefix}_${_makeVariableName(field)}'; |
| final String encodableFieldName = |
| '${_encodablePrefix}_${_makeVariableName(field)}'; |
| indent.writeln('auto& $encodableFieldName = list[$index];'); |
| 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 = getFieldHostDatatype( |
| field, |
| root.classes, |
| root.enums, |
| (TypeDeclaration x) => _baseCppTypeForBuiltinDartType(x)); |
| if (field.type.baseName == 'int') { |
| indent.format(''' |
| if (const int32_t* $pointerFieldName = std::get_if<int32_t>(&$encodableFieldName)) |
| \t$instanceVariableName = *$pointerFieldName; |
| else if (const int64_t* ${pointerFieldName}_64 = std::get_if<int64_t>(&$encodableFieldName)) |
| \t$instanceVariableName = *${pointerFieldName}_64;'''); |
| } else if (!hostDatatype.isBuiltin && |
| root.classes |
| .map((Class x) => x.name) |
| .contains(field.type.baseName)) { |
| indent.write( |
| 'if (const flutter::EncodableList* $pointerFieldName = std::get_if<flutter::EncodableList>(&$encodableFieldName)) '); |
| indent.scoped('{', '}', () { |
| indent.writeln( |
| '$instanceVariableName = ${hostDatatype.datatype}(*$pointerFieldName);'); |
| }); |
| } else { |
| indent.write( |
| 'if (const ${hostDatatype.datatype}* $pointerFieldName = std::get_if<${hostDatatype.datatype}>(&$encodableFieldName)) '); |
| indent.scoped('{', '}', () { |
| indent.writeln('$instanceVariableName = *$pointerFieldName;'); |
| }); |
| } |
| } |
| }); |
| }); |
| indent.addln(''); |
| } |
| |
| void _writeHostApiHeader(Indent indent, Api api, Root root) { |
| assert(api.location == ApiLocation.host); |
| |
| const List<String> generatedMessages = <String>[ |
| ' Generated interface from Pigeon that represents a handler of messages from Flutter.' |
| ]; |
| addDocumentationComments(indent, api.documentationComments, _docCommentSpec, |
| generatorComments: generatedMessages); |
| indent.write('class ${api.name} '); |
| indent.scoped('{', '};', () { |
| indent.scoped(' public:', '', () { |
| indent.writeln('${api.name}(const ${api.name}&) = delete;'); |
| indent.writeln('${api.name}& operator=(const ${api.name}&) = delete;'); |
| indent.writeln('virtual ~${api.name}() { };'); |
| for (final Method method in api.methods) { |
| 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 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( |
| map2(argTypes, argNames, (String argType, String argName) { |
| return '$argType $argName'; |
| })); |
| } |
| |
| addDocumentationComments( |
| indent, method.documentationComments, _docCommentSpec); |
| |
| if (method.isAsynchronous) { |
| argSignature.add('std::function<void($returnTypeName reply)> result'); |
| indent.writeln( |
| 'virtual void ${_makeMethodName(method)}(${argSignature.join(', ')}) = 0;'); |
| } else { |
| indent.writeln( |
| 'virtual $returnTypeName ${_makeMethodName(method)}(${argSignature.join(', ')}) = 0;'); |
| } |
| } |
| indent.addln(''); |
| indent.writeln('$_commentPrefix The codec used by ${api.name}.'); |
| indent.writeln('static const flutter::StandardMessageCodec& GetCodec();'); |
| indent.writeln( |
| '$_commentPrefix Sets up an instance of `${api.name}` to handle messages through the `binary_messenger`.'); |
| indent.writeln( |
| 'static void SetUp(flutter::BinaryMessenger* binary_messenger, ${api.name}* api);'); |
| indent.writeln( |
| 'static flutter::EncodableList WrapError(std::string_view error_message);'); |
| indent.writeln( |
| 'static flutter::EncodableList WrapError(const FlutterError& error);'); |
| }); |
| indent.scoped(' protected:', '', () { |
| indent.writeln('${api.name}() = default;'); |
| }); |
| }, nestCount: 0); |
| } |
| |
| void _writeHostApiSource(Indent indent, Api api, Root root) { |
| assert(api.location == ApiLocation.host); |
| |
| final String codeSerializerName = getCodecClasses(api, root).isNotEmpty |
| ? _getCodecSerializerName(api) |
| : _defaultCodecSerializer; |
| indent.format(''' |
| /// The codec used by ${api.name}. |
| const flutter::StandardMessageCodec& ${api.name}::GetCodec() { |
| \treturn flutter::StandardMessageCodec::GetInstance(&$codeSerializerName::GetInstance()); |
| } |
| '''); |
| indent.writeln( |
| '$_commentPrefix Sets up an instance of `${api.name}` to handle messages through the `binary_messenger`.'); |
| indent.write( |
| 'void ${api.name}::SetUp(flutter::BinaryMessenger* binary_messenger, ${api.name}* api) '); |
| indent.scoped('{', '}', () { |
| for (final Method method in api.methods) { |
| final String channelName = makeChannelName(api, method); |
| indent.write(''); |
| indent.scoped('{', '}', () { |
| indent.writeln( |
| 'auto channel = std::make_unique<flutter::BasicMessageChannel<flutter::EncodableValue>>('); |
| indent.inc(); |
| indent.inc(); |
| indent.writeln('binary_messenger, "$channelName", &GetCodec());'); |
| indent.dec(); |
| indent.dec(); |
| indent.write('if (api != nullptr) '); |
| indent.scoped('{', '} else {', () { |
| indent.write( |
| 'channel->SetMessageHandler([api](const flutter::EncodableValue& message, const flutter::MessageReply<flutter::EncodableValue>& reply) '); |
| indent.scoped('{', '});', () { |
| indent.writeln('flutter::EncodableList wrapped;'); |
| indent.write('try '); |
| indent.scoped('{', '}', () { |
| final List<String> methodArgument = <String>[]; |
| if (method.arguments.isNotEmpty) { |
| indent.writeln( |
| '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 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( |
| 'const auto& $encodableArgName = args.at($index);'); |
| if (!arg.type.isNullable) { |
| indent.write('if ($encodableArgName.IsNull()) '); |
| indent.scoped('{', '}', () { |
| indent.writeln( |
| 'reply(flutter::EncodableValue(WrapError("$argName unexpectedly null.")));'); |
| indent.writeln('return;'); |
| }); |
| } |
| extractEncodedArgument( |
| argName, encodableArgName, arg, hostType); |
| methodArgument.add(argName); |
| }); |
| } |
| |
| String wrapResponse(String reply, TypeDeclaration returnType) { |
| String elseBody = ''; |
| final String ifCondition; |
| final String errorGetter; |
| final String prefix = (reply != '') ? '\t' : ''; |
| |
| const String nullValue = 'flutter::EncodableValue()'; |
| if (returnType.isVoid) { |
| elseBody = |
| '$prefix\twrapped.push_back($nullValue);${indent.newline}'; |
| ifCondition = 'output.has_value()'; |
| errorGetter = 'value'; |
| } else { |
| 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\tauto output_optional = $extractedValue; |
| $prefix\tif (output_optional) { |
| $prefix\t\twrapped.push_back($wrapperType(std::move(output_optional).value())); |
| $prefix\t} else { |
| $prefix\t\twrapped.push_back($nullValue); |
| $prefix\t}${indent.newline}'''; |
| } else { |
| elseBody = |
| '$prefix\twrapped.push_back($wrapperType($extractedValue));${indent.newline}'; |
| } |
| ifCondition = 'output.has_error()'; |
| errorGetter = 'error'; |
| } |
| return '${prefix}if ($ifCondition) {${indent.newline}' |
| '$prefix\twrapped = WrapError(output.$errorGetter());${indent.newline}' |
| '$prefix} else {${indent.newline}' |
| '$elseBody' |
| '$prefix}' |
| '$prefix$reply'; |
| } |
| |
| 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}' |
| '${wrapResponse('\treply(flutter::EncodableValue(std::move(wrapped)));${indent.newline}', method.returnType)}' |
| '}', |
| ); |
| } |
| final String call = |
| 'api->${_makeMethodName(method)}(${methodArgument.join(', ')})'; |
| if (method.isAsynchronous) { |
| indent.format('$call;'); |
| } else { |
| indent.writeln('$returnTypeName output = $call;'); |
| indent.format(wrapResponse('', method.returnType)); |
| } |
| }); |
| indent.write('catch (const std::exception& exception) '); |
| indent.scoped('{', '}', () { |
| indent.writeln('wrapped = WrapError(exception.what());'); |
| if (method.isAsynchronous) { |
| indent.writeln( |
| 'reply(flutter::EncodableValue(std::move(wrapped)));'); |
| } |
| }); |
| if (!method.isAsynchronous) { |
| indent.writeln( |
| 'reply(flutter::EncodableValue(std::move(wrapped)));'); |
| } |
| }); |
| }); |
| indent.scoped(null, '}', () { |
| indent.writeln('channel->SetMessageHandler(nullptr);'); |
| }); |
| }); |
| } |
| }); |
| } |
| |
| String _getArgumentName(int count, NamedType argument) => |
| argument.name.isEmpty ? 'arg$count' : _makeVariableName(argument); |
| |
| /// Returns an argument name that can be used in a context where it is possible to collide. |
| String _getSafeArgumentName(int count, NamedType argument) => |
| '${_getArgumentName(count, argument)}_arg'; |
| |
| void _writeFlutterApiHeader(Indent indent, Api api) { |
| assert(api.location == ApiLocation.flutter); |
| |
| const List<String> generatedMessages = <String>[ |
| ' Generated class from Pigeon that represents Flutter messages that can be called from C++.' |
| ]; |
| addDocumentationComments(indent, api.documentationComments, _docCommentSpec, |
| generatorComments: generatedMessages); |
| indent.write('class ${api.name} '); |
| indent.scoped('{', '};', () { |
| indent.scoped(' private:', '', () { |
| indent.writeln('flutter::BinaryMessenger* binary_messenger_;'); |
| }); |
| indent.scoped(' public:', '', () { |
| indent.write('${api.name}(flutter::BinaryMessenger* binary_messenger);'); |
| indent.writeln(''); |
| indent.writeln('static const flutter::StandardMessageCodec& GetCodec();'); |
| for (final Method func in api.methods) { |
| final String returnType = func.returnType.isVoid |
| ? 'void' |
| : _nullSafeCppTypeForDartType(func.returnType); |
| final String callback = 'std::function<void($returnType)>&& callback'; |
| addDocumentationComments( |
| indent, func.documentationComments, _docCommentSpec); |
| if (func.arguments.isEmpty) { |
| indent.writeln('void ${func.name}($callback);'); |
| } else { |
| final Iterable<String> argTypes = func.arguments |
| .map((NamedType e) => _nullSafeCppTypeForDartType(e.type)); |
| final Iterable<String> argNames = |
| indexMap(func.arguments, _getSafeArgumentName); |
| final String argsSignature = |
| map2(argTypes, argNames, (String x, String y) => '$x $y') |
| .join(', '); |
| indent.writeln('void ${func.name}($argsSignature, $callback);'); |
| } |
| } |
| }); |
| }, nestCount: 0); |
| } |
| |
| void _writeFlutterApiSource(Indent indent, Api api, Root root) { |
| assert(api.location == ApiLocation.flutter); |
| indent.writeln( |
| '$_commentPrefix Generated class from Pigeon that represents Flutter messages that can be called from C++.'); |
| indent.write( |
| '${api.name}::${api.name}(flutter::BinaryMessenger* binary_messenger) '); |
| indent.scoped('{', '}', () { |
| indent.writeln('this->binary_messenger_ = binary_messenger;'); |
| }); |
| indent.writeln(''); |
| final String codeSerializerName = getCodecClasses(api, root).isNotEmpty |
| ? _getCodecSerializerName(api) |
| : _defaultCodecSerializer; |
| indent.format(''' |
| const flutter::StandardMessageCodec& ${api.name}::GetCodec() { |
| \treturn flutter::StandardMessageCodec::GetInstance(&$codeSerializerName::GetInstance()); |
| } |
| '''); |
| for (final Method func in api.methods) { |
| final String channelName = makeChannelName(api, func); |
| final String returnType = func.returnType.isVoid |
| ? 'void' |
| : _nullSafeCppTypeForDartType(func.returnType); |
| String sendArgument; |
| final String callback = 'std::function<void($returnType)>&& callback'; |
| if (func.arguments.isEmpty) { |
| indent.write('void ${api.name}::${func.name}($callback) '); |
| sendArgument = 'flutter::EncodableValue()'; |
| } else { |
| final Iterable<String> argTypes = func.arguments |
| .map((NamedType e) => _nullSafeCppTypeForDartType(e.type)); |
| final Iterable<String> argNames = |
| indexMap(func.arguments, _getSafeArgumentName); |
| sendArgument = |
| 'flutter::EncodableList { ${(argNames.map((String arg) => 'flutter::CustomEncodableValue($arg)')).join(', ')} }'; |
| final String argsSignature = |
| map2(argTypes, argNames, (String x, String y) => '$x $y').join(', '); |
| indent |
| .write('void ${api.name}::${func.name}($argsSignature, $callback) '); |
| } |
| indent.scoped('{', '}', () { |
| const String channel = 'channel'; |
| indent.writeln( |
| 'auto channel = std::make_unique<flutter::BasicMessageChannel<flutter::EncodableValue>>('); |
| indent.inc(); |
| indent.inc(); |
| indent.writeln('binary_messenger_, "$channelName", &GetCodec());'); |
| indent.dec(); |
| indent.dec(); |
| indent.write( |
| '$channel->Send($sendArgument, [callback](const uint8_t* reply, size_t reply_size) '); |
| indent.scoped('{', '});', () { |
| if (func.returnType.isVoid) { |
| indent.writeln('callback();'); |
| } else { |
| indent.writeln( |
| 'std::unique_ptr<flutter::EncodableValue> decoded_reply = GetCodec().DecodeMessage(reply, reply_size);'); |
| indent.writeln( |
| 'flutter::EncodableValue args = *(flutter::EncodableValue*)(decoded_reply.release());'); |
| const String output = 'output'; |
| |
| final bool isBuiltin = |
| _baseCppTypeForBuiltinDartType(func.returnType) != null; |
| final String returnTypeName = |
| _baseCppTypeForDartType(func.returnType); |
| if (func.returnType.isNullable) { |
| indent.writeln('$returnType $output{};'); |
| } else { |
| indent.writeln('$returnTypeName $output{};'); |
| } |
| const String pointerVariable = '${_pointerPrefix}_$output'; |
| if (func.returnType.baseName == 'int') { |
| indent.format(''' |
| if (const int32_t* $pointerVariable = std::get_if<int32_t>(&args)) |
| \t$output = *$pointerVariable; |
| else if (const int64_t* ${pointerVariable}_64 = std::get_if<int64_t>(&args)) |
| \t$output = *${pointerVariable}_64;'''); |
| } else if (!isBuiltin) { |
| indent.write( |
| 'if (const flutter::EncodableList* $pointerVariable = std::get_if<flutter::EncodableList>(&args)) '); |
| indent.scoped('{', '}', () { |
| indent.writeln('$output = $returnTypeName(*$pointerVariable);'); |
| }); |
| } else { |
| indent.write( |
| 'if (const $returnTypeName* $pointerVariable = std::get_if<$returnTypeName>(&args)) '); |
| indent.scoped('{', '}', () { |
| indent.writeln('$output = *$pointerVariable;'); |
| }); |
| } |
| |
| indent.writeln('callback($output);'); |
| } |
| }); |
| }); |
| } |
| } |
| |
| /// Returns a non-nullable variant of [type]. |
| HostDatatype _nonNullableType(HostDatatype type) { |
| return HostDatatype( |
| datatype: type.datatype, isBuiltin: type.isBuiltin, isNullable: false); |
| } |
| |
| String _pascalCaseFromCamelCase(String camelCase) => |
| camelCase[0].toUpperCase() + camelCase.substring(1); |
| |
| String _snakeCaseFromCamelCase(String camelCase) { |
| return camelCase.replaceAllMapped(RegExp(r'[A-Z]'), |
| (Match m) => '${m.start == 0 ? '' : '_'}${m[0]!.toLowerCase()}'); |
| } |
| |
| String _pascalCaseFromSnakeCase(String snakeCase) { |
| final String camelCase = snakeCase.replaceAllMapped( |
| RegExp(r'_([a-z])'), (Match m) => m[1]!.toUpperCase()); |
| return _pascalCaseFromCamelCase(camelCase); |
| } |
| |
| String _makeMethodName(Method method) => _pascalCaseFromCamelCase(method.name); |
| |
| String _makeGetterName(NamedType field) => _snakeCaseFromCamelCase(field.name); |
| |
| String _makeSetterName(NamedType field) => |
| 'set_${_snakeCaseFromCamelCase(field.name)}'; |
| |
| String _makeVariableName(NamedType field) => |
| _snakeCaseFromCamelCase(field.name); |
| |
| String _makeInstanceVariableName(NamedType field) => |
| '${_makeVariableName(field)}_'; |
| |
| // TODO(stuartmorgan): Remove this in favor of _isPodType once callers have |
| // all been updated to using HostDatatypes. |
| bool _isReferenceType(String dataType) { |
| switch (dataType) { |
| case 'bool': |
| case 'int64_t': |
| case 'double': |
| return false; |
| default: |
| return true; |
| } |
| } |
| |
| /// Returns true if [type] corresponds to a plain-old-data type (i.e., one that |
| /// should generally be passed by value rather than pointer/reference) in C++. |
| bool _isPodType(HostDatatype type) { |
| return !_isReferenceType(type.datatype); |
| } |
| |
| String? _baseCppTypeForBuiltinDartType(TypeDeclaration type) { |
| const Map<String, String> cppTypeForDartTypeMap = <String, String>{ |
| 'void': 'void', |
| 'bool': 'bool', |
| 'int': 'int64_t', |
| 'String': 'std::string', |
| 'double': 'double', |
| 'Uint8List': 'std::vector<uint8_t>', |
| 'Int32List': 'std::vector<int32_t>', |
| 'Int64List': 'std::vector<int64_t>', |
| 'Float64List': 'std::vector<double>', |
| 'Map': 'flutter::EncodableMap', |
| 'List': 'flutter::EncodableList', |
| }; |
| if (cppTypeForDartTypeMap.containsKey(type.baseName)) { |
| return cppTypeForDartTypeMap[type.baseName]; |
| } else { |
| return null; |
| } |
| } |
| |
| /// Returns the base C++ type (without pointer, reference, optional, etc.) for |
| /// the given [type]. |
| String _baseCppTypeForDartType(TypeDeclaration type) { |
| return _baseCppTypeForBuiltinDartType(type) ?? type.baseName; |
| } |
| |
| /// Returns the C++ type to use in a value context (variable declaration, |
| /// pass-by-value, etc.) for the given C++ base type. |
| String _valueType(HostDatatype type) { |
| final String baseType = type.datatype; |
| return type.isNullable ? 'std::optional<$baseType>' : baseType; |
| } |
| |
| /// Returns the C++ type to use in an argument context without ownership |
| /// transfer for the given base type. |
| String _unownedArgumentType(HostDatatype type) { |
| final bool isString = type.datatype == 'std::string'; |
| final String baseType = isString ? 'std::string_view' : type.datatype; |
| if (isString || _isPodType(type)) { |
| return type.isNullable ? 'const $baseType*' : baseType; |
| } |
| 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) { |
| final String baseType = type.datatype; |
| if (_isPodType(type)) { |
| // Use pointers rather than optionals even for nullable POD, since the |
| // semantics of using them is essentially identical and this makes them |
| // consistent with non-POD. |
| return type.isNullable ? 'const $baseType*' : baseType; |
| } |
| 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, |
| {bool considerReference = true}) { |
| if (type.isNullable) { |
| return 'std::optional<${_baseCppTypeForDartType(type)}>'; |
| } else { |
| String typeName = _baseCppTypeForDartType(type); |
| if (_isReferenceType(typeName)) { |
| if (considerReference) { |
| typeName = 'const $typeName&'; |
| } else { |
| typeName = 'std::unique_ptr<$typeName>'; |
| } |
| } |
| return typeName; |
| } |
| } |
| |
| String _getGuardName(String? headerFileName, String? namespace) { |
| String guardName = 'PIGEON_'; |
| if (headerFileName != null) { |
| guardName += '${headerFileName.replaceAll('.', '_').toUpperCase()}_'; |
| } |
| if (namespace != null) { |
| guardName += '${namespace.toUpperCase()}_'; |
| } |
| return '${guardName}H_'; |
| } |
| |
| void _writeSystemHeaderIncludeBlock(Indent indent, List<String> headers) { |
| headers.sort(); |
| for (final String header in headers) { |
| indent.writeln('#include <$header>'); |
| } |
| } |
| |
| /// Generates the ".h" file for the AST represented by [root] to [sink] with the |
| /// provided [options] and [headerFileName]. |
| void generateCppHeader( |
| String? headerFileName, CppOptions options, Root root, StringSink sink) { |
| final Indent indent = Indent(sink); |
| if (options.copyrightHeader != null) { |
| addLines(indent, options.copyrightHeader!, linePrefix: '// '); |
| } |
| indent.writeln('$_commentPrefix $generatedCodeWarning'); |
| indent.writeln('$_commentPrefix $seeAlsoWarning'); |
| indent.addln(''); |
| final String guardName = _getGuardName(headerFileName, options.namespace); |
| indent.writeln('#ifndef $guardName'); |
| indent.writeln('#define $guardName'); |
| |
| _writeSystemHeaderIncludeBlock(indent, <String>[ |
| 'flutter/basic_message_channel.h', |
| 'flutter/binary_messenger.h', |
| 'flutter/encodable_value.h', |
| 'flutter/standard_message_codec.h', |
| ]); |
| indent.addln(''); |
| _writeSystemHeaderIncludeBlock(indent, <String>[ |
| 'map', |
| 'string', |
| 'optional', |
| ]); |
| indent.addln(''); |
| |
| if (options.namespace != null) { |
| indent.writeln('namespace ${options.namespace} {'); |
| } |
| |
| // When generating for a Pigeon unit test, add a test fixture friend class to |
| // allow unit testing private methods, since testing serialization via public |
| // methods is essentially an end-to-end test. |
| String? testFixtureClass; |
| if (options.namespace?.endsWith('_pigeontest') ?? false) { |
| testFixtureClass = |
| '${_pascalCaseFromSnakeCase(options.namespace!.replaceAll('_pigeontest', ''))}Test'; |
| indent.writeln('class $testFixtureClass;'); |
| } |
| |
| indent.addln(''); |
| indent.writeln('$_commentPrefix Generated class from Pigeon.'); |
| |
| for (final Enum anEnum in root.enums) { |
| indent.writeln(''); |
| addDocumentationComments( |
| indent, anEnum.documentationComments, _docCommentSpec); |
| indent.write('enum class ${anEnum.name} '); |
| indent.scoped('{', '};', () { |
| enumerate(anEnum.members, (int index, final EnumMember member) { |
| addDocumentationComments( |
| indent, member.documentationComments, _docCommentSpec); |
| indent.writeln( |
| '${member.name} = $index${index == anEnum.members.length - 1 ? '' : ','}'); |
| }); |
| }); |
| } |
| |
| indent.addln(''); |
| |
| _writeErrorOr(indent, friends: root.apis.map((Api api) => api.name)); |
| |
| for (final Class klass in root.classes) { |
| _writeDataClassDeclaration(indent, klass, root, |
| // Add a hook for unit testing data classes when using the namespace |
| // used by pigeon tests. |
| testFriend: testFixtureClass); |
| } |
| |
| for (final Api api in root.apis) { |
| if (getCodecClasses(api, root).isNotEmpty) { |
| _writeCodecHeader(indent, api, root); |
| } |
| indent.addln(''); |
| if (api.location == ApiLocation.host) { |
| _writeHostApiHeader(indent, api, root); |
| } else if (api.location == ApiLocation.flutter) { |
| _writeFlutterApiHeader(indent, api); |
| } |
| } |
| |
| if (options.namespace != null) { |
| indent.writeln('} // namespace ${options.namespace}'); |
| } |
| |
| indent.writeln('#endif // $guardName'); |
| } |
| |
| /// Generates the ".cpp" file for the AST represented by [root] to [sink] with the |
| /// provided [options]. |
| void generateCppSource(CppOptions options, Root root, StringSink sink) { |
| final Indent indent = Indent(sink); |
| if (options.copyrightHeader != null) { |
| addLines(indent, options.copyrightHeader!, linePrefix: '// '); |
| } |
| indent.writeln('$_commentPrefix $generatedCodeWarning'); |
| indent.writeln('$_commentPrefix $seeAlsoWarning'); |
| indent.addln(''); |
| indent.addln('#undef _HAS_EXCEPTIONS'); |
| indent.addln(''); |
| |
| indent.writeln('#include "${options.header}"'); |
| indent.addln(''); |
| _writeSystemHeaderIncludeBlock(indent, <String>[ |
| 'flutter/basic_message_channel.h', |
| 'flutter/binary_messenger.h', |
| 'flutter/encodable_value.h', |
| 'flutter/standard_message_codec.h', |
| ]); |
| indent.addln(''); |
| _writeSystemHeaderIncludeBlock(indent, <String>[ |
| 'map', |
| 'string', |
| 'optional', |
| ]); |
| indent.addln(''); |
| |
| if (options.namespace != null) { |
| indent.writeln('namespace ${options.namespace} {'); |
| } |
| |
| for (final Class klass in root.classes) { |
| _writeDataClassImplementation(indent, klass, root); |
| } |
| |
| for (final Api api in root.apis) { |
| if (getCodecClasses(api, root).isNotEmpty) { |
| _writeCodecSource(indent, api, root); |
| indent.addln(''); |
| } |
| if (api.location == ApiLocation.host) { |
| _writeHostApiSource(indent, api, root); |
| |
| indent.addln(''); |
| indent.format(''' |
| flutter::EncodableList ${api.name}::WrapError(std::string_view error_message) { |
| \treturn flutter::EncodableList({ |
| \t\tflutter::EncodableValue(std::string(error_message)), |
| \t\tflutter::EncodableValue("Error"), |
| \t\tflutter::EncodableValue() |
| \t}); |
| } |
| flutter::EncodableList ${api.name}::WrapError(const FlutterError& error) { |
| \treturn flutter::EncodableList({ |
| \t\tflutter::EncodableValue(error.message()), |
| \t\tflutter::EncodableValue(error.code()), |
| \t\terror.details() |
| \t}); |
| }'''); |
| indent.addln(''); |
| } else if (api.location == ApiLocation.flutter) { |
| _writeFlutterApiSource(indent, api, root); |
| } |
| } |
| |
| if (options.namespace != null) { |
| indent.writeln('} // namespace ${options.namespace}'); |
| } |
| } |
| |
| /// Validates an AST to make sure the cpp generator supports everything. |
| List<Error> validateCpp(CppOptions options, Root root) { |
| final List<Error> result = <Error>[]; |
| for (final Api api in root.apis) { |
| for (final Method method in api.methods) { |
| for (final NamedType arg in method.arguments) { |
| if (isEnum(root, arg.type)) { |
| // TODO(gaaclarke): Add line number and filename. |
| result.add(Error( |
| message: |
| "Nullable enum types aren't supported in C++ arguments in method:${api.name}.${method.name} argument:(${arg.type.baseName} ${arg.name}).")); |
| } |
| } |
| } |
| } |
| return result; |
| } |