The C Programming Language, 2nd Edition, by Kernighan and Ritchie
Exercise 8.03 on page 179
Design and write _flushbuf
, fflush
, and fclose
.
Solution by Gregory Pietsch
/* Editor's note: Gregory didn't supply a main() for this. Normally, in these situations, * I'd supply one Richard Heathfield, so that you can easily run and test the code. But, in this case, * I wouldn't know where to start! If anyone wants to fill the gap, please let Richard Heathfield know. * Thanks. * RJH, 28 June 2000 */ #include <stdio.h> /* on p.176 */ #include "syscalls.h" /* or stdlib.h */ /* _flushbuf - flush a buffer * According to the code on p. 176, _flushbuf * is what putc calls when the buffer is full. * EOF as the character causes everything to * be written -- I don't tack on the EOF. */ int _flushbuf(int c, FILE *f) { int num_written, bufsize; unsigned char uc = c; if ((f->flag & (_WRITE|_EOF|_ERR)) != _WRITE) return EOF; if (f->base == NULL && ((f->flag & _UNBUF) == 0)) { /* no buffer yet */ if ((f->base = malloc(BUFSIZ)) == NULL) /* couldn't allocate a buffer, so try unbuffered */ f->flag |= _UNBUF; else { f->ptr = f->base; f->cnt = BUFSIZ - 1; } } if (f->flag & _UNBUF) { /* unbuffered write */ f->ptr = f->base = NULL; f->cnt = 0; if (c == EOF) return EOF; num_written = write(f->fd, &uc, 1); bufsize = 1; } else { /* buffered write */ if (c != EOF) f->ptr++ = uc; bufsize = (int)(f->ptr - f->base); num_written = write(f->fd, fp->base, bufsize); f->ptr = f->base; f->cnt = BUFSIZ - 1; } if (num_written == bufsize) return c; else { f->flag |= _ERR; return EOF; } } /* fflush */ int fflush(FILE *f) { int retval; int i; retval = 0; if (f == NULL) { /* flush all output streams */ for (i = 0; i < OPEN_MAX; i++) { if ((_iob[i]->flag & _WRITE) && (fflush(_iob[i]) == -1)) retval = -1; } } else { if ((f->flag & _WRITE) == 0) return -1; _flushbuf(EOF, f); if (f->flag & _ERR) retval = -1; } return retval; } /* fclose */ int fclose(FILE *f) { int fd; if (f == NULL) return -1; fd = f->fd; fflush(f); f->cnt = 0; f->ptr = NULL; if (f->base != NULL) free(f->base); f->base = NULL; f->flag = 0; f->fd = -1; return close(fd); }
Solution by codybartfast (cat 0)
This works in the basic example in main
(at the end), but each time I look at the code I see another potential bug, or
something where it is unclear what the correct behaviour is. I think it would be a lot of work to test all the edge case.
#include <fcntl.h> #include <stdlib.h> #include <unistd.h> #undef NULL #define NULL 0 #define EOF (-1) #define OPEN_MAX 20 typedef struct _iobuf { int cnt; char *ptr; char *base; int flag; int fd; int bufsize; } FILE; enum flags { _READ = 01, _WRITE = 02, _UNBUF = 04, _EOF = 010, _ERR = 020 }; #define feof(p) (((p)->flag & _EOF) != 0) #define ferror(p) (((p)->flag & _ERR) != 0) #define fileno(p) ((p)->fd) #define isopen(p) ((p)->flag & (_READ | _WRITE)) #define markclosed(p) ((p)->flag &= ~(_READ | _WRITE)) #define notreadyfor(p, rd_wr) (((p)->flag & (rd_wr | _EOF | _ERR)) != rd_wr) #define getc(p) (--(p)->cnt >= 0 ? (unsigned char)*(p)->ptr++ : _fillbuf(p)) #define putc(x, p) (--(p)->cnt >= 0 ? *(p)->ptr++ = (x) : _flushbuf((x), p)) #define getchar() getc(stdin) #define putchar(x) putc(x, stdout) FILE _iob[OPEN_MAX] = { { 0, (char *)0, (char *)0, _READ, 0, 0 }, { 0, (char *)0, (char *)0, _WRITE, 1, 0 }, { 0, (char *)0, (char *)0, _WRITE | _UNBUF, 2, 0 } }; #define stdin (&_iob[0]) #define stdout (&_iob[1]) #define stderr (&_iob[2]) FILE *fopen(char *name, char *mode); int fflush(FILE *); int fclose(FILE *); static int _fillbuf(FILE *); static int _flushbuf(int, FILE *); #define BUFSIZE 1024 #define PERMS 0666 int fflush(FILE *fp) { int n; if (fp == NULL) { int i, rslt = 0; for (i = 0; i < OPEN_MAX; i++) if ((fp = &_iob[i])->flag & _WRITE) rslt |= fflush(fp); return rslt; } if (notreadyfor(fp, _WRITE)) return EOF; if (fp->base == NULL) return 0; if ((n = fp->bufsize - fp->cnt)) if (write(fp->fd, fp->base, n) != n) { fp->flag |= _ERR; return EOF; } fp->cnt = fp->bufsize; fp->ptr = fp->base; return 0; } int _flushbuf(int c, FILE *fp) { if (notreadyfor(fp, _WRITE)) return EOF; if (fp->base == NULL) { fp->bufsize = (fp->flag & _UNBUF) ? 1 : BUFSIZE; if ((fp->base = (char *)malloc(fp->bufsize)) == NULL) return EOF; fp->ptr = fp->base; fp->cnt = fp->bufsize; } else if (fflush(fp)) return EOF; *fp->ptr++ = c; fp->cnt--; return 0; } int fclose(FILE *fp) { int rslt = 0; if (fp == NULL) return EOF; if (fp->flag & _WRITE) rslt = fflush(fp); free(fp->base); fp->base = NULL; if (fp->fd <= 2) return rslt; markclosed(fp); return rslt | close(fp->fd); } FILE *fopen(char *name, char *mode) { int fd; FILE *fp; if (*mode != 'r' && *mode != 'w' && *mode != 'a') return NULL; for (fp = _iob; fp < _iob + OPEN_MAX; fp++) if (!isopen(fp)) break; if (fp >= _iob + 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->bufsize = 0; fp->flag = (*mode == 'r') ? _READ : _WRITE; return fp; } int _fillbuf(FILE *fp) { if (notreadyfor(fp, _READ)) return EOF; if (fp->base == NULL) { fp->bufsize = (fp->flag & _UNBUF) ? 1 : BUFSIZE; if ((fp->base = (char *)malloc(fp->bufsize)) == NULL) return EOF; } fp->ptr = fp->base; fp->cnt = read(fp->fd, fp->ptr, fp->bufsize); if (--fp->cnt < 0) { if (fp->cnt == -1) fp->flag |= _EOF; if (fp->cnt == -2) fp->flag |= _ERR; fp->cnt = 0; return EOF; } return (unsigned char)*fp->ptr++; } int main(int argc, char *argv[]) { FILE *source, *temp; char c, *spath, *tpath; if (argc != 3) { write(2, &"error: Expect 3 args!\n", 22); return 1; } spath = argv[1]; tpath = argv[2]; if ((source = fopen(spath, "r")) == NULL) { write(2, &"error: failed to open source!\n", 30); return 1; } if ((temp = fopen(tpath, "w")) == NULL) { write(2, &"error: failed to open temp to write!\n", 37); return 1; } /* copy from source to temp */ while ((c = getc(source)) != EOF) putc(c, temp); fclose(source); fclose(temp); if ((temp = fopen(tpath, "r")) == NULL) { write(2, &"error: failed to open temp to read!\n", 36); return 1; } /* copy from temp to stdout */ while ((c = getc(temp)) != EOF) putchar(c); fflush(NULL); return 0; }
Latest code on github
Solution by anonymous
I used the descriptions from the standard library functions fflush and fclose on page 242 to help design my functions. For reference these are the descriptions:
int fflush(FILE *stream) On an output stream, fflush causes any buffered but unwritten data to be written; on an input stream, the effect is undefined. It returns EOF for a write error, and zero otherwise. fflush (NULL) flushes all output streams.
int fclose(FILE *stream) fclose flushes any unwritten data for stream, discards any unread buffered input, frees any automatically allocated buffer, then closes the stream. It returns EOF if any errors occurred, and zero otherwise.
Based on the function prototype for _flushbuf provided by the book and the way putc uses it (page 176), I was able to come up with how _flushbuf should be designed. Since fflush essentially does what _flushbuf should, I created fflush as the primary function to flush the FILE's input. I made _flushbuf call fflush and then store char c from putc. _flushbuf does basic error handling and leaves the rest to fflush. If fflush doesn't return an error, it stores the provided char in the buffer.
The fflush function checks if the fp is a valid pointer, if the FILE has no error, and it is for writing only. It then sets the bufsize depending on the UNBUF flag. If there isn't a buffer, it creates one so it can store the provided char in it. If there is a buffer, it writes its contents to its file descriptor. Finally, it resets the ptr to the buffer to the start and updates the number of chars that can fit in the buffer. Based on the standard library version, if NULL is provided, it should flush all output (write) buffers. So I added that to fflush as well by going through all _iob FILEs and passing them to itself. I originally had code that stopped the loop early if it found a FILE that wasn't open, but I realized that a file with a lower index could have been closed before a file with a high index in _iob. So I removed that optimization.
With the other two functions done, all fclose needs to do is call fflush for the FILE if it was for writing, free the buffer from malloc (if applicable), call the close function on the file descriptor, and then reset the FILE's values to 0/-1/NULL. I choose -1 for the file descriptor since 0 is the file descriptor for stdin.
I noticed that Greogry Pietsch's code is similar to mine, but mine might be a little simpler. I choose to write the buffer contents in fflush instead of _flushbuf which prevented the need to handle EOF and pass EOF from fflush. Overall though, the logic is essentially the same. I would just add some error handing for a FILE pointer provided that did not come from _iob.
codybartfast, we wrote very similar code but you added a lot of optimization via preprocessor definitions. I also noticed that you added the bufsize to the FILE struct. This could be useful if you wanted to include functionally for changing the buffer size after it has been allocated. Plus, it prevents the need to check the _UNBUF flag every time. Also, to comment on your uncertainty, I think the only place you would get into trouble is when a file is closed using your fclose. You run free on fp-base without checking to see if fp-base points to anything. You also don't check to see if fp is a FILE from _iob. and you don't update fp-ptr or fp-fd which could be improperly used after closing. Other than that, you code seems sound. Additionally, you don't allow stdin, stdout, or stderr to be fully closed even though this is allowed by the real function. I doubt this matters much since this is an exercise, but thought I would mention it. Finally, I like your use of write to print errors. My extra error function seems unnecessary now after seeing the way you did it.
Here is my code
#include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <string.h> /* Exercise 8-3. Design and write _flushbuf, fflush, and fclose. */ #ifdef NULL #undef NULL #endif typedef struct _iobuf { int cnt; // characters left char *ptr; // next character position char *base; // location of buffer int flag; // mode of file access int fd; // file descriptor } FILE; enum _flags { _READ = 01, // file open for reading _WRITE = 02, // file open for writing _UNBUF = 04, // file is unbuffered _EOF = 010, // EOF has occurred on this file _ERR = 020 // error occurred on this file }; #define NULL 0 #define EOF (-1) #define BUFSIZ 1024 #define OPEN_MAX 20 // max #files open at once #define stdin (&_iob[0]) #define stdout (&_iob[1]) #define stderr (&_iob[2]) extern FILE _iob[OPEN_MAX]; int _fillbuf(FILE *fp); int _flushbuf(int c, FILE *fp); void error(char *msg); #define feof(p) (((p)->flag & _EOF) == _EOF) #define ferror(p) (((p)->flag & _ERR) == _ERR) #define fileno(p) ((p)->fd) #define getc(p) (--(p)->cnt >= 0 ? (unsigned char) *(p)->ptr++ : _fillbuf(p)) #define putc(x, p) (--(p)->cnt >= 0 ? *(p)->ptr++ = (x) : _flushbuf((x), p)) #define getchar() getc(stdin) #define putchar(x) putc((x), stdout) #define PERMS 0666 // RW for owner, group, others #define MAXERRORMSG 1000 FILE _iob[OPEN_MAX] = // stdin, stdout, stderr { { 0, (char *) 0, (char *) 0, _READ, 0 }, { 0, (char *) 0, (char *) 0, _WRITE, 1 }, { 0, (char *) 0, (char *) 0, _WRITE | _UNBUF, 2 } }; FILE *fopen(char *name, char *mode); int fflush(FILE *fp); int fclose(FILE *fp); int main(int argc, char *argv[]) { int c; char msg[MAXERRORMSG]; FILE *fpIn, *fpOut = stdout; // fpOut == stdout for usage 1 and 2 if (argc == 1) // read from stdin and write to stdout fpIn = stdin; else if (argc == 2) // read from file and write to stdout { if ((fpIn = fopen(*++argv, "r")) == NULL) error(strcat(strcat(msg, "error: couldn't open file "), *argv)); // the double strcat adds "error..." to msg and then filename to msg after "error..." string } else if (argc == 3) // read from file1 and write to file2 { if ((fpIn = fopen(*++argv, "r")) == NULL) error(strcat(strcat(msg, "error: couldn't open file "), *argv)); if ((fpOut = fopen(*++argv, "w")) == NULL) error(strcat(strcat(msg, "error: couldn't create/write to file "), *argv)); } else error("usage 1: ./mystdio\nusage 2: ./mystdio input_file\nusage 3: ./mystdio intputfile outputfile"); while ((c = getc(fpIn)) != EOF) // read from fpIn and write to fpOut putc(c, fpOut); fclose(fpIn); // close the file since reached EOF. It is fine if it is stdin since exiting fclose(fpOut); // close the file since reached EOF. It is fine if it is stdout since exiting exit(0); } // opens file. Returns NULL if could not open file/bad mode provided, otherwise returns file ptr FILE *fopen(char *name, char *mode) { int fd; FILE *fp; if (*mode != 'r' && *mode != 'w' && *mode != 'a') return NULL; for (fp = _iob; fp < _iob + OPEN_MAX; fp++) if ((fp->flag & (_READ | _WRITE)) == 0) // if both _READ and _WRITE bits not set break; if (fp >= _iob + OPEN_MAX) // no free slots 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) // couldn't access name return NULL; fp->fd = fd; fp->cnt = 0; fp->base = NULL; fp->flag = (*mode == 'r') ? _READ : _WRITE; // remove all flags and set only _READ or _WRITE return fp; } // allocate and fill input buffer. If error or EOF, return EOF, otherwise return next char in buffer int _fillbuf(FILE *fp) { if ((fp->flag & (_READ | _EOF | _ERR)) != _READ) // if _READ is not set or _EOF or _ERR is set return EOF; // only _READ should be set out of those three. Return EOF int bufsize = (fp->flag & _UNBUF) ? 1 : BUFSIZ; // get buffer size if (fp->base == NULL) // no buffer yet if ((fp->base = (char *) malloc(bufsize)) == NULL) // create buffer return EOF; // failed to create buffer, return EOF fp->ptr = fp->base; // reset ptr to base since all chars in buffer have already been read fp->cnt = read(fp->fd, fp->ptr, bufsize); // store number of chars read from fd in cnt. Overwrite buffer with up to bufsize number of chars in buffer pointed to by ptr if (--fp->cnt < 0) // if cnt - 1 is less than 0 { if (fp->cnt == -1) // if == -1, reached EOF fp->flag |= _EOF; // turn on _EOF bit else fp->flag |= _ERR; // if < -1, error occurred turn on _ERR bit fp->cnt = 0; // reset number to indicate no chars left without having negative number return EOF; } return (unsigned char) *fp->ptr++; // if read was successful, return char read from input } // calls fflush to write unread buffered data to output. Stores char in new buffer. Returns EOF if error, otherwise 0 int _flushbuf(int c, FILE *fp) { if (fp == NULL) return EOF; // invalid pointer provided else if (fflush(fp) == EOF) return EOF; // an error occured in fflush *fp->ptr++ = (char) c; // store provided char in buffer. Make sure to cast to the int to char since buffer is based on size of char fp->cnt--; // update the counter for the number of chars that can fit in the buffer (just stored one) return 0; } // if fp is write FILE, writes unwritten buffer to output. if fp == NULL, flushes all write FILES. Returns EOF if error or read FILE provided, otherwise 0. int fflush(FILE *fp) { if (fp == NULL) // if fp == NULL, then flush all buffers { int result = 0; for (int i = 0; i < OPEN_MAX; i++) // go through all FILEs in _iob. Can't intelligently break from loop early since an older FILE can be closed before a newer one if (((&_iob[i])->flag & _WRITE) == _WRITE && fflush(&_iob[i]) == EOF) // if _WRITE flag set, flush it. Also, if fflush returns error, update result result = EOF; return result; // if any error occurred, return EOF, otherwise return 0 } else if (fp < _iob || fp >= _iob + OPEN_MAX) return EOF; // invalid pointer provided else if ((fp->flag & (_WRITE | _ERR | _READ)) != _WRITE) // if _WRITE is not set or _ERR or _READ is set. return EOF; // only _WRITE should be set out of those three. Return EOF int bufsize = (fp->flag & _UNBUF) ? 1 : BUFSIZ; // get buffer size if (fp->base == NULL) // no buffer yet so nothing to flush. Create buffer to store provided char { if ((fp->base = (char *) malloc(bufsize)) == NULL) { fp->flag |= _ERR; // turn on _ERR bit return EOF; // malloc failed to allocate a buffer } } else // buffer exists, so write contents to file { int n = fp->ptr - fp->base; // gets number of characters in buffer if (write(fp->fd, fp->base, n) != n) { fp->flag |= _ERR; // turn on _ERR bit return EOF; // error writing buffer, return EOF } } fp->ptr = fp->base; // reset ptr to base of buffer since data was already written or the buffer was just created fp->cnt = bufsize; // update the counter for the number of chars that can fit in the buffer so putc will work correctly return 0; } // flushes any unwritten data for fp, discards any unread buffered input, frees any automatically allocated buffer, then closes the file. Returns EOF if error, otherwise 0 int fclose(FILE *fp) { int result = 0; if (fp == NULL || fp < _iob || fp >= _iob + OPEN_MAX) return EOF; // invalid pointer provided if ((fp->flag & _WRITE) == _WRITE) // if file is for writing result = fflush(fp); // flush anything in buffer to output and store result if (fp->base != NULL) // if there is a buffer that malloc allocated (applies to both read and write FILEs) free(fp->base); // free it since we don't want a memory leak if (close(fp->fd) != 0) // close file (note: it's legal to close stdin, stdout, and stderr. Just reopen them (e.g. freopen) or exit program afterwards to prevent issues) result = EOF; // close returns 0 if no errors. So update result to error since it failed to close fp->cnt = fp->flag = 0; // reset cnt and flag to 0 fp->fd = -1; // reset fd to -1 (which indicates error and should prevent improper usage of this FILE's fd) fp->ptr = fp->base = NULL; // and reset the pointers to NULL. All of this resetting makes the FILE object avaliable for future fopen calls return result; // defaults to 0 unless fflush or close returned an error } // print an error message and exit. Since normal printf messages aren't coded, use putc for stderr void error(char *msg) { fflush(NULL); // flush all buffers before program exits and error is printed while (*msg != '\0') putc(*msg++, stderr); // write error chars to stderr putc('\n', stderr); // finish it off with '\n', just in case fflush(stderr); // make sure error message is printed exit(1); // close program }