====== Daten & Serialisierung ====== Daten in eine Datei, über ein Netz oder in eine Konfiguration bringen — und wieder zurück. Diese Seite ordnet die Formate ''std.yaml'', ''std.json'', ''std.pack'', ''std.base64'' und ''std.xml'' ein, zeigt für jedes den Arbeitsweg und benennt, **was in lyxc 1.0.16F tatsächlich trägt** und was nicht. → [[lyx_-_programmiersprache:units:yaml|std.yaml]] · [[lyx_-_programmiersprache:units:json|std.json]] · [[lyx_-_programmiersprache:units:pack|std.pack]] · [[lyx_-_programmiersprache:units:base64|std.base64]] · [[lyx_-_programmiersprache:units:conv|std.conv]] ---- ===== 1. Welches Format für welchen Zweck? ===== ^ Aufgabe ^ Format ^ Unit ^ Zustand ^ | Konfigurationsdatei, von Menschen gepflegt | YAML | ''std.yaml'' | **einsatzfähig**, auch verschachtelt — Struktur nicht im Betrieb erweitern | | Eigenes Binärformat, Datei oder Protokoll | Pack | ''std.pack'' | **einsatzfähig** | | Binärdaten in Text verpacken (URLs, Header, Data-URLs) | Base64 | ''std.base64'' | **einsatzfähig** in beide Richtungen | | JSON erzeugen, prüfen, Werte maskieren | JSON | ''std.json'' | teilweise — siehe Abschnitt 4 | | Zahlen als Bytes, Endianness, Hex/Binär | — | ''std.conv'' | einsatzfähig | | XML schreiben, prüfen, flach lesen | XML | ''std.xml'' | einsatzfähig — kein Dokumentbaum, siehe Abschnitt 7 | Zwei Grundentscheidungen vorweg: * **Menschen oder Maschinen?** Was ein Mensch bearbeitet, gehört nach YAML. Was nur zwischen zwei Programmen fließt, gehört in ein Binärformat — kompakter, schneller, ohne Parser-Überraschungen. * **Selbst festlegen oder fremdes Format bedienen?** Wer das Format bestimmt, nimmt ''std.pack''. Wer JSON von einer fremden API bekommt, hat in 1.0.16F mehr Arbeit, als ihm lieb ist (Abschnitt 4). ---- ===== 2. YAML — Konfiguration ===== ''std.yaml'' hält ein Dokument als Handle (''int64'') und greift über Pfad-Zeichenketten darauf zu. Lesen, Ändern, Schreiben — alles über dasselbe Handle. import std.io; import std.alloc; import std.yaml; fn main(): int64 { var doc: int64 := ParseString("name: server\nport: 8080\ndebug: true\n"c); if (doc == 0) { PrintLn("Parse fehlgeschlagen"); return 1; } PrintLn(GetString(doc, "name"c, "?"c)); // server PrintLn(IntToStr(GetInt(doc, "port"c, 0))); // 8080 PrintBoolLn(GetBool(doc, "debug"c, false)); // true PrintBoolLn(HasPath(doc, "fehlt"c)); // false SetInt(doc, "port"c, 9090); PrintLn(IntToStr(GetInt(doc, "port"c, 0))); // 9090 var out: int64 := alloc(4096); var n: int64 := WriteString(doc, out as pchar, 4096); Print("geschrieben: "); PrintLn(IntToStr(n)); // 36 var s: pchar := out as pchar; PrintLn(s); // name: server // port: 9090 // debug: true return 0; } ==== Der Werkzeugkasten ==== ^ Lesen ^ Schreiben ^ Prüfen ^ | ''GetString'', ''GetInt'', ''GetFloat'', ''GetBool'' | ''SetString'', ''SetInt'', ''SetFloat'', ''SetBool'' | ''HasPath'', ''GetType'' | | ''GetArray'', ''GetArrayLen'', ''GetObject'', ''GetKeys'' | ''SetArray'', ''SetObject'', ''DeleteKey'' | ''IsValidKey'', ''NeedsQuoting'' | | ''LoadFile(path)'' | ''SaveFile(doc, path)'', ''WriteFile'', ''WriteString'' | — | Jede ''Get''-Funktion nimmt einen **Vorgabewert** entgegen. Das ist bequem und gefährlich zugleich: Ein fehlender Schlüssel ist damit nicht vom vorhandenen Schlüssel mit demselben Wert zu unterscheiden. Wo der Unterschied zählt, vorher ''HasPath'' fragen. ==== Verschachtelte Pfade ==== Ein Punktpfad greift in die Struktur: var doc: int64 := LoadFile("cfg.yaml"c); PrintLn(GetString(doc, "server.host"c, "?"c)); // localhost PrintLn(IntToStr(GetInt(doc, "server.port"c, 0))); // 8080 PrintLn(IntToStr(GetArrayLen(doc, "users"c))); // 2 SetInt(doc, "server.port"c, 9090); // ändert den vorhandenen Wert SaveFile(doc, "cfg.yaml"c); > **Zwei Reste beim Schreiben und Prüfen:** > > * ''SetString(doc, "server.name"c, "prod"c)'' für einen **neuen** Schlüssel unter einem vorhandenen Knoten hängt einen **zweiten** ''server:''-Block an das Dateiende, statt in den bestehenden zu schreiben. Vorhandene Schlüssel zu ändern arbeitet dagegen korrekt. > * ''GetType(doc, "server"c)'' liefert für einen Knoten weiterhin ''0''. > > Praktische Folge: die Struktur einer Konfiguration beim Schreiben nicht erweitern — Schlüssel, die später gesetzt werden sollen, von Anfang an in der Datei anlegen. Geprüft mit lyxc 1.0.16F, stdlib-Stand ''80479041''. > ([[https://github.com/SEOLizer/LyX-Compiler/issues/1312|Issue #1312]]) ==== Datei rein, Datei raus ==== var doc: int64 := LoadFile("config.yaml"c); if (doc == 0) { PrintLn("Datei nicht lesbar"); return 1; } SetString(doc, "stand"c, "produktiv"c); if (!SaveFile(doc, "config.yaml"c)) { PrintLn("Schreiben fehlgeschlagen"); return 1; } ''LoadFile'' gibt ''0'' zurück, wenn die Datei fehlt oder nicht lesbar ist — der Rückgabewert ist also **immer** zu prüfen, sonst laufen alle folgenden ''Get''-Aufrufe stumm in ihre Vorgabewerte. ---- ===== 3. Pack — eigenes Binärformat ===== ''std.pack'' schreibt Werte hintereinander in einen Puffer. Kein Schema, keine Feldnamen, kein Overhead — Schreib- und Lesereihenfolge müssen zusammenpassen, sonst kommt Unsinn heraus. import std.io; import std.alloc; import std.pack; fn main(): int64 { var buf: int64 := alloc(256); var pos: int64 := 0; pos := pos + PackInt64(buf, pos, -1234567); pos := pos + PackInt32(buf, pos, -42); pos := pos + PackBool(buf, pos, true); pos := pos + PackString(buf, pos, "hallo"c as int64); Print("Bytes gesamt: "); PrintLn(IntToStr(pos)); // 19 var rp: int64 := 0; PrintLn(IntToStr(UnpackInt64(buf, rp))); rp := rp + 8; // -1234567 PrintLn(IntToStr(UnpackInt32(buf, rp))); rp := rp + 4; // -42 PrintBoolLn(UnpackBool(buf, rp)); rp := rp + 1; // true var slen: int64 := ReadVarInt(buf, rp); // Länge zuerst var sptr: int64 := UnpackString(buf, rp); // dann die Zeichen var i: int64 := 0; while (i < slen) { PrintChar(peek8(sptr + i)); i := i + 1; } PrintLn(""); // hallo free(buf, 256); return 0; } > **Die ''Pack''-Funktionen geben die Anzahl geschriebener Bytes zurück, nicht die neue Position.** ''pos := PackInt64(buf, pos, x)'' setzt die Position auf 8 zurück, statt sie vorzurücken — die folgenden Werte überschreiben einander, und beim Lesen kommt Unsinn heraus (''-40694658876570247'' statt ''-1234567''). Richtig ist immer: > > > pos := pos + PackInt64(buf, pos, x); > ==== Feste und variable Breiten ==== ^ Funktion ^ Bytes ^ Anmerkung ^ | ''PackInt64'' / ''UnpackInt64'' | 8, little-endian | Standardfall | | ''PackInt32'' / ''UnpackInt32'' | 4 | Ergebnis **vorzeichenbehaftet** ausgelegt | | ''PackInt16'', ''PackInt8'' | 2 / 1 | für kompakte Felder | | ''PackBool'' | 1 | | | ''PackFloat64'' / ''PackFloat32'' | 8 / 4 | | | ''WriteVarInt'' / ''ReadVarInt'' | 1–10 | kleine Zahlen kosten ein Byte | | ''PackString'' | VarInt-Länge + Zeichen | ''VarIntSize'' liefert die Präfixgröße | | ''PackNull'' / ''IsNull'' | 1 | Marker ''255'' | | ''PackArrayStart'' / ''UnpackArrayStart'' | VarInt | Elementzahl voranstellen | ''UnpackString'' liefert einen Zeiger auf die **Zeichen** (das Längenpräfix ist übersprungen). Die Länge muss man sich vorher mit ''ReadVarInt'' an derselben Position holen — sie steht nicht am Ende und die Zeichen sind **nicht** nullterminiert. ==== Ein Datensatz mit Kopf ==== // Schreiben: erst die Anzahl, dann die Elemente var pos: int64 := 0; pos := pos + PackArrayStart(buf, pos, 3); var k: int64 := 0; while (k < 3) { pos := pos + PackInt32(buf, pos, werte[k]); k := k + 1; } // Lesen: Anzahl holen, Position um die Präfixgröße vorrücken var anzahl: int64 := UnpackArrayStart(buf, 0); var rp: int64 := VarIntSize(anzahl); Für Formate, die andere Programme lesen sollen, gehört an den Anfang **immer** eine Version: ''PackInt16(buf, 0, 1)''. Ohne sie lässt sich ein Format später nicht mehr erweitern, ohne alle alten Dateien unlesbar zu machen. ---- ===== 4. JSON ===== ''std.json'' arbeitet intern mit einem **pipe-separierten** Zwischenformat: ein JSON-Array wird zu ''1|2|3'' und zurück. Einen Objektzugriff („hole ''user.name''") gibt es nicht. Was nachweislich trägt: import std.io; import std.alloc; import std.json; fn main(): int64 { PrintBoolLn(isValidJSON("{\"a\":1}"c)); // true PrintBoolLn(isValidJSON("[1,2,3]"c)); // true PrintBoolLn(isValidJSON("nonsense"c)); // false var d1: int64 := alloc(1024); var rc: int64 := parseArray(d1 as pchar, "[1,2,3]"c); // rc = ERR_JSON_OK var pipe: pchar := d1 as pchar; PrintLn(pipe); // 1|2|3 var d2: int64 := alloc(1024); var json: pchar := stringify(d2 as pchar, "1|2|3"c); PrintLn(json); // ["1","2","3"] var d3: int64 := alloc(1024); var esc: pchar := JSONEscape(d3 as pchar, "a\"b"c); PrintLn(esc); // "a\"b" return 0; } * ''isValidJSON'' prüft Wohlgeformtheit — brauchbar als Eingangsprüfung, bevor man selbst Hand anlegt. * ''parseArray'' und ''toArray'' liefern das Pipe-Format; der Rückgabewert von ''parseArray'' ist ein **Fehlercode** (''ERR_JSON_OK'' = 0), nicht die Elementzahl. * ''stringify'' und ''serializeArray'' gehen den Weg zurück. ''stringify'' setzt alle Elemente als **Zeichenketten** — aus ''1|2|3'' wird ''["1","2","3"]'', nicht ''[1,2,3]''. * ''JSONEscape'' maskiert korrekt und ist das Werkzeug der Wahl, wenn man JSON selbst zusammensetzt. ==== JSON erzeugen, ohne den Parser zu brauchen ==== Ausgehende Anfragen sind der einfachere Fall: Man baut den Text selbst und maskiert nur die Werte. var esc: int64 := alloc(512); var sicher: pchar := JSONEscape(esc as pchar, benutzername); // {"name": , "aktiv": true} ==== Antworten auswerten ==== Für den Weg hinein — fremdes JSON in eigene Werte — fehlt in 1.0.16F der Objektzugriff. Drei gangbare Wege: - **Format wählen, wenn man kann.** Bei einer eigenen API auf ''std.pack'' oder YAML ausweichen. - **Gezielt suchen statt vollständig parsen.** Mit ''std.string'' die Position von ''"feld":'' suchen und den Wert bis zum nächsten Komma oder ''}'' herausschneiden. Robust genug für bekannte, stabile Antworten — kein Ersatz für einen Parser bei fremden Daten. - **Arrays über das Pipe-Format.** Reine Wertelisten (''[1,2,3]'') lassen sich mit ''toArray'' verarbeiten. ---- ===== 5. Base64 ===== Binärdaten durch Kanäle bringen, die nur Text vertragen: HTTP-Header, URLs, Data-URLs, Mail-Anhänge. import std.io; import std.alloc; import std.base64; fn main(): int64 { var out: int64 := alloc(256); var n: int64 := Encode("Hallo Welt"c, out as pchar); var s: pchar := out as pchar; Print("Länge: "); PrintLn(IntToStr(n)); // 16 PrintLn(s); // SGFsbG8gV2VsdA== PrintBoolLn(IsValid(s)); // true return 0; } ^ Funktion ^ Zweck ^ | ''Encode'' / ''EncodeBytes'' | Standard-Alphabet; ''EncodeBytes'' auch für Daten mit Nullbytes | | ''EncodeUrlSafe'' | ''-'' und ''_'' statt ''+'' und ''/'' — für URLs und JWT | | ''EncodedLen'' / ''DecodedLen'' | Puffergröße **vorher** ausrechnen | | ''IsValid'' / ''IsValidChar'' / ''HasValidPadding'' | Eingangsprüfung | | ''AddPadding'' / ''StripPadding'' | ''=''-Auffüllung angleichen | | ''EncodeBasicAuth'' | fertiger Wert für einen ''Authorization''-Header | | ''EncodeDataUrl'' | ''data:image/png;base64,…'' für HTML/SVG | Der Rückweg arbeitet ebenso: var back: int64 := alloc(256); var m: int64 := Decode("SGFsbG8gV2VsdA=="c, back as pchar, 256); var t: pchar := back as pchar; PrintLn(t); // Hallo Welt (m = 10) Puffer immer über ''EncodedLen''/''DecodedLen'' bemessen und ein Byte für die Nullterminierung zugeben — Base64 wächst um ein Drittel. ---- ===== 6. Zahlen und Bytes — std.conv ===== Die Brücke zwischen Zahlen und ihrer Byte-Darstellung. Unverzichtbar, sobald ein fremdes Binärformat oder ein Netzprotokoll im Spiel ist. import std.io; import std.conv; fn main(): int64 { PrintLn(IntToHex32(255)); // 000000FF PrintLn(IntToStr(SwapEndian32(1))); // 16777216 PrintLn(IntToStr(ParseHex("FF"c))); // 255 return 0; } ^ Gruppe ^ Funktionen ^ | Darstellung | ''IntToHex8/16/32/64'', ''IntToBin8/16/32/64'', ''IntToOct'', ''IntToStrWithComma'' | | Zurücklesen | ''ParseHex'', ''ParseBin'', ''ParseOct'' | | Byte-Reihenfolge | ''SwapEndian16/32/64'', ''GetByte'', ''SetByte'' | | Bits | ''GetBit'', ''SetBit'', ''ClearBit'', ''ToggleBit'', ''ExtractBits'', ''InsertBits'' | | Breiten | ''SignExtend8/16/32'', ''ZeroExtend8/16/32'', ''ClampUint8/16/32'' | | Ausrichtung | ''AlignUp'', ''AlignDown'', ''IsAligned'' | ''SwapEndian32'' ist die Antwort auf Netzwerk-Byte-Reihenfolge: Werte aus Paket-Headern kommen big-endian, Lyx rechnet little-endian. ''SignExtend8/16/32'' braucht, wer schmale vorzeichenbehaftete Felder aus einem Puffer liest — ohne den Schritt wird aus ''-1'' die Zahl ''255''. ---- ===== 7. XML ===== ''std.xml'' schreibt und prüft XML; zum Lesen gibt es eine flache Array-Darstellung statt eines Baums. import std.io; import std.alloc; import std.xml; fn main(): int64 { var doc: pchar := "server8080"c; PrintBoolLn(IsValid(doc)); // true Print("Elemente: "); PrintLn(IntToStr(CountElements(doc))); // 3 var esc: int64 := alloc(256); EscapeText("a // Inhalt return 0; } ^ Aufgabe ^ Funktion ^ | Prüfen und zählen | ''IsValid'', ''CountElements'' | | Maskieren | ''EscapeText'', ''EscapeAttribute'' | | Schreiben | ''WriteDeclaration'', ''WriteElement'', ''WriteDocument'' | | Lesen | ''ParseString'', ''XMLToArray'', ''GetArrayEntryName'', ''GetArrayEntryText'' | | Formatieren | ''PrettyPrint'' | * **Einen Dokumentbaum gibt es nicht.** ''XMLToArray'' liefert eine flache Liste von Name/Text-Paaren; Verschachtelung und Attribute gehen dabei verloren. Für tief strukturiertes XML ist das zu wenig. * ''PrettyPrint'' übernimmt vorhandene Zeilenumbrüche mit — die Ausgabe enthält den Textinhalt samt Umbruch vor dem schließenden Tag. * ''EscapeText'' und ''EscapeAttribute'' sind auch dann das richtige Werkzeug, wenn man die Ausgabe von Hand zusammensetzt. ---- ===== 8. Muster, die überall gelten ===== ==== Puffer bemessen, nicht raten ==== Jede Serialisierungsfunktion schreibt in einen Puffer, den der Aufrufer stellt. Es gibt **keine** Größenprüfung — ein zu kleiner Puffer überschreibt Nachbarspeicher. Die Größe kommt entweder aus einer Rechenfunktion (''EncodedLen'', ''VarIntSize'', ''StringPackSize'') oder aus einer bewusst gesetzten Obergrenze, die man beim Schreiben mitprüft. ==== Ergebnisse als Zeichenkette ausgeben ==== var s: pchar := puffer as pchar; PrintLn(s); // richtig PrintLn(satz.name); // pchar-FELD: gibt die ADRESSE aus Ein ''pchar''-**Feld** eines Structs geht als Zahl durch die Ausgabe — der Umweg über eine Variable hilft. Die Typumwandlung im Argument (''%%PrintLn(puffer as pchar)%%'') war bis lyxc 1.0.15G ebenso betroffen und arbeitet seit **1.0.16F** korrekt; ''PrintStrLn'' in keinem der Fälle. ==== Drei Fehlerarten auseinanderhalten ==== ^ Fehler ^ Erkennbar an ^ Reaktion ^ | Eingabe fehlt | ''LoadFile'' → 0, ''ParseString'' → 0 | abbrechen, melden | | Eingabe ist kaputt | ''isValidJSON'' → false, Fehlercode ≠ 0 | ablehnen, nicht raten | | Feld fehlt | ''HasPath'' → false | Vorgabewert bewusst setzen | Der dritte Fall ist der gefährliche: Er sieht wie ein Erfolg aus. Wer ''GetInt(doc, "port"c, 0)'' schreibt und nie ''HasPath'' fragt, startet seinen Dienst irgendwann auf Port 0 und sucht den Fehler woanders. ==== Freigeben ==== ^ Angelegt mit ^ Freigegeben mit ^ | ''alloc(n)'' | ''free(ptr, n)'' — dieselbe Größe | | ''ParseString'', ''LoadFile'' (YAML-Handle) | über die Lebensdauer des Programms halten oder Dokument neu laden | ---- ===== 9. Zustand auf einen Blick (lyxc 1.0.16F) ===== ^ Unit ^ Kodieren/Schreiben ^ Dekodieren/Lesen ^ | ''std.yaml'' | ✅ ''WriteString'', ''SaveFile'' · ⚠️ neuer Schlüssel unter vorhandenem Knoten hängt einen zweiten Block an | ✅ auch verschachtelte Pfade | | ''std.pack'' | ✅ | ✅ | | ''std.base64'' | ✅ | ✅ | | ''std.json'' | ✅ ''stringify'', ''JSONEscape'' | ⚠️ nur Arrays über Pipe-Format | | ''std.conv'' | ✅ | ✅ | | ''std.xml'' | ✅ ''WriteDocument'', ''EscapeText'' | ⚠️ flache Liste, kein Baum, keine Attribute | ---- ===== 10. Weiterführend ===== * [[lyx_-_programmiersprache:units:yaml|std.yaml]] · [[lyx_-_programmiersprache:units:json|std.json]] · [[lyx_-_programmiersprache:units:pack|std.pack]] · [[lyx_-_programmiersprache:units:base64|std.base64]] · [[lyx_-_programmiersprache:units:conv|std.conv]] · [[lyx_-_programmiersprache:units:xml|std.xml]] * [[lyx_-_programmiersprache:guides:netzwerk|Netzwerk-Guide]] — Antworten holen, die hier ausgewertet werden * [[lyx_-_programmiersprache:sprache:rohspeicher|Rohspeicher: alloc, peek & poke]] * [[lyx_-_programmiersprache:sprache:std-fehlerkonventionen|Fehlerkonventionen der Standardbibliothek]] Alle Codebeispiele dieser Seite sind mit lyxc 1.0.16F übersetzt und ausgeführt worden; die angegebenen Ausgaben sind die tatsächlichen.