Auto-packing to/from cells/slices/builders
A short demo of how it looks:
struct Point {
x: int8;
y: int8;
}
var value: Point = { x: 10, y: 20 };
// makes a cell containing "0A14"
var c = value.toCell();
// back to { x: 10, y: 20 }
var p = Point.fromCell(c);
Key features of auto-serialization
- supports all types: unions, tensors, nullables, generics, atomics, ...
- allows you to specify serialization prefixes (particularly, opcodes)
- allows you to manage cell references and when to load them
- lets you control error codes and other behavior
- unpacks data from a cell or a slice, mutate it or not
- packs data to a cell or a builder
- warns if data potentially exceeds 1023 bits
- more efficient than manual serialization
List of supported types and how they are serialized
A small reminder: Tolk has intN
types (int8
, uint64
, etc.). Of course, they can be nested, like nullable int32?
or a tensor (uint5, int128)
.
They are just integers at the TVM level, they can hold any value at runtime: overflow only happens at serialization.
For example, if you assign 256 to uint8, asm command "8 STU" will fail with code 5 (integer out of range).
Type | TL/B Equivalent | Serialization Notes |
---|---|---|
int8 , uint55 , etc. | same as TL/B | N STI / N STU |
coins | TL/B varint16 | STGRAMS |
bytes8 , bits123 , etc. | just N bits | runtime check + STSLICE (1) |
address | MsgAddress (internal/external/none) | STSLICE (2) |
bool | one bit | 1 STI |
cell | untyped reference, TL-B ^Cell | STREF |
cell? | maybe reference, TL-B (Maybe ^Cell) | STOPTREF |
Cell<T> | typed reference, TL-B ^T | STREF |
Cell<T>? | maybe typed reference, TL-B (Maybe ^T) | STOPTREF |
RemainingBitsAndRefs | rest of slice | STSLICE |
builder | only for writing, not for reading | STBR |
T? | TL/B (Maybe T) | 1 STI + IF ... |
T1 | T2 | TL/B (Either T1 T2) | 1 STI + IF ... + ELSE ... (3) |
T1 | T2 | ... | TL/B multiple constructors | IF ... + ELSE IF ... + ELSE ... (4) |
(T1, T2) | TL/B (Pair T1 T2) = one by one | pack T1 + pack T2 |
(T1, T2, ...) | nested pairs = one by one | pack T1 + pack T2 + ... |
SomeStruct | fields one by one | like a tensor |
-
(1) By analogy with
intN
, there is arebytesN
types. It's just aslice
under the hood: the type shows how to serialize this slice. By default, beforeSTSLICE
, the compiler inserts runtime checks (get bits/refs count + compare with N + compare with 0). These checks ensure that serialized binary data will be correct, but they cost gas. However, if you guarantee that a slice is valid (for example, it comes from trusted sources), pass an optionskipBitsNFieldsValidation
to disable runtime checks. -
(2) In TVM, all addresses are also plain slices. Type
address
indicates that it's a slice containing some valid address (internal/external/none). It's packed withSTSLICE
(no runtime checks) and loaded withLDMSGADDR
. Don't confuse "address none" with null! "None" is a valid address (two zero bits), whereasaddress?
is "maybe address" (bit '0' OR bit '1' + address). -
(3) TL/B Either is expressed with a union
T1 | T2
. For example,int32 | int64
is packed as ('0' + 32-bit int OR '1' + 64-bit int). However, if T1 and T2 are both structures with manual serialization prefixes, those prefixes are used instead of a 0/1 bit. -
(4) To (un)pack a union, say,
Msg1 | Msg2 | Msg3
, we need serialization prefixes. For structures, you can specify them manually (or the compiler will generate them right here). For primitives, likeint32 | int64 | int128 | int256
, the compiler generates a prefix tree (00/01/10/11 in this case). Read "auto-generating serialization prefixes" below.
Some examples of valid types
struct A {
f1: int8; // just int8
f2: int8?; // maybe int8
f3: address; // internal/external/none
f4: bool; // TRUE (-1) serialized as bit '1'
f5: B; // embed fields of struct B
f6: B?; // maybe B
f7: coins; // used for money amounts
r1: cell; // always-existing untyped ref
r2: Cell<B>; // typed ref
r3: Cell<int32>?; // optional ref that stores int32
u1: int32 | int64; // Either
u2: B | C; // also Either
u3: B | C | D; // manual or autogenerated prefixes
u4: bits4 | bits8?; // autogenerated prefix tree
// even this works
e: Point | Cell<Point>;
// rest of slice
rest: RemainingBitsAndRefs;
}
Serialization prefixes and opcodes
Declaring a struct, there is a special syntax to provide pack prefixes. Typically, you'll use 32-bit prefixes for messages opcodes, or arbitrary prefixes is case you'd like to express TL/B multiple constructors.
struct (0x7362d09c) TransferNotification {
queryId: uint64;
...
}
Prefixes can be of any width, they are not restricted to be 32 bit.
0x000F
— 16-bit prefix0x0F
— 8-bit prefix0b010
— 3-bit prefix0b00001111
— 8-bit prefix
Declaring messages with 32-bit opcodes does not differ from declaring any other structs. Say, you the following TL/B scheme:
asset_simple$001 workchain:int8 ptr:bits32 = Asset;
asset_booking$1000 order_id:uint64 = Asset;
...
You can express the same with structures and union types:
struct (0b001) AssetSimple {
workchain: int8;
ptr: bits32;
}
struct (0b1000) AssetBooking {
orderId: uint64;
}
type Asset = AssetSimple | AssetBooking | ...;
struct ProvideAssetMessage {
...
asset: Asset;
}
When deserializing, Asset
will follow manually provided prefixes:
// msg.asset is parsed as '001' + int8 + bits32 OR ...
val msg = ProvideAssetMessage.fromSlice(s);
// now, msg.asset is just a union
// you can match it
match (msg.asset) {
AssetSimple => { // smart cast
msg.asset.workchain
msg.asset.ptr
}
...
}
// or test with `is` operator
if (msg.asset is AssetBooking) {
...
}
// or do any other things with a union:
// prefixes play their role only in deserialization process
When serializing, everything also works as expected:
val out: ProvideAssetMessage = {
...,
asset: AssetSimple { // will be serialized as
workchain: ..., // '001' + int8 + bits32
ptr: ...
}
}