libc: FILE* v2 — буферизация потоков (вариант B+)

- единый ленивый буфер 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 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 16:09:13 +03:00
parent 4a081501d8
commit 48d552bf3a
27 changed files with 1093 additions and 302 deletions
+161
View File
@@ -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» невозможна;
- цена: ~112128 Б 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*).
+14
View File
@@ -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]);
}
}
+38
View File
@@ -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 <stdio.h>
/* Таблица потоков (_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
+18
View File
@@ -0,0 +1,18 @@
/*
* _file_buf — ленивое выделение буфера потока: malloc(BUFSIZ) при
* первой буферизуемой операции. NULL при нехватке памяти — поток
* НЕ ломается, вызывающие уходят на прямой syscall (небуферизованный
* fallback), попытка выделения повторится при следующей операции.
*/
#include <stdlib.h>
#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;
}
+12
View File
@@ -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];
+42
View File
@@ -0,0 +1,42 @@
/*
* _file_sync — нейтрализовать буфер потока (см. _file.h и
* docs/file-buffering-design.md, «семантика инвалидации»).
*
* Направление «запись»: сбросить несброшенный хвост одним write();
* короткая запись = ошибка потока. Направление «чтение»: откатить
* позицию fd назад на непрочитанный readahead (+1 за hold-байт
* ungetc) — после этого реальная позиция fd совпадает с логической
* позицией потока, буфер объявляется пустым.
*/
#include <unistd.h>
#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;
}
+11
View File
@@ -0,0 +1,11 @@
/*
* clearerr — сбросить флаги _F_EOF и _F_ERROR потока. NULL-безопасно.
*/
#include <stdio.h>
#include <stdint.h>
void clearerr(FILE *fp)
{
if (fp) fp->flags &= (uint8_t)~(_F_EOF | _F_ERROR);
}
+28
View File
@@ -0,0 +1,28 @@
/*
* fclose — закрыть поток: сбросить буфер (_file_sync), закрыть fd,
* освободить буфер и слот таблицы. ВАЖНО: ошибки отложенной записи
* вылезают именно здесь — проверять результат fclose у пишущих
* потоков. Консольные псевдопотоки (stdin/stdout/stderr) распознаются
* по флагам и успешно игнорируются.
*/
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#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;
}
+11
View File
@@ -0,0 +1,11 @@
/*
* feof — 1, если на потоке защёлкнут флаг конца файла (_F_EOF),
* иначе 0. NULL-безопасно.
*/
#include <stdio.h>
int feof(FILE *fp)
{
return fp && (fp->flags & _F_EOF) ? 1 : 0;
}
+11
View File
@@ -0,0 +1,11 @@
/*
* ferror — 1, если на потоке защёлкнут флаг ошибки (_F_ERROR),
* иначе 0. NULL-безопасно.
*/
#include <stdio.h>
int ferror(FILE *fp)
{
return fp && (fp->flags & _F_ERROR) ? 1 : 0;
}
+22
View File
@@ -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);
}
+101
View File
@@ -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 <unistd.h>
#include <errno.h>
#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;
}
+84
View File
@@ -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;
}
-290
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
/* ---- 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);
}
+89
View File
@@ -0,0 +1,89 @@
/*
* fopen — открыть файл как буферизованный поток FILE* (вариант B+).
*
* FILE берётся из статической таблицы _file_slots (свободный слот:
* flags == 0), без malloc — сам буфер выделится лениво при первой
* операции. Первый успешный fopen вешает _fclosall на atexit, чтобы
* exit() сбрасывал несброшенную запись всех потоков.
* NULL при: плохом режиме (EINVAL), занятой таблице (EMFILE), ошибке
* open (errno от него).
*/
#include <stdlib.h>
#include <sprinter_exit.h> /* atexit */
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#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;
}
+15
View File
@@ -0,0 +1,15 @@
/*
* fprintf — форматированный вывод в поток; обёртка над vfprintf.
*/
#include <stdarg.h>
#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;
}
+100
View File
@@ -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 <unistd.h>
#include <errno.h>
#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;
}
+17
View File
@@ -0,0 +1,17 @@
/*
* fputs — записать ASCIIZ-строку в поток (перевод строки НЕ
* добавляется, по стандарту C). Тонкая обёртка над fwrite — тот
* сам разруливает консоль/буфер/большие блоки.
* Возвращает 0 при успехе, EOF при ошибке.
*/
#include <string.h>
#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;
}
+82
View File
@@ -0,0 +1,82 @@
/*
* fread — читает nmemb элементов по size байт из потока в ptr
* (буферизованно, вариант B+).
*
* Порядок: hold-байт → хвост буфера (memcpy) → большой остаток
* (>= BUFSIZ) одним read() напрямую в ptr мимо буфера → мелкий
* остаток через дозаполнение буфера. Возвращает число ПОЛНЫХ
* элементов; короткое чтение ставит _F_EOF, ошибка — _F_ERROR.
* Консольный поток — посимвольно через getchar().
*/
#include <string.h>
#include <unistd.h>
#include <errno.h>
#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;
}
+26
View File
@@ -0,0 +1,26 @@
/*
* fseek — установить позицию файлового потока (SEEK_SET/CUR/END).
*
* Сначала _file_sync: сброс накопленной записи либо откат readahead
* (после него позиция fd = логической позиции потока, поэтому
* SEEK_CUR считается корректно), затем голый lseek. ungetc-байт
* отбрасывается (по стандарту). Успех снимает _F_EOF.
* Для консольных псевдопотоков — EBADF/-1.
*/
#include <unistd.h>
#include <errno.h>
#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;
}
+28
View File
@@ -0,0 +1,28 @@
/*
* ftell — логическая позиция потока. Без побочных эффектов: позиция
* fd корректируется на состояние буфера арифметикой —
* режим записи: fd_pos + level (несброшенный хвост впереди fd);
* режим чтения: fd_pos level (1, если hold от ungetc).
* Для консольных псевдопотоков — EBADF/-1.
*/
#include <unistd.h>
#include <errno.h>
#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;
}
+60
View File
@@ -0,0 +1,60 @@
/*
* fwrite — пишет nmemb элементов по size байт из ptr в поток
* (буферизованно, вариант B+).
*
* Большой блок (>= BUFSIZ, или нет буфера): сброс накопленного и один
* write() напрямую из ptr мимо буфера. Мелкий: memcpy в буфер (со
* сбросом ПЕРЕД записью, если не влезает). Если поток был в режиме
* чтения — _file_sync откатит readahead. Возвращает число ПОЛНЫХ
* записанных элементов; ошибка/короткая запись ставит _F_ERROR.
* Ошибки буферизованной части могут вылезти позже (fflush/fclose).
* Консольный поток — посимвольно через putchar().
*/
#include <string.h>
#include <unistd.h>
#include <errno.h>
#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;
}
+15
View File
@@ -0,0 +1,15 @@
/*
* rewind — перемотать поток в начало: fseek(fp, 0, SEEK_SET) плюс
* сброс флагов _F_EOF и _F_ERROR (в отличие от fseek, который снимает
* только _F_EOF).
*/
#include <stdio.h>
#include <stdint.h>
void rewind(FILE *fp)
{
if (!fp) return;
fseek(fp, 0L, SEEK_SET);
fp->flags &= (uint8_t)~(_F_EOF | _F_ERROR);
}
+27
View File
@@ -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 <stdio.h>
/* Консольные потоки не буферизуются: 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;
+21
View File
@@ -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;
}
+23
View File
@@ -0,0 +1,23 @@
/*
* vfprintf — форматированный вывод в поток: vsprintf (из z80.lib SDCC)
* в статический буфер, затем fwrite (тот сам разруливает консоль/
* буфер). Не реентерабельно (один статический буфер) — на
* однопоточном Z80 не проблема. Результат длиннее 255 символов —
* усечение ответственность вызывающего (как у cprintf).
*/
#include <stdarg.h>
#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;
}
+37 -12
View File
@@ -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/ * 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 * read/write/close/lseek). FILE * adds a per-stream 512-byte buffer,
* a few flag bits. No buffering — every fread/fwrite/fputc maps 1:1 * lazily malloc'ed on first buffered operation, shared between reading
* to a syscall. Sprinter's I/O is already char-oriented at the ESTEX * (readahead) and writing (accumulation) with automatic direction
* level, so this stays minimal. * 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 * stdin / stdout / stderr are predefined "virtual" FILE * pointers
* that talk to the console via putchar()/getchar() (NOT through a fd). * that talk to the console via putchar()/getchar() (NOT through a fd)
* That keeps printf-routed output going through our CR/LF mapping. * and are never buffered. That keeps printf-routed output going
* through our CR/LF mapping.
*/ */
#ifndef STDIO_H #ifndef STDIO_H
@@ -34,18 +39,34 @@ char puts (const char *);
int putchar(int); int putchar(int);
int getchar(void); int getchar(void);
/* ---- FILE * (minimal, unbuffered) ---------------------------------- */ /* ---- FILE * (buffered, variant B+) --------------------------------- */
/* Internal layout — opaque to user. fd == -1 marks the console /* Максимум одновременно открытых fopen-потоков (консольные псевдо-
* pseudo-streams (stdin/stdout/stderr). */ * потоки не в счёт). 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 { typedef struct __FILE {
int fd; /* underlying POSIX fd, or -1 for console */ int fd; /* POSIX fd (манипулятор DSS) */
uint8_t flags; /* see _F_* below */ uint8_t flags; /* см. _F_* ниже; 0 = слот свободен */
uint8_t *buf; /* ленивый буфер BUFSIZ байт или NULL */
uint8_t *curp; /* текущая позиция в буфере */
uint16_t level; /* байт в буфере (смысл зависит от _F_DIROUT) */
int hold; /* байт от ungetc, EOF = пусто */
} FILE; } FILE;
#define _F_READ 0x01 #define _F_READ 0x01
#define _F_WRITE 0x02 #define _F_WRITE 0x02
#define _F_APPEND 0x04 #define _F_APPEND 0x04
#define _F_DIROUT 0x08 /* буфер сейчас копит запись (иначе readahead) */
#define _F_EOF 0x10 #define _F_EOF 0x10
#define _F_ERROR 0x20 #define _F_ERROR 0x20
#define _F_CONIN 0x40 /* console pseudo-stream — uses getchar() */ #define _F_CONIN 0x40 /* console pseudo-stream — uses getchar() */
@@ -70,6 +91,10 @@ int fputc (int c, FILE *fp);
int fgetc (FILE *fp); int fgetc (FILE *fp);
int fputs (const char *s, FILE *fp); int fputs (const char *s, FILE *fp);
char *fgets (char *buf, int n, 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 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); size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp);