From 67b8ade3bde2776cc2be5a05154d78c8cca4d293 Mon Sep 17 00:00:00 2001 From: Nate Bradac Date: Fri, 24 Jul 2026 17:34:37 -0500 Subject: [PATCH] [Cpp] Emit fromString()/operator>> for enum types --- .../sbe/generation/cpp/CppGenerator.java | 90 +++++++++++++- sbe-tool/src/test/cpp/CMakeLists.txt | 29 +++++ sbe-tool/src/test/cpp/CppEnumParseTest.cpp | 60 ++++++++++ .../cpp/CppEnumParseGeneratorTest.java | 112 ++++++++++++++++++ 4 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 sbe-tool/src/test/cpp/CppEnumParseTest.cpp create mode 100644 sbe-tool/src/test/java/uk/co/real_logic/sbe/generation/cpp/CppEnumParseGeneratorTest.java diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppGenerator.java index 5d3762c274..76b5576107 100755 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppGenerator.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppGenerator.java @@ -63,6 +63,8 @@ public class CppGenerator implements CodeGenerator private final String precedenceChecksFlagName; private final boolean shouldSupportTypesPackageNames; private final Map namespaceByType = new HashMap<>(); + // Opt-in: generate enum string parsing (fromString + operator>>), the inverse of c_str/operator<<. + private final boolean shouldGenerateEnumParse = Boolean.getBoolean("sbe.cpp.generate.enum.parse"); /** * Create a new Cpp language {@link CodeGenerator}. @@ -1419,6 +1421,11 @@ private void generateEnum(final List tokens) throws IOException out.append(generateEnumDisplay(tokens.subList(1, tokens.size() - 1), enumToken)); + if (shouldGenerateEnumParse) + { + out.append(generateEnumParse(tokens.subList(1, tokens.size() - 1), enumToken)); + } + out.append("};\n\n"); out.append(CppUtil.closingBraces(namespaces.length)).append("\n#endif\n"); } @@ -1855,10 +1862,13 @@ private CharSequence generateFileHeader( return sb; } - private static CharSequence generateEnumFileHeader(final CharSequence[] namespaces, final String className) + private CharSequence generateEnumFileHeader(final CharSequence[] namespaces, final String className) { final StringBuilder sb = new StringBuilder(); + // fromString() pulls in , but only while it is emitted inline. + final String fromStringInclude = shouldGenerateEnumParse ? "#include \n" : ""; + sb.append("/* Generated SBE (Simple Binary Encoding) message codec */\n"); sb.append(String.format( @@ -1876,6 +1886,7 @@ private static CharSequence generateEnumFileHeader(final CharSequence[] namespac "#include \n" + "#include \n" + "#include \n" + + "%3$s" + "\n" + "#define SBE_NULLVALUE_INT8 (std::numeric_limits::min)()\n" + @@ -1887,7 +1898,8 @@ private static CharSequence generateEnumFileHeader(final CharSequence[] namespac "#define SBE_NULLVALUE_UINT32 (std::numeric_limits::max)()\n" + "#define SBE_NULLVALUE_UINT64 (std::numeric_limits::max)()\n", String.join("_", namespaces).toUpperCase(), - className.toUpperCase())); + className.toUpperCase(), + fromStringInclude)); sb.append("\nnamespace "); sb.append(String.join(" {\nnamespace ", namespaces)); @@ -4123,6 +4135,80 @@ private CharSequence generateEnumDisplay(final List tokens, final Token e return sb; } + private CharSequence generateEnumParse(final List tokens, final Token encodingToken) + { + final String enumName = formatClassName(encodingToken.applicableTypeName()); + final StringBuilder sb = new StringBuilder(); + + new Formatter(sb).format("\n" + + " static %1$s::Value fromString(const char *str)\n" + + " {\n", + enumName); + + appendEnumFromStringBody(sb, tokens, enumName, INDENT + INDENT); + sb.append(" }\n\n"); + + new Formatter(sb).format( + " template\n" + + " friend std::basic_istream & operator >> (\n" + + " std::basic_istream &is, %1$s::Value &m)\n" + + " {\n" + + " std::string str;\n" + + " is >> str;\n" + + " m = %1$s::fromString(str.c_str());\n" + + " return is;\n" + + " }\n", + enumName); + + return sb; + } + + // Body of the enum fromString() reverse lookup, emitted at the given indent. + private void appendEnumFromStringBody( + final StringBuilder sb, final List tokens, final String enumName, final String indent) + { + new Formatter(sb).format( + indent + "static const std::unordered_map stringToEnumMap = []()\n" + + indent + "{\n" + + indent + " std::unordered_map map;\n", + enumName); + + for (final Token token : tokens) + { + new Formatter(sb).format( + indent + " map[\"%1$s\"] = %1$s;\n", + formatForCppKeyword(token.name())); + } + + sb.append(indent).append(" map[\"NULL_VALUE\"] = NULL_VALUE;\n"); + + if (shouldDecodeUnknownEnumValues) + { + sb.append(indent).append(" map[\"SBE_UNKNOWN\"] = SBE_UNKNOWN;\n"); + } + + sb.append(indent).append(" return map;\n"); + sb.append(indent).append("}();\n\n"); + + sb.append(indent).append("auto it = stringToEnumMap.find(str);\n"); + sb.append(indent).append("if (it != stringToEnumMap.end())\n"); + sb.append(indent).append("{\n"); + sb.append(indent).append(" return it->second;\n"); + sb.append(indent).append("}\n\n"); + + if (shouldDecodeUnknownEnumValues) + { + sb.append(indent).append("return SBE_UNKNOWN;\n"); + } + else + { + new Formatter(sb).format( + indent + "throw std::runtime_error(\"unknown enum value: \" + std::string(str) +\n" + + indent + " \" for enum %1$s [E103]\");\n", + enumName); + } + } + private Object[] generateMessageLengthArgs( final List groups, final List varData, diff --git a/sbe-tool/src/test/cpp/CMakeLists.txt b/sbe-tool/src/test/cpp/CMakeLists.txt index 0137b6de35..828a14c6bb 100644 --- a/sbe-tool/src/test/cpp/CMakeLists.txt +++ b/sbe-tool/src/test/cpp/CMakeLists.txt @@ -103,6 +103,35 @@ sbe_test(FieldAccessOrderCheckTest codecs) target_compile_definitions(FieldAccessOrderCheckTest PRIVATE SBE_ENABLE_PRECEDENCE_CHECKS_IN_TESTS) sbe_test(VersionedMessageTest codecs) +# --------------------------------------------------------------------------------------------------- +# sbe.cpp.generate.enum.parse: compile-and-run test for the generated fromString()/operator>>. +# Regenerates the code-generation schema into its own directory with the option enabled. +# --------------------------------------------------------------------------------------------------- +set(ENUM_PARSE_DIR ${CODEC_TARGET_DIR}/cpp_enum_parse) +add_custom_command( + OUTPUT ${ENUM_PARSE_DIR} + DEPENDS ${CODE_GENERATION_SCHEMA} sbe-jar ${SBE_JAR} + COMMAND + ${Java_JAVA_EXECUTABLE} --add-opens java.base/jdk.internal.misc=ALL-UNNAMED + -Dsbe.output.dir=${ENUM_PARSE_DIR} + -Dsbe.target.language=cpp + -Dsbe.keyword.append.token=X + -Dsbe.cpp.generate.enum.parse=true + -jar ${SBE_JAR} + ${CODE_GENERATION_SCHEMA} +) +add_custom_target(codecs_enum_parse DEPENDS ${ENUM_PARSE_DIR}) + +add_executable(CppEnumParseTest CppEnumParseTest.cpp) +add_dependencies(CppEnumParseTest gmock codecs_enum_parse) +target_compile_features(CppEnumParseTest PRIVATE cxx_std_17) +target_include_directories(CppEnumParseTest PRIVATE ${ENUM_PARSE_DIR}) +target_compile_options(CppEnumParseTest PRIVATE $<$:-Werror>) +target_compile_options(CppEnumParseTest PRIVATE $<$:-Werror>) +target_compile_options(CppEnumParseTest PRIVATE $<$:-Werror>) +target_link_libraries(CppEnumParseTest gmock_main ${CMAKE_THREAD_LIBS_INIT}) +add_test(NAME CppEnumParseTest COMMAND CppEnumParseTest) + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") # Check if the GCC version supports C++17 if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "7.0") diff --git a/sbe-tool/src/test/cpp/CppEnumParseTest.cpp b/sbe-tool/src/test/cpp/CppEnumParseTest.cpp new file mode 100644 index 0000000000..876d2a9da3 --- /dev/null +++ b/sbe-tool/src/test/cpp/CppEnumParseTest.cpp @@ -0,0 +1,60 @@ +/* + * Copyright 2013-2025 Real Logic Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Compile-and-run coverage for sbe.cpp.generate.enum.parse: the generated enum must expose +// fromString()/operator>> as the inverse of c_str()/operator<<. The codecs for this target are +// generated with the option enabled (see CMakeLists.txt). + +#include +#include + +#include "gtest/gtest.h" + +#include "code_generation_test/Model.h" + +using namespace code::generation::test; + +TEST(CppEnumParseTest, FromStringInvertsCStr) +{ + EXPECT_EQ(Model::A, Model::fromString(Model::c_str(Model::A))); + EXPECT_EQ(Model::B, Model::fromString(Model::c_str(Model::B))); + EXPECT_EQ(Model::C, Model::fromString(Model::c_str(Model::C))); + EXPECT_EQ(Model::NULL_VALUE, Model::fromString(Model::c_str(Model::NULL_VALUE))); +} + +TEST(CppEnumParseTest, FromStringMatchesLiteralNames) +{ + EXPECT_EQ(Model::A, Model::fromString("A")); + EXPECT_EQ(Model::NULL_VALUE, Model::fromString("NULL_VALUE")); +} + +TEST(CppEnumParseTest, StreamOperatorsRoundTrip) +{ + std::ostringstream out; + out << Model::B; + EXPECT_EQ("B", out.str()); + + std::istringstream in("C"); + Model::Value value = Model::NULL_VALUE; + in >> value; + EXPECT_EQ(Model::C, value); +} + +// Model has no decode-unknown policy, so an unrecognised name must throw (mirrors c_str/get). +TEST(CppEnumParseTest, FromStringThrowsOnUnknown) +{ + EXPECT_THROW(Model::fromString("NOT_A_VALUE"), std::runtime_error); +} diff --git a/sbe-tool/src/test/java/uk/co/real_logic/sbe/generation/cpp/CppEnumParseGeneratorTest.java b/sbe-tool/src/test/java/uk/co/real_logic/sbe/generation/cpp/CppEnumParseGeneratorTest.java new file mode 100644 index 0000000000..b97dfbf50f --- /dev/null +++ b/sbe-tool/src/test/java/uk/co/real_logic/sbe/generation/cpp/CppEnumParseGeneratorTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2013-2025 Real Logic Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package uk.co.real_logic.sbe.generation.cpp; + +import org.agrona.generation.StringWriterOutputManager; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.api.parallel.Resources; +import uk.co.real_logic.sbe.Tests; +import uk.co.real_logic.sbe.ir.Ir; +import uk.co.real_logic.sbe.xml.IrGenerator; +import uk.co.real_logic.sbe.xml.MessageSchema; +import uk.co.real_logic.sbe.xml.ParserOptions; + +import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static uk.co.real_logic.sbe.xml.XmlSchemaParser.parse; + +/** + * Coverage for the {@code sbe.cpp.generate.enum.parse} option, which emits + * {@code fromString()}/{@code operator>>} for enums (the inverse of {@code c_str()}/{@code operator<<}). + */ +@ResourceLock(Resources.SYSTEM_PROPERTIES) +class CppEnumParseGeneratorTest +{ + // Generate the schema under the given generator system-properties (restored afterwards) and + // return the source of the "Model" enum. + private String generateModelHeader(final String... properties) throws Exception + { + final Map toRestore = new LinkedHashMap<>(); + // The GlobalKeywords message uses reserved words, so a keyword token is required. + setProperty(toRestore, "sbe.keyword.append.token", "X"); + for (int i = 0; i < properties.length; i += 2) + { + setProperty(toRestore, properties[i], properties[i + 1]); + } + + try (InputStream in = Tests.getLocalResource("code-generation-schema.xml")) + { + final ParserOptions options = ParserOptions.builder().stopOnError(true).build(); + final MessageSchema schema = parse(in, options); + final Ir ir = new IrGenerator().generate(schema); + final StringWriterOutputManager outputManager = new StringWriterOutputManager(); + outputManager.setPackageName(ir.applicableNamespace()); + + new CppGenerator(ir, false, outputManager).generate(); + + return outputManager.getSource("code.generation.test.Model").toString(); + } + finally + { + for (final Map.Entry e : toRestore.entrySet()) + { + if (null == e.getValue()) + { + System.clearProperty(e.getKey()); + } + else + { + System.setProperty(e.getKey(), e.getValue()); + } + } + } + } + + private static void setProperty(final Map toRestore, final String key, final String value) + { + toRestore.putIfAbsent(key, System.getProperty(key)); + System.setProperty(key, value); + } + + @Test + void doesNotEmitEnumParsingByDefault() throws Exception + { + final String model = generateModelHeader(); + assertThat(model, not(containsString("fromString"))); + assertThat(model, not(containsString("operator >> ("))); + assertThat(model, not(containsString("#include "))); + } + + @Test + void emitsFromStringAndInputOperatorWhenEnabled() throws Exception + { + final String model = generateModelHeader("sbe.cpp.generate.enum.parse", "true"); + + assertThat(model, containsString("static Model::Value fromString(const char *str)")); + assertThat(model, containsString("operator >> (")); + // fromString() pulls in for the inline reverse-lookup map + assertThat(model, containsString("#include ")); + // the reverse map covers the same names c_str()/operator<< emit + assertThat(model, containsString("map[\"A\"] = A;")); + assertThat(model, containsString("map[\"NULL_VALUE\"] = NULL_VALUE;")); + } +}