Lyx trennt Datei-Arbeit in vier Ebenen: Builtins (rohe Syscalls, ohne Import), std.fs (Komfort: Pfade, Verzeichnisse, Kopieren), std.fs_ext (Metadaten, positioniertes Lesen, Nullkopie) und die Spezialisten std.mmap_ext, std.inotify, std.xattr, std.io_uring.
Es gibt keinen Datei-Handle-Typ und keine Streams. Ein geöffnetes Objekt ist ein int64-Deskriptor, ein Puffer ist eine Adresse — wem die Freigabe gehört, klärt Handles & FD, den Umgang mit rohen Puffern Rohspeicher. Wer das akzeptiert, kommt mit sehr wenig API aus.
→ Guides · Standard Library · Netzwerk-Guide
import std.fs; // Pfade, Verzeichnisse, ReadFile/WriteFile, Glob
import std.fs_ext; // stat, PRead/PWrite, Fsync, Sendfile, GetCwd
import std.alloc; // alloc / free für Puffer
| Ebene | Import nötig? | Wofür |
|---|---|---|
Builtins: open close read write lseek stat unlink rename mkdir rmdir pipe truncate ioctl getdents64 symlink readlink mmap munmap | nein | Rohzugriff, wenn genau ein Syscall gebraucht wird |
std.fs | ja | Datei in einem Aufruf lesen/schreiben, Pfade zerlegen, Verzeichnisse auflisten und rekursiv durchlaufen, Glob |
std.fs_ext | ja | stat-Auswertung, pread/pwrite, readv/writev, fsync, sendfile, Arbeitsverzeichnis |
std.mmap_ext / std.inotify / std.xattr / std.io_uring | ja | Speicherabbildung, Dateiüberwachung, erweiterte Attribute + Dateisystem-Belegung, asynchrone I/O |
<WRAP center round important 60%>
std.fs und std.fs_ext exportieren beide FileExists und FileSize. Wer beide importiert, bekommt:
sema error: mehrdeutiges Symbol 'FileExists' — exportiert von 'std.fs' und 'std.fs_ext'
Lyx hat keine qualifizierten Zugriffe (std.fs.FileExists gibt es nicht), also: eine der beiden Units wählen. Faustregel — Pfad-orientiert arbeiten → std.fs; über offene Deskriptoren arbeiten → std.fs_ext.
</WRAP>
Der kürzeste Weg braucht keinen Deskriptor und kein open:
unit main;
import std.fs;
import std.io;
import std.string;
fn main(): int64 {
var path: pchar := "/tmp/lyx_fs_demo.txt"c;
var text: pchar := "Hallo Lyx\n"c;
if (WriteFile(path, text, StrLen(text)) < 0) {
PrintLn("Schreiben fehlgeschlagen");
return 1;
}
var buf: [256]char;
var n: int64 := ReadFile(path, buf as pchar, 255);
PrintLn(StrConcat("Bytes gelesen: ", IntToStr(n)));
PrintLn(StrConcat("Groesse: ", IntToStr(FileSize(path))));
if (FileExists(path)) { PrintLn("existiert"); }
DeleteFile(path);
return 0;
}
Bytes gelesen: 10 Groesse: 10 existiert
| Funktion | Bedeutung |
|---|---|
WriteFile(path, buf, len) | Anlegen/Überschreiben, gibt geschriebene Bytes zurück (<0 = Fehler) |
AppendFile(path, buf, len) | Anhängen |
ReadFile(path, buf, max) | Liest bis max Bytes, gibt gelesene Bytes zurück |
FileSize(path) / FileSizeFast(path) | Größe (letzteres über stat, ohne Öffnen) |
FileCopy / FileMove / Rename / DeleteFile / Remove | Datei-Operationen, bool |
Chmod(path, mode) | Rechte setzen (DEFAULT_MODE = 0644) |
Puffer nicht vergessen zu terminieren. ReadFile schreibt keine abschließende 0 — wer den Puffer als pchar ausgeben will, setzt sie selbst: poke8(buf + n, 0).
unit main;
import std.fs;
import std.io;
import std.string;
fn main(): int64 {
var dl: int64 := DirList("demo"c);
if (dl == 0) { PrintLn("DirList fehlgeschlagen"); return 1; }
var n: int64 := DirEntryCount(dl);
var i: int64 := 0;
while (i < n) {
var name: pchar := DirEntryName(dl, i);
var t: int64 := DirEntryTypeAt(dl, i);
var kind: pchar := "Datei";
if (t == DT_DIR) { kind := "Verz."; }
PrintLn(StrConcat(StrConcat(kind, " "), name));
i := i + 1;
}
DirFree(dl);
var g: int64 := FileGlob("demo/*.txt"c);
PrintLn(StrConcat("Glob-Treffer: ", IntToStr(GlobCount(g))));
GlobFree(g);
var w: int64 := DirWalk("demo"c); // rekursiv
PrintLn(StrConcat("Rekursiv gefunden: ", IntToStr(DirWalkCount(w))));
DirWalkFree(w);
return 0;
}
Datei a.txt Verz. sub Datei b.log Glob-Treffer: 1 Rekursiv gefunden: 4
. und .. werden von DirList bereits herausgefiltert. Die Reihenfolge ist die des Dateisystems, nicht sortiert.
| Gruppe | Funktionen |
|---|---|
| Auflisten | DirList → DirEntryCount / DirEntryName(dl,i) / DirEntryTypeAt(dl,i) / DirEntryType(dl,name,len) → DirFree |
| Rekursiv | DirWalk → DirWalkCount / DirWalkPath(w,i,outBuf) / DirWalkType → DirWalkFree |
| Muster | FileGlob → GlobCount / GlobGet(g,i,outBuf) → GlobFree; GlobMatch(pattern, name) für Einzelprüfung |
| Anlegen | Mkdir(path, mode), MkDirAll(path) (wie mkdir -p), Rmdir |
| Temporär | MkTemp(pathBuf), TmpFile() |
Jede der drei Listen-APIs braucht ihr eigenes Free. DirFree / DirWalkFree / GlobFree sind nicht austauschbar.
Eintragstypen: DT_REG (Datei), DT_DIR, DT_LNK, DT_FIFO, DT_SOCK, DT_CHR, DT_BLK, DT_UNKNOWN.
Alle Pfad-Funktionen schreiben in einen vom Aufrufer gestellten Zielpuffer und geben ihn zurück:
| Funktion | Ergebnis für /var/log/app.tar.gz |
|---|---|
PathDir(dest, path) | /var/log |
PathBase(dest, path) | app.tar.gz |
PathExt(dest, path) | gz |
PathJoin(dest, a, b) | fügt mit genau einem / zusammen |
PathNormalize(dest, src) | löst . und .. auf |
PathResolve(dest, base, rel) | relativ gegen Basis auflösen |
IsAbsolutePath(path) | bool |
PathContainedIn(root, path) | bool — Schutz gegen Path-Traversal, vor jedem Zugriff auf vom Nutzer gelieferte Pfade verwenden |
unit main;
import std.fs_ext;
import std.alloc;
import std.io;
import std.string;
fn main(): int64 {
var fd: int64 := open("demo/a.txt"c, 0, 0); // O_RDONLY
if (fd < 0) { PrintLn("open fehlgeschlagen"); return 1; }
var st: int64 := alloc(STAT_SIZE);
if (Fstat(fd, st) < 0) { PrintLn("fstat fehlgeschlagen"); return 1; }
PrintLn(StrConcat("Groesse: ", IntToStr(StatFileSize(st))));
PrintLn(StrConcat("mtime: ", IntToStr(StatMtime(st))));
if (StatIsFile(st)) { PrintLn("regulaere Datei"); }
free(st, 0);
var buf: int64 := alloc(64);
var n: int64 := PRead(fd, buf, 16, 0); // ohne lseek
PrintLn(StrConcat("PRead Bytes: ", IntToStr(n)));
free(buf, 0);
close(fd);
var cwd: int64 := alloc(PATH_MAX);
GetCwd(cwd, PATH_MAX);
PrintLn(StrConcat("CWD: ", cwd as pchar));
free(cwd, 0);
return 0;
}
Groesse: 17 mtime: 1786470295 regulaere Datei PRead Bytes: 16 CWD: /tmp/…/scratchpad
| Aufgabe | Funktion |
|---|---|
| Metadaten | Fstat(fd, statBuf) mit statBuf = alloc(STAT_SIZE); auswerten mit StatFileSize / StatMtime / StatMode / StatIsDir / StatIsFile |
| Positioniert lesen/schreiben | PRead(fd, buf, n, off) / PWrite(…) — thread-sicher, verändert die Dateiposition nicht |
| Streuend lesen/schreiben | IovecSet(iov, idx, base, len) + ReadV / WriteV (IOVEC_SIZE = 16 Bytes pro Eintrag) |
| Auf Platte zwingen | Fsync(fd) (Daten + Metadaten), Fdatasync(fd) (nur Daten, schneller) |
| Kürzen/Verlängern | Truncate(path, len) / Ftruncate(fd, len) |
| Rechte prüfen | FileAccess(path, mode) mit R_OK/W_OK/X_OK/F_OK, bequemer: FileReadable / FileWritable |
| Arbeitsverzeichnis | GetCwd(buf, size), Chdir(path) |
| Nullkopie | Sendfile(outFd, inFd, offsetPtr, count) — kopiert im Kernel, ohne Userspace-Puffer |
Sendfile ist der schnellste Weg Datei→Datei und Datei→Socket:
var src: int64 := open("demo/a.txt"c, 0, 0);
var dst: int64 := open("demo/kopie.txt"c, 65 | 512, 420); // O_WRONLY|O_CREAT|O_TRUNC, 0644
var moved: int64 := Sendfile(dst, src, 0, 4096); // offsetPtr = 0 → ab aktueller Position
Fsync(dst);
close(src); close(dst);
mmap und munmap sind Builtins; std.mmap_ext liefert die Feinsteuerung.
unit main;
import std.fs_ext;
import std.mmap_ext;
import std.io;
import std.string;
con PROT_READ: int64 := 1;
con MAP_PRIVATE: int64 := 2;
fn main(): int64 {
var fd: int64 := open("demo/a.txt"c, 0, 0);
if (fd < 0) { return 1; }
var size: int64 := 4096;
var addr: int64 := mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr <= 0) { PrintLn("mmap fehlgeschlagen"); close(fd); return 1; }
MmapAdvise(addr, size, MADV_SEQUENTIAL); // Kernel-Hinweis
PrintLn(StrConcat("erstes Byte: ", IntToStr(peek8(addr))));
if (MemIsResident(addr)) { PrintLn("Seite ist im RAM"); }
munmap(addr, size);
close(fd);
return 0;
}
erstes Byte: 105 Seite ist im RAM
| Funktion | Zweck |
|---|---|
MmapAdvise(addr, len, advice) | MADV_SEQUENTIAL (Readahead hoch), MADV_RANDOM, MADV_WILLNEED (vorladen), MADV_DONTNEED, MADV_HUGEPAGE, MADV_COLD, MADV_PAGEOUT |
MmapSync(addr, len, flags) | Änderungen zurückschreiben — MS_SYNC (blockierend) oder MS_ASYNC |
MmapResize / MmapResizeFixed | mremap — Abbildung vergrößern, ohne neu zu kopieren |
MemFdCreate(name, flags) | anonyme Datei im RAM (MFD_CLOEXEC, MFD_ALLOW_SEALING) — ideal für IPC ohne Dateisystem |
MemLock / MemUnlock | Seiten gegen Swap sperren (Schlüsselmaterial!) |
MemIsResident(addr) / MemInCore(addr, len, vecOut) | prüfen, ob Seiten im RAM liegen |
MmapFreePages(addr, len) | Speicher an den Kernel zurückgeben, Abbildung bleibt |
Größe immer auf PAGE_SIZE (4096) aufrunden — der Kernel arbeitet nur seitenweise, und munmap mit falscher Länge lässt Reste liegen.
unit main;
import std.inotify;
import std.alloc;
import std.fs;
import std.io;
import std.string;
fn main(): int64 {
var ifd: int64 := InotifyCreate();
if (ifd < 0) { PrintLn("inotify nicht verfuegbar"); return 1; }
var wd: int64 := InotifyWatch(ifd, "demo"c, IN_CREATE | IN_DELETE | IN_MODIFY);
if (wd < 0) { PrintLn("Watch fehlgeschlagen"); return 1; }
WriteFile("demo/neu.txt"c, "x"c, 1); // löst Ereignisse aus
var buf: int64 := alloc(4096);
var n: int64 := InotifyRead(ifd, buf, 4096); // blockiert bis Ereignis
var p: int64 := buf;
var ende: int64 := buf + n;
while (p < ende) {
var mask: int64 := InotifyEventMask(p);
var name: int64 := InotifyEventName(p);
var was: pchar := "sonstiges";
if ((mask & IN_CREATE) != 0) { was := "erstellt"; }
if ((mask & IN_MODIFY) != 0) { was := "geaendert"; }
if ((mask & IN_DELETE) != 0) { was := "geloescht"; }
PrintLn(StrConcat(StrConcat(name as pchar, ": "), was));
p := InotifyEventNext(p); // nächster Eintrag im Puffer
}
free(buf, 0);
InotifyUnwatch(ifd, wd);
close(ifd);
return 0;
}
neu.txt: erstellt neu.txt: geaendert
Ein InotifyRead liefert mehrere Ereignisse variabler Länge am Stück. Deshalb immer die Schleife mit InotifyEventNext verwenden — feste Schrittweiten sind falsch (INOTIFY_EVENT_HDR_SIZE = 16 plus Namenslänge).
| Maske | Bedeutung |
|---|---|
IN_CREATE / IN_DELETE / IN_MODIFY | Datei erstellt / gelöscht / geändert |
IN_CLOSE_WRITE | Schreib-Datei geschlossen — das richtige Signal für „Datei fertig geschrieben„, nicht IN_MODIFY |
IN_MOVED_FROM / IN_MOVED_TO | Verschiebung; über InotifyEventCookie paarbar |
IN_DELETE_SELF / IN_MOVE_SELF | überwachtes Objekt selbst betroffen |
IN_ISDIR | zusätzliches Bit: Ereignis betrifft ein Verzeichnis |
IN_Q_OVERFLOW | Queue übergelaufen — Ereignisse wurden verloren, Zustand neu einlesen |
IN_ALL_EVENTS | alles |
InotifyCreate überwacht nicht rekursiv. Für Bäume: DirWalk laufen lassen und jedes Verzeichnis einzeln registrieren, bei IN_CREATE | IN_ISDIR nachregistrieren.
unit main;
import std.xattr;
import std.fs_ext;
import std.alloc;
import std.io;
import std.string;
fn main(): int64 {
var val: pchar := "andreas"c;
if (XattrSet("demo/a.txt"c, "user.autor"c, val as int64, StrLen(val), 0) < 0) {
PrintLn("xattr nicht unterstuetzt (Dateisystem?)");
} else {
var buf: int64 := alloc(256);
var n: int64 := XattrGet("demo/a.txt"c, "user.autor"c, buf, 256);
poke8(buf + n, 0);
PrintLn(StrConcat("Autor: ", buf as pchar));
free(buf, 0);
XattrRemove("demo/a.txt"c, "user.autor"c);
}
var sfb: int64 := alloc(STATFS_SIZE);
if (StatFs("/tmp"c, sfb) == 0) {
PrintLn(StrConcat("frei (MB): ", IntToStr(StatFsFreeBytes(sfb) / 1048576)));
}
free(sfb, 0);
return 0;
}
Autor: andreas frei (MB): 39079
user. beginnen, sonst lehnt der Kernel ab.XATTR_CREATE = nur anlegen, XATTR_REPLACE = nur ersetzen, 0 = beides.FXattrSet / FXattrGet / FXattrList / FXattrRemove.StatFs(path, buf) + StatFsTotalBytes / StatFsFreeBytes / StatFsBlockSize.FileAllocate(fd, offset, size); Löcher stanzen mit FileAllocateMode(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, off, size).Für viele parallele Lese-/Schreibvorgänge ohne Threads. Ablauf: Ring anlegen → SQE holen → Operation eintragen → abschicken → CQE einsammeln → als gesehen markieren.
unit main;
import std.io_uring;
import std.alloc;
import std.io;
import std.string;
fn main(): int64 {
var ring: int64 := IoUringCreate(8); // Ringgröße (Zweierpotenz)
if (ring == 0) { PrintLn("io_uring nicht verfuegbar"); return 1; }
var fd: int64 := open("demo/a.txt"c, 0, 0);
var buf: int64 := alloc(256);
var sqe: int64 := IoUringGetSQE(ring);
IoUringSQESetRead(sqe, fd, buf, 64, 0, 42); // len=64, offset=0, userData=42
IoUringSubmitAndWait(ring, 1);
var cqe: int64 := IoUringWaitCQE(ring);
var res: int64 := IoUringCQEResult(cqe); // >=0 Bytes, <0 negativer errno
var ud: int64 := IoUringCQEUserData(cqe);
IoUringCQESeen(ring, cqe); // Pflicht — sonst läuft der Ring voll
PrintLn(StrConcat("gelesen: ", IntToStr(res)));
PrintLn(StrConcat("userData: ", IntToStr(ud)));
free(buf, 0);
close(fd);
IoUringFree(ring);
return 0;
}
gelesen: 17 userData: 42
userData ist frei wählbar und der einzige Weg, eine Completion ihrer Anfrage zuzuordnen — typischerweise ein Index oder eine Objektadresse.IoUringSQESetRead / IoUringSQESetWrite / IoUringSQESetNop; die Opcodes für Netzwerk (IORING_OP_ACCEPT, IORING_OP_SEND, IORING_OP_RECV, IORING_OP_CONNECT) sind als Konstanten vorhanden und können direkt ins SQE gepokt werden.IoUringSubmit + IoUringPeekCQE (gibt 0 zurück, wenn nichts fertig ist) — das ist die Ereignisschleifen-Variante.| Aufgabe | Empfehlung |
|---|---|
| Kleine Datei ganz lesen/schreiben | ReadFile / WriteFile (std.fs) |
| Konfigurationsdatei zeilenweise | ReadFile + std.string-Funktionen |
| Datei > RAM durchsuchen | mmap + MADV_SEQUENTIAL |
| Datei kopieren | FileCopy (bequem) oder Sendfile (schnell, kein Userspace-Puffer) |
| Nebenläufig aus einer Datei lesen | PRead — kein gemeinsamer Positionszeiger |
| Datei→Socket ausliefern | Sendfile |
| Verzeichnis überwachen | std.inotify, Signal IN_CLOSE_WRITE |
| Hunderte parallele I/Os | std.io_uring |
| Datenbank-artige Haltbarkeit | schreiben → Fdatasync(fd) → Rename (atomarer Austausch) |
| Metadaten an Dateien hängen | std.xattr, Namensraum user. |
| Archive/Kompression | eigene Units: std.tar, std.zip, std.gzip, std.zstd, std.brotli, std.zlib, std.rar |
free. std.alloc bietet alloc(n) / free(ptr, size) und malloc(n) / calloc(n,sz) / realloc_mem / free_mem(ptr). Beide arbeiten seit 1.0.17C ohne C-Bibliothek über eigene mmap-Belegung (bis dahin gab malloc im statischen Binary 0 zurück — #1179). Nicht mischen: ein alloc-Zeiger gehört an free, ein malloc-Zeiger an free_mem. Rückgabe immer gegen 0 prüfen.std.fs gibt oft bool zurück, std.fs_ext und die Builtins geben 0 bei Erfolg bzw. den negativen errno bei Fehler, ReadFile/WriteFile geben Bytezahlen. Immer gegen < 0 prüfen, nie gegen != 0.defer und keine Destruktoren — jeder Pfad, der die Funktion verlässt, muss close selbst aufrufen. Bei Fehlerbehandlung mit frühem return ist das die häufigste Lücke.O_*-Flags sind Zahlen, keine Namen. std.fs exportiert O_RDONLY (0), O_WRONLY (1), O_RDWR (2), O_CREAT (64), O_TRUNC (512), O_APPEND (1024), O_EXCL (128), O_DIRECTORY (65536). Wer nur std.fs_ext importiert, definiert sie selbst — mit genau diesen Werten, sonst öffnet open scheinbar zufällig etwas anderes.mode ist dezimal, nicht oktal. 0644 schreibt man in Lyx als 420, 0755 als 493. Dafür gibt es DEFAULT_MODE und DEFAULT_DIR_MODE.PathNormalize, dann PathContainedIn(root, path) prüfen.DirWalk lädt das Ergebnis komplett in den Speicher. Bei sehr großen Bäumen besser DirList pro Ebene und selbst absteigen.Weiterführend: Netzwerk-Guide (epoll, Sockets) · Daten & Serialisierung · RTOS & Nebenläufigkeit · Audio & Video