From 3a68397cb1314a9fa1a69eee8075ad036acee946 Mon Sep 17 00:00:00 2001 From: fenghao Date: Sat, 22 Aug 2026 02:30:34 -0700 Subject: [PATCH] [Rust] Use the schema's null value when generating enums The Rust generator emitted the primitive type's default null value for an enum instead of the one declared in the schema, so a uint8 enum encoding that declares nullValue="254" generated NullVal = 0xff_u8. Java, C++ and Go generate 254 for the same schema, so a Rust encoder wrote a null value a peer in another language does not recognise. IrGenerator records the null value on the enum's BEGIN_ENUM token, while the VALID_VALUE tokens carry only byteOrder, primitiveType and constValue. generateEnum read it from messageBody.get(0), a valid value token, so applicableNullValue() fell back to primitiveType.nullValue(). JavaGenerator reads it from tokens.get(0), which is why Java is unaffected. The same mistake appeared in the enum discriminant and in From for . Resolve the null value once and pass it down so the two cannot disagree. optional_enum_nullify.xml already covers an enum with a declared null value, but declares 255, which is also the uint8 default, so it passes either way. The new issue1116 schema declares 254 to separate the two. Resolves #1116 --- build.gradle | 1 + rust/Cargo.toml | 1 + rust/tests/issue_1116_test.rs | 44 +++++++++++++++++++ .../sbe/generation/rust/RustGenerator.java | 16 +++---- sbe-tool/src/test/resources/issue1116.xml | 25 +++++++++++ 5 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 rust/tests/issue_1116_test.rs create mode 100644 sbe-tool/src/test/resources/issue1116.xml diff --git a/build.gradle b/build.gradle index e63452a9ef..28e6d6f3dc 100644 --- a/build.gradle +++ b/build.gradle @@ -682,6 +682,7 @@ tasks.register('generateRustTestCodecs', JavaExec) { 'sbe-tool/src/test/resources/issue1028.xml', 'sbe-tool/src/test/resources/issue1057.xml', 'sbe-tool/src/test/resources/issue1066.xml', + 'sbe-tool/src/test/resources/issue1116.xml', 'sbe-tool/src/test/resources/optional_enum_nullify.xml', 'sbe-tool/src/test/resources/basic-variable-length-schema.xml', 'sbe-tool/src/test/resources/example-bigendian-test-schema.xml', diff --git a/rust/Cargo.toml b/rust/Cargo.toml index df19774eb3..1853a174fc 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -19,6 +19,7 @@ issue_987 = { path = "../generated/rust/issue987" } issue_1028 = { path = "../generated/rust/issue1028" } issue_1057 = { path = "../generated/rust/issue1057" } issue_1066 = { path = "../generated/rust/issue1066" } +issue_1116 = { path = "../generated/rust/issue1116" } baseline_bigendian = { path = "../generated/rust/baseline_bigendian" } nested_composite_name = { path = "../generated/rust/nested_composite_name" } sbe_tests = { path = "../generated/rust/sbe_tests" } diff --git a/rust/tests/issue_1116_test.rs b/rust/tests/issue_1116_test.rs new file mode 100644 index 0000000000..41e08bba2f --- /dev/null +++ b/rust/tests/issue_1116_test.rs @@ -0,0 +1,44 @@ +use issue_1116::{ + foo_bar::FooBar, + issue_1116_codec::{self, Issue1116Decoder, Issue1116Encoder}, + message_header_codec, ReadBuf, WriteBuf, +}; + +/// The schema declares `nullValue="254"` on the type `FooBar` encodes to, so the generated null +/// value has to be 254 rather than the uint8 default of 255. The Java, C++ and Go generators all +/// emit 254 for this schema. +#[test] +fn enum_null_value_comes_from_the_schema() { + // The discriminant, which is what the encoder writes. + assert_eq!(FooBar::NullVal as u8, 254); + + // The `From` conversion, which is generated separately from the discriminant. + assert_eq!(u8::from(FooBar::NullVal), 254); + + assert_eq!(FooBar::from(254), FooBar::NullVal); +} + +/// A peer decoding this message in another language reads the schema's null value off the wire, so +/// that is the byte the Rust encoder has to write. +#[test] +fn encodes_and_decodes_the_schema_null_value() { + let mut buffer = vec![0u8; 256]; + + { + let mut encoder = Issue1116Encoder::default().wrap( + WriteBuf::new(&mut buffer), + message_header_codec::ENCODED_LENGTH, + ); + encoder.foo_bar(FooBar::NullVal); + } + + assert_eq!(buffer[message_header_codec::ENCODED_LENGTH], 254); + + let decoder = Issue1116Decoder::default().wrap( + ReadBuf::new(&buffer), + message_header_codec::ENCODED_LENGTH, + issue_1116_codec::SBE_BLOCK_LENGTH, + 0, + ); + assert_eq!(decoder.foo_bar(), FooBar::NullVal); +} diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustGenerator.java index 628055a3e3..2d6b310e62 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustGenerator.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustGenerator.java @@ -1615,10 +1615,13 @@ private static void generateEnum( indent(writer, 1, "%s = %s, \n", token.name(), literal); } + // The null value is declared on the enum itself, or on the type it encodes to, so it + // has to be read from the enum token. A valid value token never carries one, which makes + // applicableNullValue() fall back to the primitive type default instead of the schema one. + final CharSequence nullVal = rustNullLiteral(enumTokens.get(0).encoding()); + // null value { - final Encoding encoding = messageBody.get(0).encoding(); - final CharSequence nullVal = rustNullLiteral(encoding); indent(writer, 1, "#[default]\n"); indent(writer, 1, "NullVal = %s, \n", nullVal); } @@ -1628,7 +1631,7 @@ private static void generateEnum( generateFromPrimitiveForEnum(enumRustName, primitiveType, messageBody, writer); // Into impl - generateFromEnumForPrimitive(enumRustName, primitiveType, messageBody, writer); + generateFromEnumForPrimitive(enumRustName, primitiveType, messageBody, nullVal, writer); // FromStr impl generateFromStrImplForEnum(enumRustName, messageBody, writer); @@ -1666,6 +1669,7 @@ private static void generateFromEnumForPrimitive( final String enumRustName, final String primitiveType, final List messageBody, + final CharSequence nullVal, final Appendable writer) throws IOException { indent(writer, 0, "impl From<%s> for %s {\n", enumRustName, primitiveType); @@ -1680,11 +1684,7 @@ private static void generateFromEnumForPrimitive( indent(writer, 3, "%s::%s => %s, \n", enumRustName, token.name(), literal); } - { - final Encoding encoding = messageBody.get(0).encoding(); - final CharSequence nullVal = rustNullLiteral(encoding); - indent(writer, 3, "%s::NullVal => %s,\n", enumRustName, nullVal); - } + indent(writer, 3, "%s::NullVal => %s,\n", enumRustName, nullVal); indent(writer, 2, "}\n"); indent(writer, 1, "}\n"); indent(writer, 0, "}\n"); diff --git a/sbe-tool/src/test/resources/issue1116.xml b/sbe-tool/src/test/resources/issue1116.xml new file mode 100644 index 0000000000..229a76fada --- /dev/null +++ b/sbe-tool/src/test/resources/issue1116.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + 0 + 1 + + + + + +