1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674
|
Serialization
=============
Storm has a library that provides serialization facilities for objects. These facilities are
implemented by the classes `core.io.ObjIStream` and `core.io.ObjOStream`, which specify the binary
format and provide the low-level operations required to implement serialization. These classes are
complemented by a serialization library written in Basic Storm, that generates the members `read`
and `write` for the classes that should be serialized.
The format used for serialization contains enough information to inspect it without having access to
the original type information; the needed type information is stored inside the serialization
stream. The class `util.serialize.TextObjStream` can be used to inspect and pretty-print previously
serialized objects.
The serialization format is designed to allow serializing multiple objects inside a single stream
while minimizing overhead. Therefore, type information is only sent once in one session (represented
by an instance of `ObjIStream` or `ObjOStream` respectively), and thus care needs to be taken to
ensure that the encoder and decoder use the same length of sessions. For example, if the encoder
writes two objects using one instance of `ObjOStream`, but the reader reads the two objects using
different instances of `ObjIStream`, deserialization will most likely fail.
Serializing Data
----------------
Using the serialization is fairly easy. All serializable types provide a `write` and a `read`
function that handles serialization and deserialization respectively. They shall have the following
signatures (`T` is the type being serialized):
- `void write(T this, ObjOStream to)`: The `write` function is a member function of `T`. It writes the
entire object recursively to the stream.
- `T read(ObjIStream from)` : The `read` function is a static member function of `T` that reads an
instance of `T` from the stream and returns the created instance.
Both `ObjOStream` and `ObjIStream` have a constructor that takes a single `IStream` or `OStream`,
and they are thus easily created. As mentioned above, these object streams keep track of state
associated with the serialization in order to not duplicate data in the stream, and to keep the
serialized data stream intact. Therefore, if multiple objects are written using one `OStream`
instance, they have to be read back using one `IStream` instance. Conversely, if multiple objects
are written using multiple `OStream` instances, they have to be read back using multiple `IStream`
instances.
The serialization handles inheritance, shared instances and cyclic structures, just like the clone
mechanism does. This means an object that contains (directly or indirectly) two references to the
same object will be serialized and deserialized just like expected, each distinct object is
serialized once, and all references that were originally referring to the same object will still
refer to the same object in the deserialized object. The same is true for structures containing
cycles. Each object serialized at the top-level (i.e. the first call to `write` or `read`) are
considered separate entities in this regard, meaning that two top-level objects will not share any
data with each other, even if they have some objects in common. This means that reading two objects
from a single `ObjIStream` at top-level guarantees that the two object hierarchies are disjoint
(both of them are, of course, also disjoint with the original, serialized object, which may no
longer exist).
As mentioned earlier, it is possible to inspect serialized data in textual format using the class
`util.serialize.TextObjStream`. This object works much like `ObjIStream`, but instead of producing
objects, it produces human readable text. For this stream, just like with `ObjIStream`, it is
important to match the number of instances when reading with the number of instances when writing.
Limiting Impact of Untrusted Data
---------------------------------
To provide basic protection against deserialization of untrusted data, it is possible to impose
various limits on the size of the deserialized data. The `ObjIStream` provides the following
variables for this purpose:
- `maxReadSize`: Limits the total size of each object hierarchy that is read through a top-level
call to `read`. This number accounts for all objects and arrays created by the deserialized
data. However, it does not account for all forms of overhead (e.g. object headers, extra memory
used for hash tables, etc.). As such, do not assume that the limit corresponds to an exact number
of bytes.
- `maxArraySize`: Limits the number of array allocations. Array allocations could be particularly
problematic since ambiguous references causes the garbage collector to retain a small number of
objects that could be reclaimed. Large array allocations are more likely to be pinned in this
manner, and the impact of retaining them are quite significant. This is why the array size can be
controlled separately.
- `maxTypeDescSize`: Limits the size of the data retained for memory used internally for type
descriptions. This is to avoid deserializing a stream that defines a large data structure with a
large number of members to exhaust available memory.
It is also possible to set all of these variables at the same time by setting `maxSize`. After
creating an `ObjIStream`, all of them are set to the maximum `Nat` value, which corresponds to just
below 4 GiB.
Making Classes Serializable
---------------------------
Usually, you don't want to serialize a large amount of primitive types. Usually, it makes more sense
to create a type that represents the data you want to serialize, and treat it as a whole unit. Of
course this is possible in Storm as well, and as long as all data members are serializable, Storm
can generate the serialization code for you automatically.
The function `util.serialize.serializalble(Type)` does just that. Call `serializable` and give a
type, and the function will examine the type and generate `write` and `read` functions that
correspond to the data in the class. The function is designed to be usable with
[options](md:/Language_Reference/Basic_Storm/Definitions/Types) in Basic Storm. As such, in Basic
Storm (and other languages with similar mechanisms), one can easily make a class serializable as
follows:
```bs
use lang:bs:macro; // for named{}
use util:serialize; // for serializable
class MyClass : serializable {
Int a;
Str b;
OtherClass c;
}
```
The Serialization Interface
---------------------------
There might be times when you need to serialize types that consist of non-serializable types, or
when the standard representation is not suitable for some reason. This is also possible to solve,
but requires more knowledge on the interfaces used by the serialization.
The serialization interface assumes that all serializable types (except for primitive types) contain
four functions (two of which are `read` and `write` discussed earlier) as follows:
- `void write(T this, ObjOStream to)`: Write the object to the stream, as mentioned earlier.
- `T read(ObjIStream from)` (static): Read an object from the stream, as mentioned earlier.
- `SerializedType serializedType()` (static): Generate a type description of this type for the serialization. This
function is called when it is needed by the serialization system. You typically don't need to call it directly.
- `void __init(T this, ObjIStream from)` (constructor): Initialize the object from a stream, used while reading.
All of these functions are generated automatically when using the `serializable` function mentioned
earlier. However, all types implementing `write`, `read` and `serializedType` are considered
serializable, and as such it is possible to make all types serializable by implementing these
functions manually. Therefore, we will take a closer look at each function below:
### The write function
The write function is responsible for writing the entire object to the stream. This function is
responsible for informing the `ObjOStream` of the extents of the object by calling the `startX` and
`end` functions as appropriate.
The write function is expected to start by calling `startValue` if the type is a value or
`startClass` if the type is a class type. `startValue` takes one parameter, a `Type` value that
corresponds to the type to be serialized (it is expected to have a `serializedType` member as
above). `startValue` then returns a boolean indicating whether or not this instance needs
serialization. If serialization is required, the read function is expected to serialize any data
contained in the object, and then call the `end` function of the `ObjOStream` class to indicate that
serialization is complete. If the class has a superclass, the superclass is expected to be
serialized before any data is serialized by calling the superclass' `write` function. Serialization
of the contained data depends on the contents of the `SerializedType` provided, as will be explained
later.
If the serialized object is a class type, the `write` function is expected to call `startClass`
instead of `startValue`. `startClass` works very much like `startValue`, but in order to properly
handle shared instances and cyclic hierarchies, `startClass` takes an additional parameter: a
reference to the instance being serialized. Except for this difference, serialization of class types
work the same as for value types. As such, a `write` function usually looks like this:
```bs
use lang:bs:macro; // for named{}
class MyClass {
Int a;
Str b;
// other members...
void write(ObjOStream to) {
if (to.startClass(named{MyClass}, this)) {
// call super:write(to) if it exists
a.write(to);
b.write(to);
to.end();
}
}
}
```
Note that `end` is not called if `startClass` or `startValue` returns `false`.
The write function is the only function (except for `serializedType`, if used) that is required for
serialization of objects.
### The read function
Since the built-in serialization attempts to be robust, deserialization is a bit more complicated
than serialization. Deserialization starts in the `read` function. However, the `read` function is
usually very simple, and can be implemented automatically by the `serializable` function provided
the other three functions are present. The `write` functions job is simply to call `readValue` or
`readClass` depending on what is being read from the stream.
The `readValue` function takes two parameters: a `Type` instance corresponding to the type being
deserialized and a raw pointer to previously allocated memory big enough to hold an instance of the
deserialized type. Because of this generic, low-level API, it is not generally possible to call this
function from higher level languages, such as Basic Storm, since they do not allow access to raw
memory in this fashion. However, if the other three functions are present, the `serializable`
function will happily generate a suitable `read` function for any type.
The `readClass` function is a bit better in this regard, as it only takes a `Type` parameter
indicating the type to deserialize. The resulting instance is returned in the form of an `Object`
reference. As such, the `read` function for class types can be implemented as follows (note that the
implementation provided by the `serializable` function is slightly more efficient, as it skips the
type check required in Basic Storm):
```bs
use lang:bs:macro; // for named{}
class MyClass {
// ...
MyClass read(ObjIStream from) : static {
if (x = from.readClass(named{MyClass})) {
x;
} else {
// Or something else...
MyClass();
}
}
}
```
Both `readClass` and `readValue` will locate the `serializedType` member of the type that is to be
deserialized (in the case of `readClass`, it may be a subclass), call that function, and call the
deserialization constructor contained in the resulting `SerializedType` class to create an instance
of the deserialized type. Do not assume that objects are created in the same order that the
corresponding 'read' functions are called; the `ObjIStream` may need to cache objects when the order
of members in the stream don't match the order of members in the current code, and it may even
re-use previously created instances in case multiple references refer to the same object.
### The serializedType function
The `serializedType` function is sometimes called by the serialization mechanism during
deserialization to retrieve type information about arbitrary types in the system. The task of this
function is therefore simple: return an instance of `SerializedType` that describes the aspects of
the type relevant for serialization.
There are three subclasses of `SerializedType` that are recognized by the serialization system:
`SeralizedStdType`, `SerializedTuples` and `SerializedMaybe`. All of these subclasses, or the base
class, may be returned from the `serializedType` function. Each subclass represent a standard
serialization format that can be interpreted without having access to the implementation of the
class itself. The base class, however, represents an object that is serialized in a nonstandard way
and thus not possible to deserialize without access to the actual implementation.
- `SerializedType`: Contains the minimum amount of information required by the serialization system
to function: A reference to the `Type` and a pointer to the read constructor used for
deserialization. If a `serializedType` returns an instance of this type, it indicates that the
serialized representation of the type is custom. Since the high-level serialization relies on type
information that is not available during custom serialization, custom serialization functions may
**not** call any serialization functions for other objects. Custom serialization **must** instead
call low-level operations on the underlying stream (available as a member variable of the
`ObjOStream` and `ObjIStream`). It is still possible to serialize primitive types in this manner,
as they provide `read` and `write` functions for regular `OStream` and `IStream` objects.
- `SerializedStdType`: Indicates that the type is a standard type, consisting of a number of named
member variables. Thus, this class contains the name of each member variable and its type. The
type is expected to serialize its members in the order specified in this type by calling the
corresponding `write` and `read` functions. This is the type generated by `serializable` if
nothing else is specified.
- `SerializedTuples`: Indicates that the type consists of a number of tuples, each tuple consisting
of a fixed number of elements with pre-defined types. Thus, this class contains information on
which types are stored in each tuple. The type is expected to serialize its data by first writing
a `Nat` containing the number of tuples stored, followed by that many tuples stored by calling the
`write` or `read` function of each type in the tuple the specified number of times. This type is
used by the standard containers, such as `Array` and `Map`.
- `SerializedMaybe`: Indicates that the type may contain a value of some type. The class contains
information about a single type that may be stored in the stream. The type is expected to be
serialized by writing a single `Bool` that indicates if the type is present or not. If so, the
bool is followed by the type itself. This type is used by the `Maybe` type.
In most languages, it is necessary to implement this function by creating the desired instance each
time the function is called, which is slightly inefficient (the object streams make sure to only
call the function once for each instance). The `serialized` function generates a version that always
returns the same instance, which makes this implementation slightly more efficient. If a `write`
function and a read constructor is present, the `serialized` function will generate a
`serializedType` function that returns an instance of `SerializedType` to indicate that the type has
custom serialization.
### The read constructor
The read constructor takes a single `ObjIStream` as a parameter, and is responsible for populating
an object from a stream, the opposite of the `write` function. As previously mentioned, the read
constructor is called as needed by the serialization system, and it is located using the
`serializedType` function.
The read constructor is usually a bit simpler than the `write` function. Since the `read` function
is responsible for calling `readX` (corresponding to `startX` for `write`), the read constructor
does not need to do that. The read constructor can simply assume that an instance of the current
type is to be read from the stream immediately. For both value and class types, the constructor is
assumed to read the data for the class in the same way data is written (except, of course, calling
`read` instead of `write`), and using the retrieved data to initialize the object. If the object has
a superclass, the superclass is expected to be read before any members of the subclass. As such,
read constructors are typically implemented as follows in Basic Storm:
```bs
class MyClass {
Int a;
Str b;
// other members...
init(ObjIStream from) {
// call super(from) if it exists
init {
a = Int:read(from);
b = Int:read(from);
}
from.end();
}
}
```
If you implement custom serialization and deserialize an array, you are additionally expected to
call the `checkArrayAlloc` function before allocating the array. This allows the `ObjIStream` to
keep track of size limits properly. If the limits are exceeded, the `checkArrayAlloc` function
throws an appropriate error.
### Examples
To summarize custom serialization, we provide two examples in Basic Storm. The first example shows
how one can implement the standard serialization using the custom serialization mechanisms described
above. Here, we have class with two members, `a` and `b` that are serialized:
```bs
use lang:bs:macro; // for named{}
use util:serialize; // for serializable
class MyClass : serializable /* provides "read", which is difficult to implement in Basic Storm */ {
Int a;
Str b;
SerializedType serializedType() : static {
SerializedStdType t(named{MyClass}, &__init(MyClass, ObjIStream));
t.add("a", named{Int});
t.add("b", named{Str});
t;
}
void write(ObjOStream to) {
if (to.startClass(serializedType(), this)) {
a.write(to);
b.write(to);
to.end();
}
}
init(ObjIStream from) {
init {
a = Int:read(from);
b = Str:read(from);
}
from.end();
}
}
```
The next example shows how to implement custom serialization of the same class. This version will
produce a serialized stream that does not contain enough information to read without the
implementation of `MyClass`. Note, that this is unnecessary, as `MyClass` is simple enough to be
serializable using the standard mechanisms:
```bs
use lang:bs:macro; // for named{}
use util:serialize; // for serializable
class MyClass : serializable /* provides "read" and "serializedType" */ {
Int a;
Str b;
void write(ObjOStream to) {
// Note: 'serializedType' is generated by the 'serializable' decorator.
if (to.startClass(serializdType(), this)) {
// Note: We're writing to the underlying 'OStream' rather than the 'ObjOStream'.
OStream stream = to.to;
a.write(stream);
b.write(stream);
to.end();
}
}
init(ObjIStream from) {
// Note: We're reading from the underlying 'IStream' rather than the 'ObjIStream'.
IStream stream = from.from;
init {
a = Int:read(stream);
b = Str:read(stream);
}
from.end();
}
}
```
Robustness of the format
-------------------------
When storing data for longer periods of time, it is important to know what changes can be made to
the serialized classes without losing the ability to read previously stored data. For this reason,
this section describes what changes are handled by the serialization mechanism, and which are not.
All of the following scenarios assume that some data structure is serialized to an external
location. The data representation is then changed as indicated before the previously stored data is
deserialized again. Furthermore, we assume that the implementation of any classes that have custom
serialization remain unchanged, as the serialization system can make no guarantees about them. The
serialized data may, however, include types that use custom serialization as long as that
implementation remain compatible with the previous version.
### Member variables
Member variables are usually stored in the serialized stream in the order they were declared in the
class (or, more precisely, the order in which they are stored in memory, which currently correspond
to the order the member variables are declared, but may change in the future). Thus, it is
reasonable to question if re-ordering the member variables break compatibility.
Indeed, changing the order of member variables will change the order in which they are written to
the stream. However, the serialization system will detect this using the names of the members and
act as if the members appear in the order the deserializing program expects them to. This does
require additional bookkeeping and causes a small performance impact on deserialization. Since names
are important, care must be taken when re-naming members in serializable classes to not break
compatibility. Compatibility can be retained by implementing custom serialization as shown above,
and using the old name for the variable in the `SerializedStdType` instance returned from
`serializedType`. In the future, it will be possible to specify this without implementing
serialization manually.
Adding and removing member variables is supported. If a stream contains a member variable that does
not exist in the current version, the variable is simply ignored. On the other hand, if a member
variable is missing from the stream, the system will either use the initializer specified in the
class (e.g. `Int x = 10;`), or the default constructor of the type. If neither initializer nor a
default constructor exists, deserialization will fail with an exception. As such, the system
supports removing member variables, and adding member variables as long as a default value is
specified.
### Types
It is possible to deserialize a stream into an object representation as long as all types used in
the stream are still present in the program. All types need to have the same name and reside in the
same package as the time they were serialized. If inheritance is used, the superclasses of all
serialized objects also need to remain the same. It is, however, possible to deserialize a value
type into a class type (assuming the contents match), but not vice versa.
The serialization system only examines the types that actually appear in a stream. This means that
types that *could* appear in a stream, but do not actually appear in the stream, may be changed in
any way without impacting the ability to deserialize streams. This means that it is possible to add
new subclasses to previously serialized classes without impairing the ability to deserialize old
streams, and newly serialized streams will be readable by old implementations as long as the new
classes are not actually serialized.
The binary format
------------------
Primitive types are encoded as follows:
- `Bool`: A single byte containing the value 0 for `false`. Other values (usually 1) indicate `true`.
- `Byte`: A single byte containing the value.
- `Int` and `Nat`: 4 bytes stored in big endian.
- `Long` and `Word`: 8 bytes stored in big endian.
- `Float`: The IEEE754 representation stored as a `Nat`.
- `Double`: The IEEE754 representation stored as a `Word`.
- `Str`: A `Nat` containing the length of the string followed by that many bytes of data, encoded in UTF-8.
Each serialized type is assigned a type id in the form of a `Nat` in order to refer to types in an
efficient manner. Type identifiers may appear before they are defined in the stream, for example
when referring to member types in a class. However, types are always defined whenever the first
instance of that particular type is read or written to the stream, so that the reader knows the
format of the serialized objects without access to the actual implementation. Primitive types
have pre-defined ids, defined by the enum `core.io.StoredId`. The id `core.io.StoredId.endId` (value
0) is of special interest. It is used to indicate the absence of a type, or the end of a
list. Identifiers below `core.io.StoredId.firstCustomId` are reserved for future primitive types,
and shall not be used.
A type description is stored as follows:
- A `Byte` indicating what kind of type is stored, described by the `core.io.TypeInfo` enum.
- A `Str` containing the name of the type, serialized as follows: The format is similar to the
textual representation of `core.lang.Name`. Each part is stored as a string ending with the ASCII
character 1. If parameters are present, they are surrounded by the ASCII character 2 and 3 (for
start- and end parentheses). Each parameter ends in either the ASCII character 4 if passed by
value or 5 if passed by reference.
- A `Nat` containing the id of the parent type, or `core.io.StoredId.endId` if none.
The remainder of the type description depends on which values are present in the first byte
(`core.io.TypeInfo` is a bitmask):
- `tuple`: This is an array of tuples. A series of `Nat` values containing the id of each type
in the tuple are stored. The end of the list is indicated by `core.io.StoredId.endId`.
- `maybe`: This is a maybe-type. A single `Nat` containing the contained type id is stored.
- `custom`: This is a type with custom serialization. Noting more is stored, but the serialized
data for instances of this type can not be extracted without asking the actual type.
- Otherwise: This is a regular class- or value type (depending on the presence of `classType`).
The members of the class or value are stored as pairs, first a `Nat` containing the type id of
the member, followed by a `Str` containing the member's name. The end of the list is indicated
by a type with id `core.io.StoredId.endId`. No string follows the last type id.
Each object in a session starts with a single `Nat` describing the type of the stored object,
followed by the object itself (which, for the first object in a session, starts with a type
definition unless a primitive type was stored). Types are then stored depending on their type
(described by the type description) as follows:
If the stored value is a class type (i.e. `core.io.TypeInfo.classType` is not present in the first
byte of the type description), a `Nat` describing the object's id is stored first. If this id was
serialized before, nothing more is stored; this is a reference to the previously stored
object. Otherwise, a `Nat` containing the type of the object follows (as the dynamic type might
differ from the static type). If this is a new type id, the type description directly follows the
new id, as we need to access that. If any parent classes are not yet serialized, they also
follow. Finally, the data of the object is stored as described below.
Values are simpler, as their static type always matches their dynamic type, and they are always
copied. Therefore, values are always stored as the data of the object, without the additional
headers required for storing class types.
The data of the object also depends on the first byte of the type description as follows:
- If the type is a built-in type, use the default serialization of that type (the `read` and `write`
functions for a regular `IStream` and `OStream`).
- If the type is an array of tuples, a `Nat` containing the number of elements in the array, followed
by that many repetitions of the types in the type description.
- If the type is a maybe type, a `Bool` indicating the presence of a value is stored first. If the
value is true, then a single instance of the contained type is stored directly afterwards.
- If the type is a custom type, the representation is unspecified.
- If the type is a regular class- or value type, each member is serialized in the order specified
by the type description.
Example
---------
Consider the code below, found in the file `serialization.bs` in the `demo` package:
```bs
<?include:demo/serialization.bs?>
```
The function `serializeExample` serialize an instance of the `Wrap` class and as the first session,
followed by an array of `Val` objects as the second session. Then, the textual representation is
printed followed by the binary representation. The textual representations illustrate how the data
is structured by the serialization mechanism, and should be fairly apparent from the output:
Session 1 (the `Wrap` object):
```
demo.Wrap (instance 0) {
a: demo.Val {
a: 1i
b: "One"
}
b: demo.Val {
a: 2i
b: "Two"
}
c: demo.Derived (instance 1) {
a: 3i
b: 4i
}
d: demo.Base (instance 2) {
a: 5i
}
e: <link to instance 2>
}
```
Session 2 (the array of `Val` objects):
```
core.Array(demo.Val) (instance 0) [
demo.Val {
a: 10i
b: "Ten"
}
demo.Val {
a: 20i
b: "Twenty"
}
]
```
The data is represented in the stream as follows. The data is annotated to show how each part is
represented by the format.
```
00 00 00 20 // Type-id of the root object of this session (32, user defined)
// This is a type we don't know about, so we read a type description:
01 // -Type flags (class type)
// -Next is a string containing the mangled name of the class:
00 00 00 0A // --Length of the string (10 bytes)
64 65 6D 6F // --"demo" in UTF-8
01 // --End of this part.
57 72 61 70 // --"Wrap" in UTF-8
01 // --End of this part.
00 00 00 00 // -Parent type (none)
// -This is a compound type, so we read its members.
00 00 00 21 // -Type-id of the first member (33, user defined)
00 00 00 01 61 // -Name of the member (string, "a")
00 00 00 21 // -Type-id of the second member (33, user defined)
00 00 00 01 62 // -Name of the member (string, "b")
00 00 00 22 // -Type-id of the third member (34, user defined)
00 00 00 01 63 // -Name of the member (string, "c")
00 00 00 22 // -Type-id of the fourth member (34, user defined)
00 00 00 01 64 // -Name of the member (string, "d")
00 00 00 22 // -Type-id of the fifth member (43, user defined)
00 00 00 01 65 // -Name of the member (string, "e")
00 00 00 00 // -No more members (type-id 0 is not used)
00 00 00 00 // Instance of this class (0, not present earlier)
// Since instance 0 was not serialized earlier, we read the data now
00 00 00 20 // Actual type of the instance (32, Wrap)
// Start reading an instance of type 33 for "a".
// -Type 33 is not known yet, read the type description first.
00 // --Type flags (value type)
00 00 00 09 // ---Length of the type name (9 bytes)
64 65 6D 6F 01 // ---The first part ("demo")
56 61 6C 01 // ---The second part ("Val")
00 00 00 00 // --Parent type (none)
00 00 00 03 // --Type-id of the first member (3, Int)
00 00 00 01 61 // --Name of the member (string, "a")
00 00 00 09 // --Type-id of the second member (9, Str)
00 00 00 01 62 // --Name of the member (string, "b")
00 00 00 00 // --No more members
// -Now, we start reading the "Val" instance for "a" inside "Wrap"
00 00 00 01 // -Value for "a" inside the "Val" instance (1)
00 00 00 03 // -Length of the string for "b" in the "Val" instance (3 bytes)
4F 6E 65 // --Content ("One")
// Done reading "Val", continue reading an instance for "b".
00 00 00 02 // -Value for "a" inside the "Val" instance (2)
00 00 00 03 // -Length of the string for "b" in the "Val" instance (3 bytes)
54 77 6F // --Content ("Two")
// Trying to read an instance of id 34 for "c".
// -Type 34 is not known yet, read the type description first.
01 // --Type flags (class type)
00 00 00 0A // --Length of the type name (10 bytes)
64 65 6D 6F 01 // ---The first part ("demo")
42 61 73 65 01 // ---The second part ("Base")
00 00 00 00 // --Parent type (none)
00 00 00 03 // --Type-id of the first member (3, Int)
00 00 00 01 61 // --Name of the member (string, "a")
00 00 00 00 // --No more members
00 00 00 01 // -Instance id (1, not present earlier)
00 00 00 23 // -Actual type (35) differs from formal type, previousl unknown.
// -Trying to read an instance of 35, we need a type description.
01 // --Type flags (class type)
00 00 00 0D // --Length of the type name (13 bytes)
64 65 6D 6F 01 // ---The first part ("demo")
44 65 72 69 //
76 65 64 01 // ---The second part ("Derived")
00 00 00 22 // --Parent type (34, Base)
00 00 00 03 // --Type of the first member (3, Int)
00 00 00 01 62 // --Name of the member (string, "b")
00 00 00 00 // --No more members
00 00 00 03 // -Data for "a" inside "Base" (3)
00 00 00 04 // -Data for "b" inside "Derived" (4)
// Continue reading an instance of type 34 for "d"
00 00 00 02 // -Instance id (2, not present earlier)
00 00 00 22 // -Actual type (34, Base)
00 00 00 05 // -Value for "a" inside "Base")
// Continue reading an instance of type 34 for "e"
00 00 00 02 // -Instance id (2, previously known, reuse that instance)
// Done reading "Wrap". Session 1 completed.
00 00 00 24 // Start of session 2, type-id of the first type (36, not known)
03 // -Type flags (class type, tuple)
00 00 00 17 // -Length of the type name (23 bytes)
63 6F 72 65 01 // --The first part ("core")
41 72 72 61 79 // --The second part: "Array"
02 64 65 6D 6F // --"<demo"
01 56 61 6C 01 // --".Val."
04 03 01 // --",>."
00 00 00 00 // -Parent type (none)
00 00 00 21 // -First type in each tuple (33, Val)
00 00 00 00 // -End of the list
00 00 00 00 // Instance id (0, not present earlier in this session)
00 00 00 24 // Actual type (36, Array<demo.Val>)
00 00 00 02 // Number of elements (2)
// Reading element 0, type 33
00 00 00 0A // -Value for "a" (10)
00 00 00 03 //
54 65 6E // -Value for "b" (string, "Ten")
// Reading element 1, type 33
00 00 00 14 // -Value for "a" (20)
00 00 00 06 54 //
77 65 6E 74 79 // -Value for "b" (string, "Twenty")
// Done reading "Array<demo.Val>". Session 2 completed.
```
|