Motivation

To better understand the nature of simple library calls like “printf()”, I wanted to build a logger with minimal library usage. In doing so, the goal was to force me to research system call level functions that can help build the intuition behind function calls I might otherwise take for granted. This is not intended to be a reusable library, but simply as a learning scaffold.

The Prototype

Below is the full experiment (35 LOC). The goal was for a working model, not for performance or API design.


#include <unistd.h>

static int write_fully(int fd, const void *msg, size_t len) {
    size_t total_written = 0;
    const char *p = msg;
    while (total_written < len) {
        size_t left_to_write = len - total_written;
        ssize_t written = write(fd, p, left_to_write);
        if (written == -1) {
            const char error_msg[] = "Write failed!\n";
            size_t error_len = sizeof(error_msg) - 1;
            write(2, error_msg, error_len);
            return -1;
        }
        if (written == 0) {
            const char error_msg[] = "Write made no progress!\n";
            size_t error_len = sizeof(error_msg) - 1;
            write(2, error_msg, error_len);
            return -1;
        }
        total_written += written;
        p += written;
    }

    return 0;
        
}

int main(){
    const char msg[] = "Hello, world!\n";
    size_t len = sizeof(msg);
    int fd = 1;
    
    write_fully(fd, msg, len);
}

Observations

Initially, I underestimated just how much work could go into something as simple as a print statement to a terminal. Initial research forced me to learn deeply about stdin, stdout, and stderr. File descriptors, how C compiled variables into memory pointers, how even something as simple as a termination character could create subtle issues in downstream pipes or socket-based consumers that do not expect null-terminated buffers. Notably, I even failed to realize just how often writes were interrupted, leading to partial writes - leading to iterative changes in its attempts to redo it. Each of these things became a rabbit hole of investigation, and what began as just a simple project, became 7 hours of research just to write 35 lines of code.

What I Got Wrong Initially

When I started the project, I assumed there was a very simple way just to use write() to send information to the terminal - but as I realized, errors and partial writes were far more common than I expected. After a while, I was able to create a loop that not only retried those failures (excluding EINTR handling, which I plan to add explicitly), but made sure not to accidentally duplicate the output through iterative pointer arithmetic. All other fails were captured by immediate checks following each loop - which saved me from a few messy infinite outputs to my terminal.

Next Steps

  • Add EINTR
  • Compare this approach to ‘writev’
  • Decide if retry semantics should be caller-controlled
  • Work on other system level functions, possible sample profilers.