I used to write loads of C, and recently I came back to write some C bindings to a Rust library, and it was _not_ fun. It was dead simple from the C side - mostly just a few functions that constructed or handled opaque pointers. The problem is the error handling is atrocious, and that was so hard to present neatly. The consequence is an overly verbose API in which I've essentially manually monomorphised in C every result. For sure, it's not idiomatic C, but it is properly handling errors, which arguably is one of the main failures of idiomatic C.
It's not just the API either. So once you've got your "nice" error handling API, you have to manually handle resource clean up - no branches that exit for you; every function call is accompanied by a result check and a goto to cleanup code (so your pointers need to be defined properly prior to any calls and initialised to null). Honestly, I don't know I ever lived with this mess. It was a painful and laborious experience to get to something that was just about ok.
> So once you've got your "nice" error handling API, you have to manually handle resource clean up - no branches that exit for you; every function call is accompanied by a result check and a goto to cleanup code (so your pointers need to be defined properly prior to any calls and initialised to null).
I have a stack allocator that will release everything allocated in the function on exit, and I have macros to replace my use of the return keyword that also deallocate everything in the stack allocator before returning.
So my code actually has code cleanup on return.
C does not have to be laborious. Maybe the fact that I have done things like that is part of why I like C?
Where can I can get such a stack cleanup system? Also, can you call arbitrary clean up code (this is hardware that needs cleaning up as well as structures allocated in rust that need to freed in rust)?
I use a stack allocator that knows about setjmp()/longjmp(), functions, scopes, and destructors. (Destructors are a function pointer type in my code.)
Basically, you tell the stack allocator to store a jmp_buf, then you setjmp() on it. Then keep going.
For a function, a special destructor is used as a marker. Same with scopes.
The same is also true of a setjmp() destructor, which is actually what activates longjmp().
The stack allocator should have three functions to unwind itself until it reaches the end of the next scope, function, or jmp, respectively.
Then, when you need an exception, or to exit a function or scope, you call the correct stack allocator function to clean up everything to that point. Then you return or whatever.
This can run arbitrary code on cleanup for an item if you write a "destructor" with that arbitrary code. For example, one idea I had was to use the stack allocator to ensure a mutex or other lock was always released. This would be done by storing a pointer to the mutex and writing a "destructor" that unlocks the mutex when given a pointer to the mutex. Boom. Scope-based mutex unlocking.
It's not just the API either. So once you've got your "nice" error handling API, you have to manually handle resource clean up - no branches that exit for you; every function call is accompanied by a result check and a goto to cleanup code (so your pointers need to be defined properly prior to any calls and initialised to null). Honestly, I don't know I ever lived with this mess. It was a painful and laborious experience to get to something that was just about ok.