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.