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 beifreemuss exakt der beiallocü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
allocist ein Syscall (mmap) oder ein einfacher Pointer-Bump;peek/pokesind 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
