[HN Gopher] Let's write a setjmp ___________________________________________________________________ Let's write a setjmp Author : jmillikin Score : 207 points Date : 2023-02-12 07:52 UTC (15 hours ago) (HTM) web link (nullprogram.com) (TXT) w3m dump (nullprogram.com) | Simran-B wrote: | Is it considered harmful, and if so, why? | [deleted] | pm215 wrote: | Mostly for the usual "don't reinvent a wheel the language | standard library already provides" reasons, I think. For | instance glibc's x86setjmp/sigsetjmp have been updated to | support shadow stacks, but if you'd rolled your own you'd have | to do that yourself: | https://elixir.bootlin.com/glibc/glibc-2.35/source/sysdeps/i... | j16sdiz wrote: | Yes, but the author was trying to avoid using libc. | | > Yesterday I wrote that setjmp is handy and that it would be | nice to have without linking the C standard library. | | Related article from the same author. | https://nullprogram.com/blog/2023/02/11/ | pm215 wrote: | Personally I think that's throwing the baby out with the | bathwater for most use cases. You could rephrase my comment | as "if you're already committed to reinventing half of | libc's wheels, this one is not really any harder or more | awkward than most, but if you're not aiming for that | overall goal then reinventing just this one wheel is a bad | plan" if you like. | randomNumber7 wrote: | It's like goto, but you can jump over function boundaries. | Checking out the other comments here its not considered | harmful... | adrian_b wrote: | "setjmp" and "longjmp" are the mechanism for implementing | exceptions in C, i.e. they are just another form of writing | "catch" and "throw". | | The implementation of exceptions, i.e. of jumps over multiple | levels of nested functions and blocks, is much simpler in C | than in C++, because there are no destructors that must be | called when unwinding the stack, so it is enough to restore the | CPU registers to the values correct for the program point where | the exception must be caught. | | This is needed because at the point where the exception is | thrown it is not known whether any of the nested functions that | must be skipped has modified any of the registers that a | function is expected to preserve and where the original values | of the registers have been saved. | | Any kind of exceptions can be easily misused, which is why it | is recommended to be careful with the use of "setjmp" and | "longjmp", but they are not more harmful than the use of | exceptions in any other language. | | The only additional problem of C is that since there are no | implicitly called destructors, like in C++ and similar | languages, in C the programmer must do what the compiler would | do in C++. | | This means that if the nested functions that are skipped by a | "longjmp" have allocated heap memory, opened files or sockets | etc., such resources must be freed in the exception handler | marked by a "setjmp". | | Therefore the C programmer must keep track of the resources | that might have been allocated in the nested functions, e.g. by | recording the allocations in some global table. | aardvark179 wrote: | As well as being harder to work with correctly setjmp and | longjmp are often a lot slower than other exception handling | systems as you are paying the full cost of saving lots of | register state even if an exception isn't thrown. I've seen | this cause serious performance issues on Windows on x86-64 in | the past. | | It also doesn't really compose well, so if library A is using | it for exceptions and library B is doing something else | clever then it's hard to coordinate those two uses. | yvdriess wrote: | Can you elaborate? Isn't the c++ exception system built on | top of setjmp/longjmp? | | For setjmp to have a measurable performance impact, you | have to be calling it an awful lot. It is just a handful of | mov instructions without dependencies. | bregma wrote: | None of the popular C++ vendors implement C++ exceptions | using setjmp/longjmp on any of the more (and very few of | the less) common targets. Maintaining C++ language | runtimes is a part of my day job and I am thankful they | don't. | jcranmer wrote: | > Isn't the c++ exception system built on top of | setjmp/longjmp? | | It is not. The dominant C++ exception system is based on | "zero-cost exception handling", which refers to the fact | that you do not call any extra code (like setjmp) until | an exception is to be thrown. All of that logic is | instead encapsulated in tables of exception-handling data | and sophisticated unwind routines that look up those | tables to figure out where to transfer control-flow to. | | > For setjmp to have a measurable performance impact, you | have to be calling it an awful lot. | | If you built the C++ exception system on top of | setjmp/longjmp, you would have to call setjmp on every | instance that begins a try block. This includes implicit | try blocks generated for every object that has a | nontrivial destructor (which must be called if you unwind | through the block). So yeah, you would be calling it an | awful lot... | aardvark179 wrote: | Depending on the ABI it's saving a lot of registers, and | it is very easy to end up calling it a lot if you cannot | guarantee the code that may be called by functions you | call and you need to do any cleanup. | | C++ and other languages tend to make the fast path of no | exception extremely fast and store additional data about | functions that allow for stack unwinding when an | exception is thrown. | ur-whale wrote: | > "setjmp" and "longjmp" are the mechanism for implementing | exceptions in C | | Correct, but reductive. | | They can be used for many other things. | someweirdperson wrote: | > They can be used for many other things. | | The underlying primitive offers even more flexibility, but | exceptions (in the sense of programming languages) can also | be used for many other things than exceptions (in the sense | of natural language, to handle exceptional cases). | bogomipz wrote: | >"... but exceptions (in the sense of programming | languages) can also be used for many other things than | exceptions ..." | | Can you elaborate? What are the other ways exceptions are | used when not used to handle exceptional situations? | adrian_b wrote: | The more general POV is that "setjmp" defines a | continuation and "longjmp" invokes it. | | While the most frequent use of explicit continuations is | for implementing exceptions, you are right that there are | many other programming techniques based on explicit | continuations (for instance coroutines). | rwmj wrote: | I'm now wondering if anyone has written an actual | continuation library in C (ie saving and restoring the | whole stack). Is it possible? | menaerus wrote: | Coroutines are one good example. | yvdriess wrote: | yes and yes. | speps wrote: | I'm not sure you need to use assembly for writing the MSVC | version, it supports the "naked" calling convention: | https://learn.microsoft.com/en-us/cpp/cpp/rules-and-limitati... | | Similarly, it has the "noreturn" decl spec: | https://learn.microsoft.com/en-us/cpp/cpp/noreturn | | It'd be interesting if that works the same as in GCC for the | article 's objective. | flohofwoe wrote: | MSVC doesn't support inline assembly for x86-64 (the declspecs | are just hinting the compiler to not generate function | entry/exit code and that it shouldn't be confused by the | function not returning to the caller - not sure if | declspec(naked) even works on x86-64 because I think it only | makes sense with inline assembly). | pjmlp wrote: | Besides external Assembler, MSVC's way is to use intrinsics. | | https://learn.microsoft.com/en-us/cpp/intrinsics/compiler-in... | | Which is kind of nice and already proven in 1961 as a better way | to do low level coding in Burroughs B5000, nowadays still sold as | Unisys ClearPath MCP (naturally with improvements). | | Intrinsics can be better understood by the compiler type system, | both for safety and optimization algorithms. | flohofwoe wrote: | Do the intrinsics allow to load and store registers from and to | memory? That's kind of the whole point why assembly is | required. | | Most cross-platform co-routine libraries I've seens for C or | C++ use a small separate assembly file for the stack switching | magic on MSVC, so if it's possible to do the same with | intrinsics, then it's definitely not a well known technique ;) | | PS: at least the list of cross-platform intrinsics looks way | too high level for a self-rolled setjmp/longjmp: | https://learn.microsoft.com/en-us/cpp/intrinsics/intrinsics-... | megous wrote: | setjmp/longjmp don't switch stacks (they just "move" up the | current stack) | fanf2 wrote: | This is fun :-) I was pleased to learn about __builtin_longjmp. | There's a small aside in this article about the signal mask, | which skates past another horrible abyss - which might even make | it sensible to DIY longjmp. | | Some of the nastiness can be seen in the POSIX rationale for | sigsetjmp | (https://pubs.opengroup.org/onlinepubs/9699919799.2018edition...) | which says that on BSD-like systems, setjmp and _setjmp | correspond to sigsetjmp and setjmp on System V Unixes. The effect | is that setjmp might or might not involve a system call to adjust | the signal mask. The syscall overhead might be OK for exceptional | error recovery, such as the arena out of memory example, but it's | likely to be more troublesome if you are implementing coroutines. | | But why would they need to mess with the signal mask? Well, if | you are using BSD-style signals or you are using sigaction | correctly, a signal handler will run with its signal masked. If | you decide to longjmp out of the handler, you also need to take | care to unmask the signal. On BSD-like systems, longjmp does that | for you. | | The problem is that longjmp out of a signal handler is basically | impossible to do correctly. (There's a whole flamewar in the wg14 | committee documents on this subject.) So this is another example | of libc being optimized for the unusual, broken case at the cost | of the typical case. | flykespice wrote: | I didn't know of setjump usage until I studied Lua's source code, | they used it everywhere as a replacement for exceptions in C | stevefan1999 wrote: | I wonder if setjmp/longjmp can be implemented in hardware, i.e. | introduce an instruction set that points, save and switch the | current registers called register window that can be switched by | saving a pointer. | | This way both setjmp and longjmp are basically few cycles and | exception handling would be hella fast. | CalChris wrote: | The DEC VAX-11 instruction set had SVPCTX and LDPCTX for the | kernel which made context switch simple. But they were reserved | instructions; so they couldn't be used for _setjmp()_ and | _longjmp()_. VAX-11 also had queue instructions which made | rescheduling simple. This was basically: SETIPL | SVPCTX INSQUE FFS REMQUE LDPCTX | SETIPL | | Yeah, there was a little more. See page 188 of ... | http://bitsavers.informatik.uni-stuttgart.de/pdf/dec/vax/vms... | snarfy wrote: | Intel has e.g. push all/pop all instructions. It doesn't save | everything needed for setjmp/longjmp (like fpu state) but does | a decent job. | smcameron wrote: | > The solution is to qualify r with volatile, which forces the | compiler to store the variable on the stack and never cache it in | a variable. | | Should the last word of that sentence be "register"? | magicalhippo wrote: | This takes me back to the late 90s when I was a teen and decided | to try to implement preemptive multi-threading in Turbo Pascal. | I'd gotten a book on assembly so was somewhat versed in that, and | took on the challenge. | | Having to save and restore the registers was obvious, but as I | recall it the main challenge was figuring out the right sequence | to do that while also preserving the CPU flags. | | I only used the timer interrupt to switch between threads in a | round-robin way, so interactivity wasn't the best, but it worked. | Was quite pleased with myself for accomplishing that. | | I think it was a pretty decent challenge, and one that was quite | instructive. Not just did you need to know some details about the | various CPU instructions (like which ones affected flags), but it | was also a kind of a puzzle to arrange it all the right way. | FpUser wrote: | Ha, I did exactly the same in exactly same language but that | was the end of 80s. Interactivity was just fine as I've | increased frequency of timer interrupt to 4096 times / s and | would pass control to standard handler every so often so it | would work as standard at about 18 times/s. When not calling | old interrupt I would execute threads switch and control logic. | Turbo Pascal supported built in assembly so writing all low | level stuff was piece of cake. I also used built in assembly to | implement graphics. | thdespou wrote: | This is probably the geekiest article I can digest as a Front end | engineer. | jagrsw wrote: | While some programs/libs (most notably libjpeg) use | setjmp/longjmp, I tend to avoid them: * Does | setjmp/longjmp save/restore all registers (eg simd) of modern CPU | variations? Can incosistiences happen here? * As a goto, it | restores some context (registers) but not others (memory), which | can be counterintuitive. Does it work with volatile variables | * Does it play well with less standard C features like | attribute(cleanup) or with C++ features like exception handling | or class destructors? To avoid issues, it may be best to stick to | very procedural and basic C when using setjmp/longjmp? * | Except registers and memory, there are also IO, FS, Net (at | least) contexts, and these are not restored (cannot be really), | so this notion of "restore certain vars to their original states" | might not work well with certain types of code | | Bc some programming gods recommend using setjmp/longjmp, my | hesitance is likely unfounded. | Findecanor wrote: | The only local variables you can count on being preserved are | "volatile" variables initialised before the setjmp() call, and | then not changed. | | That's all the guarantee the C standard gives. Anything else is | specific to compiler/OS. | adrian_b wrote: | As you have phrased them, the standard guarantees seem weaker | than they are actually specified in the C standard. | | Where a "longjmp" returns after a "setjmp", everything is | preserved exactly like after a normal function call | (including the registers specified by the ABI), | | "except that the values of objects of automatic storage | duration that are local to the function containing the | invocation of the corresponding setjmp macro that do not have | volatile-qualified type and have been changed between the | setjmp invocation and longjmp call are indeterminate." | | For the most frequent use case, when "longjmp" is used to | throw exceptions, the only local variables that can become | indeterminate, according to the standard, are those that have | been passed as arguments to functions invoked inside the C | equivalent of a "try" block, or which have been explicitly | modified in another way there. Any local variable that is not | assigned to or passed as an argument there is preserved. | | The standard behavior is normal, because where a "longjmp" | returns it is not known where the execution of the nested | functions has been aborted and whether any of their output | parameters already store their expected final values or only | some unpredictable temporary values. | adrian_b wrote: | "setjmp/longjmp" must save/restore only those registers that | are defined by the C ABI as being preserved across function | calls. | | The program point to where "longjmp" jumps is viewed by the | compiler as a function return point, so the compiler assumes | that all the other registers hold undefined values. | | For most CPUs, the C ABI defines the SIMD registers as being | _not_ preserved across function calls, so "setjmp" does not | need to save them. Had some of them been defined as preserved | registers, "setjmp" would have also saved those. | | "setjmp/longjmp" cannot be used in any C++ program, unless all | the nested functions that occur between a "setjmp" and a | "longjmp" are C functions. This may happen in C libraries which | are linked into C++ programs and which may use "setjmp/longjmp" | internally, without interfering with C++ objects. | | As you say, "attribute(cleanup)" is not something specified in | the C language standard, so you cannot expect that "longjmp" | will invoke the cleanup function, unless you use a specific | libc implementation, whose documentation explicitly says that | its "longjmp" will invoke the cleanup functions, when the | program is compiled with a certain C compiler. I am not aware | of any such "longjmp" implementation. | | If you want to use exceptions in a C program, you must use | "setjmp/longjmp". If you do not want to use exceptions in a C | program, you have no need for them. | | The same happens in any other programming language. If you do | not want to use exceptions in a C++ program, you have no need | to use keywords like "try" and "throw". | murdoze wrote: | One of the largest C++ projects I worked on was compiled with | exceptions disabled (-fno-exceptions). | | Still in production, for 20 years now. | chaosite wrote: | Google famously doesn't use exceptions for C++, and | Google's C++ code is not exception-safe (https://google.git | hub.io/styleguide/cppguide.html#Exceptions.) | flohofwoe wrote: | I wonder though if setjmp/longjmp was designed with | exceptions in mind, or just as a general escape hatch for the | 'tamed' structured goto in C (my guess would be the latter). | [deleted] | comex wrote: | > I am not aware of any such "longjmp" implementation. | | Clang on Windows behaves this way! That is, when targeting | the Visual C++ runtime, as opposed to MinGW. | | With the Visual C++ toolchain and runtime, longjmp behaves | like a C++ exception throw and goes through a whole stack | unwinding routine, unlike on Unix (and MinGW) where it just | sets a few registers and branches. This has the downside of | being much slower, but the benefit that longjmp will run | destructors of C++ local variables as it unwinds (as well as | SEH `__finally` blocks), making it less of a footgun in C++ | code. | | Visual C++ itself doesn't support GCC extensions like | `__attribute__((cleanup))`, but these days Clang is highly | compatible with Visual C++ while also supporting GCC | extensions. | | Edit: Windows also has the distinction of, on x86-64, | treating the SIMD registers xmm6-xmm15 as callee-saved | (preserved across function calls). As you mention, this means | that setjmp has to save them, which is fine except that it | bloats the size of jmp_buf. | bregma wrote: | Specifically, The C++ Programming Language standard ISO/IEC | 14882:2011 18.10/4 [support.runtime] says this. | | > A setjmp/longjmp call pair has undefined behavior if | replacing the setjmp and longjmp by catch and throw would | invoke any non-trivial destructors for any automatic objects. | | Of course, the C++ language runtime is usually written in | pure C, and at least one naive implementation of the C++ | try/catch mechanism uses setjmp/longjmp under the hood. | smcameron wrote: | > While some programs/libs (most notably libjpeg) use | setjmp/longjmp | | Also libpng. What is it with these graphics guys that they | can't just return an error code like a normal person? | duskwuff wrote: | libjpeg is a fairly old library (1991), so it isn't terribly | surprising that it doesn't follow modern programming | conventions. | | libpng is only slightly newer (1995), and may have adopted | the pattern from libjpeg. | mjburgess wrote: | After learning about effect systems, and generalising effects, my | view on `setjmp` has changed considerably -- it seems effect | systems "effectively" offer a design pattern for their use in | languages without them. | | ie., it feels that there's an async-await.h, try-catch.h, etc. to | be written which would serve as c-ish design patterns. I'd be | interested, then, in to what degree other langs can do the same. | | As a side point, i've not yet read a good defence of why | programming in C is so fun and satisfying in this way. People | reduce the Rust/C issue to memory saftey... but isn't there | something inherently wonderful about `while(*this++ = *that++)` | (etc. etc.) ? | | (A sense of fun beaten out of you by too many annotation | guaranteeing its saftey?) | anonymousiam wrote: | I once took an "Advanced C Programming" class (which would have | been better named as "How to do OOP in a language never | intended for it") where the instructor expressly prohibited the | use of "*i++" and several other language elements because he | thought they were confusing. I got into many arguments with the | instructor throughout the course, and I figured I would get a | poor grade, but he still gave me an A. My main disagreement | with him was this: If the language elements are there and well | defined, why prohibit their use? The course after all was | "Advanced C", wasn't it? | Karellen wrote: | "Programs must be written for people to read, and only | incidentally for machines to execute" | | -- Abelson & Sussman, _Structure and Interpretation of | Computer Programs_ , 1984. | | It's possible to write English with bad structure, clumsy | metaphors, obscure vocabulary, and non-non-non-usual | idiosyncrasies. And other people might be technically capable | of understanding it if they really want to, and try hard | enough. | | After all, the language elements are there and well-defined, | so why would anyone ever complain about "bad" writing? | | (Out of curiosity, do you object to people who say the use of | "goto" should be seriously restricted, or even prohibited, in | most programs? Do you specifically use gotos to make the | point that they can still be be useful and productive? The | language element is there and well-defined.) | badsectoracula wrote: | That has been my argument for "preprocessor abuse" - there | isn't such thing as preprocessor "abuse"[0], it is part of | the language and provides some form of extensibility in a | language with an already limited set of features. | | If anything the preprocessor needs more features (let me | include files from macros and do loops dammit :-P). | | [0] ok, i can think of some uses that might count, like | "#define BEGIN {", etc that serve no practical purpose, but i | don't think anyone called these "preprocessor abuse". | kevin_thibedeau wrote: | You can have all that with m4. It's a perfectly suitable C | preprocessor. | lisper wrote: | > isn't there something inherently wonderful about `while( | _this++ =_ that++)` (etc. etc.) ? | | There is something inherently wonderful about riding a | motorcycle without a helmet too. That doesn't mean it is wise. | tomcam wrote: | About your last two questions yes and yes. | | I love C. | flohofwoe wrote: | These days the 'fashionable' way to implement async-await seems | to be via compiler magic by transforming async functions into a | 'switch-case state machine' and a hidden context pointer | argument. | | In vanilla C (without compiler magic) I've mostly seen it | implemented via 'green threads' aka 'fibers' aka 'stack- | switching', but TBH I'm not sure if this can be implemented | with the standard setjmp/longjmp, I've mostly seen it | implemented without (and instead use two small assembly | functions for the context switch). | | One downside of stack-switching is that it doesn't work on | WASM. | chaosite wrote: | stack switching with setjmp/longjmp can be implemented like | this: https://stackoverflow.com/a/8817009/1116739 | | But it's messy enough that you'd want a library/framework to | help you handle it. | mananaysiempre wrote: | POSIX pre-2008 had (and Linux/Glibc and | {Free,Net,DragonFly}BSD still have) <ucontext.h> with | proper stack switching functions, used as a fallback in a | number of coroutine libraries. The fallback status is due | to a self-inflicted inefficiency: they save and restore the | signal mask, thus still need to go through the kernel (why | e.g. Linux does not put signal mask manipulation in the | vDSO, I don't know). POSIX yanked them and now recommends | rewriting to use POSIX threads instead, which is asinine. | gpderetta wrote: | And the 2008 was about the time that coroutines were | gaining popularity again! | Findecanor wrote: | Putting each tasklet's stack in different places on the | actual stack and jumping between them is inherently unsafe | and not portable. You must be sure that each tasklet does | not consume too much stack so that it does not overwrite | another. | | On BSD Unices, you can only longjump back up the stack. | Otherwise, longjmp() will call longjmperror() and terminate | the program. | chaosite wrote: | > Putting each tasklet's stack in different places on the | actual stack and jumping between them is inherently | unsafe and not portable. You must be sure that each | tasklet does not consume too much stack so that it does | not overwrite another. | | It's absolutely unsafe and a ridiculous to do, but what's | the reasoning for it being unportable? Wouldn't it just | be just as unsafe anywhere that C compiles? | | > On BSD Unices, you can only longjump back up the stack. | Otherwise, longjmp() will call longjmperror() and | terminate the program. | | The manpage claims that the semantics is not "only jump | back up the stack", but rather that you can't longjmp to | "[...] an environment that that has already returned". | Technically, the tasklet the we're longjmp'ing to never | terminates, right? | comex wrote: | In any case, you definitely can't do it on Windows and | Emscripten (WebAssembly), where longjmp invokes the same | stack unwinding behavior as C++ exception handling, | rather than just setting some registers and jumping. | Windows has its own APIs for tasklets (fibers); no such | luck on Emscripten. | samsquire wrote: | I wrote an unrolled switch statement in Java to simulate | eager async/await across threads. | | https://github.com/samsquire/multiversion-concurrency- | contro... | | The goal is that a compiler should generate this for you. | This code is equivalent to the following: | task1: while True: handle1 = async | task2(); handle2 = async task3(); | print(await handle1) print(await handle2) | task2: n = 0 while True: yield | n++ task3: n = 0 while True: | yield n++ | | It doesn't actually run task2 and task3 both eagerly, I've | not got around to scheduling the tasks on DIFFERENT threads. | They currently queue onto a single thread, so task2 and task3 | are parallel to task1 (but maybe not at the same time) but | task2 and task3 are not parallel to each other. This is my | goal. | | I also ported the coroutines in this blog post | https://blog.dziban.net/coroutines/ to GNU Assembler | https://github.com/samsquire/assembly/blob/main/coroutines.S | | This might be useful to someone who wants to port this to C. | This uses the stack switching idea. So they are stackful | coroutines. | | There's also Tina a header only coroutine library | https://slembcke.github.io/Tina | | I also played with protothreads http://dunkels.com/adam/pt/ | | I also asked in a stackoverflow post how to use a thread pool | with C++20 coroutines: | https://stackoverflow.com/questions/74520133/how-can-i- | pass-... Someone provided some example code, which seems to | work with C++ coroutines :-) | duped wrote: | The big problem with setjmp/longjmp for fibers is that a call | to longjmp is undefined behavior if the `jmp_buf` argument | was created by a call to `setjmp` on a different thread (1). | That means fibers cannot be easily relocated onto a different | thread, making M:N threading tricky to implement and erasing | a lot of the benefit of fibers. | | And that said, implementing a super-fast | setcontext/swapcontext is like twenty lines of assembly with | not too many gotchas, if you don't care about saving a few | things that require syscalls. | | But all that said the real downside of stack switching is | that it's overkill for coroutines that can be implemented as | a finite state machine, unless the runtime supports growable | stacks (otherwise you pay a big cost on fiber creation, and | eat a lot of memory for many fibers). There are a few | languages that do this and it's super cool, but C isn't one | of them. | | WASM will almost certainly support stack switching, iirc | there have been proposals for wasmtime to support it already? | | (1) https://pubs.opengroup.org/onlinepubs/9699919799/function | s/l... | lstodd wrote: | > And that said, implementing a super-fast | setcontext/swapcontext is like twenty lines of assembly | with not too many gotchas, if you don't care about saving a | few things that require syscalls. | | sigaltstack(2) wasn't all that prohibitive when I did that | in Python 2.6 back then. Was it 2009? | | I seriously can't understand this obsession with FSMs. A | naive setcontext()-based implementation outperfomed both | greenlets (with its crazy legacy of memcpy-ing parts of | stack from stackless) and Tornado/Twisted (with them being | pure-python and therefore lacking any means to force some | async on client libraries. which one does in C) while | letting everyone write some nice clean synchronous-looking | code. | | 10 years later we end up with half a language hacked up and | still nowhere near the ease of use that was coded in a week | or so. | ckastner wrote: | > inherently wonderful about `while(*this++ = *that++)` | | As someone who hasn't used C as a primary language in some | project for over a decade, I read this and (1) realize that LHS | and RHS both post-increment, (2) don't remember if there is | some UB I might be overlooking, (3) realize that the operator | is assignment "=", not comparison "==", and I can't even | remember what the loop termination criterion here would be. | Until "*that++" is equivalent to false, or something? | | It may be beautiful to the experienced programmer, but I | personally would consider this just "clever" (which is a | criticism, not a compliment). It feels like someone needlessly | tried to pack everything into a single line of code. | | This is something I'd write out more verbosely, if only to make | reading it simpler. The compiler will probably generate the | same machine code either way. | | I'm fully aware that to the seasoned C developer, my criticism | might come across as naive. However, even the fully seasoned C | developer can get careless, or become tired, and in C, every | one of those little things can come back and bite you in those | situations. | | Edit: removed the double pointer dereferencing remark, must | have been an artifact from HN's special treatment of the | asterisk. | | Edit-Edit: I was probably wrong. I don't think it's possible to | create a more verbose version without it affecting performance. | sfpotter wrote: | Could we try to keep the topic on the article itself instead | of complaints about C? It sucks to come and read the comments | about this very nice article and have to scroll and scroll | until I finally get to comments written by people who | actually have something to say. This is a great blog, and the | author puts a ton of effort into their posts. It's hard for | me not to view comments like this as being a bit thoughtless | and inconsiderate. | mjburgess wrote: | That's why comments collapse. | | There's little more useful to say here, other than I don't | think most people would agree with your view of what HN is. | | It's a discussion forum, not an exegetical seminar. "On | topic" is whatever the topic of discussion is; and this is | not constrained to the article. | dilap wrote: | Since we're already way off-topic, allow me to share my | idea for solving this perennial problem: When commenting, | there is a little selector: | | [ ] My comment is on-topic and positive to neutral | | [ ] My comment is critical | | [ ] My comment is off-topic | | You've got to select one. When viewing, the comment | thread defaults to just showing on-topic, non-negative | comments, but you can see the other stuff, too, if you | want. | | This solves two seemingly contradictory desires: the | ability to read comments on things that interest you | without having to fend off waves of negativity and wade | through pools of offtopic text and the ability to speak | freely. | Nullabillity wrote: | It sounds like you're looking for an upvote counter, not | a comment section. | dilap wrote: | No, that's not it at all. The point is to give a small | amount of structure to the comments section, so people | can see what they want. | | Think of it as very roughly analoguous to the different | sections of a newspaper. | subarctic wrote: | That's not really how comment threads work. If you reply to | a specific comment, you're replying to the stuff they said | in that comment. The guy's not even complaining about c | either, he's commenting on the bit of c code that the | parent commenter wrote | [deleted] | flohofwoe wrote: | It's entirely valid C and (assuming this and that are byte | pointers) copies a range of bytes until (and including) a | zero byte is reached. | | With a suffficient warning level (e.g. -Wall on gcc, which | should always be enabled anyway, together with -Wextra), | compilers will complain about the '=' and ask you to add a | pair of braces to make clear that this is actually intended: | while( (*this++ = *that++) ); | | It's also one of those cases where the C code matches the | output assembly pretty well: | | https://www.godbolt.org/z/nz1jbz4Er | | As far as "obfuscated C" goes, this is a very tame example | though, it's just a straightforward usage of language | features, which might look strange only when coming from | other languages that don't have pointers or a post-increment | operator). | ckastner wrote: | Doesn't that kind of prove my point? | | The code as written was described as beautiful, yet would | not have passed -Wall. | | It's those little things that can easily get you in C, and | there are so many little things to consider. | kevin_thibedeau wrote: | GCC warnings can be overly pedantic. It's setup to warn | about common footguns but doesn't know what your intent | is. In this case it's a common enough idiom to assign | within a control statement that GCC has the extra parens | escape hatch. | | You shouldn't just blindly let your tooling dictate how | you work. It's a tool that's supposed to work _for_ you, | not control you. -Wall and -Wextra are good baselines but | I always disable some of their warnings because I don 't | need the hassle on known good code. | flohofwoe wrote: | That extra pair of braces doesn't make the code 'ugly' ;) | | And the code without braces is still entirely valid | standard C, the warning is essentially just a lint to | protect against typos (similar to JS linters warning | about '===' vs '=='). | | PS: let's see if the alternatives would be any more | readable: char c; while (c = | *that++) { *this++ = c; } | | ...this is already buggy because it doesn't copy the | final zero byte, so the test must happen inside the loop | body and also lets try to get rid of the post-increment: | while (true) { char c = *that; | *this = c; this += 1; that += 1; | if (c == 0) { break; } | } | | ...hmm not really any more readable... | | Let's try with an index... while (true) | { char c = that[i]; this[i] = c; | i += 1; if (c == 0) { break; | } } | | ...might be a bit easier to grasp when used to other | languages, but readability hasn't improved all that much | I'd say... | | For reference, MUSL also just uses the original approach: | | https://github.com/esmil/musl/blob/master/src/string/strc | py.... | ckastner wrote: | I was unclear, sorry: I didn't mean to say that the extra | braces make it uglier, I meant to point out that | something that was described as beautiful was actually | flawed. | | The flaw was minor in this case because the identifier | names and lack of body make the intention clear, but my | point is that there are a lot of minor things in C that | can come and bite you at any time. | | Edit: You are right, I don't see a way this could have | been implemented more readable without sacrificing some | performance. | | First thing I thought of was: void | cp(const char* from, char* to) { while (*from) | { *to = *from; to++; | from++; } } | | But that does not reduce to the original case. | flohofwoe wrote: | I've added a couple of examples trying to find a more | readable version, which actually isn't trivial. Sorry for | the 'post-edit' :) | | As for performance: I don't think such details matter | much, first, compilers are pretty good to turn "readable | but inefficient" code into the same optimal output (aka | "zero cost abstraction"). | | And a really performance-oriented strcpy() wouldn't | simply copy byte by byte anyway, but try to move data in | bigger chunks (like 64-bit or even SIMD registers). | Whether this is then actually faster also depends on the | CPU though. | layer8 wrote: | I would use a do-while, because at least one char is | always copied: char c; do | { c = *src++; *dst++ = c; | } while (c != 0); | | The post-increment is idiomatic enough in C-based | languages that I wouldn't worry about that. | bee_rider wrote: | I don't write much C, but to an outsider like me this is | a pretty big improvement. | | It is a shame post-test loops aren't more popular, given | the similarity to the assembly they output. Seems more | mechanically sympathetic. Oh well, at least it is an | excuse to whip out the goto. | layer8 wrote: | In Pascal, you could do repeat | ... until c = 0; | | which might be even clearer for this use case. | jlg23 wrote: | Yes, with -Wall it triggers a warning, but the warning is | a false positive because assignment is indeed the | intention. | mjburgess wrote: | `this` and `that` are arrows which range over a stream of | data; `=` is copy, and `++` moves the arrow along the | stream. | | This isn't a "clever one-liner" it is a clear and precise | syntax for expressing the operation the machine actually | performs. | | while(copy(current(stream_a), current(stream_b)) and not | end_of_stream(stream_a)) | | You might prefer the above, but then, that's every other | major language. The beauty of C is that the above code | has to compile to something like the C version. C just | allows you to actually express it | mypalmike wrote: | C lacks the expression of many useful common CPU | capabilities. Integer rotation and overflow checking come | to mind immediately. | umanwizard wrote: | > it is a clear and precise syntax for expressing the | operation the machine actually performs. | | No it's not. Your compiler will almost certainly | translate this into vector instructions, at least. | umanwizard wrote: | For posterity, I was apparently wrong. It doesn't | autovectorize, with gcc 12.2 -O3 on godbolt, at least. | gpderetta wrote: | > the output assembly pretty well | | Ironically, the compiler os likely to recognize this as a | strcpy and replace it with a possibly vectorized | implementation. | flohofwoe wrote: | I actually tried to make that happen, but was | unsuccessful on GCC and Clang (I've seen this in the past | for mempcy() though). | naasking wrote: | You might be interested in libhandler: | | https://github.com/koka-lang/libhandler | | Basically an effect handling library for C written by the folks | developing the koka language. | matthews2 wrote: | Related and also good fun, writing your own fibers! | https://graphitemaster.github.io/fibers/ | IncRnd wrote: | I remember doing this 30ish years ago. It was all the rage. | Lots of people did that to get multi-threading on the OS of the | time. ___________________________________________________________________ (page generated 2023-02-12 23:00 UTC)