Do you have any kinda system for dealing with errors in C? I looked at
some of your code and you seem to be using the libc convention (if one can
call it that) of returning 0 on success and when it comes to structs
returned by value you use .ok/.err fields. For context, I personally
prefer to return a true value on success and I use by-value structs mostly
as fat pointers where the first field is always the 'lean' pointer, which
is null on error. Whatever the system, there's often a need to propagate
error. I have several macros for this, e.g.
Window* W = OK( Create_Window() );
If the expression passed to OK() produces 0, OK() returns an error result
from the current function, otherwise it returns the result of the
expresion. Another example:
Node P = SOME( Parent_Of( N ) );
Node here is a fat pointer type and SOME() checks if the lean pointer
inside of it is nonzero. Together these macros allow for writing more
compact code while still handling errors properly. I even cobbled together
a macro called PIPE() that takes a sequence of expressions and has
semantics similar to `f() && g() && h() && ...` where each function
returns a fat pointer. There's more going on with these macros but that's
the gist. Problem I've run into is that in order to return an error result
(i.e. a zero scalar or some kind of zeroed struct) I need to know the
return type of the current function when inside its body. I had to resort
to a hack so now I define functions like so:
fun( int,Sum, int A, int B ) return A + B; end
Which is about as fugly as it gets, hence this email. Have you ever
experimented with this kind of stuff? I know that in Rust and Zig there
are conveniences specifically for error propagation. Error handling in
general is one of those perennial problems with C, but unlike string
handling where pretty much everybody sane uses some kind of {.Data, .Size}
type, error handling is all over the place.
> I looked at some of your code and you seem to be using the libc
> convention
Thanks for taking a look! Programs using the old libc convention would be
older, and .ok/.err is the newer stuff. I've got a nearly-complete post in
the works about this coming out soon. In short, I've gravitated towards
returning structures instead of in-band signaling and out parameters. It's
like a form of multiple return values. Once I got over that hump, using an
.ok boolean to signal errors was easy. The main difference between .ok and
.err is whether a zero-initialized object ought to be invalid or valid.
With .ok I can zero-initialize a return value at the top of a function,
and if there's any error I just return it as-is. A success return must
flip it to true. In more complicated code there are few such cases, and
forgetting to flip it will be caught quickly (being the happy path). I'm
currently quite happy with this situation, and, combined with no longer
checking for allocation failures, I don't feel an urge to improve error
propagation beyond this.
I bet this idea would go well with your macros if you consistently name
this return value. The macro could always return "r" (or whatever) knowing
that it reliably defaults to an error state.
Personally I don't like macros the way you describe. I like control flow
out in the open, not hidden, and especially no hidden return statements.
I'm also one of those people who _likes_ explicit "err != nil" in Go. In
most cases when I see complaints, the real issue is too many accidental
error paths. For example:
* Validating arguments and returning errors instead of assert/panic. This
is surprisingly common.
* Propagating such checks from libraries. For example, in C checking the
result of pthread_mutex_lock. It's a misunderstanding of the interface. Of
course it's painful to introduce error paths every time a program touches
a lock.
* Managing resources at the wrong level, or holding them for too long. For
example, people write C as though it's still the 1980s and a .ini file is
too large to reliably fit in memory. So they hold a file handle and do
lots of reads on it, any of which could fail, requiring a handle close on
each case. Just load the whole file into memory! Use that 64-bit address
space to your advantage.
* Using libraries with crummy interfaces that needlessly demand resource
and/or lifetime management, which has combinatorial effects on all the
error paths. (Often as a result of the mistakes above.) Annoyingly common,
and while the right answer is to replace it with something better, that's
easier said than done. Recent example I saw when helping a beginner: The
ncurses "menus" library. So difficult to use correctly even the official
documentation leaks memory in its examples.
* When writing, not optimistically batching error checks over many writes.
This is the case for .err. Do a bunch of operations, then check the error
bit to see if any failed. "FILE *" isn't so great at this — doesn't no-op
when the error bit is set — so just don't use it when it's not a good fit.
> where pretty much everybody sane uses some kind of {.Data, .Size}
Right! Far too late now, but I wish it had been standardized early so we'd
all be speaking a common data/size representation.