====== std.pack — Binäre Serialisierung ======
→ [[lyx_-_programmiersprache:units|Zurück zur Unit-Übersicht]]
Schreibt und liest Werte in einem rohen Byte-Puffer: Ganzzahlen fester Breite (little-endian), VarInts, Bool, Strings mit Längenpräfix, Null-Marker und Array-Header. Kein Format-Header, keine Typkennung im Datenstrom — Schreiber und Leser müssen sich über die Reihenfolge einig sein.
Einsatzbereiche: eigene Binärformate, IPC, Netzwerkprotokolle, kompakte Ablage strukturierter Daten.
**Autor:** Andreas Röne\\
**Copyright:** 2024–2025 Andreas Röne\\
**Quelle:** ''std/pack.lyx''
**Seit lyxc 1.0.20F stimmt ''VarIntSize'' wieder mit ''WriteVarInt'' überein** (#1501), und der Überlauf ab 2^62 ist behoben. Rundlauf nachgemessen:
0 -> 1 Byte, VarIntSize=1, zurueck=0 ✓
300 -> 2 Byte, VarIntSize=2, zurueck=300 ✓
-1 -> 1 Byte, VarIntSize=1, zurueck=-1 ✓
4611686018427387904 -> 10 Byte, VarIntSize=10, zurueck identisch ✓
Die vorausberechnete Puffergröße trägt damit wieder, auch für 2^62.
===== Import =====
import std.pack;
----
===== Grundregeln =====
Drei Dinge, die man vor dem ersten Aufruf wissen muss:
**Der Rückgabewert ist die Bytezahl, nicht die neue Position.** ''PackInt64'' liefert 8, ''PackInt8'' liefert 1. Die Schreibposition führt der Aufrufer selbst weiter: ''pos := pos + PackInt64(buf, pos, wert);''. (Frühere Fassungen dieser Seite behaupteten das Gegenteil.)
**Der Puffer gehört dem Aufrufer.** Keine Funktion allokiert oder prüft Grenzen. Wie viel Platz nötig ist, rechnet man vorher aus — für Strings hilft ''StringPackSize'', für VarInts ''VarIntSize''.
**Es gibt keine Selbstbeschreibung.** Im Puffer steht nur der nackte Wert. Wer einen ''Int32'' geschrieben hat, muss beim Lesen ''UnpackInt32'' an derselben Position aufrufen — ein Versatz von einem Byte liefert stillschweigend Unsinn.
----
===== Funktionen =====
==== Ganzzahlen fester Breite ====
Alle little-endian, alle vorzeichenbehaftet interpretiert.
^ Signatur ^ Bytes ^ Beschreibung ^
| ''PackInt64(buf, pos, val: int64): int64'' | 8 | Schreibt 64-Bit-Wert |
| ''UnpackInt64(buf, pos: int64): int64'' | 8 | Liest 64-Bit-Wert |
| ''PackInt32(buf, pos, val: int64): int64'' | 4 | Schreibt untere 32 Bit |
| ''UnpackInt32(buf, pos: int64): int64'' | 4 | Liest 32 Bit, vorzeichenrichtig |
| ''PackInt16(buf, pos, val: int64): int64'' | 2 | Schreibt untere 16 Bit |
| ''UnpackInt16(buf, pos: int64): int64'' | 2 | Liest 16 Bit, vorzeichenrichtig |
| ''PackInt8(buf, pos, val: int64): int64'' | 1 | Schreibt unteres Byte |
| ''UnpackInt8(buf, pos: int64): int64'' | 1 | Liest 1 Byte, vorzeichenrichtig |
| ''PackBool(buf, pos, val: bool): int64'' | 1 | Schreibt 1 oder 0 |
| ''UnpackBool(buf, pos: int64): bool'' | 1 | Jeder Wert ungleich 0 gilt als ''true'' |
Die Pack-Funktionen schneiden den Wert auf ihre Breite zu, ohne einen Überlauf zu melden: ''PackInt8(buf, 0, 300)'' schreibt 44.
==== VarInt ====
Kompakte Kodierung mit 7 Nutzbits je Byte; das höchste Bit zeigt an, dass ein weiteres Byte folgt. Kleine Zahlen brauchen 1 Byte.
Seit dem Fix von #1463 wird **ZickZack-kodiert**: das Vorzeichen wandert ins unterste Bit, damit kleine negative Beträge kurz bleiben (−1 belegt 1 Byte statt zehn). Der Betragsbereich je Bytezahl halbiert sich dadurch — 63 passt noch in ein Byte, 64 braucht bereits zwei. **Das Format hat sich damit geändert:** Dateien, die eine ältere Fassung geschrieben hat, lassen sich nicht mehr lesen.
^ Signatur ^ Beschreibung ^
| ''WriteVarInt(buf, pos, val: int64): int64'' | Schreibt ''val'' als VarInt (ZickZack-kodiert), liefert die Bytezahl. Negative Werte sind zulässig; Beträge ab 2^62 laufen über (#1501) |
| ''ReadVarInt(buf, pos: int64): int64'' | Liest ein VarInt; bricht spätestens nach 10 Byte ab |
| ''VarIntSize(val: int64): int64'' | Bytezahl, die ''val'' belegen würde — **stimmt nicht mit ''WriteVarInt'' überein** (#1501) |
''VarIntSize'' ist beim Lesen unentbehrlich: ''ReadVarInt'' liefert nur den Wert, nicht die verbrauchte Bytezahl — die muss man sich über ''VarIntSize(gelesenerWert)'' zurückrechnen.
==== Strings ====
^ Signatur ^ Beschreibung ^
| ''PackString(buf, pos, s: int64): int64'' | Schreibt Längen-VarInt und danach die Zeichen (ohne Nullbyte); liefert die Gesamtbytezahl |
| ''UnpackString(buf, pos: int64): int64'' | Liefert einen Zeiger auf das **erste Zeichen** hinter dem Längenpräfix. **Nicht nullterminiert** |
| ''StringPackSize(s: int64): int64'' | Platzbedarf: Längenpräfix plus Zeichen |
==== Null-Marker und Arrays ====
^ Signatur ^ Beschreibung ^
| ''PackNull(buf, pos: int64): int64'' | Schreibt das Byte 255 als „kein Wert" |
| ''IsNull(buf, pos: int64): bool'' | Ob an ''pos'' das Byte 255 steht |
| ''PackArrayStart(buf, pos, count: int64): int64'' | Schreibt die Elementzahl als VarInt |
| ''UnpackArrayStart(buf, pos: int64): int64'' | Liest die Elementzahl |
''PackNull'' und ''IsNull'' sind eine reine Verabredung: 255 ist ein gültiges Datenbyte, ein Null-Marker ist also nur dort erkennbar, wo das Format an dieser Stelle einen erwartet.
==== Fließkomma ====
^ Signatur ^ Bytes ^ Beschreibung ^
| ''PackFloat64(buf, pos, val: f64): int64'' | 8 | IEEE-754-Bitmuster, little-endian |
| ''UnpackFloat64(buf, pos: int64): f64'' | 8 | Liest es zurück |
| ''PackFloat32(buf, pos, val: f64): int64'' | 4 | Rechnet auf 32-Bit-IEEE-754 um (Exponent-Bias 127, 23 Mantissenbits) |
| ''UnpackFloat32(buf, pos: int64): f64'' | 4 | Liest es zurück |
Der Rundlauf ist geprüft: 3.5, 1/3, −0.75 und 42.0 kommen unverändert zurück, ''PackFloat32'' verarbeitet 3.5 und 0.25 korrekt. Bei ''Float32'' ist der übliche Genauigkeitsverlust einzurechnen — 1/3 lässt sich in 32 Bit nicht exakt darstellen.
----
===== Beispiele =====
==== Ganzzahlen schreiben und lesen ====
import std.io;
import std.pack;
import std.alloc;
fn main(): int64 {
var buf: int64 := alloc(256);
var pos: int64 := 0;
// Schreiben: jede Pack-Funktion liefert die Anzahl geschriebener Bytes,
// die Position fuehrt der Aufrufer selbst weiter
pos := pos + PackInt64(buf, pos, -1234567890123);
pos := pos + PackInt32(buf, pos, -1000);
pos := pos + PackInt16(buf, pos, -300);
pos := pos + PackInt8(buf, pos, -5);
pos := pos + PackBool(buf, pos, true);
pos := pos + PackFloat64(buf, pos, fDiv(7 as f64, 2 as f64));
PrintLn("geschrieben: ", IntToStr(pos), " Bytes");
// Lesen: dieselbe Reihenfolge, Positionen erneut fortschreiben
var p: int64 := 0;
PrintLn("Int64 = ", IntToStr(UnpackInt64(buf, p))); p := p + 8;
PrintLn("Int32 = ", IntToStr(UnpackInt32(buf, p))); p := p + 4;
PrintLn("Int16 = ", IntToStr(UnpackInt16(buf, p))); p := p + 2;
PrintLn("Int8 = ", IntToStr(UnpackInt8(buf, p))); p := p + 1;
PrintLn("Bool = ", IntToStr(UnpackBool(buf, p) as int64)); p := p + 1;
PrintLn("Float64 = ", FloatToStr(UnpackFloat64(buf, p), 4));
return 0;
}
Ausgabe:
geschrieben: 24 Bytes
Int64 = -1234567890123
Int32 = -1000
Int16 = -300
Int8 = -5
Bool = 1
Float64 = 3.500000
Negative Werte kommen bei allen festen Breiten korrekt zurück — die Vorzeichenauslegung beim Entpacken wurde unter #1270 nachgezogen, die Fließkommadarstellung unter #1462.
==== Datensatz mit Strings und Array-Header ====
import std.io;
import std.pack;
import std.alloc;
fn main(): int64 {
var buf: int64 := alloc(256);
// Datensatz: Anzahl, dann je Eintrag ein String und eine Zahl
var pos: int64 := 0;
pos := pos + PackArrayStart(buf, pos, 3);
pos := pos + PackString(buf, pos, "Muster" as int64);
pos := pos + PackInt32(buf, pos, 42);
pos := pos + PackString(buf, pos, "Beispiel" as int64);
pos := pos + PackInt32(buf, pos, -7);
pos := pos + PackString(buf, pos, "Test" as int64);
pos := pos + PackInt32(buf, pos, 1000);
PrintLn("Datensatz: ", IntToStr(pos), " Bytes");
// Lesen
var p: int64 := 0;
var anzahl: int64 := UnpackArrayStart(buf, p);
p := p + VarIntSize(anzahl);
PrintLn("Eintraege: ", IntToStr(anzahl));
var i: int64 := 0;
while (i < anzahl) {
var slen: int64 := ReadVarInt(buf, p);
var sptr: int64 := UnpackString(buf, p);
// Der Zeiger ist NICHT nullterminiert — Zeichen einzeln ausgeben
Print(" '");
var j: int64 := 0;
while (j < slen) { PrintChar(peek8(sptr + j)); j := j + 1; }
p := p + VarIntSize(slen) + slen;
var zahl: int64 := UnpackInt32(buf, p);
p := p + 4;
PrintLn("' -> ", IntToStr(zahl));
i := i + 1;
}
return 0;
}
Ausgabe:
Datensatz: 34 Bytes
Eintraege: 3
'Muster' -> 42
'Beispiel' -> -7
'Test' -> 1000
Der Positionsvorschub beim Lesen eines Strings ist ''VarIntSize(slen) + slen'' — man braucht die Länge also **zweimal**: einmal zum Vorrücken, einmal zum Ausgeben. ''UnpackString'' allein reicht nicht.
----
===== Fallstricke =====
Geprüft mit ''lyxc 1.0.21A''.
==== VarIntSize passt nicht zu WriteVarInt (#1501) ====
Seit der Umstellung auf ZickZack rechnet ''WriteVarInt'' mit dem verdoppelten Betrag, ''VarIntSize'' aber weiterhin mit dem Originalwert. Für einen Teil des Wertebereichs meldet es deshalb **ein Byte zu wenig**:
^ Wert ^ ''WriteVarInt'' schreibt ^ ''VarIntSize'' meldet ^
| 63 | 1 | 1 |
| **64** | **2** | **1** |
| **127** | **2** | **1** |
| 128 | 2 | 2 |
| **8192** | **3** | **2** |
| −1 | 1 | 1 |
| **−65** | **2** | **1** |
| **−1000** | **2** | **1** |
Das trifft genau den dokumentierten Zweck der Funktion: Wer damit die Puffergröße vorausberechnet, reserviert zu knapp, und ''WriteVarInt'' schreibt darüber hinaus. Beim Lesen läuft die Positionsfortschreibung ''p := p + VarIntSize(wert)'' aus dem Takt.
Bis zum Fix die Bytezahl vom Schreiben **zurückgeben lassen** statt sie zu schätzen:
var geschrieben: int64 := WriteVarInt(buf, pos, wert);
pos := pos + geschrieben; // verlaesslich
Beim Lesen hilft das nicht — dort bleibt nur, Längen mit fester Breite (''PackInt32'') abzulegen, solange #1501 offen ist.
==== Beträge ab 2^62 laufen über (#1501) ====
4611686018427387903 -> 9 Byte, Rundlauf korrekt
4611686018427387904 -> 1 Byte, Rundlauf falsch
9223372036854775807 -> 1 Byte, gelesen wird 63
-9223372036854775808 -> 1 Byte, gelesen wird -64
Die ZickZack-Abbildung verdoppelt den Wert und läuft dabei für große Beträge über. Vor dem Fix von #1463 liefen wenigstens die nicht-negativen Werte bis zum int64-Maximum korrekt durch; dieser Bereich ist derzeit verloren. Für sehr große Zahlen ''PackInt64'' mit fester Breite verwenden.
==== UnpackString liefert keinen nullterminierten String ====
Der Zeiger zeigt in den Puffer, direkt hinter das Längenpräfix. Ein Nullbyte steht dort nicht:
var b: int64 := alloc(64);
var i: int64 := 0;
while (i < 64) { poke8(b + i, 88); i := i + 1; } // Puffer mit 'X' fuellen
PackString(b, 0, "Hallo" as int64);
var p: int64 := UnpackString(b, 0);
PrintLn("'", p as pchar, "'");
PrintLn("StrLen: ", IntToStr(StrLen(p as pchar)));
'HalloXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
StrLen: 63
Die Länge muss immer separat über ''ReadVarInt(buf, pos)'' geholt werden. Wer den Wert als ''pchar'' weiterreichen will, kopiert ihn in einen eigenen Puffer und terminiert dort selbst.
==== Kein Grenzschutz, keine Typprüfung ====
Keine Funktion prüft, ob der Puffer groß genug ist — ''PackInt64'' schreibt 8 Byte, wohin man zeigt. Ebenso wenig prüft ''UnpackInt32'', ob dort je ein Int32 stand. Ein um ein Byte verschobener Lesevorgang liefert eine plausible Zahl statt eines Fehlers.
Praktische Absicherung: Puffergröße vorab aus ''VarIntSize''/''StringPackSize'' und den festen Breiten summieren, und Schreib- wie Lesecode in **einer** Funktion nebeneinander halten, damit die Reihenfolge nicht auseinanderläuft.
==== Pack-Funktionen schneiden still zu ====
''PackInt8(buf, pos, 300)'' schreibt 44, ''PackInt16(buf, pos, 70000)'' schreibt 4464 — jeweils ohne Meldung. Wer Werte fremder Herkunft packt, prüft den Bereich vorher selbst.
----
===== Verwandte Units =====
* [[lyx_-_programmiersprache:units:base64|std.base64]] — Binärdaten in Text umwandeln
* [[lyx_-_programmiersprache:units:json|std.json]] — textbasierte, selbstbeschreibende Serialisierung
* [[lyx_-_programmiersprache:units:net:asn1|std.net.asn1]] — ASN.1/BER mit Typkennung im Datenstrom
* [[lyx_-_programmiersprache:units:alloc|std.alloc]] — Puffer für die Pack-Funktionen