Asymptotically Typeable

Home Blog RSS

C: Fork as a knife

Thu, 10 Dec 2020

Did you know that C's int fork() can fail? I didn't until I read retvals, terrible teaching, and admitting we have a problem. First thing I did afterwards was type in my trusty terminal man fork and summon my machine's inner librarian. Here's what this man page, the one installed on my machine, had to say about fork():

This function creates a new process. The return value is the zero in the child and the process-id number of the child in the parent, or -1 upon error. In the latter case, ERRNO indicates the problem.

Cool, cool... Scrolling down to some examples I find:
@load "fork"
...
if ((pid = fork()) == 0)
    print "hello from the child"
else
    print "hello from the parent"
Paraphrasing Rachelbythebay's blog post: WTF?! Where's the else if (pid < 0)? Why isn't anyone writing it? The blog post concludes:
  1. We're not including it in our examples, so those who are learning from our examples won't know exist, and,
  2. the (arguably) worse reason of all: laziness combined with a lack of creativity; the classical reason why a programmer won't do something: "it makes my code messy".

Anyways, messy-code is not a valid excuse, it never was and it never will be. Any code can always be "un-messified" with a little bit of creativity. At worse it can be hid behind a function call; else handle_fork_fail() won't ruin the zen energy of any code if you ask me. Why couldn't the man page example have been like this?

@load "fork"
...
if ((pid = fork()) == 0)
    print "hello from the child"
else if (pid > 0)
    print "hello from the parent"
else
    ...

As example-writers we must always be aware that we will be imitated often, and especially by those who know least. And we must always be aware that our code will likely be copy-pasted pretty much everywhere (example: your next car's firmware).

Rachelbythebay's blog post points to, what I believe is, the fundamental issue. It's not in the examples as much as it's in fork() itself. Luckily the blog puts it better than I could, here: if too many users are wrong, it's probably your fault.

Now here's an anecdote. When I arrived in the french part of Switzerland I was already speaking the official French, le français Parisien for the last fifteen years of my life. First day there what do you do? You go buy some food and the stuff the airport ruined. Normal. I get to the cashier and I heard a number I had never heard before. You should know that in official French, numbers like 99 are literally pronounced "four twenties ten nine" (quatre-vingt-dix-neuf 4*20+10+9=99). What I heard was a new word, something like "nine-ty nine" ("nenate" neuf), the Swiss got creative, and it blew my mind!

Coming back to fork(), we are complicating our lives when we use ad-hoc return values, and explicitly encoding statuses as integers isn't helping, at least hide them behind a typedef or a #define, have some shame. Try it with me, say this out loud: "if the new process is zero", sounds funky, no?

If you want to surrender to the status-quo and be pragmatic, here's a fork() example for you.

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
  
int _g_last_pid_value;
int (*old_fork)() = fork;

#define last_fork_in_child     if (_g_last_pid_value == 0)
#define last_fork_in_parent    if (_g_last_pid_value > 0)
#define last_fork_error        if (_g_last_pid_value < 0)
#define fork()                 (_g_last_pid_value = old_fork())

int main() {
   int pid = fork();

   last_fork_in_child     printf("hello from the child\n");
   last_fork_in_parent    printf("hello from the parent (child=%d)\n", pid);
   last_fork_error        fprintf(stderr, "error %d\n%s\n", errno, strerror(errno));

   return 0;
}

But, if you want to do better, ask yourself: What are you telling your user? Are you telling them "hey buddy, don't forget about the error case, no biggie if you forget, but please try to remember it", or are you saying "listen pal, that's not gonna work if you won't tell me what to do if it fails"?

If you want to absolutely force the user to provide an error handler then more sophisticated macros will be required. My go-to strategy is a type-driven approach, this way if an handler isn't provided the compiler will always let me know (I don't have to wait till the runtime). I implemented a proof-of-concept in D (the full code is at the end), here's an example illustrating my fork's interface:

MyFork.with_child_handler({
            writeln("Hello from the child");
      })
      .with_parent_handler((int pid) {
            writefln("Hello from the parent (child=%d)", pid);
      })
      .with_error_handler({
            writeln("An error occurred");
      })
      .run();

The handlers can be provided in any order, and exactly one handler must be provided, providing two will produce a compile error. The actual forking is done when run() is executed. Basically there are, eight phantom types (represented by mixed-in structs):

struct MyFork {
    MyForkWithChild with_child_handler(ChildHandler handler);
    MyForkWithParent with_parent_handler(ParentHandler handler);
    MyForkWithError with_error_handler(ErrorHandler handler);
}
...
struct MyForkWithParent {
    private ParentHandler parent_handler;
    MyForkWithChildParent with_child_handler(ChildHandler handler);
    MyForkWithParentError with_error_handler(ErrorHandler handler);
}
...
struct MyForkWithChildParentError {
    private ChildHandler child_handler;
    private ParentHandler parent_handler;
    private ErrorHandler error_handler;
    public void run() {
         int pid = fork();
         if (pid == 0) child_handler();
         else if (pid > 0) parent_handler();
         else error_handler();
    }
}

To reiterate, my point is: messy code has never been a valid excuse, and never will be. Besides sequencing .with_..._handlers we could use named arguments. Or we could use fancier macros. Or we could simulate the whole thing with function calls like the following example does. There are a million ways to tidy up your code and do error handling.

int (*old_fork)() = fork;
void fork(void (*child)(), void (*parent)(int), void (*error)()) {
    int pid = old_fork();
    if (pid == 0) child();
    else if (pid > 0) parent(pid);
    else error();
}

For the interested here is the complete compilable/executable source code of the D code mentioned above. The eight phantom types are not hand-written (you think I'm insane?) they are generated at compile-time by D's mixins and compile-time-function-execution (C++ template magic allows you to do that too). You could try this code by copy-pasting it in https://run.dlang.io. The implementation of the macros and templates and co. may be complicated, but the interface ought to be as simple as possible and should encode our requirements correctly (preaching The Right Thing). If we wanted to force the user to provide the handlers in a specific order then we would not need eight structs. Similarly, if we wanted to allow the user to override the handler (by calling with_child_handler many times for example) then we would not need eight structs. Eight is the number of possible states we could be in (states representing which handlers were provided). The struct that has all three handlers is the only struct with a run() function. If we wanted to make the parent handler optional then all structs with a child handler and an error handler (two of them) will have a copy of run().

import std.stdio;
  
mixin template HasChildHandler() {
    private void delegate() child_handler;
}
mixin template HasParentHandler() {
    private void delegate(int) parent_handler;
}
mixin template HasErrorHandler() {
    private void delegate() error_handler;
}
  
  
mixin template ProvideHandlerSetter(T, DG, string field) {
    mixin(`T with_` ~ field ~ `_handler(DG dg) {
        auto value = T();
        value.` ~ field ~ `_handler = dg;
        static if (__traits(compiles, this.child_handler)
                && __traits(compiles, value.child_handler))
            value.child_handler = this.child_handler;
        static if (__traits(compiles, this.parent_handler)
                && __traits(compiles, value.parent_handler))
            value.parent_handler = this.parent_handler;
        static if (__traits(compiles, this.error_handler)
                && __traits(compiles, value.error_handler))
            value.error_handler = this.error_handler;
        return value;
    }`);
}
 
 
mixin template ProvideChildHandlerSetter(T) {
    mixin ProvideHandlerSetter!(T, void delegate(), "child");
}
mixin template ProvideParentHandlerSetter(T) {
    mixin ProvideHandlerSetter!(T, void delegate(int), "parent");
}
mixin template ProvideErrorHandlerSetter(T) {
    mixin ProvideHandlerSetter!(T, void delegate(), "error");
}
 
 
mixin({
    string structName(bool child, bool parent, bool error) {
        string name = "MyFork" ~ (child || parent || error ? "With" : "");
        if (child) name ~= "Child";
        if (parent) name ~= "Parent";
        if (error) name ~= "Error";
        return name;
    }
    string result = "";
    for (int i=0; i<8; i++) {
        bool child = (i & 0b001) == 1;
        bool parent = ((i & 0b010) >> 1) == 1;
        bool error = ((i & 0b100) >> 2) == 1;
        result ~= "struct " ~ structName(child, parent, error) ~ " {";
        result ~= (child ? "mixin HasChildHandler" : "mixin ProvideChildHandlerSetter!" ~ structName(true, parent, error)) ~ ";";
        result ~= (parent ? "mixin HasParentHandler" : "mixin ProvideParentHandlerSetter!" ~ structName(child, true, error)) ~ ";";
        result ~= (error ? "mixin HasErrorHandler" : "mixin ProvideErrorHandlerSetter!" ~ structName(child, parent, true)) ~ ";";
        if (child && parent && error) {
            result ~= q{void run() {
                import core.sys.posix.unistd : fork, pid_t;
                pid_t pid = fork();
                if (pid == 0) child_handler();
                else if (pid > 0) parent_handler(pid);
                else error_handler();
            }};
        }
        result ~= "}";
    }
    return result;
}());
 
     
void main() {
    MyFork()
        .with_error_handler({
            "ERROR".writeln;
         })
        .with_child_handler({
            "Hello from the child".writeln;
         })
        .with_parent_handler((child_pid) {
            "Hello from the parent (child=%d)".writefln(child_pid);
         })
        .run();
}