====== 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