toString() for parcelable/union types for C++/NDK

Note that enum types already have toString(enum) function.
This is for the C++/NDK backends.

Bug: 134658111
Test: aidl_integration_test / aidl_unittests
Change-Id: I9ca2dcc1a508d9dac2b7dad8f735bbbfe3f67717
diff --git a/aidl_to_cpp_common.cpp b/aidl_to_cpp_common.cpp
index fac699a..58dafdb 100644
--- a/aidl_to_cpp_common.cpp
+++ b/aidl_to_cpp_common.cpp
@@ -32,6 +32,14 @@
 namespace aidl {
 namespace cpp {
 
+namespace {
+constexpr char kToStringHelper[] =
+    R"(template <typename _T, ::std::enable_if_t<::std::is_same_v<::std::string, decltype(std::declval<_T>().toString())>, int> = 0>
+static inline ::std::string _call_toString(const _T& _t) { return _t.toString(); }
+static inline ::std::string _call_toString(...) { return "{no toString() implemented}"; }
+)";
+}
+
 string ClassName(const AidlDefinedType& defined_type, ClassNames type) {
   string base_name = defined_type.GetName();
   if (base_name.length() >= 2 && base_name[0] == 'I' && isupper(base_name[1])) {
@@ -94,6 +102,96 @@
   return prefix + a.GetName();
 }
 
+string ToString(const AidlTypeSpecifier& type, const string& expr);
+string ToStringNullable(const AidlTypeSpecifier& type, const string& expr);
+string ToStringNullableVector(const AidlTypeSpecifier& element_type, const string& expr);
+string ToStringVector(const AidlTypeSpecifier& element_type, const string& expr);
+string ToStringRaw(const AidlTypeSpecifier& type, const string& expr);
+
+string ToStringNullable(const AidlTypeSpecifier& type, const string& expr) {
+  if (AidlTypenames::IsPrimitiveTypename(type.GetName())) {
+    // we don't allow @nullable for primitives
+    return ToStringRaw(type, expr);
+  }
+  return "((" + expr + ") ? " + ToStringRaw(type, "*" + expr) + ": \"(null)\")";
+}
+
+string ToStringVector(const AidlTypeSpecifier& element_type, const string& expr) {
+  return "[&](){ std::ostringstream o; o << \"[\"; bool first = true; for (const auto& v: " + expr +
+         ") { (void)v; if (first) first = false; else o << \", \"; o << " +
+         ToStringRaw(element_type, "v") + "; }; o << \"]\"; return o.str(); }()";
+}
+
+string ToStringNullableVector(const AidlTypeSpecifier& element_type, const string& expr) {
+  return "[&](){ if (!(" + expr +
+         ")) return std::string(\"(null)\"); std::ostringstream o; o << \"[\"; bool first = true; "
+         "for (const auto& v: *(" +
+         expr + ")) { (void)v; if (first) first = false; else o << \", \"; o << " +
+         ToStringNullable(element_type, "v") + "; }; o << \"]\"; return o.str(); }()";
+}
+
+string ToStringRaw(const AidlTypeSpecifier& type, const string& expr) {
+  if (AidlTypenames::IsBuiltinTypename(type.GetName())) {
+    if (AidlTypenames::IsPrimitiveTypename(type.GetName())) {
+      if (type.GetName() == "boolean") {
+        return "(" + expr + "?\"true\":\"false\")";
+      }
+      if (type.GetName() == "char") {
+        return "std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>().to_bytes(" +
+               expr + ")";
+      }
+      return "std::to_string(" + expr + ")";
+    }
+    if (type.GetName() == "String") {
+      return "(std::ostringstream() << " + expr + ").str()";
+    }
+    // ""(empty string) for unsupported types
+    return "\"\"";
+  }
+
+  const AidlDefinedType* defined_type = type.GetDefinedType();
+  AIDL_FATAL_IF(defined_type == nullptr, type);
+
+  if (defined_type->AsInterface()) {
+    // ""(empty string) for unsupported types
+    return "\"\"";
+  }
+  if (defined_type->AsEnumDeclaration()) {
+    const auto ns = Join(defined_type->GetSplitPackage(), "::");
+    return ns + "::toString(" + expr + ")";
+  }
+  return "_call_toString(" + expr + ")";
+}
+
+string ToString(const AidlTypeSpecifier& type, const string& expr) {
+  static const std::set<string> kNotSupported = {"Map", "IBinder", "ParcelFileDescriptor",
+                                                 "ParcelableHolder"};
+  if (kNotSupported.find(type.GetName()) != kNotSupported.end()) {
+    // ""(empty string) for unsupported types
+    return "\"\"";
+  }
+  if (type.IsArray() && type.IsNullable()) {
+    const auto& element_type = type.ArrayBase();
+    return ToStringNullableVector(element_type, expr);
+  }
+  if (type.GetName() == "List" && type.IsNullable()) {
+    const auto& element_type = *type.GetTypeParameters()[0];
+    return ToStringNullableVector(element_type, expr);
+  }
+  if (type.IsArray()) {
+    const auto& element_type = type.ArrayBase();
+    return ToStringVector(element_type, expr);
+  }
+  if (type.GetName() == "List") {
+    const auto& element_type = *type.GetTypeParameters()[0];
+    return ToStringVector(element_type, expr);
+  }
+  if (type.IsNullable()) {
+    return ToStringNullable(type, expr);
+  }
+  return ToStringRaw(type, expr);
+}
+
 struct TypeInfo {
   // name of the type in C++ output
   std::string cpp_name;
@@ -423,6 +521,68 @@
   out << "\n";
 }
 
+// Output may look like:
+// inline std::string toString() const {
+//   std::ostringstream os;
+//   os << "MyData{";
+//   os << "field1: " << field1;
+//   os << ", field2: " << v.field2;
+//   ...
+//   os << "}";
+//   return os.str();
+// }
+void GenerateToString(CodeWriter& out, const AidlStructuredParcelable& parcelable) {
+  out << kToStringHelper;
+  out << "inline std::string toString() const {\n";
+  out.Indent();
+  out << "std::ostringstream os;\n";
+  out << "os << \"" << parcelable.GetName() << "{\";\n";
+  bool first = true;
+  for (const auto& f : parcelable.GetFields()) {
+    if (first) {
+      out << "os << \"";
+      first = false;
+    } else {
+      out << "os << \", ";
+    }
+    out << f->GetName() << ": \" << " << ToString(f->GetType(), f->GetName()) << ";\n";
+  }
+  out << "os << \"}\";\n";
+  out << "return os.str();\n";
+  out.Dedent();
+  out << "}\n";
+}
+
+// Output may look like:
+// inline std::string toString() const {
+//   std::ostringstream os;
+//   os << "MyData{";
+//   switch (v.getTag()) {
+//   case MyData::field: os << "field: " << v.get<MyData::field>(); break;
+//   ...
+//   }
+//   os << "}";
+//   return os.str();
+// }
+void GenerateToString(CodeWriter& out, const AidlUnionDecl& parcelable) {
+  out << kToStringHelper;
+  out << "inline std::string toString() const {\n";
+  out.Indent();
+  out << "std::ostringstream os;\n";
+  out << "os << \"" + parcelable.GetName() + "{\";\n";
+  out << "switch (getTag()) {\n";
+  for (const auto& f : parcelable.GetFields()) {
+    const string tag = f->GetName();
+    out << "case " << tag << ": os << \"" << tag << ": \" << "
+        << ToString(f->GetType(), "get<" + tag + ">()") << "; break;\n";
+  }
+  out << "}\n";
+  out << "os << \"}\";\n";
+  out << "return os.str();\n";
+  out.Dedent();
+  out << "}\n";
+}
+
 const vector<string> UnionWriter::headers{
     "type_traits",  // std::is_same_v
     "utility",      // std::mode/forward for value
diff --git a/aidl_to_cpp_common.h b/aidl_to_cpp_common.h
index 3c6996e..d11b067 100644
--- a/aidl_to_cpp_common.h
+++ b/aidl_to_cpp_common.h
@@ -77,6 +77,9 @@
 
 void GenerateParcelableComparisonOperators(CodeWriter& out, const AidlParcelable& parcelable);
 
+void GenerateToString(CodeWriter& out, const AidlStructuredParcelable& parcelable);
+void GenerateToString(CodeWriter& out, const AidlUnionDecl& parcelable);
+
 struct ParcelWriterContext {
   string status_type;
   string status_ok;
diff --git a/aidl_unittest.cpp b/aidl_unittest.cpp
index 1bcde1c..df29b8c 100644
--- a/aidl_unittest.cpp
+++ b/aidl_unittest.cpp
@@ -2973,7 +2973,10 @@
 #include <binder/Parcel.h>
 #include <binder/ParcelFileDescriptor.h>
 #include <binder/Status.h>
+#include <codecvt>
 #include <cstdint>
+#include <locale>
+#include <sstream>
 #include <type_traits>
 #include <utility>
 #include <variant>
@@ -3062,6 +3065,20 @@
     static const std::string DESCIPTOR = "a.Foo";
     return DESCIPTOR;
   }
+  template <typename _T, ::std::enable_if_t<::std::is_same_v<::std::string, decltype(std::declval<_T>().toString())>, int> = 0>
+  static inline ::std::string _call_toString(const _T& _t) { return _t.toString(); }
+  static inline ::std::string _call_toString(...) { return "{no toString() implemented}"; }
+  inline std::string toString() const {
+    std::ostringstream os;
+    os << "Foo{";
+    switch (getTag()) {
+    case ns: os << "ns: " << [&](){ std::ostringstream o; o << "["; bool first = true; for (const auto& v: get<ns>()) { (void)v; if (first) first = false; else o << ", "; o << std::to_string(v); }; o << "]"; return o.str(); }(); break;
+    case e: os << "e: " << a::toString(get<e>()); break;
+    case pfd: os << "pfd: " << ""; break;
+    }
+    os << "}";
+    return os.str();
+  }
 private:
   std::variant<::std::vector<int32_t>, ::a::ByteEnum, ::android::os::ParcelFileDescriptor> _value;
 };  // class Foo
@@ -3114,6 +3131,9 @@
 const char kUnionExampleExpectedOutputNdkHeader[] = R"(#pragma once
 #include <android/binder_interface_utils.h>
 #include <android/binder_parcelable_utils.h>
+#include <codecvt>
+#include <locale>
+#include <sstream>
 
 #include <type_traits>
 #include <utility>
@@ -3191,6 +3211,20 @@
   binder_status_t readFromParcel(const AParcel* _parcel);
   binder_status_t writeToParcel(AParcel* _parcel) const;
   static const ::ndk::parcelable_stability_t _aidl_stability = ::ndk::STABILITY_LOCAL;
+  template <typename _T, ::std::enable_if_t<::std::is_same_v<::std::string, decltype(std::declval<_T>().toString())>, int> = 0>
+  static inline ::std::string _call_toString(const _T& _t) { return _t.toString(); }
+  static inline ::std::string _call_toString(...) { return "{no toString() implemented}"; }
+  inline std::string toString() const {
+    std::ostringstream os;
+    os << "Foo{";
+    switch (getTag()) {
+    case ns: os << "ns: " << [&](){ std::ostringstream o; o << "["; bool first = true; for (const auto& v: get<ns>()) { (void)v; if (first) first = false; else o << ", "; o << std::to_string(v); }; o << "]"; return o.str(); }(); break;
+    case e: os << "e: " << a::toString(get<e>()); break;
+    case pfd: os << "pfd: " << ""; break;
+    }
+    os << "}";
+    return os.str();
+  }
 private:
   std::variant<std::vector<int32_t>, ::aidl::a::ByteEnum, ::ndk::ScopedFileDescriptor> _value;
 };
diff --git a/generate_cpp.cpp b/generate_cpp.cpp
index 616d472..770c3c7 100644
--- a/generate_cpp.cpp
+++ b/generate_cpp.cpp
@@ -1234,6 +1234,14 @@
                                    "  return DESCIPTOR;\n"
                                    "}\n",
                                    parcel.GetCanonicalName().c_str()))));
+
+  // toString() method
+  includes.insert("codecvt");  // std::codecvt_utf8_utf16
+  includes.insert("locale");   // std::wstrinig_convert
+  includes.insert("sstream");  // std::ostringstream
+  const string code = CodeWriter::RunWith(&GenerateToString, parcel);
+  parcel_class->AddPublic(std::make_unique<LiteralDecl>(code));
+
   return unique_ptr<Document>{
       new CppHeader{vector<string>(includes.begin(), includes.end()),
                     NestInNamespaces(std::move(parcel_class), parcel.GetSplitPackage())}};
diff --git a/generate_ndk.cpp b/generate_ndk.cpp
index 8718561..717f60c 100644
--- a/generate_ndk.cpp
+++ b/generate_ndk.cpp
@@ -982,7 +982,11 @@
   out << "#pragma once\n";
   out << "#include <android/binder_interface_utils.h>\n";
   out << "#include <android/binder_parcelable_utils.h>\n";
-  out << "\n";
+
+  // used by toString()
+  out << "#include <codecvt>\n";
+  out << "#include <locale>\n";
+  out << "#include <sstream>\n";
 
   GenerateHeaderIncludes(out, types, defined_type);
 
@@ -1021,6 +1025,9 @@
 
   out << "static const ::ndk::parcelable_stability_t _aidl_stability = ::ndk::"
       << (defined_type.IsVintfStability() ? "STABILITY_VINTF" : "STABILITY_LOCAL") << ";\n";
+
+  cpp::GenerateToString(out, defined_type);
+
   out.Dedent();
   out << "};\n";
   LeaveNdkNamespace(out, defined_type);
@@ -1112,6 +1119,12 @@
   out << "#pragma once\n";
   out << "#include <android/binder_interface_utils.h>\n";
   out << "#include <android/binder_parcelable_utils.h>\n";
+
+  // used by toString()
+  out << "#include <codecvt>\n";
+  out << "#include <locale>\n";
+  out << "#include <sstream>\n";
+
   out << "\n";
 
   for (const auto& header : cpp::UnionWriter::headers) {
@@ -1138,6 +1151,7 @@
 
   out << "static const ::ndk::parcelable_stability_t _aidl_stability = ::ndk::"
       << (defined_type.IsVintfStability() ? "STABILITY_VINTF" : "STABILITY_LOCAL") << ";\n";
+  cpp::GenerateToString(out, defined_type);
   out.Dedent();
   out << "private:\n";
   out.Indent();
diff --git a/tests/aidl_test_client_parcelables.cpp b/tests/aidl_test_client_parcelables.cpp
index 696261e..1b3a51e 100644
--- a/tests/aidl_test_client_parcelables.cpp
+++ b/tests/aidl_test_client_parcelables.cpp
@@ -14,11 +14,13 @@
  * limitations under the License.
  */
 
+#include <android/aidl/tests/ParcelableForToString.h>
 #include <android/aidl/tests/extension/MyExt.h>
 #include <android/aidl/tests/extension/MyExt2.h>
 #include <android/aidl/tests/extension/MyExtLike.h>
 #include "aidl_test_client.h"
 
+#include <string>
 #include <vector>
 
 using android::IInterface;
@@ -29,6 +31,8 @@
 using android::aidl::tests::INamedCallback;
 using android::aidl::tests::IntEnum;
 using android::aidl::tests::ITestService;
+using android::aidl::tests::OtherParcelableForToString;
+using android::aidl::tests::ParcelableForToString;
 using android::aidl::tests::SimpleParcelable;
 using android::aidl::tests::StructuredParcelable;
 using android::aidl::tests::Union;
@@ -38,6 +42,7 @@
 using android::aidl::tests::extension::MyExtLike;
 using android::binder::Status;
 using android::os::PersistableBundle;
+using std::string;
 using std::vector;
 
 TEST_F(AidlTest, RepeatSimpleParcelable) {
@@ -378,3 +383,66 @@
     EXPECT_EQ(ext2, *actualExt2);
   }
 }
+
+TEST_F(AidlTest, ParcelableToString) {
+  ParcelableForToString p;
+  p.intValue = 10;
+  p.intArray = {20, 30};
+  p.longValue = 100L;
+  p.longArray = {200L, 300L};
+  p.doubleValue = 3.14;
+  p.doubleArray = {1.1, 1.2};
+  p.floatValue = 3.14f;
+  p.floatArray = {1.1f, 1.2f};
+  p.byteValue = 3;
+  p.byteArray = {5, 6};
+  p.booleanValue = true;
+  p.booleanArray = {true, false};
+  p.stringValue = String16("this is a string");
+  p.stringArray = {String16("hello"), String16("world")};
+  p.stringList = {String16("alice"), String16("bob")};
+  OtherParcelableForToString op;
+  op.field = String16("other");
+  p.parcelableValue = op;
+  p.parcelableArray = {op, op};
+  p.enumValue = IntEnum::FOO;
+  p.enumArray = {IntEnum::FOO, IntEnum::BAR};
+  // p.nullArray = null;
+  // p.nullList = null;
+  GenericStructuredParcelable<int32_t, StructuredParcelable, IntEnum> gen;
+  gen.a = 1;
+  gen.b = 2;
+  p.parcelableGeneric = gen;
+  p.unionValue = Union(std::vector<std::string>{"union", "value"});
+
+  const string expected =
+      "ParcelableForToString{"
+      "intValue: 10, "
+      "intArray: [20, 30], "
+      "longValue: 100, "
+      "longArray: [200, 300], "
+      "doubleValue: 3.140000, "
+      "doubleArray: [1.100000, 1.200000], "
+      "floatValue: 3.140000, "
+      "floatArray: [1.100000, 1.200000], "
+      "byteValue: 3, "
+      "byteArray: [5, 6], "
+      "booleanValue: true, "
+      "booleanArray: [true, false], "
+      "stringValue: this is a string, "
+      "stringArray: [hello, world], "
+      "stringList: [alice, bob], "
+      "parcelableValue: OtherParcelableForToString{field: other}, "
+      "parcelableArray: ["
+      "OtherParcelableForToString{field: other}, "
+      "OtherParcelableForToString{field: other}], "
+      "enumValue: FOO, "
+      "enumArray: [FOO, BAR], "
+      "nullArray: [], "
+      "nullList: [], "
+      "parcelableGeneric: GenericStructuredParcelable{a: 1, b: 2}, "
+      "unionValue: Union{ss: [union, value]}"
+      "}";
+
+  EXPECT_EQ(expected, p.toString());
+}