11 ms·
Stupid Smart Pointers in C
- adrianN 2y agoI wonder how that affects compiler optimization
- dhsysusbsjsi 2y agoand stack protection cookies
- jcranmer 2y agoLet's just say there's a reason the author is compiling everything with -O0.
- pjdesno 2y agoNote that this will probably cause branch prediction misses, just like thread switching does - modern CPUs have a return address predictor which is just a simple stack. I don’t think you can avoid this without compiler support.
- rwmj 2y agoReally, don't do this, it's a portability and safety nightmare (aside from C not being memory safe already). C programmers are better off with either of these two techniques: * Use __attribute__((cleanup)). It's available in GCC and Clang, and we hope will be added to the C spec one day. This is widely used by open source software, eg. in systemd. * Use a pool allocator like Samba's talloc (https://talloc.samba.org/talloc/doc/html/libtalloc__tutorial.html https://talloc.samba.org/talloc/doc/html/libtalloc__tutorial...) or Apache's APR. (I didn't include using reference counting, since although that is also widely used, I've seen it cause so many bugs, plus it interacts badly with how modern CPUs work.)
- MrBuddyCasino 2y ago> __attribute__((cleanup)) Interesting. I'm not very proficient in C, this looks like some sort of finalizers for local variables?
- rwmj 2y agoCorrect. You can use it in a simple way to free memory, but we've also used it to create scoped locks[1]. This being C, it's not without its problems. You cannot use it for values that you want to return from the function (as you don't want those to be freed), so any such variables cannot be automatically cleaned up on error paths either. Also there's no automated checking (it's not Rust!) Note it's {...} scoped, not function scoped, which makes it more useful than Golang's defer. [1] https://gitlab.com/nbdkit/nbdkit/-/blob/8b36e5a2ea331eed2a735627facd05d70a183284/common/utils/cleanup.h#L59 https://gitlab.com/nbdkit/nbdkit/-/blob/8b36e5a2ea331eed2a73...
- fpoling 2y agoWhile Go rules effectively prevents usage of defer in loops, it is useful occasionally to write: if complex_nested_condition { defer cleanup() }
- TheDong 2y agoEven with scope-based defer, you can accomplish conditional defers easily enough. In a sane language, where conditions are expressions, you could just do: defer if complex_nested_condition { cleanup() } else { noop() } In Go, you could do: defer func(run bool) { if !run { return } }(condition) Which admittedly wastes stack space with a noop function in the false case, but whatever. I feel like the number of times I've needed conditional defers is almost zero, while the number of times I've had to make a new function to ensure scoping is correct is huge. Of especial note, 'mu.Lock(), defer mu.Unlock()' not being scope-based is the largest source of deadlocks in code. People don't use 'defer' because the scoping rules are wrong, code panics before the manual unlock call, and then the program is deadlocked forever.
- NekkoDroid 2y ago> You cannot use it for values that you want to return from the function I would say this is only half true. With some macro magic you can actually also return the values :) https://github.com/systemd/systemd/blob/0201114bb7f347015ed4d85256637921dc304fe0/src/fundamental/macro-fundamental.h#L413-L422 https://github.com/systemd/systemd/blob/0201114bb7f347015ed4... To be fair though, you probably meant without any such shenanigans.
- maccard 2y ago> It's available in GCC and Clang, and we hope will be added to the C spec one day. This is widely used by open source software, eg. in systemd. It’s odd that the suggestion for a feature lacking in C is to use a non standard but well used supported path. c’s main selling point (IMO) is that it _is_ a standard, and relying on compiler vendor extensions kind of defeats the purpose of that.
- milesrout 2y agoC's main selling point is not standardisation. It was widely used before standardisation and only standardised because it was useful.
- rwmj 2y agoIt's so widely used by OS software that you're likely using already, that it's unlikely to be removed and much more likely to be standardized. This is in fact how standardization ought to work - standardize the proven best practices.
- maccard 2y agoI agree. But if we follow that logic then any compiler specific feature of either or clang is fair game, even if it’s not standard. MSVC doesn’t support it when compiling in C mode, for example.
- lelanthran 2y ago> But if we follow that logic then any compiler specific feature of either or clang is fair game, even if it’s not standard. Well, yeah... How do you think Annex K got in?
- dietr1ch 2y ago> relying on compiler vendor extensions kind of defeats the purpose of that. Let's be honest, how many compilers are available, and how many of those would you actually use? The answer isn't more than 4 and the 2 compilers you are most likely to use among those already support this and probably won't stop supporting without a good alternative. I like standardisation, but you have to be realistic when it helps you without a large real cost other than fighting your ideals for getting this into the standard first.
- deleted 2y ago[deleted]
- masklinn 2y ago> we hope will be added to the C spec one day defer seems to be making significant progress (having a passionate and motivated advocate in Meneide, and a full TS)
- rwmj 2y agoLast time I looked this was (golang-like) function scoped, not { } scoped, which means it's a bad idea. My feedback was the committee should simply standardize the existing attribute / behaviour, as that is widely used already. (EDIT: I'm wrong, see reply)
- masklinn 2y ago> Last time I looked this was (golang-like) function scoped, not { } scoped, which means it's a bad idea. Might have been the previous attempt from years ago, because being block scoped (unlike go) literally has its own section in https://thephd.dev/c2y-the-defer-technical-specification-its-time-go-go-go#scope-based https://thephd.dev/c2y-the-defer-technical-specification-its...
- EPWN3D 2y agodefer is nice, but I really want the cleanup attribute since it could in theory by applied to the return type of a function. In other words you could have malloc return a pointer with the cleanup attribute that automatically frees it at end of scope if it's non-NULL. (And if you want to persist the pointer just assign to a different variable and zero out the one malloc gave you.)
- masklinn 2y ago> In other words you could have malloc return a pointer with the cleanup attribute that automatically frees it at end of scope if it's non-NULL. That is not, as far as I know, how __attribute__((cleanup)) works. It just invokes the callback when the value goes out of scope. So you can't have malloc return an implicitly cleanup'd pointer unless malloc is a macro, in which case you can do the same with a defer block.
- legohead 2y agoMy C programs never consumed gigs of memory. So I (like many others I assume) made a memory manager and never freed anything. You'd ask it for memory and it kept a list of various sizes it allocated and returned what you needed to be re-used. Freeing and allocating is slow, and error prone, so just avoid it!
- KerrAvon 2y agoA venerable and completely reasonable approach for resource-constrained environments and/or programs with very small memory requirements (kilobytes).
- adamrezich 2y agoWhat makes you think that this approach is only useful for resource-constrained circumstances?
- rwmj 2y agoYeah it's reasonable. Unfortunately if you do it, and you run tools like Coverity, it'll produces reams of complaints about how you're leaking memory :-( There was one project which was genuinely a short-lived program that never needed to free memory, but in the end I gave in and added free() statements everywhere. Otherwise we could never have got it into RHEL.
- adamrezich 2y agoWhy not just put all the free()s at the end of main() behind an #ifdef DEBUG or something?
- legohead 2y agoWhat are your issues with the memory requirements being small? One of the programs was a MUD that consumed a couple hundred megabytes, and I never had issues with it. I mentioned gigabytes because of how mine specifically worked. It allocated chunks in powers of 2, so there was some % of memory that wasn't being used. For instance, If you only need 20 bytes for a string, you got back a pointer for a chunk of 32 bytes. Being just a game, and side project, I never gave it much thought, so I'm curious to hear your input.
- pajko 2y agoThere's a complete implementation available at https://github.com/Snaipe/libcsptr https://github.com/Snaipe/libcsptr
- cryptonector 2y agoSure, but TFA is a very clever hack -- disgusting, yes, impractical and non-portable, true, subject to sad limitations -of course-, but genius and entertaining. The "tl;dr" summary is that `free_on_exit()` replaces the caller's return address with a trampoline that calls a `do_exit()` function that frees all the memory marked to be freed "on exit", and it uses its own stack of cleanup handler closures so-to-speak. When returning all the allocations are freed and then the original return address is returned to.
- forrestthewoods 2y ago> Use __attribute__((cleanup)). It's available in GCC and Clang Really, don’t do this, it’s a portability nightmare. If you’re going to write C stick to code that easy to run under MSVC.
- rossy 2y agoRunning under MSVC is overrated and more trouble than its worth. Clang and mingw-w64 GCC work just fine for targeting Windows.
- fc417fc802 2y agoRunning under MSVC implies I'm building on Windows. In reality I'm cross compiling with Clang.
- forrestthewoods 2y agoIt’s a shame that glibc is so badly designed that cross-compiling for Linux is only vaguely possible because Andrew Kelley moved mountains via Zig.
- forrestthewoods 2y agoDisagree. It’s fine and trivially easy. It’s only hard for Linux people who build software for Linux first and only. Then have a shocked pikachu face when they need to run in a different environment. mingw is radically more problematic than MSVC. Don’t use mingw.
- acbits 2y agohttps://github.com/acbits/reftrack-plugin https://github.com/acbits/reftrack-plugin Hey, I wrote this GCC plugin to automate reference counting in C. Any bug reports welcome.
- bch 2y ago> ... plus [reference counting] interacts badly with how modern CPUs work. Can you expand on this?
- sweetjuly 2y agoI'm not sure what the reference to "modern CPUs" is, but a common complaint is that most reasonable reference counting implementations suffer very badly under contention. Specifically, if an object's reference count is contended (an object is being accessed/its pointer copied by many threads), it's possible that incrementing and then decrementing the reference count can take several hundred cycles due to either cache lines ping-ponging between cores or relatively expensive remote atomics (some Arm CPUs, I believe, allow the memory system to execute atomic operations in caches themselves to try and cope with contention/avoid moving cache lines back and forth by simply leaving them in a shared cache). In reality, your milage will heavily vary. If you don't have contention (you don't commonly share objects across multiple threads concurrently), it's likely that reference counting will perform very well. Whether this is the common case really depends on the kinds of software you write.
- magicalhippo 2y agoSame object doesn't have to be contended if the reference count is sharing cache line with another reference count. I've seen this happen in cases arrays of objects are allocated, ie one per thread, and then handed to a thread pool to work on. Even if heap allocated, if the object is just a reference counter and a few pointers, the memory allocator can fit several of them next to each other causing them to share cache lines, which causes the performance issues with atomic operations. Depends on implementation of things of course, but can be a pitfall.
- bsenftner 2y ago[flagged]
- bsenftner 2y ago[flagged]
- UncleMeat 2y agoI downvoted because in my mind you are winging it. "Just give it back" works well for simple cases, I suppose. We observe that engineering teams struggle to write correct code without tools helping them. This is just an unavoidable fact. Even with tools that are unsound we still see oodles of memory safety bugs. This is true for small projects run by individuals up to massive projects with hundreds or thousands of developers. There are few activities as humbling as taking a project and throwing the sanitizers at it. And bugs aren't "well you called malloc at the top of the function and forgot to call free at the bottom." Real systems have lifetime management that is vastly more complex than this and it is just not the case that telling people to not suck mitigates bugs.
- bsenftner 2y agoI'm advocating to design, and then follow the design, and when the design is found lacking redesign to include the new understanding. This writing of software career is all about understanding, and automating that understanding. Due to market pressures, many companies try to make due with developers that take shortcuts, these shortcut takers the majority of developers today, skewing the intellectual foundations of the entire industry. Taking shortcuts does not negate the fact that taking a shortcut is short sheeting one's understanding of what is actually occurring in that situation. These shortcuts are lazy non-understandings, and that harms the project, it's architecture, and increases the cognitive load on maintenance. It's creating problems for others and bailing, hoping you're not trapped maintaining the complex mess.
- UncleMeat 2y ago
- scoopr 2y agoOh well, maybe we'll soon have `defer`? [0] [0] https://thephd.dev/c2y-the-defer-technical-specification-its-time-go-go-go https://thephd.dev/c2y-the-defer-technical-specification-its...
- usrnm 2y agoThat's sad. Having migrated from C++ to golang a few years ago, I find defer vastly inferior to C++ destructors. Rust did it right with its drop trait, I think it's a much better approach
- fuhsnn 2y agoThe proposed C defer is scope-based unlike Go. So in the spirit of OP article, you can basically hand roll not only C++ destructor as defer {obj.dtor()} but also Rust Drop as defer {obj.notmoved() ? Drop()}
- DanielHB 2y agoWhat do you mean defer isn't scope based in Go? (not super experienced Go developer)
- masklinn 2y agoIn Go, defers are function scoped not block scoped.
- TheDong 2y agoI mean, you just write all your scopes as `(func() { })()` in go, and it works out fine. Adding `func() {}()` scopes won't break existing code usually, though if you use 'break' or 'continue' you might have to make some changes to make it compile, like so: https://go.dev/play/p/_Gq4QYtyMmp https://go.dev/play/p/_Gq4QYtyMmp see, no other issues, works exactly like you'd expect
- DanielHB 2y ago
- Joker_vD 2y agoWow, an actual, purposeful, and quite general return-pointer-smashing gadget, built right into the program itself. Just what any program written in C needs.
- abcd_f 2y agoHacky and not really fit for production for more reasons than one, but clever and nice nonetheless. Good stuff.
- feverzsj 2y agoJust C programmer's daily struggle to mimic a fraction of C++.
- deleted 2y ago[deleted]
- Tewboo 2y agoSmart pointers in C often feel like trying to force a square peg into a round hole. They’re powerful, but without native language support like C++, they can lead to more complexity than they solve.
- torlok 2y agoI've heard enough "C is superior to C++" arguments from game developers who then go and use header structs for inheritance, or X macros, enums, and switch statements for virtual functions, to know that more complexity isn't an issue as long as people feel clever and validated.
- unclad5968 2y agoI don't think they do those things for validation. They do those things for control. Even c++ game developers create there own entire standard library replacements.
- dima55 2y agoX macros can do tons of stuff C++ can't. And if you stick with C, you avoid mountains of C++ problems, even with all the stuff you're talking about.
- salgernon 2y agoI love using x macros with c++ to create static types and hooks to disambiguate from basic types. This is more applicable to final executables than libraries - I would never provide anyone with an API based on the mess it creates, but it allows application code to be strongly checked and makes it really easy to add whole classes of assertions to debug builds.
- torlok 2y agoI never said X macros are bad on their own. With C++ you can code exactly as you would in C, but you don't have to manually implement C++ features when there's a need. C++ doesn't have any more problems than C, it's programmers who abuse language features that creates problems.
- qalmakka 2y agoOr rather, given that every relevant C compiler is also a c++ compiler, just compile as c++ and use std::unique_ptr? I love C but I just can't understand the mental gymnastics of people that prefer this kind of hacks compared to just using C++
- kllrnohj 2y agoThere's a lot of either "I like to pretend C is simple and simple is good" or "C++ has things I don't like, so I will refuse to also use the things I do like out of spite". You see it all over the place here whenever C or C++ comes up.
- procaryote 2y agoUsing C++ while keeping people on a common subset of C++ features is very hard in practice. Google for example do a pretty extreme amount of restrictive code standard, training, linting and reinventing libraries to that end. A lot of the motivation behind inventing golang seems to be to remove C++ toys, because it's too hard to get people to not use them if they're there.
- shakna 2y agoUnfortunately, that's not true. C is not 100% compatible with C++. There's a whole heap of incompatibilities that you can hit, that will prevent a lot of non-trivial C programs from compiling under C++. Things like character literals being a char in C++ and an int in C. Or C allowing designated initialisers for arrays, but C++ not.
- qalmakka 2y agoI know, but it's not incredibly hard to work with them to be fair. People have been mixing C and C++ for decades now, we know where the sharp edges are pretty well
- afarah1 2y agoSee also the "arena allocator", which has been discussed here before: https://nullprogram.com/blog/2023/09/27/ https://nullprogram.com/blog/2023/09/27/ I haven't used it personally yet, but it addresses the same issue with a different approach, also related to stack-like lifetimes. I've used simple reference counting before, also somewhat relevant in this context, and which skeeto also has a nice post about: https://nullprogram.com/blog/2015/02/17/ https://nullprogram.com/blog/2015/02/17/
- p0w3n3d 2y agoI see undefined behaviours. they walk they talk they [0?W0OF??0?r??reeBSD they don't know they're undefined.
- queuebert 2y agoC is the LS engine of programming languages. People love to drop it in and mod it until it blows up.
- whatsakandr 2y agoThis article should of had the conclusion of this is why you should use arena allocator.
- Dwedit 2y agoHighjacking the return address can only be done if you know you actually have a return address, and a reliable way to get to that return address. Function inlining can change that, adding local variables could change that, omitting frame pointer, etc. It would also need to be a function that will truly be implemented as one following the ABI, which usually happens when the function is exported. Often times, internal functions won't follow the platform ABI exactly. Just changing the compiler version is probably enough to break anything like this. Save the return address highjacking stuff for assembly code. --- Meanwhile, I personally have written C code that does mess with the stack pointer. It's GBA homebrew, so the program won't quit or finish execution, and resetting the stack pointer has the effect of giving you a little more stack memory.
- flohofwoe 2y agoIMHO trying to emulate smart pointers in C is fixing a problem that shouldn't exist in the first place, and is also a problem in C++ code that uses smart pointers for memory management of individual objects. Objects often come in batches of the same type and similar maximum lifetime, so let's make use of that. Instead of tracking the individual lifetimes of thousands of objects it is often possible to group thousands of objects into just a handful of lifetime buckets. Then use one arena allocator per lifetime bucket, and at the end of the 'bucket lifetime' discard the entire arena with all items in it (which of course assumes that there are no destructors to be called). And suddenly you reduced a tricky problem (manually keeping track of thousands of lifetimes) to a trivial problem (manually keeping track of only a handful lifetimes). And for the doubters: Zig demonstrates quite nicely that this approach works well also for big code bases, at least when the stdlib is built around that idea.
- dhooper 2y agoThis is the way
- procaryote 2y agoIt makes the code so much simpler. And quite probably faster as there's less malloc/free churn. A lot of problems break down to: * we need this effectively forever (i.e. until config reload) * we need this very briefly when processing a task or request Sometimes you need a cache that has intermediate lifetimes, but that is a much smaller problem to deal with, and you can often cope with manual memory management for that Hook any file handles and other resource cleanup functions into the same pools and you have a pretty easy life.
- qwertox 2y agoNot to mention that any future CPU microcode update released in order to mitigate some serious CVE might break the entire product you've been shipping, just because it relied on some stack manipulation wizardry.
- PeterWhittaker 2y ago1) 2018 2) I recently discovered the implementation of free_on_exit won't work if called directly from main if gcc aligns the stack. In this case, main adds padding between the saved eip and the saved ebp, (example). I think this can be fixed some tweaking, and will update this article when it is fixed. I do not believe the article was updated, suggesting that the "tweaking" was far more complex than the author expected... ...which doesn't surprise me, because the overall tone is one of a clever but far-less-experienced-than-they-think programmer having what they think is a flash of insight and realizing thereby they can solve simply a problem that has plagued the industry and community for decades.
- cryptonector 2y agoRight, and then there's interactions with StackGuard-like features.
- mac3n 2y agothis is way overkill the way i do this in C looks like initialize all resource pointers to NULL; attempt all allocations; if all pointers are non-NULL, do the thing (typically calling another routine) free all non-NULL pointers realloc(ptr, 0) nicely handles allocations and possible-NULL deallocations
- mac3n 2y agoif you must have a `free_on_exit()` (for example, if you allocate a variable number of pointers in a loop) then build your own defer stack registering pointers using memory that you allocate
- zabzonk 2y agomight as well free the NULL pointers as well - this is totally valid C and can simplify the code
- wavemode 2y agoI suppose the implication is that, checking for null allows your cleanup logic to be more complex than simply calling free() for example, the object could be managing an open file, or an open socket
- zabzonk 2y agosimply checking for NULL doesn't allow that - for example for something that might have been opened with fopen() needs to be closed with fclose() rather than free(), and you can't tell that from the pointer.
- heftig 2y agoI think the implication was that they're pointers to objects that own resources (like containing FILE handles) and need to be "freed" with a custom function, not just "free". void my_thing_free(MyThing *thing) { fclose(thing->file); free(thing); } assuming an associated "my_thing_new" that only returns a valid pointer when both the allocation and the fopen succeeded.
- anacrolix 2y agoThe naive assumption is that shared_ptr is always better than manual tracking. It's not. Tracking and cleaning up resources individually is a burden at scale.
- casenmgreen 2y agoI maintain a combined error and resource state per thread. It is first argument to all functions. If no errors, function proceeds - if error, function instead simply immediately returns. When allocating resource, resource is recorded in a btree in the state. When in a function an error occurs, error is recorded in state; after this point no code executes, because all code runs only if no errors. At end of function is boilerplate error, which is added to error state if an error has occurred. So for example if we try to open file and out of disk, we first get error "fopen failed no disk", then second error "opening file failed", and then all parent functions in the current call stack will submit their errors, and you get a call stack. Program then proceeds to exit(), and immediately before exit frees all resources (and in correct order) as recorded in btree, and prints error stack.
- ForTheKidz 2y agoWhy bother freeing before exit?
- lionkor 2y agoAssuming that this would include properly closing sockets, etc. which the OS doesn't do the same way you might want to
- PortiaBerries 2y agoAround 1994, when I was a nerdy, homeschooled 8th grader teaching myself coding I came up with something I was inordinately proud of. I had gotten a book on PowerPC assembly so using Metrowerks CodeWarrior on my brand-new PowerMac 7100 I wrote a C function with inlined assembly that I called debugf. As I recall, it had the same signature as printf, called sprintf and then passed that resulting string to DebugStr. But the part I was proud of was that it erased itself from the stack so when the debugger popped up it was pointing to the line where you called debugf. I'm still proud of it :-).
- nanolith 2y agoIt's a clever use of assembler, but in production code, it's much better to use a bounded model checker, like CBMC, to verify memory safety across all possible execution paths.
- deleted 2y ago[deleted]
- jay-barronville 2y agoLike many C hacks, this is a fun one, but hacks like this should absolutely be avoided for any serious C code–the magic is simply not worth the potential bugs!
- cryptonector 2y agoPuTTY is written in a Duff's Device co-routine hack on steroids that I'm sure someone at one point said "should absolutely be avoided for any serious C code–the magic is simply not worth the potential bugs!"! And PuTTY is no small program, and it's very successful. One person's hack-that-should-be-avoided-at-all-costs can be someone else's secret sauce.
- devit 2y agoThat's completely asinine since it can't be made to work properly with inlining (including LTO), architectures that use a shadow stack or don't use a frame pointer, and also ridiculously inefficient and requiring assembly code for all architectures. Use C++ or __attribute__((cleanup)) instead.
- epolanski 2y agoMaybe it is. Or maybe the asinine "thing", is some folks lack of text comprehension skills that can't distinguish an experiment from a best practice recommendation despite a title and content that clearly does not invite that.
- kazinator 2y agoThis kind of approach of allocating objects to a context which is freed all at once is implemented in GNU obstacks.
- lionkor 2y ago> Managing memory in C is difficult and error prone. C++ solves this with smart pointers like std::unique_ptr and std::shared_ptr. No, it does not. Smart pointers are useful to help model lifetimes and ownership, but the real killer feature is RAII. Add that to C (standardized) and you can make smart pointers, and any other memory management primitive you need. Smart pointers are not a solution, they are one of many tools enabled by RAII.