#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
//compile with: gcc -Wall -Wextra cc-c.c -o cc-c
//run with: ./cc-c
//K&R2 Exercise 8-3. Design and write _flushbuf, fflush, and fclose.
//
//In plain words: 8.5 built the READING half of stdio (getc and
//_fillbuf). This is the WRITING half:
// _flushbuf what the putc macro calls when the output buffer is full
// -- the mirror of _fillbuf
// fflush write out whatever is waiting in the buffer, now
// fclose flush, give back the buffer, close the fd, free the slot
// in the table (8.5 had no way to free one: full after 20)
//"Design" means the book leaves the decisions to you. Each one is
//marked DESIGN: below.
//
//The 8.5 code (fopen, _fillbuf, getc) is here unchanged, with the
//my_ / M_ names; the book's putc macro is added. main:
// 1. the first putc: cnt 0 sends it to _flushbuf, which mallocs
// 2. 2500 putc: write() runs at 1025 and 2049 -- the file on disk is
// behind what the program has "written"
// 3. fflush catches the file up
// 4. unbuffered (stderr): one write() per putc, at once
// 5. buffered stdout: a line sits in the buffer until fflush
// 6. without fclose the bytes never reach the file -- the real exit()
// flushes every FILE; it doesn't know our table exists
// 7. fclose frees the slot: 100 fopen/fclose pairs, same fd every time
// 8. fclose on a read file, fclose twice, fflush(NULL)
// 9. errors: putc on a read file; /dev/full makes every write() fail
// 10. the book's putc macro returns -1 for byte 0xff (the 8.5 cast
// problem, from the writing side)
#define MY_OPEN_MAX 20
#define MY_BUFSIZ 1024
#define PERMS 0666
#define DEMO "flush-demo.txt" //made and unlinked by main
typedef struct my_iobuf {
int cnt; //reading: characters left; writing: space left
char *ptr; //next character position
char *base; //location of buffer
int flag; //mode of file access
int fd; //file descriptor
} MYFILE;
enum my_flags {
M_READ = 01,
M_WRITE = 02,
M_UNBUF = 04,
M_EOF = 010,
M_ERR = 020
};
MYFILE my_iob[MY_OPEN_MAX] = {
{ 0, (char *) 0, (char *) 0, M_READ, 0 },
{ 0, (char *) 0, (char *) 0, M_WRITE, 1 },
{ 0, (char *) 0, (char *) 0, M_WRITE | M_UNBUF, 2 }
};
#define my_stdin (&my_iob[0])
#define my_stdout (&my_iob[1])
#define my_stderr (&my_iob[2])
int my_fillbuf(MYFILE *);
int my_flushbuf(int, MYFILE *);
int my_fflush(MYFILE *);
#define my_feof(p) (((p)->flag & M_EOF) != 0)
#define my_ferror(p) (((p)->flag & M_ERR) != 0)
#define my_getc(p) (--(p)->cnt >= 0 \
? (unsigned char) *(p)->ptr++ : my_fillbuf(p))
//the book's putc, from 8.5. For writing, cnt counts SPACE left: while
//there is room, store x and move on; when cnt goes below 0, the buffer
//is full and _flushbuf takes over
#define my_putc(x,p) (--(p)->cnt >= 0 \
? *(p)->ptr++ = (x) : my_flushbuf((x),p))
#define my_putchar(x) my_putc((x), my_stdout)
int nwrites = 0; //not in the book: how many times write() was called
//my_fopen: open file, return file ptr -- 8.5, unchanged
MYFILE *my_fopen(char *name, char *mode)
{
int fd;
MYFILE *fp;
if (*mode != 'r' && *mode != 'w' && *mode != 'a')
return NULL;
for (fp = my_iob; fp < my_iob + MY_OPEN_MAX; fp++)
if ((fp->flag & (M_READ | M_WRITE)) == 0)
break;
if (fp >= my_iob + MY_OPEN_MAX)
return NULL;
if (*mode == 'w')
fd = creat(name, PERMS);
else if (*mode == 'a') {
if ((fd = open(name, O_WRONLY, 0)) == -1)
fd = creat(name, PERMS);
lseek(fd, 0L, 2);
} else
fd = open(name, O_RDONLY, 0);
if (fd == -1)
return NULL;
fp->fd = fd;
fp->cnt = 0;
fp->base = NULL;
fp->flag = (*mode == 'r') ? M_READ : M_WRITE;
return fp;
}
//my_fillbuf: allocate and fill input buffer -- 8.5, unchanged
int my_fillbuf(MYFILE *fp)
{
int bufsize;
if ((fp->flag & (M_READ | M_EOF | M_ERR)) != M_READ)
return EOF;
bufsize = (fp->flag & M_UNBUF) ? 1 : MY_BUFSIZ;
if (fp->base == NULL)
if ((fp->base = (char *) malloc(bufsize)) == NULL)
return EOF;
fp->ptr = fp->base;
fp->cnt = read(fp->fd, fp->ptr, bufsize);
if (--fp->cnt < 0) {
if (fp->cnt == -1)
fp->flag |= M_EOF;
else
fp->flag |= M_ERR;
fp->cnt = 0;
return EOF;
}
return (unsigned char) *fp->ptr++;
}
//=== the exercise ======================================================
//my_flushbuf: allocate or flush output buffer, then store c in it
int my_flushbuf(int c, MYFILE *fp)
{
unsigned char uc = c;
//IDIOM: (flag & (A|B)) != A -- "A set, B clear", the _fillbuf test
//for writing: open for writing, and no error so far
if ((fp->flag & (M_WRITE | M_ERR)) != M_WRITE)
return EOF;
//DESIGN: unbuffered means no buffer at all -- write c right now.
//(Giving it a 1-byte buffer, like _fillbuf does, would leave every
//character sitting there until the NEXT putc: stderr would always
//be one character behind)
if (fp->flag & M_UNBUF) {
nwrites++; //not in the book
if (write(fp->fd, &uc, 1) != 1) {
fp->flag |= M_ERR;
return EOF;
}
fp->cnt = 0; //so the next putc comes straight back here
return uc;
}
//DESIGN: first time, get a buffer; after that, a full buffer is
//exactly what fflush empties -- so call it, don't write it twice
if (fp->base == NULL) {
if ((fp->base = (char *) malloc(MY_BUFSIZ)) == NULL) {
fp->flag |= M_ERR;
return EOF;
}
} else if (my_fflush(fp) == EOF)
return EOF;
fp->ptr = fp->base;
*fp->ptr++ = uc;
fp->cnt = MY_BUFSIZ - 1; //one place used by c
//DESIGN: return c as unsigned char, like _fillbuf -- so byte 0xff
//is 255 here, never EOF (section 10: the macro's other path isn't)
return uc;
}
//my_fflush: write out a file's buffered output; NULL means all files
int my_fflush(MYFILE *fp)
{
int n, rc;
//DESIGN: fflush(NULL) flushes every file open for writing, as in
//Appendix B; one failure makes the whole call return EOF, but the
//loop still tries every file
if (fp == NULL) {
rc = 0;
for (fp = my_iob; fp < my_iob + MY_OPEN_MAX; fp++)
if ((fp->flag & M_WRITE) && my_fflush(fp) == EOF)
rc = EOF;
return rc;
}
//DESIGN: on a file open for reading the standard says the effect is
//undefined; here it does nothing and succeeds. A slot that isn't
//open at all is an error
if ((fp->flag & M_WRITE) == 0)
return (fp->flag & M_READ) ? 0 : EOF;
if (fp->flag & M_ERR)
return EOF;
//IDIOM: ptr - base -- two pointers into one buffer: subtracting
//them counts the characters stored so far
if (fp->base != NULL && (n = fp->ptr - fp->base) > 0) {
nwrites++; //not in the book
if (write(fp->fd, fp->base, n) != n) {
fp->flag |= M_ERR;
return EOF;
}
}
fp->ptr = fp->base;
//no buffer yet (or unbuffered): cnt 0, so the next putc calls
//_flushbuf; otherwise the whole buffer is free again
fp->cnt = (fp->base == NULL) ? 0 : MY_BUFSIZ;
return 0;
}
//my_fclose: flush, free the buffer, close the fd, free the slot
int my_fclose(MYFILE *fp)
{
int rc = 0;
//DESIGN: refuse anything that isn't an open slot of the table --
//NULL, a stray pointer, or a file already closed
if (fp == NULL || fp < my_iob || fp >= my_iob + MY_OPEN_MAX
|| (fp->flag & (M_READ | M_WRITE)) == 0)
return EOF;
//DESIGN: a failed flush or close still frees the slot -- the file
//is closed either way; the EOF tells the caller data may be lost
if ((fp->flag & M_WRITE) && my_fflush(fp) == EOF)
rc = EOF;
if (close(fp->fd) == -1)
rc = EOF;
//IDIOM: free(NULL) is allowed and does nothing -- no need to test
//base first
free(fp->base);
fp->base = fp->ptr = NULL;
fp->cnt = 0;
fp->flag = 0; //flag 0: the slot is free for my_fopen again
fp->fd = -1;
return rc;
}
//=== not in the book: for main =========================================
long size_on_disk(char *name)
{
struct stat st;
return (stat(name, &st) == -1) ? -1L : (long) st.st_size;
}
int main(void)
{
MYFILE *fp, *rp;
int c, i, n, w0, fd;
char *s;
//the real stdout has its own buffer, separate from ours. Turn it
//off, so printf output and our write()s reach the screen in the
//order the program makes them
setbuf(stdout, NULL);
//--- 1. the first putc --------------------------------------------
fp = my_fopen(DEMO, "w");
printf("1. my_fopen(\"%s\", \"w\"): cnt %d, base %s\n",
DEMO, fp->cnt, fp->base == NULL ? "NULL" : "set");
my_putc('A', fp);
printf(" my_putc('A'): --cnt is -1, so my_flushbuf: malloc, 'A' stored,\n"
" cnt now %d, ptr - base %td, write() calls %d\n\n",
fp->cnt, fp->ptr - fp->base, nwrites);
//--- 2. 2500 putc --------------------------------------------------
for (i = 1; i < 2500; i++) {
w0 = nwrites;
my_putc('A' + i % 26, fp);
if (nwrites != w0)
printf("2. putc #%d found the buffer full: write() #%d, %ld bytes on disk\n",
i + 1, nwrites, size_on_disk(DEMO));
}
printf(" 2500 putc done: %ld bytes on disk, %td still in the buffer\n\n",
size_on_disk(DEMO), fp->ptr - fp->base);
//--- 3. fflush ---------------------------------------------------
n = my_fflush(fp);
printf("3. my_fflush -> %d: %ld bytes on disk, write() calls %d, cnt %d (all free)\n\n",
n, size_on_disk(DEMO), nwrites, fp->cnt);
my_fclose(fp);
//--- 4. unbuffered -----------------------------------------------
w0 = nwrites;
printf("4. \"zhouyi\\n\" to my_stderr, 7 my_putc: ");
for (s = "zhouyi\n"; *s; s++)
my_putc(*s, my_stderr);
printf(" write() calls %d -- one per character, nothing waits\n\n",
nwrites - w0);
//--- 5. buffered stdout ------------------------------------------
printf("5. \"hidden until fflush\\n\" to my_stdout with my_putchar...\n");
for (s = " hidden until fflush\n"; *s; s++)
my_putchar(*s);
printf(" ...done, and nothing on the screen yet: %td bytes in the buffer\n",
my_stdout->ptr - my_stdout->base);
printf(" now my_fflush(my_stdout):\n");
my_fflush(my_stdout);
printf(" (a real stdout on a terminal also flushes at every \\n --\n"
" line buffering, which this version doesn't have)\n\n");
//--- 6. no fclose, no data ---------------------------------------
fp = my_fopen(DEMO, "w");
for (i = 0; i < 100; i++)
my_putc('x', fp);
printf("6. 100 my_putc, no fclose: %ld bytes on disk. If main returned\n"
" now, they'd be gone: the real exit() flushes the real FILEs,\n"
" and our my_iob means nothing to it\n", size_on_disk(DEMO));
n = my_fclose(fp);
printf(" my_fclose -> %d: %ld bytes on disk\n\n", n, size_on_disk(DEMO));
//--- 7. slots come back ------------------------------------------
printf("7. 100 x (my_fopen + 3 my_getc + my_fclose) on zhouyi.txt:");
for (i = 0; i < 100; i++) {
if ((rp = my_fopen("zhouyi.txt", "r")) == NULL)
break;
my_getc(rp);
my_getc(rp);
my_getc(rp);
if (i < 3)
printf(" &my_iob[%td] fd %d,", rp - my_iob, rp->fd);
fd = rp->fd;
my_fclose(rp);
}
printf(" ...\n all %d worked (8.5 ran out at 20), the last one: fd %d too --\n"
" the kernel reuses the lowest free number, like my_fopen's slots\n\n",
i, fd);
//--- 8. the other cases -------------------------------------------
rp = my_fopen("zhouyi.txt", "r");
my_getc(rp);
printf("8. my_fflush on a read file -> %d (does nothing)\n", my_fflush(rp));
//two calls, two statements: in one printf their order isn't fixed
n = my_fclose(rp);
c = my_fclose(rp);
printf(" my_fclose on it -> %d, nothing to flush; again -> %d: already closed\n",
n, c);
printf(" my_fclose(NULL) -> %d\n", my_fclose(NULL));
fp = my_fopen(DEMO, "a");
for (s = "|appended"; *s; s++)
my_putc(*s, fp);
w0 = nwrites;
n = my_fflush(NULL);
printf(" 9 bytes appended, my_fflush(NULL) -> %d: %d write() for the whole\n"
" table, %ld bytes on disk\n", n, nwrites - w0, size_on_disk(DEMO));
my_fclose(fp);
printf("\n");
//--- 9. errors ---------------------------------------------------
rp = my_fopen("zhouyi.txt", "r");
c = my_putc('x', rp);
printf("9. my_putc on a file opened \"r\" -> %d: no M_WRITE, so EOF\n", c);
my_fclose(rp);
//Linux's /dev/full accepts open, and every write() fails: "no
//space left on device" -- a full disk, on demand
fp = my_fopen("/dev/full", "w");
for (i = 0; i < 1024; i++)
my_putc('x', fp);
printf(" /dev/full: 1024 my_putc all fit in the buffer, ferror %d -- no\n"
" write() yet, so nothing has failed yet\n", my_ferror(fp));
c = my_putc('x', fp);
printf(" putc #1025 -> %d, ferror %d: the flush failed\n", c, my_ferror(fp));
c = my_putc('x', fp);
printf(" putc #1026 -> %d: M_ERR stops every write from now on\n", c);
my_fclose(fp);
fp = my_fopen("/dev/full", "w");
my_putc('x', fp);
n = my_fclose(fp);
printf(" another file on /dev/full, 1 putc, my_fclose -> %d: the putc\n"
" \"worked\", the data was lost at close -- only fclose's return\n"
" value says so\n\n", n);
//--- 10. the book's putc and byte 0xff ----------------------------
fp = my_fopen(DEMO, "w");
c = my_putc('\xff', fp);
printf("10. first my_putc('\\xff') -> %d (via my_flushbuf: unsigned char)\n", c);
c = my_putc('\xff', fp);
printf(" second one -> %d (via the macro's *(p)->ptr++ = (x): char is\n"
" signed here, so 0xff is -1 == EOF). A loop doing\n"
" if (putc(c, fp) == EOF) would call a good write an error.\n"
" The fix: (unsigned char) (*(p)->ptr++ = (x)), as in 8.5's getc\n", c);
my_fclose(fp);
unlink(DEMO);
return 0;
}
win@DESKTOP-MEIH88T:~/webdev-projects$ gcc -Wall -Wextra cc-c.c -o cc-c
win@DESKTOP-MEIH88T:~/webdev-projects$ ./cc-c
1. my_fopen("flush-demo.txt", "w"): cnt 0, base NULL
my_putc('A'): --cnt is -1, so my_flushbuf: malloc, 'A' stored,
cnt now 1023, ptr - base 1, write() calls 0
2. putc #1025 found the buffer full: write() #1, 1024 bytes on disk
2. putc #2049 found the buffer full: write() #2, 2048 bytes on disk
2500 putc done: 2048 bytes on disk, 452 still in the buffer
3. my_fflush -> 0: 2500 bytes on disk, write() calls 3, cnt 1024 (all free)
4. "zhouyi\n" to my_stderr, 7 my_putc: zhouyi
write() calls 7 -- one per character, nothing waits
5. "hidden until fflush\n" to my_stdout with my_putchar...
...done, and nothing on the screen yet: 23 bytes in the buffer
now my_fflush(my_stdout):
hidden until fflush
(a real stdout on a terminal also flushes at every \n --
line buffering, which this version doesn't have)
6. 100 my_putc, no fclose: 0 bytes on disk. If main returned
now, they'd be gone: the real exit() flushes the real FILEs,
and our my_iob means nothing to it
my_fclose -> 0: 100 bytes on disk
7. 100 x (my_fopen + 3 my_getc + my_fclose) on zhouyi.txt: &my_iob[3] fd 3, &my_iob[3] fd 3, &my_iob[3] fd 3, ...
all 100 worked (8.5 ran out at 20), the last one: fd 3 too --
the kernel reuses the lowest free number, like my_fopen's slots
8. my_fflush on a read file -> 0 (does nothing)
my_fclose on it -> 0, nothing to flush; again -> -1: already closed
my_fclose(NULL) -> -1
9 bytes appended, my_fflush(NULL) -> 0: 1 write() for the whole
table, 109 bytes on disk
9. my_putc on a file opened "r" -> -1: no M_WRITE, so EOF
/dev/full: 1024 my_putc all fit in the buffer, ferror 0 -- no
write() yet, so nothing has failed yet
putc #1025 -> -1, ferror 1: the flush failed
putc #1026 -> -1: M_ERR stops every write from now on
another file on /dev/full, 1 putc, my_fclose -> -1: the putc
"worked", the data was lost at close -- only fclose's return
value says so
10. first my_putc('\xff') -> 255 (via my_flushbuf: unsigned char)
second one -> -1 (via the macro's *(p)->ptr++ = (x): char is
signed here, so 0xff is -1 == EOF). A loop doing
if (putc(c, fp) == EOF) would call a good write an error.
The fix: (unsigned char) (*(p)->ptr++ = (x)), as in 8.5's getc