Rohspeicher: alloc, peek & poke

Die gesamte Lyx-Standardbibliothek arbeitet auf einer einzigen Speicherabstraktion: alloc(n) liefert einen rohen Byte-Puffer als int64-Adresse, peek8/poke8 lesen und schreiben einzelne Bytes, peek64/poke64 lesen und schreiben 8-Byte-Wörter. Kein GC, keine versteckten Allokationen, kein Overhead.

Diese Seite erklärt das Muster und zeigt, wie es in der Praxis eingesetzt wird. Sie ergänzt die allgemeine Seite Memory Management um die Low-Level-Schicht, auf der die Standardbibliothek aufbaut.

std.alloc · Handle-Konzept · Fehler-Konventionen


alloc und free

std.alloc stellt zwei Grundoperationen bereit:

Funktion Signatur Beschreibung
alloc alloc(n: int64): int64 Allokiert n Bytes. Gibt die Startadresse zurück, 0 bei OOM. Inhalt undefiniert (nicht nullisiert).
free free(ptr: int64, n: int64): void Gibt n Bytes ab ptr frei. Beide Parameter sind Pflicht — Lyx hat keine versteckte Größeninformation.

import std.alloc;

fn CreateBuffer(size: int64): int64 {
    var buf: int64 := alloc(size);
    if (buf == 0) { return 0; }   // OOM
    return buf;
}

fn DestroyBuffer(buf: int64, size: int64): void {
    free(buf, size);
}

Die Größe bei free muss exakt der bei alloc übergebenen Größe entsprechen. Das ist kein Versehen — es ermöglicht dem Allocator, ohne Metadaten-Overhead zu arbeiten.

peek und poke — Roher Speicherzugriff

Lyx liest und schreibt Speicher byte-genau über vier Primitive:

Funktion Liest/Schreibt Beschreibung
peek8(addr) 1 Byte Liest uint8 an Adresse addr
poke8(addr, v) 1 Byte Schreibt uint8 v an Adresse addr
peek64(addr) 8 Bytes Liest int64 little-endian an Adresse addr
poke64(addr, v) 8 Bytes Schreibt int64 v little-endian an Adresse addr

Diese Primitiven sind in der Sprache eingebaut — kein Import nötig. Sie sind das Fundament für alle Struct-ähnlichen Datenstrukturen in der Standardbibliothek.


Structs als Byte-Offset-Konstanten

Da Lyx keine generischen Structs mit dynamischer Größe im ABI kennt, werden interne Datenstrukturen als flache Byte-Puffer mit benannten Offset-Konstanten implementiert. Das Muster durchzieht die gesamte Standardbibliothek:

// Konzeptueller Aufbau einer internen Datenstruktur (Beispiel)
//
// Offset   Größe   Feld
// 0        8       fd (Dateideskriptor)
// 8        8       buf (Zeiger auf Lesepuffer)
// 16       8       bufLen (belegte Bytes im Puffer)
// 24       8       flags
//
// Gesamtgröße: 32 Bytes

pub con MY_STRUCT_SIZE:    int64 := 32;
pub con MY_STRUCT_FD:      int64 := 0;
pub con MY_STRUCT_BUF:     int64 := 8;
pub con MY_STRUCT_BUFLEN:  int64 := 16;
pub con MY_STRUCT_FLAGS:   int64 := 24;

fn CreateMyStruct(fd: int64): int64 {
    var s: int64 := alloc(MY_STRUCT_SIZE);
    if (s == 0) { return 0; }
    poke64(s + MY_STRUCT_FD,     fd);
    poke64(s + MY_STRUCT_BUF,    0);
    poke64(s + MY_STRUCT_BUFLEN, 0);
    poke64(s + MY_STRUCT_FLAGS,  0);
    return s;
}

fn GetFd(s: int64): int64 {
    return peek64(s + MY_STRUCT_FD);
}

fn SetBufLen(s: int64, len: int64): void {
    poke64(s + MY_STRUCT_BUFLEN, len);
}

fn DestroyMyStruct(s: int64): void {
    var buf: int64 := peek64(s + MY_STRUCT_BUF);
    if (buf != 0) { free(buf, 4096); }   // innere Ressourcen zuerst
    free(s, MY_STRUCT_SIZE);
}

Dasselbe Muster findet sich in std.db.sqlite (SQLiteDB, 24 Bytes), std.db.postgres (PGConn, 120 Bytes), allen PDF-Untermodulen und allen Netzwerk-Units.


Byte-Arrays befüllen und lesen

import std.alloc;

fn ZeroFill(ptr: int64, n: int64): void {
    var i: int64 := 0;
    while (i < n) {
        poke8(ptr + i, 0);
        i := i + 1;
    }
}

fn CopyBytes(dst: int64, src: int64, n: int64): void {
    var i: int64 := 0;
    while (i < n) {
        poke8(dst + i, peek8(src + i));
        i := i + 1;
    }
}

fn ReadInt32BE(ptr: int64, off: int64): int64 {
    // Big-Endian uint32 lesen (Netzwerk-Byteorder)
    return (peek8(ptr + off)     << 24) |
           (peek8(ptr + off + 1) << 16) |
           (peek8(ptr + off + 2) <<  8) |
            peek8(ptr + off + 3);
}

fn WriteInt32BE(ptr: int64, off: int64, v: int64): void {
    poke8(ptr + off,     (v >> 24) & 0xFF);
    poke8(ptr + off + 1, (v >> 16) & 0xFF);
    poke8(ptr + off + 2, (v >>  8) & 0xFF);
    poke8(ptr + off + 3,  v        & 0xFF);
}


Dynamisch wachsende Puffer

Das Standard-Muster für wachsende Puffer in der Bibliothek — analoges Verhalten zu einem StringBuilder oder einem dynamischen Array:

import std.alloc;

// Typische Puffer-Verwaltung wie in std.pdf.builder, std.db.postgres, …
fn GrowBuf(oldBuf: int64, oldCap: int64, needed: int64): int64 {
    var newCap: int64 := oldCap * 2;
    while (newCap < needed) { newCap := newCap * 2; }

    var newBuf: int64 := alloc(newCap);
    if (newBuf == 0) { return 0; }

    // Inhalt kopieren
    var i: int64 := 0;
    while (i < oldCap) {
        poke8(newBuf + i, peek8(oldBuf + i));
        i := i + 1;
    }

    free(oldBuf, oldCap);
    return newBuf;
}


Freigabe-Reihenfolge

Bei verschachtelten Strukturen (Struct enthält Zeiger auf weiteren Heap-Speicher) gilt immer: von innen nach außen freigeben.

// Falsch — Memory Leak:
fn LeakyFree(conn: int64): void {
    free(conn, CONN_SIZE);           // conn wird freigegeben, aber
    // conn.buf wurde nicht freigegeben → Leak
}

// Richtig:
fn CorrectFree(conn: int64): void {
    var buf: int64 := peek64(conn + CONN_BUF);
    if (buf != 0) {
        free(buf, peek64(conn + CONN_BUFCAP));   // erst innere Ressource
    }
    free(conn, CONN_SIZE);                        // dann äußere Struktur
}


Warum dieser Ansatz?

Begründung für die gewählte Implementierungsstrategie:

  • Deterministische Performance — kein GC, kein Stop-the-world, keine versteckten Allokationen
  • Null Overhead — ein alloc ist ein Syscall (mmap) oder ein einfacher Pointer-Bump; peek/poke sind direkte Load/Store-Instruktionen
  • Portierbarkeit — das Muster funktioniert identisch auf x86-64, ARM64 und Android (Bionic)
  • Nachvollziehbarkeit — jeder Speicherzugriff ist im Quellcode sichtbar; kein Magic Proxy, kein Reflection

Zusammenfassung

Aufgabe Mittel
Puffer anlegen alloc(n)int64-Adresse
Puffer freigeben free(ptr, n) — Größe muss stimmen
Einzelbyte lesen/schreiben peek8(addr) / poke8(addr, v)
8-Byte-Wort lesen/schreiben peek64(addr) / poke64(addr, v)
Strukturfeld lesen peek64(base + OFFSET)
Strukturfeld schreiben poke64(base + OFFSET, v)
Mehrere Ressourcen freigeben Innen zuerst, außen zuletzt

Handle-Konzept — warum alles ein int64 ist
std.alloc — Funktionsreferenz
Memory Management — Stack, Heap, Safety

Letzte Aktualisierung: 2026-06-05