From 48d552bf3a42af81ced7e1f87a874e1f63632396 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Mon, 6 Jul 2026 16:09:13 +0300 Subject: [PATCH] =?UTF-8?q?libc:=20FILE*=20v2=20=E2=80=94=20=D0=B1=D1=83?= =?UTF-8?q?=D1=84=D0=B5=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=D0=BA=D0=BE=D0=B2=20(=D0=B2=D0=B0=D1=80?= =?UTF-8?q?=D0=B8=D0=B0=D0=BD=D1=82=20B+)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - единый ленивый буфер BUFSIZ=512 на чтение и запись с автопереключением направления (_F_DIROUT, _file_sync: запись сбрасывается write()-ом, readahead откатывается lseek-ом) - статическая таблица OPEN_MAX=8 слотов вместо malloc для FILE; _fclosall через atexit — exit() сбрасывает несброшенную запись - fread/fwrite: мелкое через буфер (memcpy), блоки >= BUFSIZ — мимо буфера одним syscall; горячие пути fgetc/fputc и сканер строк fgets (LDI до '\n') — на asm, SDCC на эти цепочки генерит ~90 инструкций с IX-фреймом - новое: ungetc (1 байт через hold, работает и на stdin), fprintf/vfprintf (vsprintf+fwrite), fflush(NULL) = все потоки - фиксы stdio-review: fwrite ставит _F_ERROR при короткой записи (issue 3), fgets(n=1) возвращает пустую строку (issue 4) - замер (MAME, HDD, 100 КБ): небуферизованная оценка ~144 с → fgetc 5 с (×29), fgets ~1 с; дизайн и отвергнутые варианты — docs/file-buffering-design.md Co-Authored-By: Claude Fable 5 --- docs/file-buffering-design.md | 161 +++++++++++++++++++ libc/file/_fclosall.c | 14 ++ libc/file/_file.h | 38 +++++ libc/file/_file_buf.c | 18 +++ libc/file/_file_slots.c | 12 ++ libc/file/_file_sync.c | 42 +++++ libc/file/clearerr.c | 11 ++ libc/file/fclose.c | 28 ++++ libc/file/feof.c | 11 ++ libc/file/ferror.c | 11 ++ libc/file/fflush.c | 22 +++ libc/file/fgetc.c | 101 ++++++++++++ libc/file/fgets.c | 84 ++++++++++ libc/file/file.c | 290 ---------------------------------- libc/file/fopen.c | 89 +++++++++++ libc/file/fprintf.c | 15 ++ libc/file/fputc.c | 100 ++++++++++++ libc/file/fputs.c | 17 ++ libc/file/fread.c | 82 ++++++++++ libc/file/fseek.c | 26 +++ libc/file/ftell.c | 28 ++++ libc/file/fwrite.c | 60 +++++++ libc/file/rewind.c | 15 ++ libc/file/std_streams.c | 27 ++++ libc/file/ungetc.c | 21 +++ libc/file/vfprintf.c | 23 +++ libc/include/stdio.h | 49 ++++-- 27 files changed, 1093 insertions(+), 302 deletions(-) create mode 100644 docs/file-buffering-design.md create mode 100644 libc/file/_fclosall.c create mode 100644 libc/file/_file.h create mode 100644 libc/file/_file_buf.c create mode 100644 libc/file/_file_slots.c create mode 100644 libc/file/_file_sync.c create mode 100644 libc/file/clearerr.c create mode 100644 libc/file/fclose.c create mode 100644 libc/file/feof.c create mode 100644 libc/file/ferror.c create mode 100644 libc/file/fflush.c create mode 100644 libc/file/fgetc.c create mode 100644 libc/file/fgets.c delete mode 100644 libc/file/file.c create mode 100644 libc/file/fopen.c create mode 100644 libc/file/fprintf.c create mode 100644 libc/file/fputc.c create mode 100644 libc/file/fputs.c create mode 100644 libc/file/fread.c create mode 100644 libc/file/fseek.c create mode 100644 libc/file/ftell.c create mode 100644 libc/file/fwrite.c create mode 100644 libc/file/rewind.c create mode 100644 libc/file/std_streams.c create mode 100644 libc/file/ungetc.c create mode 100644 libc/file/vfprintf.c diff --git a/docs/file-buffering-design.md b/docs/file-buffering-design.md new file mode 100644 index 0000000..1fc14dc --- /dev/null +++ b/docs/file-buffering-design.md @@ -0,0 +1,161 @@ +# FILE*: буферизация — анализ solid-c и варианты (2026-07-06) + +Статус: **вариант B+ РЕАЛИЗОВАН 2026-07-06** (решение пользователя). +Единый ленивый буфер BUFSIZ=512 на чтение/запись с автопереключением +направления (_F_DIROUT), статическая таблица OPEN_MAX=8 слотов +(без malloc для FILE), _fclosall через atexit, ungetc через hold, +fprintf/vfprintf через vsprintf+fwrite, fflush(NULL) = все потоки. +Внутренности: libc/file/_file.h (+_file_sync/_file_buf/_file_slots/ +_fclosall). Фактическая цена: filetest 7411→9929 Б _CODE (доля +только-читающих потребителей ~+1.3 КБ, включая malloc); программы +без FILE* не платят. Верификация: MAME filetest + fdmax + fbench. +Ниже — исходный анализ, на основании которого принималось решение. + +## Как сделано в solid-c (SRC/CLIB/STDIO.ASM) + +Структура FILE — 14 байт, статический массив `_iob[8]` (без malloc +для самих FILE; псевдопотоки stdout/stderr/stdaux/stdprn лежат ПЕРЕД +массивом и адресуются отрицательными индексами — трюк, нам не нужен): + + flags(2), level(2), curp(2), fd(2), buffer(2), hold(1), token(2), dummy(1) + +Механика: + +- **Буфер 512 Б, ленивый malloc** при первом буферизуемом fgetc/fputc + (флаг `_F_BUF` = «буфер наш, free при fclose»). Программа без + файлового I/O не платит ничего. +- **Чтение** (`_fgetc`): `level == 0` → `read(fd, buffer, 512)`, + `curp = buffer`; отдача — `*curp++`, `level--`; ставится `_F_IN`. +- **Запись** (`_fputc`): `*curp++ = c`, `level++`; при `level == 512` — + fflush (один `write` всего буфера); ставится `_F_OUT`. +- **Полудуплекс**: fputc при взведённом `_F_IN` — ОШИБКА (не + авто-flush); направление сбрасывает только fflush. +- **fflush входного потока**: `lseek(fd, -level, SEEK_CUR)` — откат + непрочитанного readahead, буфер инвалидируется. Выходного — + `write(buffer, level)`. +- **fseek/ftell** = fflush + голый lseek/ltell по fd (после flush + позиция fd совпадает с логической позицией потока). +- **ungetc**: буфер не пуст → `*--curp = c`; пуст/отсутствует → символ + в поле `hold`, `curp` указывает на hold. +- **fclosall через atexit** — сброс буферов при exit. +- Консольные потоки минуют буфер (RST-вызовы напрямую). +- **fread/fwrite — ПОБАЙТОВЫЙ цикл** через _fgetc/_fputc: большие блоки + платят call+IY-доступ за каждый байт. Это слабое место порта. + +## Варианты для нас + +**A. Полный порт solid-c** (буфер на чтение и запись). +Плюсы: ускоряются и писатели через fputc/fprintf. Минусы: полудуплекс +(«запись после чтения без fflush — ошибка») — источник тонких багов; +обязателен flush в exit (сцепка atexit+file); больше кода во всех +модулях; наши блочные fread/fwrite пришлось бы защищать от деградации. + +**B. Буферизовать ТОЛЬКО чтение (рекомендую).** +FILE += `buf(2), level(2), curp(2), hold(2)`; буфер 512 Б лениво. + +- fgetc: hold → буфер → refill. fgets остаётся циклом по fgetc + (теперь дешёвым). +- fread: сначала хвост буфера (memcpy), остаток ≥ 512 — прямой read() + в ptr одним syscall (обходя буфер), мелкий остаток — refill. +- Запись НЕ буферизуется — как сейчас: fputc = write(1 байт), + fputs/fwrite = один write() на блок. Нечего терять при аварии, + fflush остаётся no-op по данным, полудуплекса нет. +- Согласование позиций: перед write/fseek/ftell на потоке с readahead — + `lseek(fd, -(level), SEEK_CUR)` + инвалидация буфера (один общий + хелпер `_file_sync`). ftell = lseek(0,CUR) − level (без syscall не + выйдет — lseek и так syscall). +- ungetc: через hold, работает и до первого заполнения буфера. + +Плюсы: решает главную боль (парсеры), запись остаётся простой и +надёжной, никакого flush-on-exit, r+ работает через _file_sync. +Минусы: fputc-писатели остаются медленными (редкий паттерн — fputs/ +fwrite блочные и так быстрые). + +**B+. Единый буфер на чтение И запись с АВТОпереключением направления +(предложение 2026-07-06, кандидат в целевой дизайн).** +Схема solid-c, но без ловушки: направление переключает сама библиотека. + +- флаг направления в FILE: буфер сейчас «readahead» или «накопитель + записи»; +- fputc при направлении «чтение»: `_file_sync` (отмотка fd на -level, + буфер пуст) → режим записи → накопление; сброс write() при + заполнении; +- fgetc при направлении «запись»: flush (write(buf, level)) → режим + чтения → refill; +- fwrite больших блоков: flush + прямой write мимо буфера; мелких — + memcpy в буфер. fread симметрично; +- fseek/ftell/fclose: flush-или-sync по направлению; ftell = позиция + fd − level (чтение) / + level (запись); +- **обязателен реестр открытых потоков**: поле next в FILE + (регистрация в fopen, снятие в fclose) + _fclosall через atexit — + стандарт требует flush всех потоков в exit(); без этого + `fputs(...); exit(1);` теряет данные; +- цена-семантика: ошибки записи становятся ОТЛОЖЕННЫМИ (вылезают при + flush/fclose, не в момент fputc) — проверять результат fclose; +- цена-код: ~+350–500 Б против ~+200–300 у B (тянется только + использующими FILE*). + +**C. Оставить небуферизованным** («большие файлы читаются целиком в +EMM», паттерн mdview). Для приложений-парсеров среднего размера +неудобно; отвергается самим существованием П2-пункта. + +**D. Полная стандартная буферизация + setvbuf** — отвергнуто ранее +решением file_star_design (минимальный FILE*). + +## Что взять у solid-c при варианте B + +- ленивый malloc 512 Б + флаг «буфер наш»; +- откат readahead lseek'ом (механика их fflush-на-вход) — как + `_file_sync` перед write/fseek/ftell; +- ungetc с hold-байтом; +- консольные потоки мимо буфера (у нас уже так). + +Чего НЕ брать: побайтовые fread/fwrite, полудуплекс, статический +`_iob[]` с отрицательными индексами, буферизацию записи. + +## Лимит открытых файлов и статическая таблица FILE (2026-07-06) + +Факты: solid-c — OPEN_MAX = 8, статический массив из 8 FILE-структур +(+5 псевдопотоков перед ним), fdopen отвергает fd > 8. DSS-доки: +FCB строятся «в рабочих областях ДОС», код ошибки 06h = «Too many +open files» (наш EMFILE = 6 совпадает). + +**ПОДТВЕРЖДЕНО тестом fdmax (MAME, DSS 1.71.57, 2026-07-06)**: +пользователю доступно 8 манипуляторов, fd 2..9 (fd 1 держит шелл DSS +под запущенный .exe). КРИТИЧНО: 9-й OPEN не возвращает 06h — он +ВЕШАЕТ систему. Поэтому в libc/io добавлен предохранитель _fd_guard +(счётчик в open/close, отказ EMFILE на 9-м open без захода в DSS) — +таблица fopen и guard вместе закрывают и высокий, и низкий уровень. + +Следствие для B+: вместо malloc-FILE + связного списка-реестра — +**статическая таблица из 8 слотов** (свободный слот: flags == 0): +- реестр для flush-on-exit бесплатен: _fclosall = цикл по таблице; +- fopen без malloc — единственный отказ синхронен с отказом DSS + (EMFILE), утечка «fclose без free» невозможна; +- цена: ~112–128 Б BSS (в exe не входит), только у программ с fopen; +- буферы НЕ статические — остаются ленивыми malloc 512 Б (потолок + 8×512 = 4 КБ heap в худшем случае); +- объявить FOPEN_MAX 8 в stdio.h; если fdmax покажет лимит DSS < 8 — + уменьшить таблицу. + +## Семантика инвалидации (вариант B) — контрольный сценарий + +Буфер — только кэш опережающего чтения. Правило: **любая запись и +любой fseek обнуляют буфер; запись всегда идёт напрямую в файл после +отмотки позиции** (`_file_sync`: `lseek(fd, -level, SEEK_CUR)` + +`level = 0`). Буфер при записи НЕ патчится — write-through с правкой +окна отвергнут как сложный ради редкого паттерна. + +Сценарий «r+, чтение-запись-чтение» (обсуждено 2026-07-06): +read 512 в буфер → 10×fgetc (логическая поз. 10, fd на 512) → +первый fputc: sync отматывает fd на 10, буфер пуст, 10×write ложатся +на 10..19 → fseek(0) → fgetc перечитывает буфер С ДИСКА и видит +записанные байты. Протечка старой копии невозможна — она уничтожена +в момент первой записи. + +## Оценка/проверка + +Бенчмарк до/после: цикл fgets по tests/seek/big.txt с замером ptime +(тест tests/fbench), плюс filetest-регресс в MAME. Ожидание: чтение +~512× меньше syscall'ов; код file-модулей +200–300 Б (тянется только +использующими FILE*). diff --git a/libc/file/_fclosall.c b/libc/file/_fclosall.c new file mode 100644 index 0000000..85bf1f5 --- /dev/null +++ b/libc/file/_fclosall.c @@ -0,0 +1,14 @@ +/* + * _fclosall — сбросить буферы и закрыть все открытые потоки таблицы. + * Регистрируется через atexit() первым fopen — так exit() посреди + * программы не теряет несброшенную запись (требование стандарта). + */ + +#include "_file.h" + +void _fclosall(void) +{ + for (uint8_t i = 0; i < OPEN_MAX; i++) { + if (_file_slots[i].flags) fclose(&_file_slots[i]); + } +} diff --git a/libc/file/_file.h b/libc/file/_file.h new file mode 100644 index 0000000..d859b3f --- /dev/null +++ b/libc/file/_file.h @@ -0,0 +1,38 @@ +/* + * _file.h — внутренности буферизованного FILE* (вариант B+, НЕ + * публичный заголовок). Дизайн: docs/file-buffering-design.md. + * + * Инварианты: + * - слот таблицы свободен, когда flags == 0; + * - level < BUFSIZ на входе в любую операцию записи (конвенция + * «сброс ПЕРЕД записью в буфер»); + * - hold != EOF означает отложенный байт ungetc; логическая позиция + * потока на 1 меньше учтённой в level; + * - консольные псевдопотоки (_F_CONIN/_F_CONOUT) буфер не заводят + * и в _file_sync не попадают. + */ +#ifndef _FILE_INTERNAL_H +#define _FILE_INTERNAL_H + +#include + +/* Таблица потоков (_file_slots.c); свободный слот: flags == 0. */ +extern FILE _file_slots[OPEN_MAX]; + +/* Обеспечить буфер: вернуть fp->buf, при необходимости malloc(BUFSIZ). + * NULL = malloc не дал — вызывающий уходит на небуферизованный путь + * (прямой syscall на 1 байт), поток продолжает работать. */ +uint8_t *_file_buf(FILE *fp); + +/* Нейтрализовать буфер (перед seek/tell/close и при смене направления): + * - направление «запись»: write() несброшенного хвоста; + * - направление «чтение»: lseek(fd, -(level + hold), SEEK_CUR) — + * откат непрочитанного readahead и ungetc-байта. + * После вызова: level = 0, hold = EOF, _F_DIROUT снят, curp = buf. + * Возврат 0 или EOF (ставит _F_ERROR). */ +int _file_sync(FILE *fp); + +/* fclose всех открытых слотов; вешается на atexit первым fopen. */ +void _fclosall(void); + +#endif diff --git a/libc/file/_file_buf.c b/libc/file/_file_buf.c new file mode 100644 index 0000000..b018720 --- /dev/null +++ b/libc/file/_file_buf.c @@ -0,0 +1,18 @@ +/* + * _file_buf — ленивое выделение буфера потока: malloc(BUFSIZ) при + * первой буферизуемой операции. NULL при нехватке памяти — поток + * НЕ ломается, вызывающие уходят на прямой syscall (небуферизованный + * fallback), попытка выделения повторится при следующей операции. + */ + +#include +#include "_file.h" + +uint8_t *_file_buf(FILE *fp) +{ + if (!fp->buf) { + fp->buf = (uint8_t *)malloc(BUFSIZ); + fp->curp = fp->buf; + } + return fp->buf; +} diff --git a/libc/file/_file_slots.c b/libc/file/_file_slots.c new file mode 100644 index 0000000..02c52f8 --- /dev/null +++ b/libc/file/_file_slots.c @@ -0,0 +1,12 @@ +/* + * _file_slots — статическая таблица потоков FILE (только данные). + * + * OPEN_MAX слотов; свободный слот распознаётся по flags == 0 (BSS + * зануляется crt0). malloc для структур не используется — лимит + * одновременных файлов у DSS всё равно фиксированный (ошибка 06h + * "Too many open files"), таблица его зеркалит. + */ + +#include "_file.h" + +FILE _file_slots[OPEN_MAX]; diff --git a/libc/file/_file_sync.c b/libc/file/_file_sync.c new file mode 100644 index 0000000..65af565 --- /dev/null +++ b/libc/file/_file_sync.c @@ -0,0 +1,42 @@ +/* + * _file_sync — нейтрализовать буфер потока (см. _file.h и + * docs/file-buffering-design.md, «семантика инвалидации»). + * + * Направление «запись»: сбросить несброшенный хвост одним write(); + * короткая запись = ошибка потока. Направление «чтение»: откатить + * позицию fd назад на непрочитанный readahead (+1 за hold-байт + * ungetc) — после этого реальная позиция fd совпадает с логической + * позицией потока, буфер объявляется пустым. + */ + +#include +#include "_file.h" + +int _file_sync(FILE *fp) +{ + if (fp->flags & _F_DIROUT) { + uint16_t n = fp->level; + fp->flags &= (uint8_t)~_F_DIROUT; + fp->level = 0; + fp->curp = fp->buf; + if (n) { + int w = write(fp->fd, fp->buf, n); + if (w < 0 || (uint16_t)w != n) { + fp->flags |= _F_ERROR; + return EOF; + } + } + } else { + long back = fp->level; + if (fp->hold != EOF) { back++; fp->hold = EOF; } + fp->level = 0; + fp->curp = fp->buf; + if (back) { + if (lseek(fp->fd, -back, SEEK_CUR) < 0) { + fp->flags |= _F_ERROR; + return EOF; + } + } + } + return 0; +} diff --git a/libc/file/clearerr.c b/libc/file/clearerr.c new file mode 100644 index 0000000..9b8a227 --- /dev/null +++ b/libc/file/clearerr.c @@ -0,0 +1,11 @@ +/* + * clearerr — сбросить флаги _F_EOF и _F_ERROR потока. NULL-безопасно. + */ + +#include +#include + +void clearerr(FILE *fp) +{ + if (fp) fp->flags &= (uint8_t)~(_F_EOF | _F_ERROR); +} diff --git a/libc/file/fclose.c b/libc/file/fclose.c new file mode 100644 index 0000000..b1b901f --- /dev/null +++ b/libc/file/fclose.c @@ -0,0 +1,28 @@ +/* + * fclose — закрыть поток: сбросить буфер (_file_sync), закрыть fd, + * освободить буфер и слот таблицы. ВАЖНО: ошибки отложенной записи + * вылезают именно здесь — проверять результат fclose у пишущих + * потоков. Консольные псевдопотоки (stdin/stdout/stderr) распознаются + * по флагам и успешно игнорируются. + */ + +#include +#include +#include +#include "_file.h" + +int fclose(FILE *fp) +{ + if (!fp) { + errno = EBADF; + return EOF; + } + if (fp->flags & (_F_CONIN | _F_CONOUT)) { + return 0; + } + int rc = _file_sync(fp); /* сброс несброшенной записи */ + if (close(fp->fd) < 0) rc = EOF; + if (fp->buf) free(fp->buf); + fp->flags = 0; /* слот свободен */ + return rc; +} diff --git a/libc/file/feof.c b/libc/file/feof.c new file mode 100644 index 0000000..36f2513 --- /dev/null +++ b/libc/file/feof.c @@ -0,0 +1,11 @@ +/* + * feof — 1, если на потоке защёлкнут флаг конца файла (_F_EOF), + * иначе 0. NULL-безопасно. + */ + +#include + +int feof(FILE *fp) +{ + return fp && (fp->flags & _F_EOF) ? 1 : 0; +} diff --git a/libc/file/ferror.c b/libc/file/ferror.c new file mode 100644 index 0000000..b18af7b --- /dev/null +++ b/libc/file/ferror.c @@ -0,0 +1,11 @@ +/* + * ferror — 1, если на потоке защёлкнут флаг ошибки (_F_ERROR), + * иначе 0. NULL-безопасно. + */ + +#include + +int ferror(FILE *fp) +{ + return fp && (fp->flags & _F_ERROR) ? 1 : 0; +} diff --git a/libc/file/fflush.c b/libc/file/fflush.c new file mode 100644 index 0000000..72beb65 --- /dev/null +++ b/libc/file/fflush.c @@ -0,0 +1,22 @@ +/* + * fflush — сбросить буфер потока: накопленную запись — write()'ом, + * readahead — откатом позиции fd (стандартная POSIX-семантика fflush + * для читаемых потоков). fflush(NULL) сбрасывает ВСЕ открытые потоки + * таблицы (по стандарту). Консольные псевдопотоки небуферизованы — + * всегда успех. + */ + +#include "_file.h" + +int fflush(FILE *fp) +{ + if (!fp) { + int rc = 0; + for (uint8_t i = 0; i < OPEN_MAX; i++) { + if (_file_slots[i].flags && _file_sync(&_file_slots[i])) rc = EOF; + } + return rc; + } + if (fp->flags & (_F_CONIN | _F_CONOUT)) return 0; + return _file_sync(fp); +} diff --git a/libc/file/fgetc.c b/libc/file/fgetc.c new file mode 100644 index 0000000..149a389 --- /dev/null +++ b/libc/file/fgetc.c @@ -0,0 +1,101 @@ +/* + * fgetc — прочитать один байт из потока (буферизованно, вариант B+). + * + * Горячий путь («в readahead-буфере есть байт») написан на asm — + * SDCC на эту цепочку проверок генерит ~90 инструкций с IX-фреймом, + * asm укладывается в ~30 без фрейма (замер fbench: ~176 мкс/байт + * у C-версии). Всё остальное — медленный C-путь fgetc_slow: + * hold-байт от ungetc, консольный getchar(), сброс накопленной + * записи, дозаполнение буфера read(fd, buf, BUFSIZ), fallback без + * буфера. Возвращает байт, EOF на конце файла (_F_EOF) или при + * ошибке (_F_ERROR). + * + * Раскладка FILE (см. stdio.h, байтовые смещения): + * +0 fd, +2 flags, +3 buf, +5 curp, +7 level, +9 hold + */ + +#include +#include +#include "_file.h" + +/* Медленный путь; зовётся из asm-обёртки (метка _fgetc_slow видна в + * пределах этого модуля). */ +static int fgetc_slow(FILE *fp) +{ + if (!fp) { errno = EBADF; return EOF; } + if (fp->hold != EOF) { + int c = fp->hold; + fp->hold = EOF; + return c; + } + if (fp->flags & _F_CONIN) { + return getchar(); + } + if (!(fp->flags & _F_READ)) { errno = EBADF; return EOF; } + + if (fp->flags & _F_DIROUT) { + if (_file_sync(fp)) return EOF; /* сброс накопленной записи */ + } + if (fp->level == 0) { + uint8_t *b = _file_buf(fp); + if (!b) { + /* Небуферизованный fallback. */ + uint8_t ch; + int r = read(fp->fd, &ch, 1); + if (r == 0) { fp->flags |= _F_EOF; return EOF; } + if (r < 0) { fp->flags |= _F_ERROR; return EOF; } + return (int)ch; + } + int r = read(fp->fd, b, BUFSIZ); + if (r == 0) { fp->flags |= _F_EOF; return EOF; } + if (r < 0) { fp->flags |= _F_ERROR; return EOF; } + fp->level = (uint16_t)r; + fp->curp = b; + } + fp->level--; + return (int)*fp->curp++; +} + +int fgetc(FILE *fp) __naked +{ + (void)fp; + (void)fgetc_slow; /* используется из asm */ + __asm + ;; HL = fp. + ld a, h + or a, l + jr Z, _fg_slow ; NULL — C-путь выставит errno + push hl + pop iy ; IY = fp (IY — scratch в __sdcccall(1)) + + ld a, 7 (iy) ; level != 0 ? + or a, 8 (iy) + jr Z, _fg_slow ; буфер пуст/не наш режим + ld a, 2 (iy) ; flags + and a, #0x08 ; _F_DIROUT — буфер копит запись? + jr NZ, _fg_slow + ld a, 9 (iy) ; hold == EOF (0xFFFF)? + and a, 10 (iy) + inc a + jr NZ, _fg_slow ; есть отложенный ungetc-байт + + ;; --- горячий путь: level--, вернуть *curp++ --- + ld a, 7 (iy) + sub a, #1 + ld 7 (iy), a + jr NC, _fg_nb + dec 8 (iy) + _fg_nb: + ld l, 5 (iy) ; HL = curp + ld h, 6 (iy) + ld e, (hl) ; E = байт + ld d, #0 ; zero-extend в int + inc hl + ld 5 (iy), l ; curp++ + ld 6 (iy), h + ret + + _fg_slow: + jp _fgetc_slow ; HL = fp уже на месте + __endasm; +} diff --git a/libc/file/fgets.c b/libc/file/fgets.c new file mode 100644 index 0000000..838ca5b --- /dev/null +++ b/libc/file/fgets.c @@ -0,0 +1,84 @@ +/* + * fgets — прочитать строку (до n-1 символов, '\n' сохраняется) из + * потока в buf с завершающим NUL. Возвращает buf или NULL, если EOF + * наступил до первого сохранённого символа. n=1 по стандарту — + * валидный вызов: ничего не читает, возвращает пустую строку. + * + * Быстрый путь: пока строка лежит в readahead-буфере, она копируется + * блоком asm-сканером (LDI + сравнение с '\n', ~50 тактов/байт) — + * fgetc-на-символ был в ~10 раз медленнее (замер fbench). Медленные + * случаи (консоль, hold от ungetc, режим записи, дозаполнение буфера, + * EOF) уходят посимвольно через fgetc. + */ + +#include "_file.h" + +/* Копировать из src в dst до max байт, остановившись ПОСЛЕ '\n'. + * Возврат: число скопированных байт | 0x8000, если '\n' найден. + * max: 1..512 (бит 15 счётчика свободен). */ +static uint16_t fgets_scan(uint8_t *dst, const uint8_t *src, + uint16_t max) __naked +{ + (void)dst; (void)src; (void)max; + __asm + ;; HL = dst, DE = src, max на стеке (callee чистит). + pop iy ; адрес возврата + pop bc ; BC = max (> 0) + ex de, hl ; HL = src, DE = dst + push bc ; max понадобится для подсчёта + _fgs_loop: + ld a, (hl) + cp a, #0x0A ; сравнить ДО ldi: LDI не трогает Z, + ldi ; но выставляет P/V по BC ((DE)<-(HL)) + jr Z, _fgs_found ; Z — от cp + jp PE, _fgs_loop ; P/V — от LDI: BC != 0 + ;; max исчерпан, '\n' нет: count = max + pop de + jp (iy) + _fgs_found: + ;; count = max - BC, бит 15 = флаг '\n' (count <= 512 — безопасно) + pop hl ; HL = max + ld a, l + sub a, c + ld e, a + ld a, h + sbc a, b + or a, #0x80 + ld d, a + jp (iy) + __endasm; +} + +char *fgets(char *buf, int n, FILE *fp) +{ + if (!buf || n < 1 || !fp) return NULL; + uint8_t *dst = (uint8_t *)buf; + int space = n - 1; + + while (space > 0) { + if (fp->level && !(fp->flags & _F_DIROUT) && fp->hold == EOF) { + /* Быстрый блок: сканировать readahead до '\n'. */ + uint16_t take = fp->level; + if (take > (uint16_t)space) take = (uint16_t)space; + uint16_t got = fgets_scan(dst, fp->curp, take); + uint16_t cnt = got & 0x7FFF; + fp->curp += cnt; + fp->level -= cnt; + dst += cnt; + space -= (int)cnt; + if (got & 0x8000) break; /* строка закончена */ + continue; /* буфер иссяк — дозаполнить */ + } + /* Медленные случаи + refill/EOF — посимвольно. */ + int c = fgetc(fp); + if (c == EOF) { + if (dst == (uint8_t *)buf) return NULL; + break; + } + *dst++ = (uint8_t)c; + space--; + if (c == '\n') break; + } + *dst = '\0'; + return buf; +} diff --git a/libc/file/file.c b/libc/file/file.c deleted file mode 100644 index a84b120..0000000 --- a/libc/file/file.c +++ /dev/null @@ -1,290 +0,0 @@ -/* - * file.c — *** PROVISIONAL *** minimal unbuffered FILE * implementation - * on top of POSIX-style fd I/O - * (open/read/write/lseek/close). - * - * ============================================================ - * TODO (v2): replace with a proper BUFFERED implementation. - * See docs/TODO.md / Solid-C reference layout: - * - * typedef struct { - * uint flags; // +0..1 file status flags - * int level; // +2..3 empty/fill level of buffer - * char *curp; // +4..5 current active pointer - * int fd; // +6..7 underlying low-level fd - * char *buffer; // +8..9 data transfer buffer - * char hold; // +10 ungetc byte if no buffer - * short token; // +11..12 reserved - * char dummy; // +13 reserved - * } FILE; - * - * The current implementation maps each fputc/fgetc to one read/write - * syscall — fine for correctness checks, awful for throughput. Issues - * 3/4/5 from the stdio-review (fwrite short-write flag, fgets n=1, - * mode_to_flags break) are deferred until that rewrite. - * ============================================================ - * - * stdin/stdout/stderr are STATIC sentinel FILEs flagged with - * _F_CONIN/_F_CONOUT; fputc/fgetc detect them and call putchar() / - * getchar() which already handle CR/LF translation and ESTEX calls. - * - * Their fd fields are 0 / -1 / -2. Negative values were chosen because - * ESTEX OPEN can return small positive fds (1, 2, …) for ordinary - * files — if we marked stdout/stderr with fd=1/2 a real file could - * collide with their identifier. fd=0 for stdin is kept (POSIX-style) - * because ESTEX does not return 0. Even so, none of these fd fields - * is ever passed to a syscall — the _F_CONIN/_F_CONOUT flags drive - * the dispatch. - */ - -#include -#include -#include -#include -#include -#include - -/* ---- console pseudo-streams ----------------------------------------*/ -static FILE _stdin = { 0, _F_READ | _F_CONIN }; -static FILE _stdout = { -1, _F_WRITE | _F_CONOUT }; -static FILE _stderr = { -2, _F_WRITE | _F_CONOUT }; - -FILE *const stdin = &_stdin; -FILE *const stdout = &_stdout; -FILE *const stderr = &_stderr; - -/* ---- fopen / fclose -------------------------------------------------*/ - -/* Translate a fopen() mode string to the open() flags subset our - * libc/io/open.c understands. Supported: r, w, a, with optional "+" - * and trailing "b" (binary — we ignore as all I/O is binary). - */ -static int mode_to_flags(const char *mode, uint8_t *file_flags) -{ - if (!mode || !*mode) { - errno = EINVAL; - return -1; - } - int oflags = 0; - uint8_t ff = 0; - char base = *mode; - int plus = 0; - for (const char *p = mode + 1; *p; p++) { - if (*p == '+') plus = 1; - /* 'b' and 't' are ignored — all I/O is binary on Sprinter. */ - } - switch (base) { - case 'r': - oflags = plus ? O_RDWR : O_RDONLY; - ff = _F_READ | (plus ? _F_WRITE : 0); - break; - case 'w': - oflags = (plus ? O_RDWR : O_WRONLY) | O_CREAT | O_TRUNC; - ff = _F_WRITE | (plus ? _F_READ : 0); - break; - case 'a': - oflags = (plus ? O_RDWR : O_WRONLY) | O_CREAT | O_APPEND; - ff = _F_WRITE | _F_APPEND | (plus ? _F_READ : 0); - break; - default: - errno = EINVAL; - return -1; - } - *file_flags = ff; - return oflags; -} - -FILE *fopen(const char *path, const char *mode) -{ - uint8_t ff; - int oflags = mode_to_flags(mode, &ff); - if (oflags < 0) return NULL; - - int fd = open(path, oflags); - if (fd < 0) return NULL; - - FILE *fp = (FILE *)malloc(sizeof(FILE)); - if (!fp) { - int saved = errno; - close(fd); - errno = saved ? saved : ENOMEM; - return NULL; - } - fp->fd = fd; - fp->flags = ff; - return fp; -} - -int fclose(FILE *fp) -{ - if (!fp) { - errno = EBADF; - return EOF; - } - /* Don't close stdin/stdout/stderr. */ - if (fp == &_stdin || fp == &_stdout || fp == &_stderr) { - return 0; - } - int r = close(fp->fd); - free(fp); - return r < 0 ? EOF : 0; -} - -int fflush(FILE *fp) -{ - /* Unbuffered — nothing to flush. */ - (void)fp; - return 0; -} - -/* ---- char-at-a-time -------------------------------------------------*/ - -int fputc(int c, FILE *fp) -{ - if (!fp) { errno = EBADF; return EOF; } - if (fp->flags & _F_CONOUT) { - putchar(c); - return (int)c; - } - if (!(fp->flags & _F_WRITE)) { errno = EBADF; return EOF; } - uint8_t ch = (uint8_t)c; - if (write(fp->fd, &ch, 1) != 1) { - fp->flags |= _F_ERROR; - return EOF; - } - return (int)ch; -} - -int fgetc(FILE *fp) -{ - if (!fp) { errno = EBADF; return EOF; } - if (fp->flags & _F_CONIN) { - return getchar(); - } - if (!(fp->flags & _F_READ)) { errno = EBADF; return EOF; } - uint8_t ch; - int r = read(fp->fd, &ch, 1); - if (r == 0) { fp->flags |= _F_EOF; return EOF; } - if (r < 0) { fp->flags |= _F_ERROR; return EOF; } - return (int)ch; -} - -/* ---- string-at-a-time ----------------------------------------------*/ - -int fputs(const char *s, FILE *fp) -{ - if (!fp || !s) { errno = EBADF; return EOF; } - if (fp->flags & _F_CONOUT) { - while (*s) { - putchar((unsigned char)*s++);; - } - return 0; - } - if (!(fp->flags & _F_WRITE)) { errno = EBADF; return EOF; } - size_t n = strlen(s); - int w = write(fp->fd, s, (uint16_t)n); - if (w < 0 || (size_t)w != n) { - fp->flags |= _F_ERROR; - return EOF; - } - return 0; -} - -char *fgets(char *buf, int n, FILE *fp) -{ - if (!buf || n < 2 || !fp) return NULL; - int i = 0; - while (i < n - 1) { - int c = fgetc(fp); - if (c == EOF) { - if (i == 0) return NULL; - break; - } - buf[i++] = (char)c; - if (c == '\n') break; - } - buf[i] = '\0'; - return buf; -} - -/* ---- block-at-a-time -----------------------------------------------*/ - -size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fp) -{ - if (!ptr || !fp) return 0; - if (size == 0 || nmemb == 0) return 0; - if (fp->flags & _F_CONIN) { - /* line-buffered console read — not very useful but functional. */ - char *p = (char *)ptr; - size_t total = size * nmemb; - for (size_t i = 0; i < total; i++) { - int c = getchar(); - if (c == EOF) return i / size; - p[i] = (char)c; - } - return nmemb; - } - if (!(fp->flags & _F_READ)) { errno = EBADF; return 0; } - size_t total = size * nmemb; - int r = read(fp->fd, ptr, (uint16_t)total); - if (r < 0) { fp->flags |= _F_ERROR; return 0; } - if ((size_t)r < total) fp->flags |= _F_EOF; - return (size_t)r / size; -} - -size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp) -{ - if (!ptr || !fp) return 0; - if (size == 0 || nmemb == 0) return 0; - if (fp->flags & _F_CONOUT) { - const char *p = (const char *)ptr; - size_t total = size * nmemb; - for (size_t i = 0; i < total; i++) { - putchar((unsigned char)p[i]); - } - return nmemb; - } - if (!(fp->flags & _F_WRITE)) { errno = EBADF; return 0; } - size_t total = size * nmemb; - int w = write(fp->fd, ptr, (uint16_t)total); - if (w < 0) { fp->flags |= _F_ERROR; return 0; } - return (size_t)w / size; -} - -/* ---- positioning ---------------------------------------------------*/ - -int fseek(FILE *fp, long off, int whence) -{ - if (!fp || (fp->flags & (_F_CONIN | _F_CONOUT))) { - errno = EBADF; - return -1; - } - long r = lseek(fp->fd, off, whence); - if (r < 0) return -1; - fp->flags &= (uint8_t)~_F_EOF; - return 0; -} - -long ftell(FILE *fp) -{ - if (!fp || (fp->flags & (_F_CONIN | _F_CONOUT))) { - errno = EBADF; - return -1L; - } - return lseek(fp->fd, 0L, SEEK_CUR); -} - -void rewind(FILE *fp) -{ - if (!fp) return; - fseek(fp, 0L, SEEK_SET); - fp->flags &= (uint8_t)~(_F_EOF | _F_ERROR); -} - -/* ---- status --------------------------------------------------------*/ - -int feof (FILE *fp) { return fp && (fp->flags & _F_EOF) ? 1 : 0; } -int ferror(FILE *fp) { return fp && (fp->flags & _F_ERROR) ? 1 : 0; } -void clearerr(FILE *fp) { - if (fp) fp->flags &= (uint8_t)~(_F_EOF | _F_ERROR); -} diff --git a/libc/file/fopen.c b/libc/file/fopen.c new file mode 100644 index 0000000..c4291f5 --- /dev/null +++ b/libc/file/fopen.c @@ -0,0 +1,89 @@ +/* + * fopen — открыть файл как буферизованный поток FILE* (вариант B+). + * + * FILE берётся из статической таблицы _file_slots (свободный слот: + * flags == 0), без malloc — сам буфер выделится лениво при первой + * операции. Первый успешный fopen вешает _fclosall на atexit, чтобы + * exit() сбрасывал несброшенную запись всех потоков. + * NULL при: плохом режиме (EINVAL), занятой таблице (EMFILE), ошибке + * open (errno от него). + */ + +#include +#include /* atexit */ +#include +#include +#include +#include "_file.h" + +/* Транслировать строку режима fopen() в подмножество флагов open(), + * которое понимает libc/io/open.c. Поддержаны r, w, a, "+" в любом + * месте хвоста; "b"/"t" игнорируются — весь I/O бинарный. */ +static int mode_to_flags(const char *mode, uint8_t *file_flags) +{ + if (!mode || !*mode) { + errno = EINVAL; + return -1; + } + int oflags = 0; + uint8_t ff = 0; + char base = *mode; + int plus = 0; + for (const char *p = mode + 1; *p; p++) { + if (*p == '+') plus = 1; + } + switch (base) { + case 'r': + oflags = plus ? O_RDWR : O_RDONLY; + ff = _F_READ | (plus ? _F_WRITE : 0); + break; + case 'w': + oflags = (plus ? O_RDWR : O_WRONLY) | O_CREAT | O_TRUNC; + ff = _F_WRITE | (plus ? _F_READ : 0); + break; + case 'a': + oflags = (plus ? O_RDWR : O_WRONLY) | O_CREAT | O_APPEND; + ff = _F_WRITE | _F_APPEND | (plus ? _F_READ : 0); + break; + default: + errno = EINVAL; + return -1; + } + *file_flags = ff; + return oflags; +} + +FILE *fopen(const char *path, const char *mode) +{ + static uint8_t atexit_armed; + + uint8_t ff; + int oflags = mode_to_flags(mode, &ff); + if (oflags < 0) return NULL; + + /* Свободный слот таблицы — ДО open, чтобы не открывать зря. */ + FILE *fp = _file_slots; + for (uint8_t i = 0; ; i++, fp++) { + if (i >= OPEN_MAX) { + errno = EMFILE; + return NULL; + } + if (fp->flags == 0) break; + } + + int fd = open(path, oflags); + if (fd < 0) return NULL; + + fp->fd = fd; + fp->buf = NULL; + fp->curp = NULL; + fp->level = 0; + fp->hold = EOF; + fp->flags = ff; /* последним — занимает слот */ + + if (!atexit_armed) { + atexit_armed = 1; + atexit(_fclosall); /* exit() сбросит все потоки */ + } + return fp; +} diff --git a/libc/file/fprintf.c b/libc/file/fprintf.c new file mode 100644 index 0000000..85cdc20 --- /dev/null +++ b/libc/file/fprintf.c @@ -0,0 +1,15 @@ +/* + * fprintf — форматированный вывод в поток; обёртка над vfprintf. + */ + +#include +#include "_file.h" + +int fprintf(FILE *fp, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + int n = vfprintf(fp, fmt, ap); + va_end(ap); + return n; +} diff --git a/libc/file/fputc.c b/libc/file/fputc.c new file mode 100644 index 0000000..88ae042 --- /dev/null +++ b/libc/file/fputc.c @@ -0,0 +1,100 @@ +/* + * fputc — записать один байт в поток (буферизованно, вариант B+). + * + * Горячий путь («поток уже копит запись и в буфере есть место») + * написан на asm — см. пояснение в fgetc.c. Остальное — медленный + * C-путь fputc_slow: консольный putchar(), переключение направления + * (_file_sync откатывает readahead), сброс полного буфера, ленивое + * выделение, fallback без буфера (прямой write() 1 байта). + * ВНИМАНИЕ: успех fputc = «байт принят в буфер»; ошибка реальной + * записи может вылезти позже (fflush/fclose). + * + * Раскладка FILE: +0 fd, +2 flags, +3 buf, +5 curp, +7 level, +9 hold. + * asm рассчитывает на BUFSIZ == 512 (проверка уровня по старшему + * байту level < 2). + */ + +#include +#include +#include "_file.h" + +#if BUFSIZ != 512 +#error "asm-горячий путь fputc рассчитан на BUFSIZ == 512" +#endif + +/* Медленный путь; зовётся из asm-обёртки. */ +static int fputc_slow(int c, FILE *fp) +{ + if (!fp) { errno = EBADF; return EOF; } + if (fp->flags & _F_CONOUT) { + putchar(c); + return (int)(uint8_t)c; + } + if (!(fp->flags & _F_WRITE)) { errno = EBADF; return EOF; } + + if (!(fp->flags & _F_DIROUT)) { + if (_file_sync(fp)) return EOF; /* откат readahead/hold */ + } else if (fp->level >= BUFSIZ) { + if (_file_sync(fp)) return EOF; /* буфер полон — сброс */ + } + + uint8_t *b = _file_buf(fp); + if (!b) { + /* Небуферизованный fallback. */ + uint8_t ch = (uint8_t)c; + if (write(fp->fd, &ch, 1) != 1) { + fp->flags |= _F_ERROR; + return EOF; + } + return (int)ch; + } + if (!(fp->flags & _F_DIROUT)) { + fp->flags |= _F_DIROUT; + fp->curp = b; + } + *fp->curp++ = (uint8_t)c; + fp->level++; + return (int)(uint8_t)c; +} + +int fputc(int c, FILE *fp) __naked +{ + (void)c; (void)fp; + (void)fputc_slow; /* используется из asm */ + __asm + ;; HL = c, DE = fp. + ld a, d + or a, e + jr Z, _fp_slow ; NULL — C-путь выставит errno + push de + pop iy ; IY = fp + + ld a, 2 (iy) ; flags + and a, #0x08 ; _F_DIROUT — уже копим запись? + jr Z, _fp_slow ; нет (или консоль/чтение) — C-путь + ld a, 8 (iy) ; level_hi < 2 <=> level < 512 + cp a, #2 + jr NC, _fp_slow ; буфер полон — сброс в C-пути + + ;; --- горячий путь: *curp++ = c, level++ --- + ld e, 5 (iy) ; DE = curp (fp больше не нужен в DE) + ld d, 6 (iy) + ld a, l + ld (de), a + inc de + ld 5 (iy), e + ld 6 (iy), d + ld a, 7 (iy) ; level++ + inc a + ld 7 (iy), a + jr NZ, _fp_ret + inc 8 (iy) + _fp_ret: + ld e, l ; вернуть (uint8_t)c + ld d, #0 + ret + + _fp_slow: + jp _fputc_slow ; HL = c, DE = fp уже на месте + __endasm; +} diff --git a/libc/file/fputs.c b/libc/file/fputs.c new file mode 100644 index 0000000..a22e028 --- /dev/null +++ b/libc/file/fputs.c @@ -0,0 +1,17 @@ +/* + * fputs — записать ASCIIZ-строку в поток (перевод строки НЕ + * добавляется, по стандарту C). Тонкая обёртка над fwrite — тот + * сам разруливает консоль/буфер/большие блоки. + * Возвращает 0 при успехе, EOF при ошибке. + */ + +#include +#include "_file.h" + +int fputs(const char *s, FILE *fp) +{ + if (!fp || !s) return EOF; + size_t n = strlen(s); + if (n == 0) return 0; + return fwrite(s, 1, n, fp) == n ? 0 : EOF; +} diff --git a/libc/file/fread.c b/libc/file/fread.c new file mode 100644 index 0000000..21431e5 --- /dev/null +++ b/libc/file/fread.c @@ -0,0 +1,82 @@ +/* + * fread — читает nmemb элементов по size байт из потока в ptr + * (буферизованно, вариант B+). + * + * Порядок: hold-байт → хвост буфера (memcpy) → большой остаток + * (>= BUFSIZ) одним read() напрямую в ptr мимо буфера → мелкий + * остаток через дозаполнение буфера. Возвращает число ПОЛНЫХ + * элементов; короткое чтение ставит _F_EOF, ошибка — _F_ERROR. + * Консольный поток — посимвольно через getchar(). + */ + +#include +#include +#include +#include "_file.h" + +size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fp) +{ + if (!ptr || !fp) return 0; + if (size == 0 || nmemb == 0) return 0; + + uint8_t *p = (uint8_t *)ptr; + uint16_t total = size * nmemb; + uint16_t got = 0; + uint16_t n; + int r; + + if (fp->hold != EOF) { + *p++ = (uint8_t)fp->hold; + fp->hold = EOF; + got = 1; + } + if (fp->flags & _F_CONIN) { + while (got < total) { + int c = getchar(); + if (c == EOF) break; + *p++ = (uint8_t)c; + got++; + } + goto done; + } + if (!(fp->flags & _F_READ)) { errno = EBADF; return 0; } + if (fp->flags & _F_DIROUT) { + if (_file_sync(fp)) return 0; /* сброс накопленной записи */ + } + + /* 1) хвост readahead-буфера */ + if (fp->level) { + n = total - got; + if (n > fp->level) n = fp->level; + memcpy(p, fp->curp, n); + fp->curp += n; + fp->level -= n; + p += n; + got += n; + } + + n = total - got; + if (n == 0) goto done; + if (n >= BUFSIZ || !_file_buf(fp)) { + /* 2а) большой остаток (или нет буфера) — напрямую мимо буфера */ + r = read(fp->fd, p, n); + } else { + /* 2б) мелкий остаток — дозаполнить буфер и скопировать */ + r = read(fp->fd, fp->buf, BUFSIZ); + if (r >= 0) { + fp->level = (uint16_t)r; + fp->curp = fp->buf; + if ((uint16_t)r < n) n = (uint16_t)r; + memcpy(p, fp->curp, n); + fp->curp += n; + fp->level -= n; + r = (int)n; + } + } + if (r < 0) { fp->flags |= _F_ERROR; goto done; } + got += (uint16_t)r; + if (got < total) fp->flags |= _F_EOF; + +done: + return size == 1 ? got : got / size; +} diff --git a/libc/file/fseek.c b/libc/file/fseek.c new file mode 100644 index 0000000..5258c99 --- /dev/null +++ b/libc/file/fseek.c @@ -0,0 +1,26 @@ +/* + * fseek — установить позицию файлового потока (SEEK_SET/CUR/END). + * + * Сначала _file_sync: сброс накопленной записи либо откат readahead + * (после него позиция fd = логической позиции потока, поэтому + * SEEK_CUR считается корректно), затем голый lseek. ungetc-байт + * отбрасывается (по стандарту). Успех снимает _F_EOF. + * Для консольных псевдопотоков — EBADF/-1. + */ + +#include +#include +#include "_file.h" + +int fseek(FILE *fp, long off, int whence) +{ + if (!fp || (fp->flags & (_F_CONIN | _F_CONOUT))) { + errno = EBADF; + return -1; + } + if (_file_sync(fp)) return -1; + long r = lseek(fp->fd, off, whence); + if (r < 0) return -1; + fp->flags &= (uint8_t)~_F_EOF; + return 0; +} diff --git a/libc/file/ftell.c b/libc/file/ftell.c new file mode 100644 index 0000000..25a1f33 --- /dev/null +++ b/libc/file/ftell.c @@ -0,0 +1,28 @@ +/* + * ftell — логическая позиция потока. Без побочных эффектов: позиция + * fd корректируется на состояние буфера арифметикой — + * режим записи: fd_pos + level (несброшенный хвост впереди fd); + * режим чтения: fd_pos − level − (1, если hold от ungetc). + * Для консольных псевдопотоков — EBADF/-1. + */ + +#include +#include +#include "_file.h" + +long ftell(FILE *fp) +{ + if (!fp || (fp->flags & (_F_CONIN | _F_CONOUT))) { + errno = EBADF; + return -1L; + } + long pos = lseek(fp->fd, 0L, SEEK_CUR); + if (pos < 0) return -1L; + if (fp->flags & _F_DIROUT) { + pos += fp->level; + } else { + pos -= fp->level; + if (fp->hold != EOF) pos--; + } + return pos; +} diff --git a/libc/file/fwrite.c b/libc/file/fwrite.c new file mode 100644 index 0000000..b0133cc --- /dev/null +++ b/libc/file/fwrite.c @@ -0,0 +1,60 @@ +/* + * fwrite — пишет nmemb элементов по size байт из ptr в поток + * (буферизованно, вариант B+). + * + * Большой блок (>= BUFSIZ, или нет буфера): сброс накопленного и один + * write() напрямую из ptr мимо буфера. Мелкий: memcpy в буфер (со + * сбросом ПЕРЕД записью, если не влезает). Если поток был в режиме + * чтения — _file_sync откатит readahead. Возвращает число ПОЛНЫХ + * записанных элементов; ошибка/короткая запись ставит _F_ERROR. + * Ошибки буферизованной части могут вылезти позже (fflush/fclose). + * Консольный поток — посимвольно через putchar(). + */ + +#include +#include +#include +#include "_file.h" + +size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp) +{ + if (!ptr || !fp) return 0; + if (size == 0 || nmemb == 0) return 0; + + const uint8_t *p = (const uint8_t *)ptr; + size_t total = size * nmemb; + + if (fp->flags & _F_CONOUT) { + for (size_t i = 0; i < total; i++) { + putchar(p[i]); + } + return nmemb; + } + if (!(fp->flags & _F_WRITE)) { errno = EBADF; return 0; } + + if (!(fp->flags & _F_DIROUT)) { + if (_file_sync(fp)) return 0; /* откат readahead/hold */ + } + + if (total >= BUFSIZ || !_file_buf(fp)) { + /* Большой блок (или нет буфера): сброс хвоста + прямой write. */ + if (_file_sync(fp)) return 0; + int w = write(fp->fd, p, (uint16_t)total); + if (w < 0) { fp->flags |= _F_ERROR; return 0; } + if ((size_t)w < total) fp->flags |= _F_ERROR; + return (size_t)w / size; + } + + /* Мелкий блок — в буфер; сброс ПЕРЕД записью, если не влезает. */ + if (fp->level + total > BUFSIZ) { + if (_file_sync(fp)) return 0; + } + if (!(fp->flags & _F_DIROUT)) { + fp->flags |= _F_DIROUT; + fp->curp = fp->buf; + } + memcpy(fp->curp, p, total); + fp->curp += total; + fp->level += (uint16_t)total; + return nmemb; +} diff --git a/libc/file/rewind.c b/libc/file/rewind.c new file mode 100644 index 0000000..ff8624b --- /dev/null +++ b/libc/file/rewind.c @@ -0,0 +1,15 @@ +/* + * rewind — перемотать поток в начало: fseek(fp, 0, SEEK_SET) плюс + * сброс флагов _F_EOF и _F_ERROR (в отличие от fseek, который снимает + * только _F_EOF). + */ + +#include +#include + +void rewind(FILE *fp) +{ + if (!fp) return; + fseek(fp, 0L, SEEK_SET); + fp->flags &= (uint8_t)~(_F_EOF | _F_ERROR); +} diff --git a/libc/file/std_streams.c b/libc/file/std_streams.c new file mode 100644 index 0000000..f6ea2d3 --- /dev/null +++ b/libc/file/std_streams.c @@ -0,0 +1,27 @@ +/* + * std_streams — три предопределённых консольных псевдопотока. + * + * stdin/stdout/stderr — СТАТИЧЕСКИЕ FILE-заглушки с флагами + * _F_CONIN/_F_CONOUT; fputc/fgetc распознают флаги и зовут putchar()/ + * getchar(), которые уже делают трансляцию CR/LF и вызовы ESTEX. + * + * Поля fd: 0 / -1 / -2. Отрицательные значения выбраны потому, что + * ESTEX OPEN возвращает маленькие положительные fd (1, 2, …) для + * обычных файлов — с fd=1/2 у stdout/stderr реальный файл мог бы + * совпасть с ними. fd=0 у stdin оставлен (POSIX-стиль), т.к. ESTEX + * не возвращает 0. В syscall эти fd всё равно не попадают — + * диспетчеризацию ведут флаги _F_CONIN/_F_CONOUT (и по ним же fclose() + * узнаёт псевдопотоки, не ссылаясь на адреса из этого модуля). + */ + +#include + +/* Консольные потоки не буферизуются: buf/curp/level не используются. + * hold = EOF (пусто) — ungetc на stdin работает через это поле. */ +static FILE _stdin = { 0, _F_READ | _F_CONIN, 0, 0, 0, EOF }; +static FILE _stdout = { -1, _F_WRITE | _F_CONOUT, 0, 0, 0, EOF }; +static FILE _stderr = { -2, _F_WRITE | _F_CONOUT, 0, 0, 0, EOF }; + +FILE *const stdin = &_stdin; +FILE *const stdout = &_stdout; +FILE *const stderr = &_stderr; diff --git a/libc/file/ungetc.c b/libc/file/ungetc.c new file mode 100644 index 0000000..20c5c4f --- /dev/null +++ b/libc/file/ungetc.c @@ -0,0 +1,21 @@ +/* + * ungetc — вернуть один байт в поток: следующий fgetc/fread/fgets + * отдаст его первым. Гарантирован 1 байт putback (поле hold); + * повторный ungetc без чтения — EOF. Работает и на консольном stdin. + * Снимает _F_EOF (по стандарту); fseek/rewind отбрасывают hold. + * В режиме накопленной записи (_F_DIROUT) — EOF. + */ + +#include "_file.h" + +int ungetc(int c, FILE *fp) +{ + if (!fp || c == EOF) return EOF; + if (fp->hold != EOF) return EOF; /* место занято */ + if (fp->flags & _F_CONOUT) return EOF; + if (!(fp->flags & (_F_READ | _F_CONIN))) return EOF; + if (fp->flags & _F_DIROUT) return EOF; + fp->hold = (int)(uint8_t)c; + fp->flags &= (uint8_t)~_F_EOF; + return (int)(uint8_t)c; +} diff --git a/libc/file/vfprintf.c b/libc/file/vfprintf.c new file mode 100644 index 0000000..986c714 --- /dev/null +++ b/libc/file/vfprintf.c @@ -0,0 +1,23 @@ +/* + * vfprintf — форматированный вывод в поток: vsprintf (из z80.lib SDCC) + * в статический буфер, затем fwrite (тот сам разруливает консоль/ + * буфер). Не реентерабельно (один статический буфер) — на + * однопоточном Z80 не проблема. Результат длиннее 255 символов — + * усечение ответственность вызывающего (как у cprintf). + */ + +#include +#include "_file.h" + +#define VFPRINTF_BUF_SIZE 256 + +static char vfp_buf[VFPRINTF_BUF_SIZE]; + +int vfprintf(FILE *fp, const char *fmt, va_list ap) +{ + int n = vsprintf(vfp_buf, fmt, ap); + if (n > 0) { + if (fwrite(vfp_buf, 1, (size_t)n, fp) != (size_t)n) return EOF; + } + return n; +} diff --git a/libc/include/stdio.h b/libc/include/stdio.h index 1105abd..d7f6592 100644 --- a/libc/include/stdio.h +++ b/libc/include/stdio.h @@ -1,15 +1,20 @@ /* - * stdio.h — extends SDCC's z80 stdio with a minimal FILE * stream API. + * stdio.h — extends SDCC's z80 stdio with a buffered FILE * stream API. * * The bottom of the stack is the existing POSIX-style fd I/O (open/ - * read/write/close/lseek). FILE * is a thin struct wrapping a fd plus - * a few flag bits. No buffering — every fread/fwrite/fputc maps 1:1 - * to a syscall. Sprinter's I/O is already char-oriented at the ESTEX - * level, so this stays minimal. + * read/write/close/lseek). FILE * adds a per-stream 512-byte buffer, + * lazily malloc'ed on first buffered operation, shared between reading + * (readahead) and writing (accumulation) with automatic direction + * switching — see docs/file-buffering-design.md (variant B+). + * + * FILE objects live in a static table of OPEN_MAX slots (no malloc); + * a slot is free when flags == 0. exit() flushes and closes all open + * streams via an atexit-registered _fclosall (armed by first fopen). * * stdin / stdout / stderr are predefined "virtual" FILE * pointers - * that talk to the console via putchar()/getchar() (NOT through a fd). - * That keeps printf-routed output going through our CR/LF mapping. + * that talk to the console via putchar()/getchar() (NOT through a fd) + * and are never buffered. That keeps printf-routed output going + * through our CR/LF mapping. */ #ifndef STDIO_H @@ -34,18 +39,34 @@ char puts (const char *); int putchar(int); int getchar(void); -/* ---- FILE * (minimal, unbuffered) ---------------------------------- */ +/* ---- FILE * (buffered, variant B+) --------------------------------- */ -/* Internal layout — opaque to user. fd == -1 marks the console - * pseudo-streams (stdin/stdout/stderr). */ +/* Максимум одновременно открытых fopen-потоков (консольные псевдо- + * потоки не в счёт). DSS сам ограничивает число манипуляторов + * (ошибка 06h "Too many open files"); наша таблица зеркалит его. */ +#define OPEN_MAX 8 +#define FOPEN_MAX OPEN_MAX + +/* Размер ленивого буфера потока. */ +#define BUFSIZ 512 + +/* Internal layout — opaque to user. Слот таблицы свободен, когда + * flags == 0. level — число байт: непрочитанный readahead (направление + * «чтение») либо ещё не сброшенная запись (направление «запись», + * флаг _F_DIROUT). */ typedef struct __FILE { - int fd; /* underlying POSIX fd, or -1 for console */ - uint8_t flags; /* see _F_* below */ + int fd; /* POSIX fd (манипулятор DSS) */ + uint8_t flags; /* см. _F_* ниже; 0 = слот свободен */ + uint8_t *buf; /* ленивый буфер BUFSIZ байт или NULL */ + uint8_t *curp; /* текущая позиция в буфере */ + uint16_t level; /* байт в буфере (смысл зависит от _F_DIROUT) */ + int hold; /* байт от ungetc, EOF = пусто */ } FILE; #define _F_READ 0x01 #define _F_WRITE 0x02 #define _F_APPEND 0x04 +#define _F_DIROUT 0x08 /* буфер сейчас копит запись (иначе readahead) */ #define _F_EOF 0x10 #define _F_ERROR 0x20 #define _F_CONIN 0x40 /* console pseudo-stream — uses getchar() */ @@ -70,6 +91,10 @@ int fputc (int c, FILE *fp); int fgetc (FILE *fp); int fputs (const char *s, FILE *fp); char *fgets (char *buf, int n, FILE *fp); +int ungetc(int c, FILE *fp); + +int fprintf (FILE *fp, const char *fmt, ...); +int vfprintf(FILE *fp, const char *fmt, va_list ap); size_t fread (void *ptr, size_t size, size_t nmemb, FILE *fp); size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp);