thecodingidiot.com

The ReaderMultiple File Descriptors

Multiple File Descriptors

The current implementation stores its state in a single static char *leftover. That variable is shared across every call to tci_getline, regardless of which file descriptor is passed. Open two files simultaneously and alternate calls — the second call overwrites the state the first one saved.

The problem

int  fd1 = open("file1.txt", O_RDONLY);
int  fd2 = open("file2.txt", O_RDONLY);
 
char *a = tci_getline(fd1);  /* reads from file1, saves leftover for fd1 */
char *b = tci_getline(fd2);  /* reads from file2 — overwrites leftover */
char *c = tci_getline(fd1);  /* reads from file1 — leftover is wrong */

On the third call, leftover holds whatever was saved after reading fd2, not fd1. The result is either garbage or a crash.

The fix

Replace the single pointer with an array indexed by file descriptor:

#define FD_MAX 1024
 
static char     *leftover[FD_MAX];

Every access to leftover becomes leftover[fd]. The rest of the function is unchanged:

char    *tci_getline(int fd)
{
    char    buf[BUFFER_SIZE + 1];   /* +1 for null terminator */
    char    *nl;
    char    *tmp;
    ssize_t bytes;
 
    if (fd < 0 || fd >= FD_MAX)     /* reject out-of-range descriptors */
        return (NULL);
    if (!leftover[fd])
        leftover[fd] = tci_strdup(""); /* initialise slot on first call for this fd */
    while (1) {
        nl = tci_strchr(leftover[fd], '\n');
        if (nl)
            return (extract_line(&leftover[fd], nl)); /* line ready — no read needed */
        bytes = read(fd, buf, BUFFER_SIZE);
        if (bytes <= 0)
            return (flush_leftover(&leftover[fd]));   /* EOF or error */
        buf[bytes] = '\0';          /* read() does not null-terminate */
        tmp = leftover[fd];
        leftover[fd] = strjoin(leftover[fd], buf);    /* accumulate new chunk */
        free(tmp);                  /* free old leftover after joining */
    }
}

FD_MAX 1024 matches the default Linux open file descriptor limit. If fd is out of range — negative or ≥ 1024 — return NULL immediately.

The static array is zero-initialised: leftover[0] through leftover[1023] are all NULL at program start. Each fd accumulates its own state independently.

Memory at close

FILE * — the stdio stream type used by fgets and getline — is an opaque struct that owns its internal buffer. fclose() frees that buffer because the state is attached to the object being closed.

tci_getline works with a raw int fd. There is no wrapper struct, no object to attach state to. close(fd) tells the kernel to release the file descriptor; it has no knowledge of leftover[fd] in user space. That is the tradeoff of operating below stdio: the caller controls the fd, so the caller is also responsible for consuming the file fully before closing it. Reading every line until NULL is the normal use case — and that is exactly what flush_leftover handles.

Run the multi-fd suite

make re
bash test.sh

The multi-fd suite opens two fixture files, reads them interleaved, and checks that each call returns the correct line from the correct file. All tests should now pass.