Inhaltsverzeichnis

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.

std.yaml · std.json · std.pack · std.base64 · 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:


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.
(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;
}

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": <sicher>, "aktiv": true}

Antworten auswerten

Für den Weg hinein — fremdes JSON in eigene Werte — fehlt in 1.0.16F der Objektzugriff. Drei gangbare Wege:

  1. Format wählen, wenn man kann. Bei einer eigenen API auf std.pack oder YAML ausweichen.
  2. 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.
  3. 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 := "<config><name>server</name><port>8080</port></config>"c;
    PrintBoolLn(IsValid(doc));                        // true
    Print("Elemente: "); PrintLn(IntToStr(CountElements(doc)));   // 3

    var esc: int64 := alloc(256);
    EscapeText("a<b&c"c, esc as pchar);
    var e: pchar := esc as pchar;
    PrintLn(e);                                       // a&lt;b&amp;c

    var out: int64 := alloc(4096);
    WriteDocument(out as pchar, "root"c, "Inhalt"c, "1.0"c, "UTF-8"c);
    var o: pchar := out as pchar;
    PrintLn(o);
    // <?xml version="1.0" encoding="UTF-8"?>
    // <root>Inhalt</root>
    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

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

Alle Codebeispiele dieser Seite sind mit lyxc 1.0.16F übersetzt und ausgeführt worden; die angegebenen Ausgaben sind die tatsächlichen.