Cello High Level C: A Fat Pointer Library
libcello.org
libcello.org
++p
and pass that to a function that is silently expecting a length and other stuff in front of it. Calling these things "pointers" is a bit of a promotion from what they really are. Naming them "char*" or whatever is pretty dangerous if you ever do pointer movement.Seriously, if you want descriptors and typed blobs, just make up a struct for what you want and pass it around. Say what you mean. Lying like this is going to hurt you.
I agree with you: I'm not sure how this is any more powerful than any encapsulated ADT-style foo_t-wraps-a-void-star C library.
Examples from the link:
struct Header* head = calloc(1, sizeof(struct Header) + data_size);
head->type = type;
Head is not checked for null. This will segfault when heap allocations fail. [Preemptive reply to folks used to high-level languages and the Linux overcommit behavior: yes, heap allocations do fail, and yes, it is possible to handle such a failure well.] typedef void* var;
// ...
return ((var)head) + sizeof(struct Header);
It's illegal to do pointer arithmetic on void pointers. You need to cast to another type first. #define alloc_stack(T) header_init( \
(char[sizeof(struct Header) + sizeof(struct T)]){0}, T)
That's going to crash when you try to access the T on many non-x86 architectures. On x86 you will have subtle problems like atomic ops failing to work.I have seen cello come up on HN before. Seems like a cute hobby project based on some kind of flawed ideas of what "C" is. If people interested in learning C are reading, I suggest learning C "for realsies" and avoiding this thing.
On handling out-of-memory conditions:
- Yes, you can do it
- It's also hard to get right, in the general case, for large systems. It's often a pyrrhic victory
Most systems I've worked with have simply restarted, rather than risk getting complicated recovery logic wrong (and winding up in a worse situation -- corrupting persistent data, or giving wrong answers -- than if they simply crashed). A few systems have a 'reserve tank' that they can use to do a controlled crash, saving important state and whatnot before quitting. OOM can get pretty wicked.
Imagine every function in your call stack handles errors consistently. In most cases these functions will bubble up all errors to the caller. In many cases they will perform allocations themselves and free those allocations when they fall out of scope due to either success or error.
The function at the top of the stack hits a malloc error. It will bubble up its status to its caller, who will do the same for his caller, etc. The chain of functions will free intermediate allocations they made along the way. By the time you get to some top-level or near-top-level function, you can react to the error, and you likely even have quite a bit of heap space when the rest of the stack frees its work. But if you don't happen to have the heap space, then you can structure that top-level error handler so that it performs very few allocations, does allocations at upfront at initialization time, etc.
I don't think any of this is hard and I've seen it work well in practice. It's sad to me when I see the opposite, some unreasonable allocation quite reasonably fails, and it takes down the entire process because whoever wrote that code thought it was too hard to do otherwise.
But, even assuming you handle every failed page allocation perfectly and your stack does the right thing, you can still screw yourself /trivially/ by blowing out the network stack, or because the writeback daemon can't keep up with your filesystem writes. So now we're talking about clamping kernel resources with userspace logic that does things like periodic fsyncs and userspace tcp acks before attempting to send more -- good luck.
It's also a LOT of extra code, really material bloat.
One experience I had doing this: http://blog.ometer.com/2008/02/04/out-of-memory-handling-d-b...
If you haven't written the test harness to test almost every malloc failing, you might think this is easier than it really is.
Adding 30-40% more code to your code base that will almost never get tested except maybe by your unit tests ... no thanks, not if it's possibly avoidable for a given application.
Not to mention that there are coding styles that make the transactional approach less difficult. (OK, so reverting your work gets hairy in the presence of certain side effects. In many cases I would rather chose some behavior and stick with it than take down an entire process dereferencing a null pointer.)
union Header
{
var type;
max_align_t foo;
};
whereby you get max_align_t from your standard libraries or do something like this - typedef union max_align
{
int i;
long l;
double d;
void * p;
...
} max_align_t;Edit. Yes this looks to be the case for those of us using C99. Paul Eggert put in a patch for gnulib to cover this [1].
typedef union {
char *__p;
double __d;
long double __ld;
long int __i;
} max_align_t;One thing I did learn looking through this patch was the difference NULL has in C++ and C. This is certainly not something I had ever considered.
1. https://lists.gnu.org/archive/html/bug-gnulib/2014-12/msg001...
While there are some cases where this is an issue (writes that cross 4KB page boundaries), this generally isn't true for any x86/x64 processor made in the last decade. There may be legitimate portability reasons for avoiding unaligned access, but performance on x86 is probably not a good reason.
http://www.intel.com/content/dam/www/public/us/en/documents/...
8.1.1 Guaranteed Atomic Operations
The P6 family processors (and newer processors since)
guarantee that the following additional memory operation
will always be carried out atomically:
• Unaligned 16-, 32-, and 64-bit accesses to cached memory that fit within a cache line
The first P6 was Pentium Pro, which came out in 1995. It does go on to say that although modern processors will guarantee atomic operations that cross cache lines, it's a bad practice that can badly hurt performance.You want 8 byte alignment. For one, you could have doubles. (Though I just googled this and apparently gcc will 4 byte align those by default on x86) Another possibility is you could have a data structure that relies on "lock cmpxchg8b".
It is called a struct. It holds arbitrary collection of any type of data safely while enabling the lazy programmer to pass only one parameter around. It is also safe (as must as it can be in C) unlike pointers that hide data behind their back and are just asking for trouble.
I will ignore all the problems and point out the worst offender, stack allocation example. That is not how you allocate arbitrary data on the stack, automatic char array is not malloc. In fact, in C you can't do it without using special platform specific functions. Anything else will cause undefined behavior.
(If you're using Microsoft stuff, you have my condolences.)
I'm not saying it's the right choice. Just that I understand why they did it that way.
Still, I think a macro would be a perfectly reasonable way to do it.
Instead, they're more akin to vtables in C++, i.e. placing a pointer to the interface at the beginning of the object, before the data.
I need to read the source (or generated code) to fully understand how they implemented support for multiple interfaces/typeclasses.
Edit: I stand corrected. Just had a look over the source and this does a lot more than the OP link indicates. The Github README is more informative.
Some people have suggested putting array sizes in structs. But that will increase the level of indirection and hurt performance. Also it will require rewriting most applications.
With Cello's approach you just have to avoid pointer arithmetic. Everything should work.
head->type = type;
Where does type come from?