This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-fury-site.git
The following commit(s) were added to refs/heads/main by this push:
new 3215ec1 🔄 created local 'docs/specification/' from remote
'docs/specification/'
3215ec1 is described below
commit 3215ec1cd4bdb158dabb2b3e60ccd1da2fe98420
Author: chaokunyang <[email protected]>
AuthorDate: Tue Apr 2 17:22:23 2024 +0000
🔄 created local 'docs/specification/' from remote 'docs/specification/'
---
docs/specification/java_serialization_spec.md | 548 ++++++++++++++++++
docs/specification/row_format_spec.md | 8 +
docs/specification/xlang_serialization_spec.md | 757 +++++++++++++++++++++++++
3 files changed, 1313 insertions(+)
diff --git a/docs/specification/java_serialization_spec.md
b/docs/specification/java_serialization_spec.md
new file mode 100644
index 0000000..a5d0bec
--- /dev/null
+++ b/docs/specification/java_serialization_spec.md
@@ -0,0 +1,548 @@
+---
+title: Fury Java Serialization Format
+sidebar_position: 1
+id: fury_java_serialization_spec
+---
+# Fury Java Serialization Specification
+
+## Spec overview
+
+Fury Java Serialization is an automatic object serialization framework that
supports reference and polymorphism. Fury
+will
+convert an object from/to fury java serialization binary format. Fury has two
core concepts for java serialization:
+
+- **Fury Java Binary format**
+- **Framework to convert object to/from Fury Java Binary format**
+
+The serialization format is a dynamic binary format. The dynamics and
reference/polymorphism support make Fury flexible,
+much more easy to use, but
+also introduce more complexities compared to static serialization frameworks.
So the format will be more complex.
+
+Here is the overall format:
+
+```
+| fury header | object ref meta | object class meta | object value data |
+```
+
+The data are serialized using little endian byte order overall. If bytes swap
is costly for some object,
+Fury will write the byte order for that object into the data instead of
converting it to little endian.
+
+## Fury header
+
+Fury header consists starts one byte:
+
+```
+| 4 bits | 1 bit | 1 bit | 1 bit | 1 bit | optional 4 bytes
|
++---------------+-------+-------+--------+-------+------------------------------------+
+| reserved bits | oob | xlang | endian | null | unsigned int for meta start
offset |
+```
+
+- null flag: 1 when object is null, 0 otherwise. If an object is null, other
bits won't be set.
+- endian flag: 1 when data is encoded by little endian, 0 for big endian.
+- xlang flag: 1 when serialization uses xlang format, 0 when serialization
uses Fury java format.
+- oob flag: 1 when passed `BufferCallback` is not null, 0 otherwise.
+
+If meta share mode is enabled, an uncompressed unsigned int is appended to
indicate the start offset of metadata.
+
+## Reference Meta
+
+Reference tracking handles whether the object is null, and whether to track
reference for the object by writing
+corresponding flags and maintaining internal state.
+
+Reference flags:
+
+| Flag | Byte Value | Description
|
+|---------------------|------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
+| NULL FLAG | `-3` | This flag indicates the object is a null
value. We don't use another byte to indicate REF, so that we can save one byte.
|
+| REF FLAG | `-2` | This flag indicates the object is already
serialized previously, and fury will write a ref id with unsigned varint format
instead of serialize it again |
+| NOT_NULL VALUE FLAG | `-1` | This flag indicates the object is a
non-null value and fury doesn't track ref for this type of object.
|
+| REF VALUE FLAG | `0` | This flag indicates the object is
referencable and the first time to serialize.
|
+
+When reference tracking is disabled globally or for specific types, or for
certain types within a particular
+context(e.g., a field of a class), only the `NULL` and `NOT_NULL VALUE` flags
will be used for reference meta.
+
+## Class Meta
+
+Fury supports to register class by an optional id, the registration can be
used for security check and class
+identification.
+If a class is registered, it will have a user-provided or an auto-growing
unsigned int i.e. `class_id`.
+
+Depending on whether meta share mode and registration is enabled for current
class, Fury will write class meta
+differently.
+
+### Schema consistent
+
+If schema consistent mode is enabled globally or enabled for current class,
class meta will be written as follows:
+
+- If class is registered, it will be written as a fury unsigned varint:
`class_id << 1`.
+- If class is not registered:
+ - If class is not an array, fury will write one byte `0bxxxxxxx1` first,
then write class name.
+ - The first little bit is `1`, which is different from first bit `0` of
+ encoded class id. Fury can use this information to determine whether
to read class by class id for
+ deserialization.
+ - If class is not registered and class is an array, fury will write one
byte `dimensions << 1 | 1` first, then write
+ component
+ class subsequently. This can reduce array class name cost if component
class is or will be serialized.
+ - Class will be written as two enumerated fury unsigned by default:
`package name` and `class name`. If meta share
+ mode is
+ enabled,
+ class will be written as an unsigned varint which points to index in
`MetaContext`.
+
+### Schema evolution
+
+If schema evolution mode is enabled globally or enabled for current class,
class meta will be written as follows:
+
+- If meta share mode is not enabled, class meta will be written as schema
consistent mode. Additionally, field meta such
+ as field type
+ and name will be written with the field value using a key-value like layout.
+- If meta share mode is enabled, class meta will be written as a meta-share
encoded binary if class hasn't been written
+ before, otherwise an unsigned varint id which references to previous written
class meta will be written.
+
+## Meta share
+
+> This mode will forbid streaming writing since it needs to look back for
update the start offset after the whole object
+> graph
+> writing and meta collecting is finished. Only in this way we can ensure
deserialization failure doesn't lost shared
+> meta.
+> Meta streamline will be supported in the future for enclosed meta sharing
which doesn't cross multiple serializations
+> of different objects.
+
+For Schema consistent mode, class will be encoded as an enumerated string by
full class name. Here we mainly describe
+the meta layout for schema evolution mode:
+
+```
+| 8 bytes meta header | variable bytes | variable bytes |
variable bytes |
++-------------------------------+--------------------+-------------------+----------------+
+| 7 bytes hash + 1 bytes header | current class meta | parent class meta |
... |
+```
+
+Class meta are encoded from parent class to leaf class, only class with
serializable fields will be encoded.
+
+### Meta header
+
+Meta header is a 64 bits number value encoded in little endian order.
+
+- Lowest 4 digits `0b0000~0b1110` are used to record num classes. `0b1111` is
preserved to indicate that Fury need to
+ read more bytes for length using Fury unsigned int encoding. If current
class doesn't has parent class, or parent
+ class doesn't have fields to serialize, or we're in a context which
serialize fields of current class
+ only( `ObjectStreamSerializer#SlotInfo` is an example), num classes will be
1.
+- 5rd bit is used to indicate whether this class needs schema evolution.
+- Other 56 bits is used to store the unique hash of `flags + all layers class
meta`.
+
+### Single layer class meta
+
+```
+| unsigned varint | meta string | meta string |
field info: variable bytes | variable bytes | ... |
++----------------------------+-----------------------+---------------------+-------------------------------+-----------------+-----+
+| num fields + register flag | header + package name | header + class name |
header + type id + field name | next field info | ... |
+```
+
+- num fields: encode `num fields << 1 | register flag(1 when class
registered)` as unsigned varint.
+ - If class is registered, then an unsigned varint class id will be written
next, package and class name will be
+ omitted.
+ - If current class is schema consistent, then num field will be `0` to
flag it.
+ - If current class isn't schema consistent, then num field will be the
number of compatible fields. For example,
+ users
+ can use tag id to mark some field as compatible field in schema
consistent context. In such cases, schema
+ consistent
+ fields will be serialized first, then compatible fields will be
serialized next. At deserialization, Fury will use
+ fields info of those fields which aren't annotated by tag id for
deserializing schema consistent fields, then use
+ fields info in meta for deserializing compatible fields.
+- Package name encoding(omitted when class is registered):
+ - Header:
+ - If meta string encoding is `LOWER_SPECIAL` and the length of encoded
string `<=` 128, then header will be
+ `7 bits size + flag(set)`.
+ Otherwise, header will be `4 bits unset + 3 bits encoding flags +
flag(unset)`
+ - Package name:
+ - If bit flag is set, then package name will be encoded meta string
binary.
+ - Otherwise, it will be `| unsigned varint length | encoded meta
string binary |`
+- Class name encoding(omitted when class is registered)::
+ - header:
+ - If meta string encoding is in
`LOWER_SPECIAL~LOWER_UPPER_DIGIT_SPECIAL (0~3)`, and the length of encoded
+ string `<=` 32, then the header will be `5 bits size + 2 bits
encoding flags + flag(set)`.
+ - Otherwise, header will be `| unsigned varint length | encoded meta
string binary |`
+- Field info:
+ - header(8
+ bits): `reserved 1 bit + 3 bits field name encoding + polymorphism flag
+ nullability flag + ref tracking flag + tag id flag`.
+ Users can use annotation to provide those info.
+ - tag id: when set to 1, field name will be written by an unsigned
varint tag id.
+ - ref tracking: when set to 0, ref tracking will be disabled for this
field.
+ - nullability: when set to 0, this field won't be null.
+ - polymorphism: when set to 1, the actual type of field will be the
declared field type even the type if
+ not `final`.
+ - 3 bits field name encoding will be set to meta string encoding flags
when tag id is not set.
+ - type id:
+ - For registered type-consistent classes, it will be the registered
class id.
+ - Otherwise it will be encoded as `OBJECT_ID` if it isn't `final` and
`FINAL_OBJECT_ID` if it's `final`. The
+ meta
+ for such types is written separately instead of inlining here is to
reduce meta space cost if object of this
+ type is serialized in current object graph multiple times, and the
field value may be null too.
+ - Collection Type Info: collection type will have an extra byte for
elements info.
+ Users can use annotation to provide those info.
+ - elements type same
+ - elements tracking ref
+ - elements nullability
+ - elements declared type
+ - Map Type Info: map type will have an extra byte for kv items info.
+ Users can use annotation to provide those info.
+ - keys type same
+ - keys tracking ref
+ - keys nullability
+ - keys declared type
+ - values type same
+ - values tracking ref
+ - values nullability
+ - values declared type
+ - Field name: If type id is set, type id will be used instead. Otherwise
meta string encoding length and data will
+ be
+ written instead.
+
+Field order are left as implementation details, which is not exposed to
specification, the deserialization need to
+resort fields based on Fury field comparator. In this way, fury can compute
statistics for field names or types and
+using a more compact encoding.
+
+### Other layers class meta
+
+Same encoding algorithm as the previous layer except:
+
+- header + package name:
+ - Header:
+ - If package name has been written before: `varint index + sharing
flag(set)` will be written
+ - If package name hasn't been written before:
+ - If meta string encoding is `LOWER_SPECIAL` and the length of
encoded string `<=` 64, then header will be
+ `6 bits size + encoding flag(set) + sharing flag(unset)`.
+ - Otherwise, header will
+ be `3 bits unset + 3 bits encoding flags + encoding flag(unset)
+ sharing flag(unset)`
+
+## Meta String
+
+Meta string is mainly used to encode meta strings such as class name and field
names.
+
+### Encoding Algorithms
+
+String binary encoding algorithm:
+
+| Algorithm | Pattern | Description
|
+|---------------------------|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
+| LOWER_SPECIAL | `a-z._$\|` | every char is written using 5
bits, `a-z`: `0b00000~0b11001`, `._$\|`: `0b11010~0b11101`
|
+| LOWER_UPPER_DIGIT_SPECIAL | `a-zA-Z0~9._$` | every char is written using 6
bits, `a-z`: `0b00000~0b11110`, `A-Z`: `0b11010~0b110011`, `0~9`:
`0b110100~0b111101`, `._$`: `0b111110~0b1000000` |
+| UTF-8 | any chars | UTF-8 encoding
|
+
+Encoding flags:
+
+| Encoding Flag | Pattern
| Encoding Algorithm
|
+|---------------------------|-----------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
+| LOWER_SPECIAL | every char is in `a-z._$\|`
| `LOWER_SPECIAL`
|
+| REP_FIRST_LOWER_SPECIAL | every char is in `a-z._$` except first char is
upper case | replace first upper case char to lower case, then use
`LOWER_SPECIAL` |
+| REP_MUL_LOWER_SPECIAL | every char is in `a-zA-Z._$`
| replace every upper case char by `\|` + `lower case`, then use
`LOWER_SPECIAL`, use this encoding if it's smaller than Encoding `3` |
+| LOWER_UPPER_DIGIT_SPECIAL | every char is in `a-zA-Z._$`
| use `LOWER_UPPER_DIGIT_SPECIAL` encoding if it's smaller than
Encoding `2` |
+| UTF8 | any utf-8 char
| use `UTF-8` encoding
|
+| Compression | any utf-8 char
| lossless compression
|
+
+Depending on cases, one can choose encoding `flags + data` jointly, uses 3
bits of first byte for flags and other bytes
+for data.
+
+### Shared meta string
+
+The shared meta string format consists of header and encoded string binary.
Header of encoded string binary will be
+inlined
+in shared meta header.
+
+Header is written using little endian order, Fury can read this flag first to
determine how to deserialize the data.
+
+#### Write by data
+
+If string hasn't been written before, the data will be written as follows:
+
+```
+| unsigned varint: string binary size + 1 bit: not written before | 56 bits:
unique hash | 3 bits encoding flags + string binary |
+```
+
+If string binary size is less than `16` bytes, the hash will be omitted to
save spaces. Unique hash can be omitted too
+if caller pass a flag to disable it. In such cases, the format will be:
+
+```
+| unsigned varint: string binary size + 1 bit: not written before | 3 bits
encoding flags + string binary |
+```
+
+#### Write by ref
+
+If string has been written before, the data will be written as follows:
+
+```
+| unsigned varint: written string id + 1 bit: written before |
+```
+
+## Value Format
+
+### Basic types
+
+#### Bool
+
+- size: 1 byte
+- format: 0 for `false`, 1 for `true`
+
+#### Byte
+
+- size: 1 byte
+- format: write as pure byte.
+
+#### Short
+
+- size: 2 byte
+- byte order: little endian order
+
+#### Char
+
+- size: 2 byte
+- byte order: little endian order
+
+#### Unsigned int
+
+- size: 1~5 byte
+- Format: The most significant bit (MSB) in every byte indicates whether to
have the next byte. If first bit is set
+ i.e. `b & 0x80 == 0x80`, then
+ the next byte should be read until the first bit of the next byte is unset.
+
+#### Signed int
+
+- size: 1~5 byte
+- Format: First convert the number into positive unsigned int by `(v << 1) ^
(v >> 31)` ZigZag algorithm, then encoding
+ it as an unsigned int.
+
+#### Unsigned long
+
+- size: 1~9 byte
+- Fury PVL(Progressive Variable-length Long) Encoding:
+ - positive long format: first bit in every byte indicates whether to have
the next byte. If first bit is set
+ i.e. `b & 0x80 == 0x80`, then the next byte should be read until the
first bit is unset.
+
+#### Signed long
+
+- size: 1~9 byte
+- Fury SLI(Small long as int) Encoding:
+ - If long is in [-1073741824, 1073741823], encode as 4 bytes int: `|
little-endian: ((int) value) << 1 |`
+ - Otherwise write as 9 bytes: `| 0b1 | little-endian 8 bytes long |`
+- Fury PVL(Progressive Variable-length Long) Encoding:
+ - First convert the number into positive unsigned long by ` (v << 1) ^ (v
>> 63)` ZigZag algorithm to reduce cost of
+ small negative numbers, then encoding it as an unsigned long.
+
+#### Float
+
+- size: 4 byte
+- format: convert float to 4 bytes int by `Float.floatToRawIntBits`, then
write as binary by little endian order.
+
+#### Double
+
+- size: 8 byte
+- format: convert double to 8 bytes int by `Double.doubleToRawLongBits`, then
write as binary by little endian order.
+
+### String
+
+Format:
+
+```
+| header: size << 2 | 2 bits encoding flags | binary data |
+```
+
+- `size + encoding` will be concat as a long and encoded as an unsigned var
long. The little 2 bits is used for
+ encoding:
+ 0 for `latin`, 1 for `utf-16`, 2 for `utf-8`.
+- encoded string binary data based on encoding: `latin/utf-16/utf-8`.
+
+Which encoding to choose:
+
+- For JDK8: fury detect `latin` at runtime, if string is `latin` string, then
use `latin` encoding, otherwise
+ use `utf-16`.
+- For JDK9+: fury use `coder` in `String` object for encoding,
`latin`/`utf-16` will be used for encoding.
+- If the string is encoded by `utf-8`, then fury will use `utf-8` to decode
the data. But currently fury doesn't enable
+ utf-8 encoding by default for java. Cross-language string serialization of
fury uses `utf-8` by default.
+
+### Collection
+
+> All collection serializers must extend `AbstractCollectionSerializer`.
+
+Format:
+
+```
+length(unsigned varint) | collection header | elements header | elements data
+```
+
+#### Collection header
+
+- For `ArrayList/LinkedArrayList/HashSet/LinkedHashSet`, this will be empty.
+- For `TreeSet`, this will be `Comparator`
+- For subclass of `ArrayList`, this may be extra object field info.
+
+#### Elements header
+
+In most cases, all collection elements are same type and not null, elements
header will encode those homogeneous
+information to avoid the cost of writing it for every element. Specifically,
there are four kinds of information
+which will be encoded by elements header, each use one bit:
+
+- If track elements ref, use the first bit `0b1` of the header to flag it.
+- If the collection has null, use the second bit `0b10` of the header to flag
it. If ref tracking is enabled for this
+ element type, this flag is invalid.
+- If the collection element types are not declared type, use the 3rd bit
`0b100` of the header to flag it.
+- If the collection element types are different, use the 4rd bit `0b1000`
header to flag it.
+
+By default, all bits are unset, which means all elements won't track ref, all
elements are same type, not null and
+the actual element is the declared type in the custom class field.
+
+#### Elements data
+
+Based on the elements header, the serialization of elements data may skip `ref
flag`/`null flag`/`element class info`.
+
+`CollectionSerializer#write/read` can be taken as an example.
+
+### Array
+
+#### Primitive array
+
+Primitive array are taken as a binary buffer, serialization will just write
the length of array size as an unsigned int,
+then copy the whole buffer into the stream.
+
+Such serialization won't compress the array. If users want to compress
primitive array, users need to register custom
+serializers for such types.
+
+#### Object array
+
+Object array is serialized using the collection format. Object component type
will be taken as collection element
+generic
+type.
+
+### Map
+
+> All Map serializers must extend `AbstractMapSerializer`.
+
+Format:
+
+```
+| length(unsigned varint) | map header | key value pairs data |
+```
+
+#### Map header
+
+- For `HashMap/LinkedHashMap`, this will be empty.
+- For `TreeMap`, this will be `Comparator`
+- For other `Map`, this may be extra object field info.
+
+#### Map Key-Value data
+
+Map iteration is too expensive, Fury won't compute the header like for
collection before since it introduce
+[considerable overhead](https://github.com/apache/incubator-fury/issues/925).
+Users can use `MapFieldInfo` annotation to provide header in advance.
Otherwise Fury will use first key-value pair to
+predict header optimistically, and update the chunk header if the prediction
failed at some pair.
+
+Fury will serialize map chunk by chunk, every chunk has 127 pairs at most.
+
+```
+| 1 byte | 1 byte | variable bytes |
++----------------+----------------+-----------------+
+| chunk size: N | KV header | N*2 objects |
+```
+
+KV header:
+
+- If track key ref, use the first bit `0b1` of the header to flag it.
+- If the key has null, use the second bit `0b10` of the header to flag it. If
ref tracking is enabled for this
+ key type, this flag is invalid.
+- If the key types of map are different, use the 3rd bit `0b100` of the header
to flag it.
+- If the actual key type of map is not the declared key type, use the 4rd bit
`0b1000` of the header to flag it.
+- If track value ref, use the 5th bit `0b10000` of the header to flag it.
+- If the value has null, use the 6th bit `0b100000` of the header to flag it.
If ref tracking is enabled for this
+ value type, this flag is invalid.
+- If the value types of map are different, use the 7rd bit `0b1000000` header
to flag it.
+- If the value type of map is not the declared value type, use the 8rd bit
`0b10000000` of the header to flag it.
+
+If streaming write is enabled, which means Fury can't update written `chunk
size`. In such cases, map key-value data
+format will be:
+
+```
+| 1 byte | variable bytes |
++----------------+-----------------+
+| KV header | N*2 objects |
+```
+
+`KV header` will be a header marked by `MapFieldInfo` in java. For languages
such as golang, this can be computed in
+advance for non-interface types in most times.
+
+### Enum
+
+Enums are serialized as an unsigned var int. If the order of enum values
change, the deserialized enum value may not be
+the value users expect. In such cases, users must register enum serializer by
make it write enum value as an enumerated
+string with unique hash disabled.
+
+### Object
+
+Object means object of `pojo/struct/bean/record` type.
+Object will be serialized by writing its fields data in fury order.
+
+Depending on schema compatibility, objects will have different formats.
+
+#### Field order
+
+Field will be ordered as following, every group of fields will have its own
order:
+
+- primitive fields: larger size type first, smaller later, variable size type
last.
+- boxed primitive fields: same order as primitive fields
+- final fields: same type together, then sorted by field name
lexicographically.
+- collection fields: same order as final fields
+- map fields: same order as final fields
+- other fields: same order as final fields
+
+#### Schema consistent
+
+Object fields will be serialized one by one using following format:
+
+```
+Primitive field value:
+| var bytes |
++----------------+
+| value data |
++----------------+
+Boxed field value:
+| one byte | var bytes |
++-----------+---------------+
+| null flag | field value |
++-----------+---------------+
+field value of final type with ref tracking:
+| var bytes | var objects |
++-----------+-------------+
+| ref meta | value data |
++-----------+-------------+
+field value of final type without ref tracking:
+| one byte | var objects |
++-----------+-------------+
+| null flag | field value |
++-----------+-------------+
+field value of non-final type with ref tracking:
+| one byte | var bytes | var objects |
++-----------+-------------+-------------+
+| ref meta | class meta | value data |
++-----------+-------------+-------------+
+field value of non-final type without ref tracking:
+| one byte | var bytes | var objects |
++-----------+------------+------------+
+| null flag | class meta | value data |
++-----------+------------+------------+
+```
+
+#### Schema evolution
+
+Schema evolution have similar format as schema consistent mode for object
except:
+
+- For this object type itself, `schema consistent` mode will write class by
id/name, but `schema evolution` mode will
+ write class field names, types and other meta too, see [Class
meta](#class-meta).
+- Class meta of `final custom type` needs to be written too, because peers may
not have this class defined.
+
+### Class
+
+Class will be serialized using class meta format.
+
+## Implementation guidelines
+
+- Try to merge multiple bytes into an int/long write before writing to reduce
memory IO and bound check cost.
+- Read multiple bytes as an int/long, then split into multiple bytes to reduce
memory IO and bound check cost.
+- Try to use one varint/long to write flags and length together to save one
byte cost and reduce memory io.
+- Condition branches are less expensive compared to memory IO cost unless
there are too many branches.
diff --git a/docs/specification/row_format_spec.md
b/docs/specification/row_format_spec.md
new file mode 100644
index 0000000..d32c11a
--- /dev/null
+++ b/docs/specification/row_format_spec.md
@@ -0,0 +1,8 @@
+---
+title: Fury Row Format
+sidebar_position: 2
+id: fury_row_format_spec
+---
+
+# Row Format
+Coming soon
diff --git a/docs/specification/xlang_serialization_spec.md
b/docs/specification/xlang_serialization_spec.md
new file mode 100644
index 0000000..e01ec92
--- /dev/null
+++ b/docs/specification/xlang_serialization_spec.md
@@ -0,0 +1,757 @@
+---
+title: Fury Xlang Serialization Format
+sidebar_position: 0
+id: fury_xlang_serialization_spec
+---
+
+# Cross-language Serialization Specification
+
+> Format Version History:
+> - Version 0.1 - serialization spec formalized
+
+Fury xlang serialization is an automatic object serialization framework that
supports reference and polymorphism.
+Fury will convert an object from/to fury xlang serialization binary format.
+Fury has two core concepts for xlang serialization:
+
+- **Fury xlang binary format**
+- **Framework implemented in different languages to convert object to/from
Fury xlang binary format**
+
+The serialization format is a dynamic binary format. The dynamics and
reference/polymorphism support make Fury flexible,
+much more easy to use, but
+also introduce more complexities compared to static serialization frameworks.
So the format will be more complex.
+
+## Type Systems
+
+### Data Types
+
+- bool: a boolean value (true or false).
+- int8: a 8-bit signed integer.
+- int16: a 16-bit signed integer.
+- int32: a 32-bit signed integer.
+- var_int32: a 32-bit signed integer which use fury var_int32 encoding.
+- fixed_int32: a 32-bit signed integer which use two's complement encoding.
+- int64: a 64-bit signed integer.
+- var_int64: a 64-bit signed integer which use fury PVL encoding.
+- sli_int64: a 64-bit signed integer which use fury SLI encoding.
+- fixed_int64: a 64-bit signed integer which use two's complement encoding.
+- float16: a 16-bit floating point number.
+- float32: a 32-bit floating point number.
+- float64: a 64-bit floating point number including NaN and Infinity.
+- string: a text string encoded using Latin1/UTF16/UTF-8 encoding.
+- enum: a data type consisting of a set of named values. Rust enum with
non-predefined field values are not supported as
+ an enum.
+- list: a sequence of objects.
+- set: an unordered set of unique elements.
+- map: a map of key-value pairs. Mutable types such as
`list/map/set/array/tensor/arrow` are not allowed as key of map.
+- time types:
+ - duration: an absolute length of time, independent of any
calendar/timezone, as a count of nanoseconds.
+ - timestamp: a point in time, independent of any calendar/timezone, as a
count of nanoseconds. The count is relative
+ to an epoch at UTC midnight on January 1, 1970.
+- decimal: exact decimal value represented as an integer value in two's
complement.
+- binary: an variable-length array of bytes.
+- array type: only allow numeric components. Other arrays will be taken as
List. The implementation should support the
+ interoperability between array and list.
+ - array: multidimensional array which every sub-array can have different
sizes but all have same type.
+ - bool_array: one dimensional int16 array.
+ - int16_array: one dimensional int16 array.
+ - int32_array: one dimensional int32 array.
+ - int64_array: one dimensional int64 array.
+ - float16_array: one dimensional half_float_16 array.
+ - float32_array: one dimensional float32 array.
+ - float64_array: one dimensional float64 array.
+- tensor: a multidimensional dense array of fixed-size values such as a NumPy
ndarray.
+- sparse tensor: a multidimensional array whose elements are almost all zeros.
+- arrow record batch: an arrow [record
batch](https://arrow.apache.org/docs/cpp/tables.html#record-batches) object.
+- arrow table: an arrow
[table](https://arrow.apache.org/docs/cpp/tables.html#tables) object.
+
+Note:
+
+- Unsigned int/long are not added here, since not every language support those
types.
+
+### Type disambiguation
+
+Due to differences between type systems of languages, those types can't be
mapped one-to-one between languages. When
+deserializing, Fury use the target data structure type and the data type in
the data jointly to determine how to
+deserialize and populate the target data structure. For example:
+
+```java
+class Foo {
+ int[] intArray;
+ Object[] objects;
+ List<Object> objectList;
+}
+
+class Foo2 {
+ int[] intArray;
+ List<Object> objects;
+ List<Object> objectList;
+}
+```
+
+`intArray` has an `int32_array` type. But both `objects` and `objectList`
fields in the serialize data have `list` data
+type. When deserializing, the implementation will create an `Object` array for
`objects`, but create a `ArrayList`
+for `objectList` to populate its elements. And the serialized data of `Foo`
can be deserialized into `Foo2` too.
+
+Users can also provide meta hints for fields of a type, or the type whole.
Here is an example in java which use
+annotation to provide such information.
+
+```java
+
+@TypeInfo(fieldsNullable = false, trackingRef = false, polymorphic = false)
+class Foo {
+ @FieldInfo(trackingRef = false)
+ int[] intArray;
+ @FieldInfo(polymorphic = true)
+ Object object;
+ @FieldInfo(tagId = 1, nullable = true)
+ List<Object> objectList;
+}
+```
+
+Such information can be provided in other languages too:
+
+- cpp: use macro and template.
+- golang: use struct tag.
+- python: use typehint.
+- rust: use macro.
+
+### Type ID
+
+All internal data types are expressed using an ID in range `-64~-1`. Users can
use `0~32703` for representing their
+types. At runtime, all type ids are added by `64`, and then encoded as an
unsigned varint.
+
+### Type mapping
+
+See [Type mapping](../guide/xlang_type_mapping.md)
+
+## Spec overview
+
+Here is the overall format:
+
+```
+| fury header | object ref meta | object type meta | object value data |
+```
+
+The data are serialized using little endian byte order overall. If bytes swap
is costly for some object,
+Fury will write the byte order for that object into the data instead of
converting it to little endian.
+
+## Fury header
+
+Fury header consists starts one byte:
+
+```
+| 2 bytes | 4 bits | 1 bit | 1 bit | 1 bit | 1 bit |
optional 4 bytes |
++--------------+---------------+-------+-------+--------+-------+------------------------------------+
+| magic number | reserved bits | oob | xlang | endian | null | unsigned int
for meta start offset |
+```
+
+- magic number: used to identify fury serialization protocol, current version
use `0x62d4`.
+- null flag: 1 when object is null, 0 otherwise. If an object is null, other
bits won't be set.
+- endian flag: 1 when data is encoded by little endian, 0 for big endian.
+- xlang flag: 1 when serialization uses xlang format, 0 when serialization
uses Fury java format.
+- oob flag: 1 when passed `BufferCallback` is not null, 0 otherwise.
+
+If meta share mode is enabled, an uncompressed unsigned int is appended to
indicate the start offset of metadata.
+
+## Reference Meta
+
+Reference tracking handles whether the object is null, and whether to track
reference for the object by writing
+corresponding flags and maintaining internal state.
+
+Reference flags:
+
+| Flag | Byte Value | Description
|
+|---------------------|------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
+| NULL FLAG | `-3` | This flag indicates the object is a null
value. We don't use another byte to indicate REF, so that we can save one byte.
|
+| REF FLAG | `-2` | This flag indicates the object is already
serialized previously, and fury will write a ref id with unsigned varint format
instead of serialize it again |
+| NOT_NULL VALUE FLAG | `-1` | This flag indicates the object is a
non-null value and fury doesn't track ref for this type of object.
|
+| REF VALUE FLAG | `0` | This flag indicates the object is
referencable and the first time to serialize.
|
+
+When reference tracking is disabled globally or for specific types, or for
certain types within a particular
+context(e.g., a field of a type), only the `NULL` and `NOT_NULL VALUE` flags
will be used for reference meta.
+
+For languages which doesn't support reference such as rust, reference tracking
must be disabled for correct
+deserialization by fury rust implementation.
+
+For languages whose object values are not null by default:
+
+- In rust, Fury takes `Option:None` as a null value
+- In c++, Fury takes `std::nullopt` as a null value
+- In golang, Fury takes `null interface/pointer` as a null value
+
+If one want to deserialize in languages like `Java/Python/JavaScript`, he
should mark the type with all fields
+not-null by default, or using schema-evolution mode to carry the not-null
fields info in the data.
+
+## Type Meta
+
+For every type to be serialized, it must be registered with an optional ID
first. The registered type will have a
+user-provided or an auto-growing unsigned int i.e. `type_id`. The registration
can be used for security check and type
+identification. The id of user registered type will be added by `64` to make
space for Fury internal data types.
+
+Depending on whether meta share mode and registration is enabled for current
type, Fury will write type meta
+differently.
+
+### Schema consistent
+
+- If schema consistent mode is enabled globally when creating fury, type meta
will be written as a fury unsigned varint
+ of `type_id`. Schema evolution related meta will be ignored.
+- If schema evolution mode is enabled globally when creating fury, and current
class is configured to use schema
+ consistent mode like `struct` vs `table` in flatbuffers:
+ - Type meta will be add to `captured_type_defs`: `captured_type_defs[type
def stub] = map size` ahead when
+ registering type.
+ - Get index of the meta in `captured_type_defs`, write that index as `|
unsigned varint: index |`.
+
+### Schema evolution
+
+If schema evolution mode is enabled globally when creating fury, and enabled
for current type, type meta will be written
+using one of the following mode. Which mode to use is configured when creating
fury.
+
+- Normal mode(meta share not enabled):
+ - If type meta hasn't been written before, add `type def`
+ to `captured_type_defs`: `captured_type_defs[type def] = map size`.
+ - Get index of the meta in `captured_type_defs`, write that index as `|
unsigned varint: index |`.
+ - After finished the serialization of the object graph, fury will start to
write `captured_type_defs`:
+ - Firstly, set current to `meta start offset` of fury header
+ - Then write `captured_type_defs` one by one:
+ ```python
+ buffer.write_var_uint32(len(writting_type_defs) -
len(schema_consistent_type_def_stubs))
+ for type_meta in writting_type_defs:
+ if not type_meta.is_stub():
+ type_meta.write_type_def(buffer)
+ writing_type_defs = copy(schema_consistent_type_def_stubs)
+ ```
+- Meta share mode: the writing steps are same as the normal mode, but
`captured_type_defs` will be shared across
+ multiple serializations of different objects. For example, suppose we have a
batch to serialize:
+ ```python
+ captured_type_defs = {}
+ stream = ...
+ # add `Type1` to `captured_type_defs` and write `Type1`
+ fury.serialize(stream, [Type1()])
+ # add `Type2` to `captured_type_defs` and write `Type2`, `Type1` is
written before.
+ fury.serialize(stream, [Type1(), Type2()])
+ # `Type1` and `Type2` are written before, no need to write meta.
+ fury.serialize(stream, [Type1(), Type2()])
+ ```
+
+- Streaming mode(streaming mode doesn't support meta share):
+ - If type meta hasn't been written before, the data will be written as:
+ ```
+ | unsigned varint: 0b11111111 | type def |
+ ```
+ - If type meta has been written before, the data will be written as:
+ ```
+ | unsigned varint: written index << 1 |
+ ```
+ `written index` is the id in `captured_type_defs`.
+ - With this mode, `meta start offset` can be omitted.
+
+> The normal mode and meta share mode will forbid streaming writing since it
needs to look back for update the start
+> offset after the whole object graph writing and meta collecting is finished.
Only in this way we can ensure
+> deserialization failure in meta share mode doesn't lost shared meta.
+
+#### Type Def
+
+Here we mainly describe the meta layout for schema evolution mode:
+
+```
+| 8 bytes meta header | variable bytes | variable bytes |
variable bytes |
++-------------------------------+--------------------+-------------------+----------------+
+| 7 bytes hash + 1 bytes header | current type meta | parent type meta |
... |
+```
+
+Type meta are encoded from parent type to leaf type, only type with
serializable fields will be encoded.
+
+##### Meta header
+
+Meta header is a 64 bits number value encoded in little endian order.
+
+- Lowest 4 digits `0b0000~0b1110` are used to record num classes. `0b1111` is
preserved to indicate that Fury need to
+ read more bytes for length using Fury unsigned int encoding. If current type
doesn't has parent type, or parent
+ type doesn't have fields to serialize, or we're in a context which serialize
fields of current type
+ only, num classes will be 1.
+- The 5th bit is used to indicate whether this type needs schema evolution.
+- Other 56 bits are used to store the unique hash of `flags + all layers type
meta`.
+
+##### Single layer type meta
+
+```
+| unsigned varint | var uint | field info: variable bytes | variable bytes
| ... |
++-----------------+----------+-------------------------------+-----------------+-----+
+| num_fields | type id | header + type id + field name | next field info
| ... |
+```
+
+- num fields: encode `num fields` as unsigned varint.
+ - If the current type is schema consistent, then num_fields will be `0` to
flag it.
+ - If the current type isn't schema consistent, then num_fields will be the
number of compatible fields. For example,
+ users can use tag id to mark some fields as compatible fields in schema
consistent context. In such cases, schema
+ consistent fields will be serialized first, then compatible fields will
be serialized next. At deserialization,
+ Fury will use fields info of those fields which aren't annotated by tag
id for deserializing schema consistent
+ fields, then use fields info in meta for deserializing compatible fields.
+- type id: the registered id for the current type, which will be written as an
unsigned varint.
+- field info:
+ - Header(8 bits):
+ - Format:
+ - `reserved 1 bit + 3 bits field name encoding + polymorphism flag
+ nullability flag + ref tracking flag + tag id flag`.
+ - Users can use annotation to provide that info.
+ - tag id: when set to 1, the field name will be written by an
unsigned varint tag id.
+ - ref tracking: when set to 0, ref tracking will be disabled for
this field.
+ - nullability: when set to 0, this field won't be null.
+ - polymorphism: when set to 1, the actual type of field will be
the declared field type even the type if
+ not `final`.
+ - 3 bits field name encoding will be set to meta string encoding
flags when tag id is not set.
+ - Type id:
+ - For registered type-consistent classes, it will be the registered
type id.
+ - Otherwise it will be encoded as `OBJECT_ID` if it isn't `final` and
`FINAL_OBJECT_ID` if it's `final`. The
+ meta for such types is written separately instead of inlining here
is to reduce meta space cost if object of
+ this type is serialized in the current object graph multiple times,
and the field value may be null too.
+ - List Type Info: this type will have an extra byte for elements info.
+ Users can use annotation to provide that info.
+ - elements type same
+ - elements tracking ref
+ - elements nullability
+ - elements declared type
+ - Map Type Info: this type will have an extra byte for kv items info.
+ Users can use annotation to provide that info.
+ - keys type same
+ - keys tracking ref
+ - keys nullability
+ - keys declared type
+ - values type same
+ - values tracking ref
+ - values nullability
+ - values declared type
+ - Field name: If tag id is set, tag id will be used instead. Otherwise
meta string encoding length and data will
+ be written instead.
+
+Field order are left as implementation details, which is not exposed to
specification, the deserialization need to
+resort fields based on Fury field comparator. In this way, fury can compute
statistics for field names or types and
+using a more compact encoding.
+
+##### Other layers type meta
+
+Same encoding algorithm as the previous layer.
+
+## Meta String
+
+Meta string is mainly used to encode meta strings such as field names.
+
+### Encoding Algorithms
+
+String binary encoding algorithm:
+
+| Algorithm | Pattern | Description
|
+|---------------------------|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
+| LOWER_SPECIAL | `a-z._$\|` | every char is written using 5
bits, `a-z`: `0b00000~0b11001`, `._$\|`: `0b11010~0b11101`
|
+| LOWER_UPPER_DIGIT_SPECIAL | `a-zA-Z0~9._$` | every char is written using 6
bits, `a-z`: `0b00000~0b11110`, `A-Z`: `0b11010~0b110011`, `0~9`:
`0b110100~0b111101`, `._$`: `0b111110~0b1000000` |
+| UTF-8 | any chars | UTF-8 encoding
|
+
+Encoding flags:
+
+| Encoding Flag | Pattern
| Encoding Algorithm
|
+|---------------------------|-----------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
+| LOWER_SPECIAL | every char is in `a-z._$\|`
| `LOWER_SPECIAL`
|
+| REP_FIRST_LOWER_SPECIAL | every char is in `a-z._$` except first char is
upper case | replace first upper case char to lower case, then use
`LOWER_SPECIAL` |
+| REP_MUL_LOWER_SPECIAL | every char is in `a-zA-Z._$`
| replace every upper case char by `\|` + `lower case`, then use
`LOWER_SPECIAL`, use this encoding if it's smaller than Encoding `3` |
+| LOWER_UPPER_DIGIT_SPECIAL | every char is in `a-zA-Z._$`
| use `LOWER_UPPER_DIGIT_SPECIAL` encoding if it's smaller than
Encoding `2` |
+| UTF8 | any utf-8 char
| use `UTF-8` encoding
|
+| Compression | any utf-8 char
| lossless compression
|
+
+Depending on cases, one can choose encoding `flags + data` jointly, uses 3
bits of first byte for flags and other bytes
+for data.
+
+## Value Format
+
+### Basic types
+
+#### bool
+
+- size: 1 byte
+- format: 0 for `false`, 1 for `true`
+
+#### int8
+
+- size: 1 byte
+- format: write as pure byte.
+
+#### int16
+
+- size: 2 byte
+- byte order: raw bytes of little endian order
+
+#### unsigned int32
+
+- size: 4 byte
+- byte order: raw bytes of little endian order
+
+#### unsigned varint32
+
+- size: 1~5 byte
+- Format: The most significant bit (MSB) in every byte indicates whether to
have the next byte. If first bit is set
+ i.e. `b & 0x80 == 0x80`, then
+ the next byte should be read until the first bit of the next byte is unset.
+
+#### signed int32
+
+- size: 4 byte
+- byte order: raw bytes of little endian order
+
+#### signed varint32
+
+- size: 1~5 byte
+- Format: First convert the number into positive unsigned int by `(v << 1) ^
(v >> 31)` ZigZag algorithm, then encode
+ it as an unsigned varint.
+
+#### unsigned int64
+
+- size: 8 byte
+- byte order: raw bytes of little endian order
+
+#### unsigned varint64
+
+- size: 1~9 byte
+- Fury SLI(Small long as int) Encoding:
+ - If long is in `[0, 2147483647]`, encode as 4 bytes int: `|
little-endian: ((int) value) << 1 |`
+ - Otherwise write as 9 bytes: `| 0b1 | little-endian 8 bytes long |`
+- Fury PVL(Progressive Variable-length Long) Encoding:
+ - positive long format: first bit in every byte indicates whether to have
the next byte. If first bit is set
+ i.e. `b & 0x80 == 0x80`, then the next byte should be read until the
first bit is unset.
+
+#### signed int64
+
+- size: 8 byte
+- byte order: raw bytes of little endian order
+
+#### signed varint64
+
+- size: 1~9 byte
+- Fury SLI(Small long as int) Encoding:
+ - If long is in `[-1073741824, 1073741823]`, encode as 4 bytes int: `|
little-endian: ((int) value) << 1 |`
+ - Otherwise write as 9 bytes: `| 0b1 | little-endian 8 bytes long |`
+- Fury PVL(Progressive Variable-length Long) Encoding:
+ - First convert the number into positive unsigned long by `(v << 1) ^ (v
>> 63)` ZigZag algorithm to reduce cost of
+ small negative numbers, then encoding it as an unsigned long.
+
+#### float32
+
+- size: 4 byte
+- format: encode the specified floating-point value according to the IEEE 754
floating-point "single format" bit layout,
+ preserving Not-a-Number (NaN) values, then write as binary by little endian
order.
+
+#### float64
+
+- size: 8 byte
+- format: encode the specified floating-point value according to the IEEE 754
floating-point "double format" bit layout,
+ preserving Not-a-Number (NaN) values. then write as binary by little endian
order.
+
+### string
+
+Format:
+
+```
+| unsigned varint64: size << 2 `bitor` 2 bits encoding flags | binary data |
+```
+
+- `size + encoding` will be concat as a long and encoded as an unsigned
varint64. The little 2 bits is used for
+ encoding:
+ 0 for `latin1(ISO-8859-1)`, 1 for `utf-16`, 2 for `utf-8`.
+- encoded string binary data based on encoding: `latin/utf-16/utf-8`.
+
+Which encoding to choose:
+
+- For JDK8: fury detect `latin` at runtime, if string is `latin` string, then
use `latin` encoding, otherwise
+ use `utf-16`.
+- For JDK9+: fury use `coder` in `String` object for encoding,
`latin`/`utf-16` will be used for encoding.
+- If the string is encoded by `utf-8`, then fury will use `utf-8` to decode
the data. Cross-language string
+ serialization of fury uses `utf-8` by default.
+
+### list
+
+Format:
+
+```
+| unsigned varint64: length << 4 `bitor` 4 bits elements header | elements
data |
+```
+
+#### elements header
+
+In most cases, all elements are same type and not null, elements header will
encode those homogeneous
+information to avoid the cost of writing it for every element. Specifically,
there are four kinds of information
+which will be encoded by elements header, each use one bit:
+
+- If track elements ref, use the first bit `0b1` of the header to flag it.
+- If the elements have null, use the second bit `0b10` of the header to flag
it. If ref tracking is enabled for this
+ element type, this flag is invalid.
+- If the element types are not the declared type, use the 3rd bit `0b100` of
the header to flag it.
+- If the element types are different, use the 4rd bit `0b1000` header to flag
it.
+
+By default, all bits are unset, which means all elements won't track ref, all
elements are same type, not null and
+the actual element is the declared type in the custom type field.
+
+#### elements data
+
+Based on the elements header, the serialization of elements data may skip `ref
flag`/`null flag`/`element type info`.
+
+```python
+fury = ...
+buffer = ...
+elems = ...
+if element_type_is_same:
+ if not is_declared_type:
+ fury.write_type(buffer, elem_type)
+ elem_serializer = get_serializer(...)
+ if track_ref:
+ for elem in elems:
+ if not ref_resolver.write_ref_or_null(buffer, elem):
+ elem_serializer.write(buffer, elem)
+ elif has_null:
+ for elem in elems:
+ if elem is None:
+ buffer.write_byte(null_flag)
+ else:
+ buffer.write_byte(not_null_flag)
+ elem_serializer.write(buffer, elem)
+ else:
+ for elem in elems:
+ elem_serializer.write(buffer, elem)
+else:
+ if track_ref:
+ for elem in elems:
+ fury.write_ref(buffer, elem)
+ elif has_null:
+ for elem in elems:
+ fury.write_nullable(buffer, elem)
+ else:
+ for elem in elems:
+ fury.write_value(buffer, elem)
+```
+
+[`CollectionSerializer#writeElements`](https://github.com/apache/incubator-fury/blob/20a1a78b17a75a123a6f5b7094c06ff77defc0fe/java/fury-core/src/main/java/org/apache/fury/serializer/collection/AbstractCollectionSerializer.java#L302)
+can be taken as an example.
+
+### array
+
+#### primitive array
+
+Primitive array are taken as a binary buffer, serialization will just write
the length of array size as an unsigned int,
+then copy the whole buffer into the stream.
+
+Such serialization won't compress the array. If users want to compress
primitive array, users need to register custom
+serializers for such types or mark it as list type.
+
+#### object array
+
+Object array is serialized using the list format. Object component type will
be taken as list element
+generic type.
+
+### map
+
+> All Map serializers must extend `AbstractMapSerializer`.
+
+Format:
+
+```
+| length(unsigned varint) | key value chunk data | ... | key value chunk data |
+```
+
+#### map key-value chunk data
+
+Map iteration is too expensive, Fury won't compute the header like for list
since it introduce
+[considerable overhead](https://github.com/apache/incubator-fury/issues/925).
+Users can use `MapFieldInfo` annotation to provide the header in advance.
Otherwise Fury will use first key-value pair
+to predict header optimistically, and update the chunk header if the
prediction failed at some pair.
+
+Fury will serialize the map chunk by chunk, every chunk has 127 pairs at most.
+
+```
+| 1 byte | 1 byte | variable bytes |
++----------------+----------------+-----------------+
+| chunk size: N | KV header | N*2 objects |
+```
+
+KV header:
+
+- If track key ref, use the first bit `0b1` of the header to flag it.
+- If the key has null, use the second bit `0b10` of the header to flag it. If
ref tracking is enabled for this
+ key type, this flag is invalid.
+- If the key types of map are different, use the 3rd bit `0b100` of the header
to flag it.
+- If the actual key type of the map is not the declared key type, use the 4rd
bit `0b1000` of the header to flag it.
+- If track value ref, use the 5th bit `0b10000` of the header to flag it.
+- If the value has null, use the 6th bit `0b100000` of the header to flag it.
If ref tracking is enabled for this
+ value type, this flag is invalid.
+- If the value types of the map are different, use the 7rd bit `0b1000000`
header to flag it.
+- If the value type of map is not the declared value type, use the 8rd bit
`0b10000000` of the header to flag it.
+
+If streaming write is enabled, which means Fury can't update written `chunk
size`. In such cases, map key-value data
+format will be:
+
+```
+| 1 byte | variable bytes |
++----------------+-----------------+
+| KV header | N*2 objects |
+```
+
+`KV header` will be a header marked by `MapFieldInfo` in java. For languages
such as golang, this can be computed in
+advance for non-interface types most times.
+
+### enum
+
+Enums are serialized as an unsigned var int. If the order of enum values
change, the deserialized enum value may not be
+the value users expect. In such cases, users must register enum serializer by
make it write enum value as an enumerated
+string with unique hash disabled.
+
+### decimal
+
+Not supported for now.
+
+### struct
+
+Struct means object of `class/pojo/struct/bean/record` type.
+Struct will be serialized by writing its fields data in fury order.
+
+Depending on schema compatibility, structs will have different formats.
+
+#### field order
+
+Field will be ordered as following, every group of fields will have its own
order:
+
+- primitive fields: larger size type first, smaller later, variable size type
last.
+- boxed primitive fields: same order as primitive fields
+- final fields: same type together, then sorted by field name
lexicographically.
+- list fields: same order as final fields
+- map fields: same order as final fields
+- other fields: same order as final fields
+
+#### schema consistent
+
+Object will be written as:
+
+```
+| 4 byte | variable bytes |
++---------------+------------------+
+| type hash | field values |
+```
+
+Type hash is used to check the type schema consistency across languages. Type
hash will be the first 32 bits of 56 bits
+value of the type meta.
+
+Object fields will be serialized one by one using following format:
+
+```
+not null primitive field value:
+| var bytes |
++----------------+
+| value data |
++----------------+
+nullable primitive field value:
+| one byte | var bytes |
++-----------+---------------+
+| null flag | field value |
++-----------+---------------+
+field value of final type with ref tracking:
+| var bytes | var objects |
++-----------+-------------+
+| ref meta | value data |
++-----------+-------------+
+field value of final type without ref tracking:
+| one byte | var objects |
++-----------+-------------+
+| null flag | field value |
++-----------+-------------+
+field value of non-final type with ref tracking:
+| one byte | var bytes | var objects |
++-----------+-------------+-------------+
+| ref meta | type meta | value data |
++-----------+-------------+-------------+
+field value of non-final type without ref tracking:
+| one byte | var bytes | var objects |
++-----------+------------+------------+
+| null flag | type meta | value data |
++-----------+------------+------------+
+```
+
+#### Schema evolution
+
+Schema evolution have similar format as schema consistent mode for object
except:
+
+- For the object type, `schema consistent` mode will write type by id only,
but `schema evolution` mode will
+ write type consisting of field names, types and other meta too, see [Type
meta](#type-meta).
+- Type meta of `final custom type` needs to be written too, because peers may
not have this type defined.
+
+### Type
+
+Type will be serialized using type meta format.
+
+## Implementation guidelines
+
+### How to reduce memory read/write code
+
+- Try to merge multiple bytes into an int/long write before writing to reduce
memory IO and bound check cost.
+- Read multiple bytes as an int/long, then split into multiple bytes to reduce
memory IO and bound check cost.
+- Try to use one varint/long to write flags and length together to save one
byte cost and reduce memory io.
+- Condition branches are less expensive compared to memory IO cost unless
there are too many branches.
+
+### Fast deserialization for static languages without runtime codegen support
+
+For type evolution, the serializer will encode the type meta into the
serialized data. The deserializer will compare
+this meta with class meta in the current process, and use the diff to
determine how to deserialize the data.
+
+For java/javascript/python, we can use the diff to generate serializer code at
runtime and load it as class/function for
+deserialization. In this way, the type evolution will be as fast as type
consist mode.
+
+For C++/Rust, we can't generate the serializer code at runtime. So we need to
generate the code at compile-time using
+meta programming. But at that time, we don't know the type schema in other
processes, so we can't generate the
+serializer code for such inconsistent types. We may need to generate the code
which has a loop and compare field name
+one by one to decide whether to deserialize and assign the field or skip the
field value.
+
+One fast way is that we can optimize the string comparison into `jump`
instructions:
+
+- Assume the current type has `n` fields, and the peer type has `n1` fields.
+- Generate an auto growing `field id` from `0` for every sorted field in the
current type at the compile time.
+- Compare the received type meta with current type, generate same id if the
field name is same, otherwise generate an
+ auto growing id starting from `n`, cache this meta at runtime.
+- Iterate the fields of received type meta, use a `switch` to compare the
`field id` to deserialize data
+ and `assign/skip` field value. **Continuous** field id will be optimized
into `jump` in `switch` block, so it will
+ very fast.
+
+Here is an example, suppose process A has a class `Foo` with version 1 defined
as `Foo1`, process B has a class `Foo`
+with version 2 defined as `Foo2`:
+
+```c++
+// class Foo with version 1
+class Foo1 {
+ int32_t v1; // id 0
+ std::string v2; // id 1
+};
+// class Foo with version 2
+class Foo2 {
+ // id 0, but will have id 2 in process A
+ bool v0;
+ // id 1, but will have id 0 in process A
+ int32_t v1;
+ // id 2, but will have id 3 in process A
+ int64_t long_value;
+ // id 3, but will have id 1 in process A
+ std::string v2;
+ // id 4, but will have id 4 in process A
+ std::vector<std::string> list;
+};
+```
+
+When process A received serialized `Foo2` from process B, here is how it
deserialize the data:
+
+```c++
+Foo1 foo1 = ...;
+const std::vector<fury::FieldInfo> &field_infos = type_meta.field_infos;
+for (const auto &field_info : field_infos) {
+ switch (field_info.field_id) {
+ case 0:
+ foo1.v1 = buffer.read_varint32();
+ break;
+ case 1:
+ foo1.v2 = fury.read_string();
+ break;
+ default:
+ fury.skip_data(field_info);
+ }
+}
+```
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]