This is an automated email from the ASF dual-hosted git repository.
mgrigorov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro.git
The following commit(s) were added to refs/heads/main by this push:
new ea2c54b9d AVRO-4033: [C++] Filter out redundant union classes
generated by avrogencpp (#3088)
ea2c54b9d is described below
commit ea2c54b9d1bd8dd72faeacddcff430178aaf6441
Author: hwse <[email protected]>
AuthorDate: Thu Aug 22 09:50:36 2024 +0200
AVRO-4033: [C++] Filter out redundant union classes generated by avrogencpp
(#3088)
* AVRO-4033: [C++] Filter out redundant union classes generated by
avrogencpp. For a unique list of union branches only one class will be
generated. This can reduce the header size in schemas with many unions.
* AVRO-4033: [C++] Align parameter names for
UnionCodeTracker::setTraitsGenerated to be more consistent (#3088)
---------
Co-authored-by: hwse <[email protected]>
---
lang/c++/CMakeLists.txt | 3 +-
lang/c++/impl/avrogencpp.cc | 80 ++++++++++++++++------
lang/c++/jsonschemas/union_redundant_types | 22 ++++++
lang/c++/test/AvrogencppTests.cc | 103 +++++++++++++++++++++++++++++
4 files changed, 187 insertions(+), 21 deletions(-)
diff --git a/lang/c++/CMakeLists.txt b/lang/c++/CMakeLists.txt
index d4c494cef..613d17c2b 100644
--- a/lang/c++/CMakeLists.txt
+++ b/lang/c++/CMakeLists.txt
@@ -182,6 +182,7 @@ gen (primitivetypes pt)
gen (cpp_reserved_words cppres)
gen (cpp_reserved_words_union_typedef cppres_union)
gen (big_union big_union)
+gen (union_redundant_types redundant_types)
add_executable (avrogencpp impl/avrogencpp.cc)
target_link_libraries (avrogencpp avrocpp_s)
@@ -227,7 +228,7 @@ add_dependencies (AvrogencppTests bigrecord_hh
bigrecord_r_hh bigrecord2_hh
union_array_union_hh union_map_union_hh union_conflict_hh
recursive_hh reuse_hh circulardep_hh tree1_hh tree2_hh crossref_hh
primitivetypes_hh empty_record_hh cpp_reserved_words_union_typedef_hh
- union_empty_record_hh big_union_hh)
+ union_empty_record_hh big_union_hh union_redundant_types_hh)
include (InstallRequiredSystemLibraries)
diff --git a/lang/c++/impl/avrogencpp.cc b/lang/c++/impl/avrogencpp.cc
index c02ea554e..143515215 100644
--- a/lang/c++/impl/avrogencpp.cc
+++ b/lang/c++/impl/avrogencpp.cc
@@ -23,6 +23,7 @@
#include <fstream>
#include <iostream>
#include <map>
+#include <optional>
#include <set>
#include <boost/algorithm/string.hpp>
@@ -67,8 +68,22 @@ struct PendingConstructor {
PendingConstructor(string sn, string n, bool im) :
structName(std::move(sn)), memberName(std::move(n)), initMember(im) {}
};
+class UnionCodeTracker {
+ std::string schemaFile_;
+ size_t unionNumber_ = 0;
+ std::map<std::vector<std::string>, std::string> unionBranchNameMapping_;
+ std::set<std::string> generatedUnionTraits_;
+
+public:
+ explicit UnionCodeTracker(const std::string &schemaFile);
+ std::optional<std::string> getExistingUnionName(const
std::vector<std::string> &unionBranches) const;
+ std::string generateNewUnionName(const std::vector<std::string>
&unionBranches);
+ bool unionTraitsAlreadyGenerated(const std::string &unionClassName) const;
+ void setTraitsGenerated(const std::string &unionClassName);
+};
+
class CodeGen {
- size_t unionNumber_;
+ UnionCodeTracker unionTracker_;
std::ostream &os_;
bool inNamespace_;
const std::string ns_;
@@ -90,7 +105,6 @@ class CodeGen {
std::string generateEnumType(const NodePtr &n);
std::string cppTypeOf(const NodePtr &n);
std::string generateRecordType(const NodePtr &n);
- std::string unionName();
std::string generateUnionType(const NodePtr &n);
std::string generateType(const NodePtr &n);
std::string generateDeclaration(const NodePtr &n);
@@ -106,7 +120,7 @@ public:
CodeGen(std::ostream &os, std::string ns,
std::string schemaFile, std::string headerFile,
std::string guardString,
- std::string includePrefix, bool noUnion) : unionNumber_(0),
os_(os), inNamespace_(false), ns_(std::move(ns)),
+ std::string includePrefix, bool noUnion) :
unionTracker_(schemaFile), os_(os), inNamespace_(false), ns_(std::move(ns)),
schemaFile_(std::move(schemaFile)), headerFile_(std::move(headerFile)),
includePrefix_(std::move(includePrefix)), noUnion_(noUnion),
guardString_(std::move(guardString)),
@@ -295,17 +309,6 @@ void makeCanonical(string &s, bool foldCase) {
}
}
-string CodeGen::unionName() {
- string s = schemaFile_;
- string::size_type n = s.find_last_of("/\\");
- if (n != string::npos) {
- s = s.substr(n);
- }
- makeCanonical(s, false);
-
- return s + "_Union__" + boost::lexical_cast<string>(unionNumber_++) + "__";
-}
-
static void generateGetterAndSetter(ostream &os,
const string &structName, const string
&type, const string &name,
size_t idx) {
@@ -386,7 +389,11 @@ string CodeGen::generateUnionType(const NodePtr &n) {
return done[n];
}
- auto result = unionName();
+ // re-use existing union types that have the exact same branches
+ if (const auto existingName = unionTracker_.getExistingUnionName(types);
existingName.has_value()) {
+ return existingName.value();
+ }
+ const std::string result = unionTracker_.generateNewUnionName(types);
os_ << "struct " << result << " {\n"
<< "private:\n"
@@ -643,6 +650,11 @@ void CodeGen::generateRecordTraits(const NodePtr &n) {
}
void CodeGen::generateUnionTraits(const NodePtr &n) {
+ const string name = done[n];
+ const string fn = fullname(name);
+ if (unionTracker_.unionTraitsAlreadyGenerated(fn)) {
+ return;
+ }
size_t c = n->leaves();
for (size_t i = 0; i < c; ++i) {
@@ -650,9 +662,6 @@ void CodeGen::generateUnionTraits(const NodePtr &n) {
generateTraits(nn);
}
- string name = done[n];
- string fn = fullname(name);
-
os_ << "template<> struct codec_traits<" << fn << "> {\n"
<< " static void encode(Encoder& e, " << fn << " v) {\n"
<< " e.encodeUnionIndex(v.idx());\n"
@@ -696,6 +705,8 @@ void CodeGen::generateUnionTraits(const NodePtr &n) {
os_ << " }\n"
<< " }\n"
<< "};\n\n";
+
+ unionTracker_.setTraitsGenerated(fn);
}
void CodeGen::generateTraits(const NodePtr &n) {
@@ -808,8 +819,6 @@ void CodeGen::generate(const ValidSchema &schema) {
os_ << "namespace avro {\n";
- unionNumber_ = 0;
-
generateTraits(root);
os_ << "}\n";
@@ -915,3 +924,34 @@ int main(int argc, char **argv) {
return 1;
}
}
+
+UnionCodeTracker::UnionCodeTracker(const std::string &schemaFile) :
schemaFile_(schemaFile) {
+}
+
+std::optional<std::string> UnionCodeTracker::getExistingUnionName(const
std::vector<std::string> &unionBranches) const {
+ if (const auto it = unionBranchNameMapping_.find(unionBranches); it !=
unionBranchNameMapping_.end()) {
+ return it->second;
+ }
+ return std::nullopt;
+}
+
+std::string UnionCodeTracker::generateNewUnionName(const
std::vector<std::string> &unionBranches) {
+ string s = schemaFile_;
+ string::size_type n = s.find_last_of("/\\");
+ if (n != string::npos) {
+ s = s.substr(n);
+ }
+ makeCanonical(s, false);
+
+ std::string result = s + "_Union__" +
boost::lexical_cast<string>(unionNumber_++) + "__";
+ unionBranchNameMapping_.emplace(unionBranches, result);
+ return result;
+}
+
+bool UnionCodeTracker::unionTraitsAlreadyGenerated(const std::string
&unionClassName) const {
+ return generatedUnionTraits_.find(unionClassName) !=
generatedUnionTraits_.end();
+}
+
+void UnionCodeTracker::setTraitsGenerated(const std::string &unionClassName) {
+ generatedUnionTraits_.insert(unionClassName);
+}
\ No newline at end of file
diff --git a/lang/c++/jsonschemas/union_redundant_types
b/lang/c++/jsonschemas/union_redundant_types
new file mode 100644
index 000000000..b45b11b3a
--- /dev/null
+++ b/lang/c++/jsonschemas/union_redundant_types
@@ -0,0 +1,22 @@
+{
+ "type": "record",
+ "name": "RedundantUnionSchema",
+ "doc": "Schema to test the generation of redundant union types in
avrogencpp",
+ "fields" : [
+ {"name": "null_string_1", "type": ["null", "string"]},
+ {"name": "null_string_2", "type": ["null", "string"]},
+ {"name": "string_null_1", "type": ["string", "null"]},
+ {"name": "string_null_2", "type": ["string", "null"]},
+ {"name": "null_string_int", "type": ["string", "null", "int"]},
+ {"name": "null_Empty_1", "type": ["null", {"type": "record", "name":
"Empty", "fields": []}]},
+ {"name": "null_Empty_2", "type": ["null", "Empty"]},
+ {"name": "null_namespace_record_1", "type": ["null", {"type": "record",
"namespace": "example_namespace", "name": "Record", "fields": []}]},
+ {"name": "null_namespace_record_2", "type": ["null",
"example_namespace.Record"]},
+ {"name": "null_fixed_8", "type": ["null", {"type": "fixed", "size": 8,
"name": "fixed_8"}]},
+ {"name": "null_fixed_16", "type": ["null", {"type": "fixed", "size": 16,
"name": "fixed_16"}]},
+ {"name": "fixed_8_fixed_16", "type": ["fixed_8", "fixed_16"]},
+ {"name": "null_int_map_1", "type": ["null", {"type": "map", "values":
"int"}]},
+ {"name": "null_int_map_2", "type": ["null", {"type": "map", "values":
"int"}]},
+ {"name": "null_long_map", "type": ["null", {"type": "map", "values":
"long"}]}
+ ]
+}
diff --git a/lang/c++/test/AvrogencppTests.cc b/lang/c++/test/AvrogencppTests.cc
index e7d5df927..38a70d149 100644
--- a/lang/c++/test/AvrogencppTests.cc
+++ b/lang/c++/test/AvrogencppTests.cc
@@ -24,7 +24,9 @@
#include "union_array_union.hh"
#include "union_empty_record.hh"
#include "union_map_union.hh"
+#include "union_redundant_types.hh"
+#include <array>
#include <boost/test/included/unit_test.hpp>
#ifdef min
@@ -408,6 +410,105 @@ void testUnionBranchEnum() {
BOOST_CHECK_EQUAL(record.big_union.branch(), Branch::_Int);
}
+// enable use of BOOST_CHECK_EQUAL
+template<>
+struct boost::test_tools::tt_detail::print_log_value<std::type_info> {
+ void operator()(std::ostream &stream, const std::type_info &type_info)
const {
+ stream << "std::type_info{.name=" << type_info.name() << "}";
+ }
+};
+
+void testNoRedundantUnionTypes() {
+ redundant_types::RedundantUnionSchema record;
+ // ensure only one class is generated for same union
+ BOOST_CHECK_EQUAL(typeid(record.null_string_1),
typeid(record.null_string_2));
+ BOOST_CHECK_EQUAL(typeid(record.string_null_1),
typeid(record.string_null_2));
+ BOOST_CHECK_EQUAL(typeid(record.null_Empty_1),
typeid(record.null_Empty_2));
+ BOOST_CHECK_EQUAL(typeid(record.null_namespace_record_1),
typeid(record.null_namespace_record_2));
+ BOOST_CHECK_EQUAL(typeid(record.null_int_map_1),
typeid(record.null_int_map_2));
+
+ // different union types should have different class
+ BOOST_CHECK_NE(typeid(record.null_string_1), typeid(record.string_null_1));
+ BOOST_CHECK_NE(typeid(record.null_string_1),
typeid(record.null_string_int));
+ BOOST_CHECK_NE(typeid(record.null_fixed_8), typeid(record.null_fixed_16));
+ BOOST_CHECK_NE(typeid(record.null_int_map_1),
typeid(record.null_long_map));
+}
+
+void testNoRedundantUnionTypesEncodeDecode() {
+ redundant_types::RedundantUnionSchema input_record;
+ input_record.null_string_1.set_string("null_string_1");
+ input_record.null_string_2.set_string("null_string_2");
+ input_record.string_null_1.set_string("string_null_1");
+ input_record.string_null_2.set_string("string_null_2");
+ input_record.null_string_int.set_string("null_string_int");
+ input_record.null_Empty_1.set_Empty({});
+ input_record.null_Empty_2.set_Empty({});
+ input_record.null_namespace_record_1.set_Record({});
+ input_record.null_namespace_record_2.set_Record({});
+ input_record.null_fixed_8.set_fixed_8({8});
+ input_record.null_fixed_16.set_fixed_16({16});
+ input_record.fixed_8_fixed_16.set_fixed_16({16});
+ input_record.null_int_map_1.set_map({{"null_int_map_1", 1}});
+ input_record.null_int_map_2.set_map({{"null_int_map_2", 1}});
+ input_record.null_long_map.set_map({{"null_long_map", 1}});
+
+ ValidSchema s;
+ ifstream ifs("jsonschemas/union_redundant_types");
+ compileJsonSchema(ifs, s);
+
+ unique_ptr<OutputStream> os = memoryOutputStream();
+ EncoderPtr e = validatingEncoder(s, binaryEncoder());
+ e->init(*os);
+ avro::encode(*e, input_record);
+ e->flush();
+
+ DecoderPtr d = validatingDecoder(s, binaryDecoder());
+ unique_ptr<InputStream> is = memoryInputStream(*os);
+ d->init(*is);
+ redundant_types::RedundantUnionSchema result_record;
+ avro::decode(*d, result_record);
+
+ BOOST_CHECK_EQUAL(result_record.null_string_1.get_string(),
"null_string_1");
+ BOOST_CHECK_EQUAL(result_record.null_string_2.get_string(),
"null_string_2");
+ BOOST_CHECK_EQUAL(result_record.string_null_1.get_string(),
"string_null_1");
+ BOOST_CHECK_EQUAL(result_record.string_null_2.get_string(),
"string_null_2");
+ BOOST_CHECK_EQUAL(result_record.null_string_int.get_string(),
"null_string_int");
+ BOOST_CHECK(!result_record.null_Empty_1.is_null());
+ BOOST_CHECK(!result_record.null_Empty_2.is_null());
+ BOOST_CHECK(!result_record.null_namespace_record_1.is_null());
+ BOOST_CHECK(!result_record.null_namespace_record_2.is_null());
+ {
+ const auto actual = result_record.null_fixed_8.get_fixed_8();
+ const std::array<uint8_t, 8> expected{8};
+ BOOST_CHECK_EQUAL_COLLECTIONS(actual.begin(), actual.end(),
expected.begin(), expected.end());
+ }
+ {
+ const auto actual = result_record.null_fixed_16.get_fixed_16();
+ const std::array<uint8_t, 16> expected{16};
+ BOOST_CHECK_EQUAL_COLLECTIONS(actual.begin(), actual.end(),
expected.begin(), expected.end());
+ }
+ {
+ const auto actual = result_record.fixed_8_fixed_16.get_fixed_16();
+ const std::array<uint8_t, 16> expected{16};
+ BOOST_CHECK_EQUAL_COLLECTIONS(actual.begin(), actual.end(),
expected.begin(), expected.end());
+ }
+ {
+ const auto actual = result_record.null_int_map_1.get_map();
+ BOOST_CHECK_EQUAL(actual.size(), 1);
+ BOOST_CHECK_EQUAL(actual.at("null_int_map_1"), 1);
+ }
+ {
+ const auto actual = result_record.null_int_map_2.get_map();
+ BOOST_CHECK_EQUAL(actual.size(), 1);
+ BOOST_CHECK_EQUAL(actual.at("null_int_map_2"), 1);
+ }
+ {
+ const auto actual = result_record.null_long_map.get_map();
+ BOOST_CHECK_EQUAL(actual.size(), 1);
+ BOOST_CHECK_EQUAL(actual.at("null_long_map"), 1);
+ }
+}
+
boost::unit_test::test_suite *init_unit_test_suite(int /*argc*/, char *
/*argv*/[]) {
auto *ts = BOOST_TEST_SUITE("Code generator tests");
ts->add(BOOST_TEST_CASE(testEncoding));
@@ -418,5 +519,7 @@ boost::unit_test::test_suite *init_unit_test_suite(int
/*argc*/, char * /*argv*/
ts->add(BOOST_TEST_CASE(testEmptyRecord));
ts->add(BOOST_TEST_CASE(testUnionMethods));
ts->add(BOOST_TEST_CASE(testUnionBranchEnum));
+ ts->add(BOOST_TEST_CASE(testNoRedundantUnionTypes));
+ ts->add(BOOST_TEST_CASE(testNoRedundantUnionTypesEncodeDecode));
return ts;
}