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
+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;
}