Summary
Java and C++ have different string compare logic resulting in incompatible binaries when sending between services that are built in different languages.
Investigation
According to the internals documentation, the canonical comparison approach should be using strcmp as the comparison algorithm for strings.
https://flatbuffers.dev/internals/
The C++ implementation follows this logic according to its code generator
|
code_ += " int KeyCompareWithValue(const char *_{{FIELD_NAME}}) const {"; |
|
code_ += " return strcmp({{FIELD_NAME}}()->c_str(), _{{FIELD_NAME}});"; |
|
code_ += " }"; |
The Java implementation does not follow this and implements their own algorithm
|
/** |
|
* Compare string from the buffer with the 'String' object. |
|
* |
|
* @param offset_1 An 'int' index of the first string into the bb. |
|
* @param key Second string as a byte array. |
|
* @param bb A {@code ByteBuffer} to get the first string. |
|
*/ |
|
protected static int compareStrings(int offset_1, byte[] key, ByteBuffer bb) { |
|
offset_1 += bb.getInt(offset_1); |
|
int len_1 = bb.getInt(offset_1); |
|
int len_2 = key.length; |
|
int startPos_1 = offset_1 + Constants.SIZEOF_INT; |
|
int len = Math.min(len_1, len_2); |
|
for (int i = 0; i < len; i++) { |
|
if (bb.get(i + startPos_1) != key[i]) return bb.get(i + startPos_1) - key[i]; |
|
} |
|
return len_1 - len_2; |
|
} |
As a result the binaries generated from one language will be incompatible with another.
Reproduction
Summary
Because C++ compares strings using unsigned bytes (where 97 < 195), it sorts the map keys as ["a", "é"]. However, Java's Table.compareStrings() implementation uses signed bytes (where -61 < 97), meaning it expects the binary's keys to be sorted as ["é", "a"].
Schema:
table Entry {
key: string (key);
value: int;
}
table Map {
entries: [Entry];
}
root_type Map;
Data Encoded:
We are encoding a vector of Entry tables containing two elements:
{"key": "a", "value": 1}
{"key": "é", "value": 2}
C++ Binary
000000 0c 00 00 00 00 00 06 00 08 00 04 00 06 00 00 00 >................<
000010 04 00 00 00 02 00 00 00 08 00 00 00 20 00 00 00 >............ ...<
000020 ec ff ff ff 08 00 00 00 01 00 00 00 01 00 00 00 >................<
000030 61 00 00 00 08 00 0c 00 04 00 08 00 08 00 00 00 >a...............<
000040 08 00 00 00 02 00 00 00 02 00 00 00 c3 a9 00 00 >................<
000050
Java Binary
000000 0c 00 00 00 00 00 06 00 08 00 04 00 06 00 00 00 >................<
000010 04 00 00 00 02 00 00 00 1c 00 00 00 04 00 00 00 >................<
000020 f4 ff ff ff 1c 00 00 00 01 00 00 00 08 00 0c 00 >................<
000030 04 00 08 00 08 00 00 00 10 00 00 00 02 00 00 00 >................<
000040 01 00 00 00 61 00 00 00 02 00 00 00 c3 a9 00 00 >....a...........<
000050
Summary
Java and C++ have different string compare logic resulting in incompatible binaries when sending between services that are built in different languages.
Investigation
According to the internals documentation, the canonical comparison approach should be using
strcmpas the comparison algorithm for strings.https://flatbuffers.dev/internals/
The C++ implementation follows this logic according to its code generator
flatbuffers/src/idl_gen_cpp.cpp
Lines 2608 to 2610 in 81edeb1
The Java implementation does not follow this and implements their own algorithm
flatbuffers/java/src/main/java/com/google/flatbuffers/Table.java
Lines 279 to 296 in 81edeb1
As a result the binaries generated from one language will be incompatible with another.
Reproduction
Summary
Because C++ compares strings using unsigned bytes (where 97 < 195), it sorts the map keys as ["a", "é"]. However, Java's Table.compareStrings() implementation uses signed bytes (where -61 < 97), meaning it expects the binary's keys to be sorted as ["é", "a"].
Schema:
Data Encoded:
We are encoding a vector of Entry tables containing two elements:
C++ Binary
Java Binary