5 ms·
The C language shouldn't need a defer statement keyword, because it's so trivial to implement using an asm() macro that overwrites the return address. Using a
by jart 6y ago
The C language shouldn't need a defer statement keyword, because it's so trivial to implement using an asm() macro that overwrites the return address.
Using a macro is more succinct:
const char *s = gc(xasprintf("%s/%s", dir, name));
Than what's being proposed:
char *s = xasprintf("%s/%s", dir, name);
defer free(s);
See this x86 reference implementation of defer() and gc(). https://gist.github.com/jart/aed0fd7a7fa68385d19e76a63db687ff https://gist.github.com/jart/aed0fd7a7fa68385d19e76a63db687f... That should just work with GCC and Clang. That code is originally from the Cosmopolitan C Library (https://github.com/jart/cosmopolitan https://github.com/jart/cosmopolitan) so check it out if you like the Gist and want more!
Please note the macro operates at function call boundaries rather than block scoped. I consider that a feature since it behaves sort of like a memory pool. Having side effects at the block scope level requires changing compilers, the language itself, and it would cause several important gcc optimization passes to be disabled in places where it's used.
- saagarjha 6y agoWhile interesting, such a construct is obviously unsuitable for inclusion in the C standard nor can it be relied upon when writing portable code.
- jart 6y agoI never proposed that some code I posted to Gist be part of the C standard. I'm flattered however that folks are considering it for that purpose!
- mayoff 6y agoNobody's proposing your code for the C standard. The original article is proposing an addition to the standard. Your comment argues that programmers don't need the addition to the standard because there is a non-standard, non-portable hack. That is not a good argument against an addition to the standard.
- jart 6y agoThe onus is on the proposer. C is simple and should stay that way. If someone is proposing C needs to be altered (which is about the most conservative language there is when it comes to adopting features) then that person should have good arguments as to why there's no other way. Otherwise it belongs in C++. If whipping up a few lines of asm for my local architecture solves the problem, then that weakens the proposal. Maybe instead the C language committee should be focusing on standardizing the RMS notation for the asm() keyword which makes this epic hack possible. https://gist.github.com/jart/fe8d104ef93149b5ba9b72912820282c https://gist.github.com/jart/fe8d104ef93149b5ba9b72912820282...
- saagarjha 6y agoThe RMS notation is pretty icky, though, and it of course only works for x86 as well. I actually like Rust's asm macro quite a bit: https://doc.rust-lang.org/unstable-book/library-features/asm.html https://doc.rust-lang.org/unstable-book/library-features/asm.... Your Gist is excellent either way, thanks for compiling it. I'll probably reference it whenever I write inline asm now ;)
- jart 6y agoGlad I could help! Rust asm syntax looks nice, but the syntax is just the tip of the iceberg. asm() is almost a misnomer. Its true power is the constraints system that lets us control GCC/Clang internal algorithms. You may have noticed that the defer() macro uses asm() with an empty string! It's such a general tool that's become so prevalent as a practice (since Stallman invented it in the early 90's) that I would surely hope it's on the radar of language committees by now. If these definitions and mnemonics can be formalized or at least clarified by standards bodies, then they should be.
- arcticbull 6y agoI think your implementation is really, really cool. Similarly I spent a good half hour bumbling through Cosmopolitan. Love what you've built. IMO, the fact that asm() is used for a bunch of things unrelated to inline assembly -- but to your point, instead for changing compiler behaviors is more of an argument for exposing additional __attributes__, annotations and so on, rather than adding explicit support for asm() to the standard. This violates the principal of least surprise, and IMO, serves to further confuse rather than bring some predictability to C. I'd suggest also that C should embrace some movement in the standard rather than agreeing to leave things as is forever and stapling legs onto the octopus that is C++. There was a good writeup here a while ago from someone involved with the committee, that they've gotten to the point where introducing new warnings for obviously broken behavior is off the table because they want to be warnings-compatible from release to release. What I'm saying is I would rather see exposed intrinsics, primitives and other meaningful source annotations than codifying the spooky action at a distance of an empty string asm() call in the C standard. And if we're going to modify the standard there's a lot of low-hanging fruit I'd love to see cleaned up first. [edit] I'd also like to add that I agree with your thesis that if this can be built with the tools provided instead of modifying the standard, that the onus is on the proposer. Based on some of the other analyses here it seems like it can't really be done in a universal way, but I'm open minded.
- cozzyd 6y agoCan this be made to work on ARM as well?
- warmwaffles 6y agoARM implementation would be fun to look at as well.
- jart 6y agoYes, absolutely! In the case of Arm, it would need to be a pure macro (i.e. no external __defer function) since ARM ABI uses a register to store the return address. Then the unwind code would need to save the return registers x0 to x7 each time it calls free() or whatever function is being deferred. I'd write it for you, but I don't use ARM.
- liuliu 6y agoYou need to disable inlining. __builtin_frame_address can give surprising addresses when inline enabled. The code itself is likely not be wrong, but you won't confined to the lexical scope as if you see in the code (the defer can be triggered when the parent function returned). Disclaimer: I haven't looked at the code too closely.
- jart 6y agoInlining gives you the ability to control how "pooled" the free() operations end up being. You don't need to disable inlining. You just have to be mindful of how much power this macro gives you. For example, if a function that calls gc() is being called from within a loop, then it's a good idea to make sure that function isn't static.
- warmwaffles 6y agoThat gist looks interesting. You said it is from cosmopolitan, I assume it has the same license as it. Thinking about toying with that defer implementation. Looks fun. Though I still do it all manually, and am looking for ways to automate it.
- jart 6y agoThe Gist is now updated with an ISC license. So it's very permissive. Enjoy! Feel free to contact me anytime too, if you get interested in hacking on this stuff.
- est31 6y agoThe original goal of C was to allow writing programs in a platform independent way, i.e. without having to write different code for different arches. Also, there is no inline assembly support in standard C, just in various compilers.
- saagarjha 6y agoInterestingly, C++ does have an asm declaration.
- est31 6y agoIt exists, but the meaning is implementation defined. https://eel.is/c++draft/dcl.asm https://eel.is/c++draft/dcl.asm
- phkahler 6y ago>> The original goal of C was to allow writing programs in a platform independent way, i.e. without having to write different code for different arches. I think it was meant to be a portable language, not to let you write portable code. With the size of standard types being machine dependent you couldn't write completely portable code, but you could write C on a lot of hardware. It's like how 8 bit computers all had BASIC but weren't compatible. If you knew one it was easy to get going on another because at some level it was all BASIC.
- shoo 6y agotangential suggestion: if the the first line of the cosmopolitan README after the title was the description "fast portable static native textmode executable containers" then that would help newcomers more quickly understand what the project is about. i skim-read through the README and was still fairly puzzled about what the purpose of a cosmopolitan was before i saw that description hiding in the margin.
- jart 6y agoThanks for the feedback! The README has now been updated: https://github.com/jart/cosmopolitan/commit/fd22e55b42503093f5a885d8a72fcde58542ed1d https://github.com/jart/cosmopolitan/commit/fd22e55b42503093... It could use more work though. See also https://justine.storage.googleapis.com/ape.html https://justine.storage.googleapis.com/ape.html and the HN thread about it last month: https://news.ycombinator.com/item?id=24256883 https://news.ycombinator.com/item?id=24256883
- grok22 6y agoI think perhaps this is the wrong way to look at it -- having it as a standard way to do things built into the language simplifies this (not everybody needs to know tricks) and also most likely will be more portable across platforms.
- neostrauss 6y agoLooks extremely cool. Presumably __cleanup__ is more robust in a GCC environment though?
- comex 6y agoThis is a very neat hack, but it breaks in the presence of: - Inlining: it will work but the defer will be executed at the end of the caller function, which may not be what you expected. - Tail call optimization: same issue. (You mention in a comment that you use a dummy asm statement to prevent the call to `__defer` itself from being tail-call optimized, but there's still an issue if one defer-using function tail-calls another defer-using function.) - Function outlining aka hot/cold splitting (currently implemented in LLVM): arbitrary chunks of a function can be split out into their own functions; if one of those does a defer, the cleanup might be run too early, considerably more dangerous than too late. - Various CFI (control-flow integrity) implementations that are specifically designed to prevent the return address from being overwritten (by exploits). - Interprocedural register allocation (-fipa-ra in GCC) if __defer gets inlined or analyzed via link-time optimization: The compiler can make assumptions that functions won't modify certain registers that the ABI would normally allow them to modify; this will be violated if it unexpectedly jumps to __defer. This is fixable by marking __defer as __attribute__((noipa)) or reimplementing in assembly. - Targeting WebAssembly or BPF or other high-level machines that don't support overwriting return addresses. - Compilers that don't support inline assembly (MSVC). EDIT: - Targeting ARM if the compiler happens to stash the return address in an unexpected location. You can't fix this by writing to LR like you suggested; the return address needs to be in LR when you execute the ret instruction, but the compiler doesn't need to keep it there for the whole function, and usually won't. Instead, it will usually save it to the stack frame, and load it back before returning, potentially using LR for completely unrelated purposes in between. So usually you can modify the return address by using __builtin_frame_address just like on x86. But that's an implementation detail; it could decide to keep a copy in another register, and move that to LR when returning. Not sure if any compilers actually do that, though I think I might have seen something like that on PowerPC. Your approach is also relatively slow, since the cleanup code can't be inlined. (If you're going to use a GNU extension for inline asm, why not just use the GNU extension __attribute__((cleanup))? It's block-scoped, but it doesn't disable any optimization passes or anything since the compiler knows about it, it's portable, and it doesn't have the problems I mentioned.)
- jart 6y agoThank you for the thorough response. Inlining, tailcall, hot/cold: None of these are issues. They don't change the fact that memory passed to gc() will be freed. Worst case scenario is the can gets kicked down the road, which is relatively easy to predict. See https://gist.github.com/jart/5aba7fc72c7b6781dadd5949c289a0b6 https://gist.github.com/jart/5aba7fc72c7b6781dadd5949c289a0b... So long as you're not using this technique to unlock mutexes, you'll be fine. Developers who are required to use CFI need to reach out to their policymakers for authorization to modify return addresses before using the gc() macro. Folks required to use MSVC can use the existence of the gc() macro as compelling evidence for their bosses on the benefits of switching to GCC or Clang. I'll take you on your word on IPA. I've added a comment to the Gist making sure folks who use it are aware. Thanks for the awesome info on ARM. That's good to know. Also I believe the code is fast. I like the gc() macro because it can be used in expressions. I find __attribute__((__cleanup__)) unpleasant since it has strong opinions about how variables and cleanup functions need to be declared.
- coldtea 6y agoI don't think "C shouldn't implement X" and "because X is trivial as an asm macro" are two sentences that make sense together.