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:
@@ -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*).
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user