Data Encoding for Optional Values

15 min read

15 min read

15 min read

15 min read

15 min read

15 min read

15 min read

15 min read

15 min read

Slice definitions can use optional values as parameters and as fields. To make optional values truly useful, the receiver needs the ability to gracefully ignore an optional value that it does not recognize. Although the Ice encoding omits type information and therefore is not self-describing, the encoding for optional values includes enough information for a receiver to determine how many bytes an optional value occupies so that it can skip to the next value in the stream. The encoding rules nonetheless minimize the overhead associated with optional values, as you will see below.

The encoding for optional parameters and fields follows these general rules:

  • Each optional value has a corresponding integer tag that uniquely identifies it.
  • Optional values always appear after any required values.
  • Optional values appear in increasing tag order. (Unlike required values, order of declaration does not affect optional values.)
  • An optional value is encoded only if the sender has supplied a value.

Optional fields of a class or exception appear in the slice after any required fields and before the indirection table, if present. The slice flags indicate the presence of optional fields. When a slice contains optional fields, the byte value 255 follows the last optional field and marks the end of the optional fields. When the slice encodes its size, the byte count for the slice includes the optional fields and the end marker.

The encoding does not use an explicit end marker for optional parameters; the end of the encapsulation also marks the end of any optional parameters.

An optional value is encoded as the tuple <type, tag, value>, where type is the optional type that tells the receiver how to determine the number of bytes occupied by the value. The value itself is marshaled using the standard Ice encoding rules for its Slice type.

The first byte of an encoded optional value includes the optional type, and may also include the tag. The optional type occupies the first three bits of this byte, as described in the table below:

NameValueDescriptionUsed for Slice type
F1
0
The value is encoded in one byte.
bool, byte
F2
1
The value is encoded in two bytes.
short
F4
2
The value is encoded in four bytes.
int, float
F8
3
The value is encoded in eight bytes.
double, long
Size
4
The value is encoded as a size.
enum
VSize
5
A leading size value indicates the number of bytes occupied by the value.
string, fixed-size structure, container of fixed-size elements
FSize
6
A leading 32-bit integer indicates the number of bytes occupied by the value.
variable-size structure, container of variable-size elements, proxy
Class
7
Not used: Slice does not allow an optional value whose type uses classes.
none

The next five bits of the leading byte contain the tag, but only if the tag value is less than 30. Otherwise, the next five bits contain the value 30 as a marker to indicate that the tag value is encoded as a size starting with the next byte. As you can see, using tag values in the range 0 to 29 produces the most compact encoding.

Variable-size types whose encoded size cannot be determined in advance use the FSize optional type, where "FSize" denotes a leading fixed-length (32-bit) size. For these types, the sender reserves four bytes to hold the size, encodes the value as usual, then replaces the four bytes with the actual encoded size.

Fixed-size structures and containers of fixed-size elements use the VSize optional type, where "VSize" denotes a leading variable-length size. The sender can determine the encoded size of these types in advance, and therefore encodes it as a size followed by the value as usual.

The leading VSize or FSize gives the number of bytes of the encoded value that follows it. For a sequence or dictionary, this count includes the container's own leading size, the one that holds the number of elements.

Strings also use the VSize optional type but do not require an additional size because the string encoding already includes a leading size. The same is true for sequences of elements of size 1, such as a sequence of bool, a sequence of byte or a sequence of a struct with a single bool or byte field.

The following table describes the encoding of Slice types:

Slice typeOptional typeData encodingNotes
bool, byte
F1
value
short
F2
value
int, float
F4
value
long, double
F8
value
Proxy
FSize
int + data
32-bit integer holds the size of the encoded proxy.
enum
Size
size
Enumerator encoded as a size.
string
VSize
value
The encoded data for the string already contains a leading size.
sequence<type-with-size-1> such as sequence<bool>, sequence<byte>
VSize
value
The encoded data for the sequence already contains a leading size, in bytes.
sequence<fixed-size type> with fixed-size >= 2, dictionary<fixed-size key,fixed-size value>
VSize
size + value
Size can be computed before encoding the container.
sequence<variable size type>, dictionary<key,value> with a variable-size key or value
FSize
int + value
32-bit integer holds the size of the container.
Fixed-size struct
VSize
size + value
Size can be computed before encoding the structure.
Variable-size struct
FSize
int + value
32-bit integer holds the size of the structure.

The examples presented below demonstrate the encoding for optional values with typical use cases.

The following Slice operation makes use of optional input and output parameters:

Slice
bool op1(byte b,
optional(2) string name,
short sh,
optional(1) long count,
out double d,
out optional(300) Object* p);

Suppose the parameters have the values shown in the table below:

ParameterTypeValueMarshaled size (in bytes)
b
byte
77
1
name
string
"joe"
4
sh
short
99
2
count
long
88
8
d
double
3.14
8
p
proxy
hello
16
return value
bool
true
1

The parameters of the outgoing request are encoded in an encapsulation, as described in Request Payload:

Marshaled valueSize in bytesTypeByte offset
77 (b)
1
byte
0
99 (sh)
2
short
1
11 (optional type F8 + tag1)
1
byte
3
88 (count)
8
long
4
21 (optional type VSize + tag2)
1
byte
12
"joe" (name)
4
string
13

Using tag values less than 30 allows the encoding to combine the optional type and the tag into the same byte using the expression (Tag << 3) + Type.

Now consider the contents of the reply message:

Marshaled valueSize in bytesTypeByte offset
3.14 (d)
8
double
0
true (return value)
1
bool
8
246 (optional type FSize + marker 30)
1
byte
9
300 (tag)
5
size
10
16 (32-bit FSize)
4
int
15
hello (p)
16
Object*
19

The body of the reply message contains the out parameter d, the return value, and the optional parameter p. The tag value 300 is too large to combine with the optional type, therefore it appears immediately following the optional type encoded as a size. A proxy value uses the FSize optional type, meaning a 32-bit integer precedes the encoded value to specify its size.

The server here supplies the well-known proxy hello, with no facet, the twoway mode, protocol version 1.0 and encoding version 1.1, which occupies 16 bytes. If the server had supplied no value for the parameter, it would not appear in the encoding at all.

As an example, if the client does not recognize the optional proxy parameter, it can skip the parameter as follows:

  1. Extract the byte containing the optional type
  2. Examine the first three bits to determine the type
  3. Examine the five remaining bits to determine the tag's status
  4. A value of 30 means the tag is encoded separately
  5. Extract the tag as a size
  6. The optional type FSize means a 32-bit integer holds the value's size; extract the integer
  7. Skip ahead the specified number of bytes
  8. If we have reached the end of the encapsulation, there are no more optional parameters remaining

The only difference between the encoding for optional parameters and the encoding for optional fields is the latter uses a marker byte to signify the end of optional fields in a slice. Consider the following definitions:

Slice
struct Color
{
short red;
short green;
short blue;
}
class Shape
{
optional(1) string label;
}
class Rectangle extends Shape
{
int width;
int height;
optional(10) Color fill;
optional(9) Color border;
optional(11) float scale;
}

Suppose we are encoding an instance of Rectangle with the field values shown in the table below:

FieldTypeValueMarshaled size (in bytes)
label
string
"r1"
3
width
int
41
4
height
int
16
4
fill
Color
0,0,0
6
border
Color
255,255,255
6
scale
float
2.0
4

Using the sliced format, the instance data looks as follows:

Marshaled valueSize in bytesTypeByte offset
1 (instance marker)
1
byte
0
21 (slice flags: string type ID, size is present, optional fields are present)
1
byte
1
"::Rectangle" (type ID)
12
string
2
34 (byte count for slice)
4
int
14
41 (width)
4
int
18
16 (height)
4
int
22
77 (optional type VSize + tag9)
1
byte
26
6 (VSize)
1
size
27
{255,255,255} (border)
6
Color
28
85 (optional type VSize + tag10)
1
byte
34
6 (VSize)
1
size
35
{0,0,0} (fill)
6
Color
36
90 (optional type F4 + tag11)
1
byte
42
2.0 (scale)
4
float
43
255 (end marker)
1
byte
47
53 (slice flags: string type ID, size is present, optional fields are present, last slice)
1
byte
48
"::Shape" (type ID)
8
string
49
9 (byte count for slice)
4
int
57
13 (optional type VSize + tag1)
1
byte
61
"r1" (label)
3
string
62
255 (end marker)
1
byte
65

Notice the use of an end marker (byte value 255) denoting the end of the optional fields in each slice.