Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ public class CppGenerator implements CodeGenerator
private final String precedenceChecksFlagName;
private final boolean shouldSupportTypesPackageNames;
private final Map<String, String> 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}.
Expand Down Expand Up @@ -1419,6 +1421,11 @@ private void generateEnum(final List<Token> 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");
}
Expand Down Expand Up @@ -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 <unordered_map>, but only while it is emitted inline.
final String fromStringInclude = shouldGenerateEnumParse ? "#include <unordered_map>\n" : "";

sb.append("/* Generated SBE (Simple Binary Encoding) message codec */\n");

sb.append(String.format(
Expand All @@ -1876,6 +1886,7 @@ private static CharSequence generateEnumFileHeader(final CharSequence[] namespac
"#include <stdexcept>\n" +
"#include <sstream>\n" +
"#include <string>\n" +
"%3$s" +
"\n" +

"#define SBE_NULLVALUE_INT8 (std::numeric_limits<std::int8_t>::min)()\n" +
Expand All @@ -1887,7 +1898,8 @@ private static CharSequence generateEnumFileHeader(final CharSequence[] namespac
"#define SBE_NULLVALUE_UINT32 (std::numeric_limits<std::uint32_t>::max)()\n" +
"#define SBE_NULLVALUE_UINT64 (std::numeric_limits<std::uint64_t>::max)()\n",
String.join("_", namespaces).toUpperCase(),
className.toUpperCase()));
className.toUpperCase(),
fromStringInclude));

sb.append("\nnamespace ");
sb.append(String.join(" {\nnamespace ", namespaces));
Expand Down Expand Up @@ -4123,6 +4135,80 @@ private CharSequence generateEnumDisplay(final List<Token> tokens, final Token e
return sb;
}

private CharSequence generateEnumParse(final List<Token> 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<typename CharT, typename Traits>\n" +
" friend std::basic_istream<CharT, Traits> & operator >> (\n" +
" std::basic_istream<CharT, Traits> &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<Token> tokens, final String enumName, final String indent)
{
new Formatter(sb).format(
indent + "static const std::unordered_map<std::string, %1$s::Value> stringToEnumMap = []()\n" +
indent + "{\n" +
indent + " std::unordered_map<std::string, %1$s::Value> 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<Token> groups,
final List<Token> varData,
Expand Down
29 changes: 29 additions & 0 deletions sbe-tool/src/test/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 $<$<CXX_COMPILER_ID:AppleClang>:-Werror>)
target_compile_options(CppEnumParseTest PRIVATE $<$<CXX_COMPILER_ID:Clang>:-Werror>)
target_compile_options(CppEnumParseTest PRIVATE $<$<CXX_COMPILER_ID:GNU>:-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")
Expand Down
60 changes: 60 additions & 0 deletions sbe-tool/src/test/cpp/CppEnumParseTest.cpp
Original file line number Diff line number Diff line change
@@ -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 <sstream>
#include <string>

#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);
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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]);

Check failure

Code scanning / CodeQL

Array index out of bounds Error test

This array access might be out of bounds, as the index might be equal to the array length.
}

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<String, String> e : toRestore.entrySet())
{
if (null == e.getValue())
{
System.clearProperty(e.getKey());
}
else
{
System.setProperty(e.getKey(), e.getValue());
}
}
}
}

private static void setProperty(final Map<String, String> 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 <unordered_map>")));
}

@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 <unordered_map> for the inline reverse-lookup map
assertThat(model, containsString("#include <unordered_map>"));
// 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;"));
}
}
Loading