feat(stdio): getline/getdelim/stream locking

This commit is contained in:
2026-09-05 17:18:18 -04:00
parent 14c6fd6cf6
commit a2f7c33601
7 changed files with 1625 additions and 13 deletions
+503
View File
@@ -0,0 +1,503 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include "../internal/malloc.h"
#include "stdio_impl.h"
/*
* vlibc — memory streams (todo 18): fmemopen and open_memstream.
*
* Both build a normal buffered vlibc_FILE whose backing "device" is a
* memory region instead of a descriptor: the FILE's ->mem points at the
* backend struct below, and the stdio core (stdio_flush/stdio_refill/
* stdio_discard_read/fseeko/fclose) diverts its descriptor operations to
* the stdio_mem_* hooks at the bottom of this file. The buffered data
* path (cache buffer, mode switches, position invariant) is unchanged;
* only the device is synthetic.
*
* Backend state: `data` is the region, `size` its capacity, `len` the
* current data length (bytes at or beyond len read as end of file), and
* `dpos` the device-head position — the analog of the kernel file offset,
* which for a real stream the kernel tracks and stdio re-anchors around.
* Keeping dpos explicit (rather than deriving it from FILE state) is what
* keeps memory streams exact even when reads span several cache refills.
*
* Two flavors:
*
* - fmemopen(buf, size, mode) over a caller buffer. The stream does NOT
* own buf unless buf is NULL, in which case it allocates a growable
* zeroed region of its own and releases it on fclose. Modes follow
* C23 7.23.5.6: 'r' reads the whole region (length = size), 'w'
* truncates the current length to zero, 'a' starts at the current
* data length (the first NUL byte in string mode, i.e. without 'b').
* A '+' enables both directions. Writes past a fixed buffer's size
* fail (partial-write discipline like a full disk).
* - open_memstream(ptr, sizeloc): a write-only stream over a growable
* buffer it owns. After fflush or fclose the buffer is NUL-terminated
* at the data length, *ptr names it (realloc may move it), and
* *sizeloc holds the length; the caller frees *ptr after fclose.
*
* Limits: memory streams are not registered with the stdio open_list
* (that registry is private to stdio.c), so fflush(NULL) does not reach
* them; an explicit fflush(stream) works. They are also not thread-safe
* beyond what the single-threaded FILE core already is.
*/
/* Initial capacity for an open_memstream buffer. */
#define OMS_INITIAL 128
struct vlibc_memstream
{
FILE f; /* embedded stream; the public FILE * */
unsigned char *data; /* backing region */
size_t size; /* capacity of data */
size_t len; /* current data length (excludes any NUL) */
size_t dpos; /* device-head position in the region */
int fixed; /* writes past size fail (caller buffer) */
int own_data; /* release data on close (fmemopen NULL buf) */
int string_mode; /* no 'b': keep a NUL terminator at len */
char **user_ptr; /* open_memstream: caller's buffer slot */
size_t *user_len; /* open_memstream: caller's length slot */
};
/*
* Build the embedded FILE: cache buffer, flags, cursors — the same shape
* stdio_alloc_file produces for a descriptor stream, but with fd == -1
* and ->mem pointing back at the backend. Not registered in open_list
* (see the banner). The caller fills the region fields and ->pos.
*/
static struct vlibc_memstream *
ms_alloc(int m)
{
struct vlibc_memstream *ms = (struct vlibc_memstream *)__libc_malloc(sizeof(*ms));
if (ms == NULL)
{
return NULL;
}
ms->f.buf = (unsigned char *)__libc_malloc(BUFSIZ);
if (ms->f.buf == NULL)
{
__libc_free(ms);
return NULL;
}
ms->f.buf_size = BUFSIZ;
ms->f.rpos = ms->f.rstop = ms->f.buf;
ms->f.wpos = ms->f.buf;
ms->f.wstop = ms->f.buf + BUFSIZ;
ms->f.fd = -1;
ms->f.flags = m | F_OWNBUF | F_HEAP;
if ((m & F_READ) && (m & F_WRITE))
{
ms->f.flags |= F_RDWR;
}
ms->f.pos = 0;
ms->f.ungot = 0;
ms->f.lock = 0;
ms->f.lock_owner = NULL;
ms->f.next = NULL;
ms->f.mem = ms;
ms->data = NULL;
ms->size = 0;
ms->len = 0;
ms->dpos = 0;
ms->fixed = 0;
ms->own_data = 0;
ms->string_mode = 0;
ms->user_ptr = NULL;
ms->user_len = NULL;
return ms;
}
/* Release the FILE block (cache buffer + struct) on a construction error. */
static void
ms_free_file(struct vlibc_memstream *ms)
{
__libc_free(ms->f.buf);
__libc_free(ms);
}
/* Index of the first NUL byte in data[0, size), or size when none. */
static size_t
ms_nul_index(const struct vlibc_memstream *ms)
{
size_t i;
for (i = 0; i < ms->size; i++)
{
if (ms->data[i] == 0)
{
return i;
}
}
return ms->size;
}
/* Grow the owned region until it holds `need` bytes (doubling). */
static int
ms_grow(struct vlibc_memstream *ms, size_t need)
{
size_t ncap;
unsigned char *nd;
if (need <= ms->size)
{
return 1;
}
ncap = (ms->size > (size_t)-1 / 2) ? need : ms->size * 2;
if (ncap < need)
{
ncap = need;
}
nd = (unsigned char *)realloc(ms->data, ncap);
if (nd == NULL)
{
return 0; /* errno ENOMEM from realloc */
}
ms->data = nd;
ms->size = ncap;
return 1;
}
/*
* Copy n bytes from src into the region at offset at. Fixed regions take
* what fits (up to size - at); owned regions grow on demand. Extends len
* when the write passes the current end; owned regions zero the hole a
* seek past the end left behind, so stale allocator bytes never show up
* between the old length and the write. Returns the bytes written.
*/
static size_t
ms_write(struct vlibc_memstream *ms, size_t at, const unsigned char *src, size_t n)
{
size_t end;
if (ms->fixed)
{
if (at >= ms->size)
{
return 0;
}
if (n > ms->size - at)
{
n = ms->size - at;
}
}
else if (!ms_grow(ms, at + n + 1))
{
return 0;
}
if (at > ms->len && !ms->fixed)
{
__builtin_memset(ms->data + ms->len, 0, at - ms->len);
}
__builtin_memcpy(ms->data + at, src, n);
end = at + n;
if (end > ms->len)
{
ms->len = end;
}
return n;
}
/* Copy up to n bytes from offset at; nothing (EOF) once past len. */
static size_t
ms_read(struct vlibc_memstream *ms, size_t at, unsigned char *dst, size_t n)
{
if (at >= ms->len)
{
return 0;
}
if (n > ms->len - at)
{
n = ms->len - at;
}
__builtin_memcpy(dst, ms->data + at, n);
return n;
}
/* Post-flush sync: open_memstream publishes the (possibly moved) buffer
* and its NUL-terminated length; string-mode fmemopen keeps the caller's
* region readable as a string. */
static void
ms_sync(struct vlibc_memstream *ms)
{
if (ms->user_ptr != NULL)
{
ms->data[ms->len] = 0;
*ms->user_ptr = (char *)ms->data;
*ms->user_len = ms->len;
}
else if (ms->string_mode && ms->len < ms->size)
{
ms->data[ms->len] = 0;
}
}
/*
* Device hooks called by the stdio core (see stdio_impl.h). Each mirrors
* the descriptor half of the corresponding core helper, updating dpos the
* way a kernel would.
*/
int
stdio_mem_flush(FILE *f)
{
struct vlibc_memstream *ms = f->mem;
size_t n = (size_t)(f->wpos - f->buf);
if (!(f->flags & F_WRITE))
{
return 0;
}
if (n != 0)
{
size_t at = (f->flags & F_APPEND) ? ms->len : ms->dpos;
size_t w = ms_write(ms, at, f->buf, n);
if (w < n)
{
/* Fixed region full or out of memory: keep the unwritten
* remainder buffered and report the error, exactly like a
* partial descriptor write. */
size_t keep = n - w;
__builtin_memmove(f->buf, f->buf + w, keep);
f->wpos = f->buf + keep;
f->pos = (off_t)(at + w);
ms->dpos = (size_t)f->pos;
f->flags |= F_ERR;
return -1;
}
f->pos = (off_t)(at + n);
ms->dpos = (size_t)f->pos;
}
f->wpos = f->buf;
ms_sync(ms);
return 0;
}
void
stdio_mem_refill(FILE *f)
{
struct vlibc_memstream *ms = f->mem;
size_t at = ms->dpos;
size_t n;
f->rpos = f->rstop = f->buf;
f->pos = (off_t)at; /* re-anchor buf[0] at the device head */
n = ms_read(ms, at, f->buf, f->buf_size);
if (n != 0)
{
f->rstop = f->buf + n;
ms->dpos = at + n; /* device read ahead of the logical position */
}
else
{
f->flags |= F_EOF;
}
}
int
stdio_mem_discard(FILE *f)
{
struct vlibc_memstream *ms = f->mem;
if (f->rpos < f->rstop)
{
/* Unread cached data: rewind the device head to the logical
* position (the byte at rpos) and re-anchor pos there. */
f->pos += (off_t)(f->rpos - f->buf);
ms->dpos = (size_t)f->pos;
}
f->rpos = f->rstop = f->buf;
return 0;
}
// NOLINTBEGIN(bugprone-easily-swappable-parameters)
off_t
stdio_mem_lseek(FILE *f, off_t off, int whence)
{
struct vlibc_memstream *ms = f->mem;
off_t target;
if (whence == SEEK_SET)
{
target = off;
}
else if (whence == SEEK_END)
{
target = (off_t)ms->len + off;
}
else
{
errno = EINVAL;
return (off_t)-1;
}
if (target < 0)
{
errno = EINVAL;
return (off_t)-1;
}
ms->dpos = (size_t)target;
return target;
}
// NOLINTEND(bugprone-easily-swappable-parameters)
void
stdio_mem_close(FILE *f)
{
struct vlibc_memstream *ms = f->mem;
if (ms->user_ptr != NULL)
{
/* open_memstream: one last publish so the caller's pointer and
* size slots are current even without an explicit fflush
* (idempotent after a flush). */
ms->data[ms->len] = 0;
*ms->user_ptr = (char *)ms->data;
*ms->user_len = ms->len;
}
if (ms->own_data)
{
free(ms->data);
ms->data = NULL;
ms->own_data = 0;
}
}
// NOLINTBEGIN(bugprone-easily-swappable-parameters)
FILE *
fmemopen(void *buf, size_t size, const char *mode)
{
int m = 0;
int update = 0;
int binary = 0;
size_t len = 0;
size_t i;
struct vlibc_memstream *ms;
if (mode == NULL || size == 0)
{
errno = EINVAL;
return NULL;
}
switch (mode[0])
{
case 'r':
m |= F_READ;
break;
case 'w':
m |= F_WRITE;
break;
case 'a':
m |= F_WRITE | F_APPEND;
break;
default:
errno = EINVAL;
return NULL;
}
for (i = 1; mode[i] != '\0'; i++)
{
switch (mode[i])
{
case '+':
update = 1;
break;
case 'b':
binary = 1;
break;
default:
errno = EINVAL;
return NULL;
}
}
if (update)
{
m |= F_READ | F_WRITE;
}
ms = ms_alloc(m);
if (ms == NULL)
{
return NULL;
}
if (buf == NULL)
{
/* No caller buffer: allocate a zeroed, growable region of our
* own (released on fclose, see stdio_mem_close). */
ms->data = (unsigned char *)malloc(size);
if (ms->data == NULL)
{
ms_free_file(ms);
return NULL;
}
__builtin_memset(ms->data, 0, size);
ms->fixed = 0;
ms->own_data = 1;
}
else
{
ms->data = (unsigned char *)buf;
ms->fixed = 1;
ms->own_data = 0;
}
ms->size = size;
ms->string_mode = !binary;
/* Current data length per the open mode (see the banner). */
switch (mode[0])
{
case 'a':
len = binary ? size : ms_nul_index(ms);
break;
case 'r':
len = size;
break;
default:
len = 0; /* 'w': truncated */
break;
}
ms->len = len;
ms->dpos = (mode[0] == 'a') ? len : 0;
ms->f.pos = (off_t)ms->dpos;
return &ms->f;
}
// NOLINTEND(bugprone-easily-swappable-parameters)
FILE *
open_memstream(char **ptr, size_t *sizeloc)
{
struct vlibc_memstream *ms;
if (ptr == NULL || sizeloc == NULL)
{
errno = EINVAL;
return NULL;
}
ms = ms_alloc(F_WRITE);
if (ms == NULL)
{
return NULL;
}
ms->data = (unsigned char *)malloc(OMS_INITIAL);
if (ms->data == NULL)
{
ms_free_file(ms);
return NULL;
}
ms->data[0] = 0; /* the buffer starts as the empty string */
ms->size = OMS_INITIAL;
ms->fixed = 0;
ms->own_data = 0; /* the caller takes the region after fclose */
ms->user_ptr = ptr;
ms->user_len = sizeloc;
*ptr = (char *)ms->data;
*sizeloc = 0;
return &ms->f;
}
+142
View File
@@ -0,0 +1,142 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include "stdio_impl.h"
/*
* vlibc — line input (todo 18): getdelim, getline, puts.
*
* getdelim reads one line (or one delimiter-terminated field) into a
* growable caller buffer. The buffer is owned by the caller and must be
* released with free(); the stream may reallocate it as the line grows,
* updating *lineptr and *n on every move so the caller's pointer never
* dangles, even when a later realloc fails and getdelim returns -1.
*
* Character storage uses the byte count, not strlen: embedded NUL bytes
* are preserved in the line, and the returned length is what matters (the
* buffer is still NUL-terminated after the last byte read).
*
* Reads go through fgetc, so a partial final line without the delimiter
* is returned with its length at end of file, exactly like fgets; a pure
* end-of-file (or an error) before any byte is -1. Distinguishing the two
* at a mid-line stop follows the fgets discipline: a genuine EOF keeps the
* partial line, a read error (F_ERR) discards it.
*
* puts is the one plain stdout line helper not owned by the todo-15 core
* (which took fputs): write s, then a newline. Returned value is
* non-negative on success and EOF on error, matching the C standard.
*/
/* Initial capacity for a fresh getdelim buffer (both the *lineptr == NULL
* malloc case and the *n == 0 realloc-minimal case). */
#define GETDELIM_INITIAL 128
ssize_t
getdelim(char **restrict lineptr, size_t *restrict n, int delim, FILE *restrict stream)
{
char *buf;
size_t cap;
size_t used = 0;
int c;
if (lineptr == NULL || n == NULL || stream == NULL)
{
errno = EINVAL;
return -1;
}
buf = *lineptr;
cap = *n;
if (buf == NULL)
{
/* No buffer yet: a fresh allocation, published right away so an
* early -1 (immediate end of file) still hands the caller a
* usable, freeable buffer. */
buf = malloc(GETDELIM_INITIAL);
if (buf == NULL)
{
return -1; /* errno ENOMEM from the allocator */
}
cap = GETDELIM_INITIAL;
*lineptr = buf;
*n = cap;
}
else if (cap == 0)
{
/* A zero-sized buffer is unusable: start it at the minimum. */
char *nbuf = realloc(buf, GETDELIM_INITIAL);
if (nbuf == NULL)
{
return -1;
}
buf = nbuf;
cap = GETDELIM_INITIAL;
*lineptr = buf;
*n = cap;
}
for (;;)
{
if (used + 1 >= cap)
{
/* Room for one more byte plus the trailing NUL. Publish the
* grown buffer immediately so *lineptr never names a block
* this call has already freed. */
char *grown = realloc(buf, cap * 2);
if (grown == NULL)
{
*lineptr = buf;
*n = cap;
return -1;
}
buf = grown;
cap *= 2;
*lineptr = buf;
*n = cap;
}
c = fgetc(stream);
if (c == EOF)
{
if (used == 0 || (stream->flags & F_ERR))
{
/* End of file (or an error) before any byte, or a read
* error in the middle of the line. */
return -1;
}
/* Mid-line end of file: the partial line is the result. */
break;
}
buf[used++] = (char)c;
if (c == delim)
{
break;
}
}
buf[used] = '\0';
*lineptr = buf;
*n = cap;
return (ssize_t)used;
}
ssize_t
getline(char **restrict lineptr, size_t *restrict n, FILE *restrict stream)
{
return getdelim(lineptr, n, '\n', stream);
}
int
puts(const char *s)
{
if (fputs(s, stdout) == EOF)
{
return EOF;
}
return fputc('\n', stdout);
}
+178
View File
@@ -0,0 +1,178 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include "../internal/syscall.h"
#include "stdio_impl.h"
/*
* vlibc — per-stream locking and unlocked character I/O (todo 18).
*
* flockfile/ftrylockfile/funlockfile are POSIX.1-2008 base. The process is
* single-threaded until the thread todo (#45) lands, so these maintain the
* ownership state that a real lock needs — recursion depth in ->lock, the
* owning thread's FS base in ->lock_owner — without any kernel primitive.
* flockfile is recursive: the same thread may lock a stream repeatedly and
* must unlock as many times. Once real threads exist, the #46 mutex/cond
* work makes flockfile block on ->lock when another thread owns it; the
* owner/count fields here already make the recursion decision.
*
* getc_unlocked/getchar_unlocked/putc_unlocked/putchar_unlocked are the
* four POSIX.1-2008 base _unlocked forms (the remaining GNU _unlocked
* forms are L3 and deliberately not provided). They assume the caller
* holds the stream lock and run the same buffered read/write core the
* locked fgetc/fputc use, via the hidden helpers of stdio_impl.h, without
* taking the lock themselves.
*/
void
flockfile(FILE *stream)
{
void *self = __builtin_thread_pointer();
if (stream->lock == 0 || stream->lock_owner != self)
{
/* Fresh acquisition, or (once threads exist) a foreign owner:
* the #46 per-stream lock will block here until the stream is
* free. The single running thread is always the owner. */
stream->lock_owner = self;
stream->lock = 0;
}
stream->lock++;
}
int
ftrylockfile(FILE *stream)
{
if (stream->lock > 0)
{
/* Already owned (recursively or by another thread): the #46 real
* lock would fail here without blocking. */
return EBUSY;
}
stream->lock_owner = __builtin_thread_pointer();
stream->lock = 1;
return 0;
}
void
funlockfile(FILE *stream)
{
if (stream->lock > 0)
{
stream->lock--;
if (stream->lock == 0)
{
stream->lock_owner = NULL;
}
}
}
int
getc_unlocked(FILE *stream)
{
stdio_init_if_needed(stream);
if (stream->flags & F_PUSHED)
{
stream->flags &= ~F_PUSHED;
return stream->ungot;
}
if (stream->flags & F_WRITE)
{
if (!(stream->flags & F_READ))
{
/* Reading a write-only stream. */
stream->flags |= F_ERR;
return EOF;
}
if (stdio_flush(stream) < 0)
{
return EOF;
}
stream->flags &= ~F_WRITE;
stream->flags |= F_READ;
}
stream->flags |= F_READ;
if (stream->rpos >= stream->rstop)
{
stdio_refill(stream);
}
if (stream->rpos >= stream->rstop)
{
return EOF; /* F_EOF or F_ERR set by refill */
}
return *stream->rpos++;
}
int
getchar_unlocked(void)
{
return getc_unlocked(stdin);
}
// NOLINTBEGIN(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
int
putc_unlocked(int c, FILE *stream)
{
stdio_init_if_needed(stream);
if (!(stream->flags & F_WRITE) && !(stream->flags & F_RDWR))
{
/* The open mode does not allow writes. */
stream->flags |= F_ERR;
return EOF;
}
if (stream->rpos < stream->rstop)
{
/* Pending read data: discard it before writing (the read -> write
* mode switch on an update stream). */
if (stdio_discard_read(stream) < 0)
{
return EOF;
}
}
stream->flags |= F_WRITE;
if (stream->flags & F_UNBUF)
{
unsigned char ch = (unsigned char)c;
long r = __syscall3(SYS_write, stream->fd, (long)&ch, 1);
if (r != 1)
{
if (r < 0)
{
(void)syscall_ret(r); /* sets errno */
}
stream->flags |= F_ERR;
return EOF;
}
stream->pos += 1;
return ch;
}
if (stream->wpos >= stream->wstop)
{
if (stdio_flush(stream) < 0)
{
return EOF;
}
}
*stream->wpos++ = (unsigned char)c;
if ((stream->flags & F_LINEBUF) && c == '\n')
{
if (stdio_flush(stream) < 0)
{
return EOF;
}
}
return (unsigned char)c;
}
// NOLINTEND(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
int
putchar_unlocked(int c)
{
return putc_unlocked(c, stdout);
}
+45 -2
View File
@@ -284,6 +284,9 @@ stdio_alloc_file(int fd, int m)
}
f->pos = 0;
f->ungot = 0;
f->lock = 0;
f->lock_owner = NULL;
f->mem = NULL;
f->next = open_list;
open_list = f;
return f;
@@ -295,6 +298,13 @@ stdio_refill(FILE *f)
{
long n;
if (f->mem != NULL)
{
/* Memory stream: read from the backing region instead of the
* descriptor (see the stdio_impl.h memstream contract). */
stdio_mem_refill(f);
return;
}
f->rpos = f->rstop = f->buf;
n = __syscall3(SYS_read, f->fd, (long)f->buf, (long)f->buf_size);
if (n > 0)
@@ -315,6 +325,13 @@ stdio_refill(FILE *f)
int
stdio_discard_read(FILE *f)
{
if (f->mem != NULL)
{
/* Memory stream: discarding unread data only rewinds the backend
* position; there is no kernel position to seek (see the
* stdio_impl.h memstream contract). */
return stdio_mem_discard(f);
}
if (f->rpos < f->rstop)
{
/* Unread buffered data: move the kernel position back to the
@@ -340,6 +357,13 @@ stdio_flush(FILE *f)
size_t n;
long r;
if (f->mem != NULL)
{
/* Memory stream: the backend's flush does its own device write
* (and open_memstream's size sync); the partial-write discipline
* below is mirrored there. */
return stdio_mem_flush(f);
}
if (!(f->flags & F_WRITE))
{
return 0;
@@ -522,6 +546,13 @@ fclose(FILE *stream)
{
rc = EOF;
}
if (stream->mem != NULL)
{
/* Memory stream: finalize the backend (open_memstream's last
* NUL/size sync, fmemopen's owned-buffer release) before the
* FILE block below is freed. */
stdio_mem_close(stream);
}
if (stream->fd >= 0 && syscall_ret(__syscall1(SYS_close, stream->fd)) < 0)
{
rc = EOF;
@@ -946,8 +977,20 @@ fseeko(FILE *stream, off_t offset, int whence)
}
if (target != (off_t)-1)
{
/* SEEK_SET/SEEK_CUR: seek the kernel to the absolute target. */
stream->pos = stdio_lseek(stream->fd, target, SEEK_SET);
/* SEEK_SET/SEEK_CUR: seek the kernel to the absolute target.
* Memory streams resolve the target against their region. */
if (stream->mem != NULL)
{
stream->pos = stdio_mem_lseek(stream, target, SEEK_SET);
}
else
{
stream->pos = stdio_lseek(stream->fd, target, SEEK_SET);
}
}
else if (stream->mem != NULL)
{
stream->pos = stdio_mem_lseek(stream, offset, SEEK_END);
}
else
{
+64 -11
View File
@@ -25,25 +25,48 @@
*
* The helper prototypes below are shared with the formatted-I/O todos
* (16: vfprintf, 17: vfscanf) so they operate on the same buffer state.
*
* Locking (todo 18): the process is single-threaded until the thread
* todo (#45) lands, so flockfile/ftrylockfile/funlockfile only track
* ownership state here. `lock` is the recursion depth (0 = unlocked),
* `lock_owner` the owning thread's FS base while locked. When real
* threads arrive, the same todo turns `lock` into the actual per-stream
* lock word (blocking acquisition, mutex/cond state) and these two
* fields become the bookkeeping that makes flockfile recursive.
*
* Memory streams (todo 18): fmemopen/open_memstream build a FILE whose
* `mem` points at an opaque backend (defined in fmemopen.c) instead of a
* descriptor. Whenever `mem` is non-NULL the buffered core below calls
* back into fmemopen.c's stdio_mem_* hooks in place of every descriptor
* syscall, so the position invariant above is unchanged: the backend
* simply acts as an in-memory device with the same kernel-position
* semantics (reads advance it past the read-ahead, writes advance it on
* flush, discards rewind it to the logical position).
*/
#include <stdio.h>
#include "../internal/libc.h"
/* Opaque memory-stream backend (fmemopen/open_memstream, todo 18). */
struct vlibc_memstream;
struct vlibc_FILE
{
unsigned char *buf; /* buffer base: library- or user-supplied */
unsigned char *rpos; /* read mode: next byte to deliver */
unsigned char *rstop; /* read mode: end of valid buffered data */
unsigned char *wpos; /* write mode: next byte to store */
unsigned char *wstop; /* write mode: end of the buffer */
int fd; /* underlying descriptor; -1 when closed */
int flags; /* F_* bits below */
size_t buf_size; /* usable buffer size; 0 = not yet sized */
off_t pos; /* position per the invariant above */
struct vlibc_FILE *next; /* registry link for fflush(NULL) */
unsigned char ungot; /* pushed-back byte when F_PUSHED */
unsigned char *buf; /* buffer base: library- or user-supplied */
unsigned char *rpos; /* read mode: next byte to deliver */
unsigned char *rstop; /* read mode: end of valid buffered data */
unsigned char *wpos; /* write mode: next byte to store */
unsigned char *wstop; /* write mode: end of the buffer */
int fd; /* underlying descriptor; -1 when closed */
int flags; /* F_* bits below */
size_t buf_size; /* usable buffer size; 0 = not yet sized */
off_t pos; /* position per the invariant above */
struct vlibc_FILE *next; /* registry link for fflush(NULL) */
unsigned char ungot; /* pushed-back byte when F_PUSHED */
int lock; /* flockfile recursion depth; 0 = unlocked */
void *lock_owner; /* owning thread's FS base while locked */
struct vlibc_memstream *mem; /* memory-stream backend, or NULL */
};
/* Stream flags. */
@@ -94,4 +117,34 @@ stdio_parse_mode(const char *mode, int *m, int *oflags);
hidden off_t
stdio_lseek(int fd, off_t off, int whence);
/*
* Memory-stream device hooks (fmemopen.c, todo 18). The stdio core calls
* these in place of the descriptor syscalls whenever f->mem is set:
*
* - stdio_mem_flush: write the pending cache to the backing region
* (stdio_flush's device-write half, F_APPEND included).
* - stdio_mem_refill: read the next cache-full from the region
* (stdio_refill's device-read half).
* - stdio_mem_discard: rewind the device to the logical position,
* discarding unread cached data (stdio_discard_read's seek half).
* - stdio_mem_lseek: resolve a SEEK_SET/SEEK_END target against the
* region (used by fseeko instead of stdio_lseek on memory streams).
* - stdio_mem_close: run on fclose before the FILE is released
* (open_memstream's final NUL/size sync, owned-buffer release).
*/
hidden int
stdio_mem_flush(FILE *f);
hidden void
stdio_mem_refill(FILE *f);
hidden int
stdio_mem_discard(FILE *f);
hidden off_t
stdio_mem_lseek(FILE *f, off_t off, int whence);
hidden void
stdio_mem_close(FILE *f);
#endif /* VLIBC_STDIO_STDIO_IMPL_H */