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
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 beifreemuss exakt der beiallocübergebenen Größe entsprechen. Das ist kein Versehen — es ermöglicht dem Allocator, ohne Metadaten-Overhead zu arbeiten.
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.
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.
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);
}
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;
}
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
}
Begründung für die gewählte Implementierungsstrategie:
alloc ist ein Syscall (mmap) oder ein einfacher Pointer-Bump; peek/poke sind direkte Load/Store-Instruktionen| 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