[HN Gopher] Hacktical C: practical hacker's guide to the C progr...
       ___________________________________________________________________
        
       Hacktical C: practical hacker's guide to the C programming language
        
       Author : signa11
       Score  : 201 points
       Date   : 2025-04-14 10:20 UTC (2 days ago)
        
 (HTM) web link (github.com)
 (TXT) w3m dump (github.com)
        
       | kelsey978126 wrote:
       | For those also wondering like myself, this refers to hacker in
       | the whiny "security hacking is only called cracking Reeeee"
       | manner, so this is just aimed at programmers and not security
       | professionals.
        
         | qsort wrote:
         | It's the original meaning of the word. This complaint is kind
         | of ironic considering the site you're posting on :)
        
       | akdev1l wrote:
       | > Microsoft has unfortunately chosen to neglect C for a long
       | time, its compilers dragging far behind the rest of the pack.
       | 
       | Is this still true? MSVC is pretty good at compiling C++ nowadays
        
         | chaosite wrote:
         | MSVC always focused on C++, and C was treated as an
         | afterthought.
        
           | pjmlp wrote:
           | The irony is that Microsoft was the very last MS-DOS compiler
           | vendor to support C++ in their C tooling with Microsoft C/C++
           | 7 in 1992, that changed with the release of Visual C++ in
           | 1993.
        
         | qsort wrote:
         | I think he's referring to C specifically, not C++. It's true
         | that modern versions of MSVC are compliant (and they're also
         | typically faster at implementing features than gcc and clang),
         | but for the longest time there were subtle differences in their
         | C library. To this day I don't think they support VLAs, which
         | are technically standard C (At least until recently, I'm not
         | sure about the latest versions, hopefully someone more
         | knowledgeable can say more).
        
           | akdev1l wrote:
           | I see. I kind of assumed improving the C++ compiler required
           | improving the C parts as well.
           | 
           | VLA situation seems complex:
           | https://stackoverflow.com/questions/55696680/in-which-
           | versio...
        
         | DobarDabar wrote:
         | Compare performance, features or anything of Clang and MSVC and
         | you'll see the differences.
        
         | shakna wrote:
         | For C (not C++), MSVC got C17 in 2020, apart from VLAs - which
         | are never planned. No real roadmap for if/when it will get C23
         | - which is not just fully implemented in GCC, but the default
         | used standard.
        
         | pjmlp wrote:
         | They are talking about C not C++, for Microsoft C was done, it
         | was about time to move into C++.
         | 
         | This was the official position in 2012,
         | 
         | https://herbsutter.com/2012/05/03/reader-qa-what-about-vc-an...
         | 
         | However after the Microsoft reboot with Satya, there was a
         | change of heart regarding C, back in 2020, with C11 and C17
         | being supported,
         | 
         | https://devblogs.microsoft.com/cppblog/c11-and-c17-standard-...
         | 
         | And 2022
         | 
         | https://devblogs.microsoft.com/cppblog/c11-atomics-in-visual...
         | 
         | However there is no official roadmap regarding C23 support, and
         | now with the whole safety discussion going on and Secure Future
         | Initiative, probably will never happen.
         | 
         | Additionally clang is a blessed compiler at Microsoft, it is
         | included on Visual Studio, so whatever MSVC doesn't support can
         | be done in clang as alternative.
        
           | HexDecOctBin wrote:
           | They have added one feature (typeof) from C23, so maybe they
           | will add the rest when they release C++26. Or maybe they
           | won't. Microsoft is an expert in inflicting the cruelty of
           | providing just enough hope.
        
             | pjmlp wrote:
             | C++26? There are having issues with delivering C++23, since
             | the whole change in security focus with Rust, Go, C#, Java
             | first, C and C++ for existing codebases, and most likely
             | one of the reasons Herb Sutter is no longer at Microsoft.
             | 
             | https://developercommunity.visualstudio.com/t/Implement-C23
             | -...
             | 
             | https://developercommunity.visualstudio.com/t/Implement-C26
             | -...
             | 
             | Security changes,
             | 
             | https://azure.microsoft.com/en-us/blog/microsoft-azure-
             | secur...
             | 
             | https://blogs.windows.com/windowsexperience/2024/11/19/wind
             | o...
        
               | HexDecOctBin wrote:
               | Oh wow, I don't write C++, so I didn't know how bad the
               | situation was. My recollection that MSVC always
               | implemented C++ standards posthaste is clearly outdated.
               | 
               | Yup, we are never getting C23. Good thing C11 is decent
               | enough, I guess.
        
         | camel-cdr wrote:
         | Microsoft took 30 years to implement a C89 compatible
         | preprocessor: https://docs.microsoft.com/en-
         | us/cpp/preprocessor/preprocess...
        
       | lou1306 wrote:
       | > Using a stricter language helps with reducing some classes of
       | bugs, at the cost of reduced flexibility in expressing a solution
       | and increased effort creating the software.
       | 
       | First of all, those languages do not "help" "reducing" some
       | classes of bugs. They often entirely remove them.
       | 
       | Then, even assuming that any safe language with unsafe regions
       | (Rust, C#, etc) would not give you comparable flexibility at a
       | fraction of the risk... if your flexible, effortless solution
       | contains entire classes of bugs, then there is no point in
       | comparing "effort". You should at least take into account the
       | effort in providing a software with a high confidence that those
       | bugs are not there.
        
         | immibis wrote:
         | If the language has unsafe regions, it doesn't entirely remove
         | classes of bugs, since they can still occur in unsafe regions.
         | 
         | (Predictable response: "But they can only occur in unsafe
         | regions which you can grep for" and my response to that: "so?")
        
           | guappa wrote:
           | Usually they can also happen outside, if you did something
           | wrong in the unsafe region.
           | 
           | edit: I'm sorry that my captain obvious moment is turning out
           | to be some truth bomb for some. Please keep downvoting as a
           | way to regain your inner peace.
        
             | Hackbraten wrote:
             | > if you did something wrong in the unsafe region.
             | 
             | *you or anyone else in your chain of dependencies that use
             | unsafe
        
           | sjamaan wrote:
           | I suppose the better response is that it removes those
           | classes of bugs where they are absolutely unnecessary. Tricky
           | code will always be tricky, but in the straightforward 80%
           | (or more) of your code such bugs can be completely
           | eliminated.
           | 
           | It's unfortunate that C has so many truly unnecessary bugs
           | which are only caused by stupid overly "clever" exploitation
           | of undefined behaviour by compilers.
        
             | codr7 wrote:
             | Unfortunate, yes.
             | 
             | But what bugs? Suboptimal choices maybe; but any backwards
             | compatible, popular language is going to have its share of
             | those.
        
               | immibis wrote:
               | The ones GP is referring to all go away when you use -O0.
               | They're completely artificially constructed by compiler
               | writers language-lawyering the language. They were
               | unforeseeable to the people who actually wrote the
               | language, who expected interpretations like
               | "dereferencing null crashes the program" or
               | "dereferencing null accesses the interrupt vector table"
               | and absolutely were not expecting "dereferencing null
               | deletes the previous three lines of code"
        
               | codr7 wrote:
               | Which I would definitely recommend as a strong default.
        
           | lou1306 wrote:
           | No matter whether you are using C for "freedom" or
           | "flexibility" of "power", 95% of the time you only need that
           | in a very small portion of your codebase. You almost
           | definitely do _not_ need any of that in, say, the logic to
           | parse CLI arguments or config files, which however is a prime
           | example of a place where vulnerabilities are known to happen.
        
             | codr7 wrote:
             | Agreed, there's a lot to win from gluing C to a more
             | protected language, I'm a fan of embedding a scripting
             | language.
        
             | pjmlp wrote:
             | Which is in the past I would reach out to something like
             | Perl on its heyday, given its coverage of UNIX API as part
             | of the standard library, for anything manipulating CLI
             | tools or config files.
             | 
             | Nowadays pick your scripting language, and if C is really
             | needed, cleanly placing it in a loadable module with all
             | security invariants into that scripting, or managed
             | language, instead of 100% pure C source.
             | 
             | My solution since early 2000's.
        
           | oconnor663 wrote:
           | > Predictable response: "But they can only occur in unsafe
           | regions which you can grep for" and my response to that:
           | "so?"
           | 
           | The situation is both worse than this and better than this.
           | Consider the .set_len() method on Rust's Vec. It's unsafe,
           | because you could just .set_len(1_000_000) and then the Vec
           | would happily let you try to read the nonexistent millionth
           | element and segfault. However, if you could edit the standard
           | library sources, you could add this new method to Vec without
           | touching any unsafe code:                   pub fn
           | set_len_totally_safe_i_promise(&mut self, new_len: usize) {
           | self.len = new_len;         }
           | 
           | This is exactly the same as the real set_len, except it's a
           | "fn" instead of an "unsafe fn". Now the Vec API is totally
           | broken, and safe callers can corrupt memory. Also critically,
           | we didn't write any unsafe code in
           | "set_len_totally_safe_i_promise". The key detail is that this
           | new method has access to the private self.len field of Vec
           | that unsafe blocks in the same module rely on.
           | 
           | In other words, grepping for all the unsafe blocks isn't
           | sufficient for saying that a program is UB-free. You also
           | have to make sure that none of the safe code ever violates an
           | invariant that the unsafe blocks rely on. Read the comments,
           | think really hard, etc.
           | 
           | So...what's the point of all this? The point is that it lets
           | us define a notion of "soundness", such that if I only write
           | safe code, and I only use libraries that are "sound", we can
           | _guarantee_ that my program is UB-free. In other words, any
           | UB in my program would necessarily point to a bug in one of
           | my dependencies, in the stdlib, or in the compiler. (Or you
           | know, in the hardware, or in mathematics itself.) In other
           | other words, instead of auditing my entire gigantic (safe)
           | program for UB, we can reduce the problem to auditing my
           | dependencies for soundness. Crucially, this decouples the
           | difficulty of the problem from the size of my program. This
           | wouldn 't be very interesting if "safe code" was some
           | impoverished subset, like "unsigned integer arithmetic only".
           | But in fact safe code can use pointers, tagged unions,
           | pointers _into_ tagged unions, heap allocation /freeing, and
           | multithreading. Lots of large, complicated, useful, real-
           | world programs are written in 100% safe code. Here the
           | version of this story with all the caveats and footnotes:
           | https://jacko.io/safety_and_soundness.html
        
             | uecker wrote:
             | You still need to audit the safe part for other bugs...
             | 
             | But yes, this is nice and we should (and probably will)
             | have a safe mode in C too.
        
         | agentultra wrote:
         | No amount of chest-thumping about how good of a programmer you
         | are and telling everyone else to, "get good," has had any
         | effect on the rate of CVE's cause by memory safety bugs that
         | are trivial to introduce in a C program.
         | 
         | There are good reasons to use C. It's best to approach it with
         | a clear mind and a practical understanding of its limitations.
         | Be prepared to mitigate those short comings. It's no small
         | task!
        
           | uecker wrote:
           | I am not sure the number of CVEs measures anything
           | meaningful. The price for zero-days for important targets
           | goes into the millions.
           | 
           | While I am sure there can not be enough security, I am not at
           | all sure the extreme focus on memory safety is worth it, and
           | I am also not sure the added complexity of Rust is really
           | worth it. I would prefer to simplify the stack and make C
           | safer.
        
             | tialaramex wrote:
             | If that's your preference you're going about it all wrong.
             | Rust's safety is about _culture_ and you 're looking at the
             | technology, it's not that Rust doesn't have technology but
             | the technology isn't where you start.
             | 
             | This was the only notable failing of Sean's (abandoned)
             | "Safe C++" - it delivers all the technology a safe C++
             | culture would have needed, but there is no safe C++ culture
             | so it was waved away as unimportant.
             | 
             | The guy whose mine loses fifty miners in a roof collapse
             | doesn't need better mining technology, inadequate
             | technology isn't why those miners died, culture is. His
             | mine didn't have safety culture, probably because he didn't
             | give shit about safety, and his workers either shared this
             | dismissal or had no choice in the matter.
             | 
             | Also "extreme focus" is a misinterpretation. It's not an
             | extreme focus, it's just mandatory, it's like if you said
             | humans have an "extreme focus" on breathing air, they
             | really don't - they barely even think about breathing air -
             | it was just mandatory so if you don't do it then I guess
             | that stands out.
        
               | uecker wrote:
               | Let's turn it around: Do you think the mining guy that
               | does not care about safety will start caring about a
               | safety culture because there is a new safety tool? And if
               | it is mandated by government, will it be implemented in a
               | meaningful way, or just on paper?
        
               | tialaramex wrote:
               | So there's a funny thing about mouthing the words, the
               | way the human mind works the easiest way to explain to
               | ourselves why we're mouthing the words is that we agree
               | with them. And so in that sense what seems like a useless
               | paper exercise can be effective.
               | 
               | Also, relevantly here, nobody actually _wants_ these
               | terrible bugs. This is not A or B, Red or Blue, this is
               | very much Cake or Death and like, there just aren 't any
               | people queueing up for Death, there are people who don't
               | particularly want Cake but that's not the same thing at
               | all.
        
               | pjmlp wrote:
               | It will certainly be implemented in a meaningful way, if
               | the consequences for the mining guy are hard enough that
               | there won't be a second failure done by the same person.
               | 
               | Hence why I am so into cybersecurity laws, and if this is
               | the only way to make C and C++ communities embrace a
               | safety culture, instead of downplaying it as straitjacket
               | programming like in the C vs Pascal/Modula-2 Usenet
               | discussion days, then so be it.
        
             | agentultra wrote:
             | At some point, in order to make C safer, you're going to
             | have to introduce some way of writing a more formal
             | specification of the stack, heap and the lifetime of
             | references into the language.
             | 
             | Maybe that could be through a type system. Maybe that could
             | be through a more capable run-time system. We've tried
             | these avenues through other languages, through experimental
             | compilers, etc.
             | 
             | Without introducing anything new to the language we have a
             | plethora of tools at our disposal:
             | 
             | - Coq + Iris, or some other proof automation framework with
             | separation logic.
             | 
             | - TLA+, Alloy, or some form of model checking where proofs
             | are too burdensome/unnecessary
             | 
             | - AFL, Valgrind and other testing and static analysis tools
             | 
             | - Compcert: formally verified compilers
             | 
             | - MISRA and other coding guidelines
             | 
             | ... and all of this to be used in tandem in order to really
             | say that _for the parts specified and tested, we 're
             | confident there are no use-after-free memory errors or
             | leaks_. That is a lot of effort in order to make that
             | statement. The vast, vast majority of software out there
             | won't even use most of these tools. Most software
             | developers argue that they'll never use formal methods in
             | industry because it's just too hard. Maybe they'll use
             | Valgrind if you're lucky.
             | 
             | Or -- you could add something to the language in order to
             | _prevent_ at least some of the errors by definition.
             | 
             | I'm not a big Rust user. Maybe it's not great and is too
             | difficult to use, I don't know. And I do like C. I just
             | think people need to be aware that writing _safe_ C is
             | really expensive and time consuming, difficult and nothing
             | is guaranteed. It might be worth the effort to learn Rust
             | or use another language and at least get some guarantees;
             | it 's probably not as hard as writing safe C.
             | 
             | (Maybe not as safe as using Rust + formal methods, but at
             | least you'll be forced to think about your specification up
             | front before your code goes into production... and where
             | you do have unsafe code, hopefully it will be small and not
             | too hard to verify for correctness)
             | 
             |  _Update_ : fixed markup
        
               | uecker wrote:
               | I agree.
        
               | pjmlp wrote:
               | The problem is not tools don't exist, lint was created in
               | 1979 at Bell Labs after all.
               | 
               | It is the lack of culture to use them unless there is a
               | goverment mandate to impose them, like in high critical
               | computing.
        
           | codr7 wrote:
           | Definitely, but the idea is that its unique feature set is
           | worth it.
        
             | agentultra wrote:
             | Yeah, there are still good reasons to use it.
        
         | codr7 wrote:
         | So use Rust, fine by me.
         | 
         | I might too some day, who knows.
        
       | throwaway7894 wrote:
       | #define hc_task_yield(task)          do {
       | task->state = __LINE__;            return;              case
       | __LINE__:;                  } while (0)
       | 
       | That's just diabolical. I would not have thought to write "case
       | __LINE__". In the case of a macro, using __LINE__ twice expands
       | to the same value where the macro is used, even if the macro has
       | newlines. It makes sense, but TIL.
        
         | veltas wrote:
         | Credit to Simon Tatham
         | 
         | https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html
        
           | makeset wrote:
           | I knew the name sounded familiar:
           | 
           | Simon Tatham's Portable Puzzle Collection
           | https://www.chiark.greenend.org.uk/~sgtatham/puzzles/
        
             | dwattttt wrote:
             | Also author of PuTTy
        
           | quietbritishjim wrote:
           | > no commonly used high level language supports the coroutine
           | call primitive
           | 
           | Shows how old this post is. In fact I remember reading it
           | well over 10 years ago, maybe more like 20. archive.org says
           | that it's at least as old as 2001. A great article.
           | 
           | I'm very excited to see he's published a new article on C++20
           | coroutines. I've read (or maybe skimmed...) a few
           | introductions and not really got them, despite having used C#
           | and Python coroutines a lot with no problems (even making
           | changes to an async runtime for Python). Given how clear his
           | C coroutine article is, I'm optimistic about the C++ article.
           | 
           | > So, after the course, I went away and studied on my own,
           | and wrote the introduction to C++ coroutines that I'd have
           | liked to see.
           | 
           | https://www.chiark.greenend.org.uk/~sgtatham/quasiblog/corou.
           | ..
        
         | HeliumHydride wrote:
         | With GNU extensions, you can make a simpler coroutine macro
         | without switch/case abuse:                   #define CO_BEGIN
         | static void* cr_state_ = &&cr_st_0; goto *cr_state_; cr_st_0:
         | #define CO_RETURN(x) ({ __label__ resume; cr_state_ = &&resume;
         | return (x); resume:; })
        
         | gthompson512 wrote:
         | Minor correction, macros CANT have newlines, you need to splice
         | them during preprocessing using \ followed by a new line, the
         | actual code has these:
         | 
         | from
         | https://github.com/codr7/hacktical-c/blob/main/macro/macro.h
         | 
         | #define hc_align(base, size) ({ \ __auto_type _base = base; \
         | __auto_type _size = hc_min((size), _Alignof(max_align_t)); \
         | (_base) + _size - ((ptrdiff_t)(_base)) % _size; \ }) \
         | 
         | After preprocessing it is a single line.
        
           | fuhsnn wrote:
           | We might get multi-line macros in C2y standard:
           | https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3524.txt
        
         | tanelpoder wrote:
         | I've written C on-and-off for over 30 years (just various
         | throw-away prototypes and OS/app interaction microbenchmarks)
         | and it took a while + a web search to get it. Diabolical
         | indeed. Edit: And makes sense in hindsight.
        
       | pjmlp wrote:
       | > The reason I believe C is and always will be important is that
       | it stands in a class of its own as a mostly portable assembler
       | language, offering similar levels of freedom.
       | 
       | When your computer is a PDP-11, otherwise it is a high level
       | systems language like any other.
        
         | pornel wrote:
         | Less controversially, when you write C, you write for a virtual
         | machine described by the C spec, not your actual hardware.
         | 
         | Your C optimizer is emulating that VM when performing symbolic
         | execution, and the compiler backend is cross-compiling from it.
         | It's an abstract hardware that doesn't have signed overflow,
         | has a hidden extra bit for every byte of memory that says
         | whether it's initialized or not, etc.
         | 
         | Assembly-level languages let you write your own calling
         | conventions, arrange the stack how you want, and don't make
         | padding bytes in structs _cursed_.
        
           | aninteger wrote:
           | > Less controversially, when you write C, you write for a
           | virtual machine described by the C spec, not your actual
           | hardware.
           | 
           | Isn't this true for most higher level languages as well? C++
           | for instance builds on top of C and many languages call into
           | and out of C based libraries. Go might be slightly different
           | as it is interacting with slightly less C code (especially if
           | you avoid CGO).
        
           | bmandale wrote:
           | These are all such nonsensical misinterpretations of what
           | people mean when they say C is "low level". You absolutely
           | don't write C for the C abstract machine, because the C spec
           | says nothing about performance, whereas performance is one of
           | the primary reasons people write C.
           | 
           | The existence of undefined behaviour isn't proof that there
           | is a C "virtual machine" that code is being run on. Undefined
           | behaviour is a relaxation of requirements on the compiler.
           | The C abstract machine doesn't not have signed overflow,
           | rather it allows the compiler to do what it likes when signed
           | overflow is encountered. This is originally a concession to
           | portability, since the common saying is not that C is close
           | to assembly, but rather that it is "portable" assembler. It
           | is kept around because it benefits performance, which is
           | again one of the primary reasons people write C.
        
             | pjmlp wrote:
             | C performance exists thanks to UB, and the value optimising
             | compilers extract out of it, during the 8 and 16 bit home
             | computers days any average Assembly developer could write
             | better code than C compiler were able to spit out.
        
               | codr7 wrote:
               | And also because it doesn't get in your way of doing
               | exactly what you want to do.
        
               | OCASMv2 wrote:
               | If that was true then the optimizers wouldn't need to
               | exist in the first place.
        
               | codr7 wrote:
               | Compared to the alternatives.
               | 
               | It gets very frustrating to communicate at this level.
        
               | OCASMv2 wrote:
               | I don't think compilers allowing trash through is a good
               | thing.
        
               | codr7 wrote:
               | That's an opinion, another one would be that the
               | flexibility allowed by undefined behavior is one of C's
               | strengths at the same time. Strength/weakness are often
               | two sides of the same coin. Which is why these
               | discussions get a bit circular.
        
               | greenavocado wrote:
               | Have you ever seen the error steamroller?
               | https://github.com/mattdiamond/fuckitjs
        
               | dwattttt wrote:
               | ON ERROR RESUME NEXT rears its ugly head again
        
               | pjmlp wrote:
               | The alternatives outside Bell Labs were just as capable.
        
             | pornel wrote:
             | I'm not trying to prove a novel concept, just explain how
             | the C spec thinks about C:
             | 
             | > _The semantic descriptions in this International Standard
             | describe the behavior of an abstract machine in which
             | issues of optimization are irrelevant._
             | 
             | This belief that C targets the hardware directly makes C
             | devs frustrated that UB seems like an intentional trap
             | added by compilers that refuse to "just" do what the target
             | CPU does.
             | 
             | The reality is that front-end/back-end split in compilers
             | gave us the machine from the C spec as its own optimization
             | target with its own semantics.
             | 
             | Before C got formalised in this form, it wasn't very
             | portable beyond PDP. C was too opinionated and bloated for
             | 8-bit computers. It wouldn't assume 8-bit bytes (because
             | PDP-11 didn't have them), but it did assume linear memory
             | (even though most 16-bit CPUs didn't have it). All those
             | "checking wetness of water... wet" checks in ./configure
             | used to have a purpose!
             | 
             | Originally C didn't count as an assembly any more than
             | asm.js does today. C was too abstract to let programmers
             | choose addressing modes and use flags back when these
             | mattered (e.g. you could mark a variable as `register`, but
             | not specifically as an A register on 68K). C was too high
             | level for tricks like self-modifying code (pretty standard
             | practice where performance mattered until I-cache and OoO
             | killed it).
             | 
             | C is now a portable assembly more because CPUs that didn't
             | fit C's model have died out (VLIW) or remained non-standard
             | specialized targets (SIMT).
        
         | grandempire wrote:
         | Which other popular language more accurately represents a
         | random access machine of fixed word length?
        
           | alexvitkov wrote:
           | None, but that'a not what computers are. C assumes that in a
           | few places, e.g. variadic functions, and those are the worst
           | parts of the language.
        
             | grandempire wrote:
             | > but that'a not what computers are
             | 
             | Which language more accurately represents hardware then?
        
               | pjmlp wrote:
               | Assembly language from the hardware vendor.
        
               | oguz-ismail wrote:
               | isn't it translated to microcode before being executed?
        
               | pjmlp wrote:
               | Depends on the hardware design.
        
               | alexvitkov wrote:
               | C++ for one - it has atomics with well defined memory
               | barriers, and guarentees for what happens around them.
               | 
               | The real answer is obviously Assembly - pick a random
               | instruction from any random modern CPU and I'd wager
               | there's a 95% chance it's something you can't express in
               | C at all. If the goal is to model hardware (it's not),
               | it's doing a terrible job.
        
               | grandempire wrote:
               | C++ better represents the machine?
        
               | uecker wrote:
               | C has the same atomics and concurrency model as C++.
        
           | pjmlp wrote:
           | I don't know, Ada, Modula-2, Object Pascal, PL/I, NEWP, PL.8,
           | D, Zig, Mesa, ATS,....
           | 
           | But then again, you booby trapped the question with _popular
           | language_.
        
             | guywithahat wrote:
             | If a language is unpopular, people won't want to work for
             | you and you'll run into poor support. Rewriting a library
             | may take months of dev time, whereas C has an infinite
             | number of libraries to work with and examples to look at.
        
               | pjmlp wrote:
               | Moving goalposts regarding systems programming languages
               | features, some on the group predate C by a decade.
        
               | guywithahat wrote:
               | Being old doesn't mean anyone knows the language. I mean
               | if the language predates C significantly and nobody uses
               | is then there's probably a really good for it. The
               | goalposts aren't moving they're just missing the shot
        
               | OCASMv2 wrote:
               | Popularity isn't a measure of quality. Never has been and
               | certainly not in the case of programming languages.
        
               | grandempire wrote:
               | There is unpopular - and then there is can I get a
               | working toolchain for modern OS that's not emulated.
        
               | OCASMv2 wrote:
               | Still not a measure of quality.
        
               | grandempire wrote:
               | Are we having a discussion about the greatest language of
               | all time? What's your context here.
        
               | ryao wrote:
               | _wears math hat_
               | 
               | C does not have an infinite number of libraries and
               | examples. The number of libraries and examples C has is
               | quite large, and there are an infinite number of
               | theoretically possible libraries and examples, but the
               | number of libraries and examples that exist are finite.
        
             | grandempire wrote:
             | Many of those languages do not have pointers - which are
             | fundamental to how modern instruction sets work.
        
               | pjmlp wrote:
               | Yes they do, point an example from that group, and I will
               | gladly prove you wrong.
        
               | grandempire wrote:
               | Well sounds like you are confident and we are going to
               | get into a semantic argument about what qualifies as a
               | pointer.
               | 
               | So which of these languages do you think is a better
               | representation of hardware and not a PDP-11?
        
               | pjmlp wrote:
               | Better representation of the hardware?
               | 
               | None of them, you use Assembly if you want the better
               | representation of hardware.
               | 
               | Yes, I am quite confident, because I have been dispelling
               | the C myth of the true and only systems programming
               | language since the 1990's.
        
               | grandempire wrote:
               | So then your comment about C being an outdated PDP-11
               | must be equally true of other languages. So it says
               | nothing.
        
               | pjmlp wrote:
               | Not really, some of those languages predate the very
               | existence of C and PDP-11.
        
           | kryptiskt wrote:
           | C lacks sympathy with nearly all additions to hardware
           | capabilities since the late 80s. And it's only with the
           | addition of atomics that it earns the qualification of
           | "nearly". The only thing that makes it appear as lower level
           | than other languages is the lack of high-level abstraction
           | capabilities, not any special affinity for the hardware.
           | 
           | For one, would expect that a low level language wouldn't be
           | so completely worthless at bit twiddling. Another thing, if C
           | is so low level, why can't I define a new calling convention
           | optimized for my use case? Why doesn't C have a rich library
           | for working with SIMD types that has been ubiquitous in
           | processors for 25 years?
        
             | kryptiskt wrote:
             | Like, say I have a data structure that is four bits wide
             | (consisting of a couple of flags or something) and I want
             | to make an array of them and access them randomly. What
             | help do I get from C to do this? C says "fuck you".
        
               | codr7 wrote:
               | Only if you don't know C.
               | 
               | Otherwise is says, do whatever you feel like.
        
               | cv5005 wrote:
               | Pick an appropriate base type (uintN_t) for a bitset,
               | make an array of those (K * N/4) and write a couple
               | inline functions or macros to set and clear those bits.
        
             | grandempire wrote:
             | It also has pointers which are absent from most languages
             | but essential to instruction sets.
        
               | pjmlp wrote:
               | Lots of languages since the 1950's have pointers.
        
             | codr7 wrote:
             | It puts less obstacles in the way of dealing with hardware
             | than almost any other language for sure.
             | 
             | What's standardized was never as important in C land, at
             | least traditionally, which I guess partly explains why it's
             | trailing so far behind. But the stability of the language
             | is also one of its features.
        
             | cv5005 wrote:
             | simd doesnt make much sense as a standard feature/library
             | for a general purpose language. If you're doing simd its
             | because you're doing something particular for a particular
             | machine and you _want_ to leverage platform specific
             | instructions, so thats why intrinsics (or hell, even
             | externally linked blobs written in asm) is the way to go
             | and C supports that just fine.
             | 
             | But sure, if all youre doing is dot products I guess you
             | can write a standard function that will work on most simd
             | platforms, but who cares, use a linalg library instead.
        
         | NoTeslaThrow wrote:
         | > When your computer is a PDP-11, otherwise it is a high level
         | systems language like any other.
         | 
         | Describing C as "high-level" seems like deliberate abuse of the
         | term. The virtual machine abstraction doesn't imply any
         | benefits to the developer.
        
           | pjmlp wrote:
           | Neither does pretending C is a macro Assembler.
        
           | timClicks wrote:
           | That's a curious remark, although I guess it doesn't look
           | high level from the eyes of someone looking at programming
           | languages today.
           | 
           | C has always been classed as a high level language since its
           | inception. That term's meaning has shifted though. When C was
           | created, it wasn't assembly (middle) or directly writing CPU
           | op codes in binary/hex (low level).
        
           | ecb_penguin wrote:
           | > Describing C as "high-level" seems like deliberate abuse of
           | the term
           | 
           | Honestly it doesn't really matter. High level and low level
           | are relative to each-other (and machine language), and
           | nothing changes based on what label you use.
           | 
           | Best thing to do is shrug and say "ok".
        
           | ryao wrote:
           | Spend 90 days writing nothing but assembly language and then
           | tell me that you believe C is a low level language.
        
             | NoTeslaThrow wrote:
             | I don't see much difference. What is C suppose to express
             | that assembly cannot? What is assembly suppose to express
             | that C cannot? Does this conversation matter to begin with?
        
               | ryao wrote:
               | You did not spend 90 days writing assembly language. I
               | doubt you even spent 1 day.
        
               | saulpw wrote:
               | For one, C has types and type-checking.
        
         | hgs3 wrote:
         | Can you elaborate? C constructs generally map to one or a few
         | assembly instructions at most. You can easily look at C and
         | predict the generated assembly. This is in contrast to other
         | compiled languages, like Go, that inject instructions for
         | garbage collection and other runtime features.
        
           | pjmlp wrote:
           | See my list of languages on a sibling thread, same applies to
           | those, nothing special about C there.
        
         | uecker wrote:
         | While C was adapted to the PDP-11, this was adding byte-level
         | memory access. Otherwise I do no think there is anything in C
         | specific to the PDP-11, or what would this be?
         | 
         | What makes C low-level is that it can work directly with the
         | representation of objects in memory. This has nothing to do
         | with CPU features, but with direct interoperability with other
         | components of a system. And this is what C can do better than
         | any other language: solve problems by being a part of a more
         | complex system.
        
           | WalterBright wrote:
           | The post-increment and post-decrement operators mapped
           | directly onto PDP-11 CPU addressing modes.
           | 
           | The integral promotion rules come directly from the PDP-11
           | CPU instruction set.
           | 
           | If I recall correctly so does the float->double promotions.
           | 
           | CPUs started adapting to C semantics around the mid-80's. CPU
           | designers would profile C generated code and change to be
           | able to more efficiently run it.
        
             | uecker wrote:
             | Thanks. I guess the integral promotion is related to byte-
             | addressing. If you have bytes but can not directly do
             | arithmetic on them, promoting them to word size seems
             | natural.
        
         | codr7 wrote:
         | Yeah, people keep repeating that like a broken record lately,
         | it smells like Rust to me.
         | 
         | No one is claiming it was built for today's processors, just
         | that it puts less obstacles between you and the hardware than
         | almost any other language. Assembler and Forth being the two
         | I'm familiar with.
        
           | pjmlp wrote:
           | Because people keep repeating the urban myth of portable
           | assembler and being the very first systems programming
           | language.
           | 
           | One of the very first systems programming languages was
           | JOVIAL, from 1958. C's inventors were still finalising their
           | studies.
        
             | codr7 wrote:
             | I don't know how to phrase this to reach through, but
             | you're barking up the entirely wrong tree. It's not about
             | the CPU, it's about the level of freedom afforded by the
             | language; how close to hardware it allows you to go, how
             | few hoops you have to jump trough to get there.
             | 
             | The other approach, taken by Rust (and to some degree C++),
             | is to nail everything to the floor and force the programmer
             | to express a solution in a specific format that's easier to
             | verify and make guarantees about. Which is fine.
             | 
             | Both approaches have their appeal, which is best depends on
             | context.
        
               | pjmlp wrote:
               | Which is again nothing special about C, other languages
               | offer similar capabilities, I have provided a list in a
               | sibling comment.
               | 
               | Keep waiting for the examples where they can't do what
               | ISO C allows for, and if the example uses compiler
               | extensions to the ISO C, I also feel within the right to
               | use extensions to those languages on the counter example.
        
         | _kst_ wrote:
         | C is a relatively low level language, but it is _not_ assembly
         | language.
         | 
         | The difference is clear. Assembly language programs specify
         | sequences of CPU instructions. C programs specify runtime
         | behavior.
        
       | 9d wrote:
       | > C doesn't try to save you from making mistakes. It has very few
       | opinions about your code and happily assumes that you know
       | exactly what you're doing. Freedom with responsibility.
       | 
       | I love C because it doesn't make my life very inconvenient to
       | protect me from stubbing my toe in it. I hate C when I stub my
       | toe in it.
        
         | OCASMv2 wrote:
         | No, it just makes it inconvenient to try to protect yourself
         | from stubbing your toe in it.
        
           | codr7 wrote:
           | C doesn't make anything inconvenient, that's its major
           | appeal. Some things are convenient by design, yes, but it's
           | not trying to prevent you from doing anything. That's a
           | feature.
        
             | OCASMv2 wrote:
             | > C doesn't make anything inconvenient
             | 
             | Other than writing memory safe code, as history has shown.
        
               | codr7 wrote:
               | Difficult, not inconvenient.
               | 
               | Because it allows things that are difficult, like writing
               | your own memory allocators.
               | 
               | If you don't like working at that difficulty level, then
               | C programming isn't for you. And that's fine.
        
               | OCASMv2 wrote:
               | It doesn't allow me to write my own memory allocator, it
               | forces me to.
               | 
               | This line of argumentation reminds me of this:
               | 
               |  _Advertise and promote a shortcoming or a fault as a
               | virtue.
               | 
               | For example, ultra-cheap single-use film cameras are
               | advertised as "No Focusing Required." The truth is, no
               | focusing is possible, because those cameras have cheap
               | plastic fixed-focus lenses that won't move and can't be
               | focused. What is a serious shortcoming for a camera --
               | the inability to properly focus on the subject -- is sold
               | as a convenience: "You don't have to bother with
               | focusing."_
               | 
               | https://orangepapers.eth.limo/orange-
               | propaganda.html#make_vi...
        
               | codr7 wrote:
               | No it doesn't, malloc() and free() is the default.
        
         | neonsunset wrote:
         | Usually stubbing your toe does not take your whole leg.
        
         | 0xEF wrote:
         | I've heard it put another way that I enjoyed: "C assumes you
         | know what you're doing, which is only a problem if you don't
         | know what you're doing."
        
           | tialaramex wrote:
           | Having spent many, many years paid to write C, and with no
           | wish to write any more now than I learned Rust, I would
           | suggest a rewording:
           | 
           | "C assumes you know what you're doing, which is only a
           | problem _because_ you don 't know what you're doing."
           | 
           | Periodically, especially in r/cpp I run into people who are
           | apparently faultless and so don't make the mistakes that make
           | these languages dangerous, weirdly none of these people seem
           | to have written any software I can inspect to see for myself
           | what that looks like, and furthermore the universe I live in
           | doesn't seem to have any of the resulting software. I choose
           | to interpret this mystery as: People are idiots _and_ liars,
           | but of course there could be other interpretations.
        
             | psunavy03 wrote:
             | > Periodically, especially in r/cpp I run into people who
             | are apparently faultless and so don't make the mistakes
             | that make these languages dangerous, weirdly none of these
             | people seem to have written any software I can inspect to
             | see for myself what that looks like, and furthermore the
             | universe I live in doesn't seem to have any of the
             | resulting software.
             | 
             | So basically Jeff Sutherland ever since he started talking
             | about AI. "My AI agents have formed a Scrum team that's 30
             | times faster than any human developer!" Great, Jeff.
             | Working in which company's production codebase?
        
               | codr7 wrote:
               | Yeah, well, as stated: software written by humans will
               | have bugs.
               | 
               | The real danger with Rust is the cult like delusion
               | that's not the case for them.
        
               | tialaramex wrote:
               | To be sure, my Rust has bugs in it, but none of them come
               | close to the spooky nonsense that could happen in my C
               | and yet the performance is excellent. Probably more than
               | once a day Rust's compiler rejects code that an analogous
               | C compiler would wave through - and maybe it'd survive
               | testing too, at least for a while.
        
             | ryao wrote:
             | I wonder if in a few years you will never want to write
             | another line of Rust again like another developer I know
             | who used to be enamored with Rust.
             | 
             | That said, I have not written perfect C code myself, but I
             | have fixed a number of mistakes others made in their C
             | code. Many of my commits to OpenZFS were done to fix
             | others' mistakes. A few of my commits even contained my own
             | mistakes that I or others later caught. Feel free to
             | inspect the codebase yourself. You should find it is a very
             | well written codebase
        
         | codr7 wrote:
         | Oh, very much likewise, but there's always two sides to a coin.
        
         | oconnor663 wrote:
         | > It has very few opinions about your code
         | 
         | I understand where this is coming from, but I think this is
         | less true than it used to be, and (for that reason) it often
         | devolves into arguments about whether the C standard is the
         | actual source of truth for what you're "really" allowed to do
         | in C. For example, the standard says I must _never_ :
         | 
         | - cast a `struct Foo*` into a `struct Bar*` and access the Foo
         | through it (in practice we teach this as the "strict aliasing"
         | rules, and that's how all(?) compilers implement it, but that's
         | not what SS6.5 paragraph 7 of the standard says!)
         | 
         | - allow a signed integer to overflow
         | 
         | - pass a NULL pointer to memcpy, even if the length is zero
         | 
         | - read an unitialized object, even if I "don't care" what value
         | I get
         | 
         | - read and write a value from different threads without locking
         | or atomics, even if I know exactly what instructions those
         | reads and writes compile into and the ISA manual says it's 100%
         | fine to do that
         | 
         | All of these are ways that (modern, standard) C doesn't really
         | "do what the programmer said". A lot of big real-world projects
         | build with flags like -fno-strict-aliasing, so that they can
         | get away with doing these things even though the standard says
         | they shouldn't. But then, are they really writing C or "C with
         | custom extensions"? When we compare C to other languages, whose
         | extensions are we talking about?
        
           | ryao wrote:
           | cast a `struct Foo*` into a `struct Bar*` and access the Foo
           | through it (in practice we teach this as the "strict
           | aliasing" rules, and that's how all(?) compilers implement
           | it, but that's not what SS6.5 paragraph 7 of the standard
           | says!)
           | 
           | Use the union type. Abusing it for aliasing violates the
           | standard too, but GCC and Clang implement an extension that
           | permits this. Alternatively, just allocate a char array and
           | cast it as you please. Strict aliasing does not apply to char
           | arrays if I recall.                 allow a signed integer to
           | overflow
           | 
           | Is this still true? I thought that the reason for this is
           | because C left the implementation to define how signed
           | arithmetic worked, meaning you could not assume two's
           | complement, but the most recent C standard was supposed to
           | mandate two's complement.                 pass a NULL pointer
           | to memcpy, even if the length is zero
           | 
           | There is a reason for this. memcpy is allowed to start
           | reading early as a performance optimization, before it does a
           | branch that checks if reading is only. I do wonder what
           | happens if you only want to copy 1 byte and that byte has
           | invalid memory right next to it. Presumably, this
           | optimization would read more than a byte.
           | read an unitialized object, even if I "don't care" what value
           | I get
           | 
           | You are probably doing something wrong if you do this. It is
           | not even good as an entropy source.                 read and
           | write a value from different threads without locking or
           | atomics, even if I know exactly what instructions those reads
           | and writes compile into and the ISA manual says it's 100%
           | fine to do that
           | 
           | Earlier C standards likely did not say anything about this
           | because they did not support multithreading, but outside of
           | possibly reading/writing to hardware registers, you do not
           | want to do this because of races. Even if you think you know
           | better, you almost certainly do not.
        
             | lifthrasiir wrote:
             | > the most recent C standard was supposed to mandate two's
             | complement.
             | 
             | While that's true, overflows are not automatically wrapping
             | because they instead may trap for several reasons. (C++
             | _does_ require wrapping now in comparison. [1])
             | 
             | [1] https://www.open-
             | std.org/jtc1/sc22/wg14/www/docs/n2412.pdf
             | 
             | > memcpy is allowed to start reading early as a performance
             | optimization, [...]
             | 
             | Most modern memcpy implementations would branch on the
             | length anyway, because word-based copying is generally
             | faster than byte-based copying whenever possible. Also many
             | would try SIMD when the copy size exceeds some threshold
             | for the same reason.
             | 
             | >> read an unitialized object, even if I "don't care" what
             | value I get
             | 
             | > You are probably doing something wrong if you do this.
             | 
             | The GP meant the case like this. Consider `struct foo {
             | bool avail; int value; } foos[100];` where `value` would be
             | only set when `avail` is true. If we are summing all
             | available `value`s, we may want to avoid a branch
             | misprediction by something like `accum += foos[i].avail *
             | foos[i].value;` for each `foos[i]`, since the actual
             | `value` shouldn't matter when `avail` is false. But the
             | current specification prohibits this construction because
             | it considers that each read from `foos[i].value` may be
             | different from each other (!). In reality, this kind of
             | issues is so widespread that LLVM has a special "poison"
             | value which gets resolved to some fixed value after the
             | first use.
        
               | ryao wrote:
               | Thanks for the explanations.
               | 
               | As for the last one, I would probably bzero() that
               | structure, as it is faster than setting just 1 field to
               | zero in a loop, which presumably is what you would do
               | until you have some need to "allocate" a value. That
               | would avoid the problem entirely.
               | 
               | I know bzero() was removed from POSIX, but "bzero()" is
               | nicer to write than "memset() it to zero".
        
             | quietbritishjim wrote:
             | > > cast a `struct Foo*` into a `struct Bar*` and access
             | the Foo through it (in practice we teach this as the
             | "strict aliasing" rules, and that's how all(?) compilers
             | implement it, but that's not what SS6.5 paragraph 7 of the
             | standard says!)
             | 
             | > Use the union type. Abusing it for aliasing violates the
             | standard too, but GCC and Clang implement an extension that
             | permits this. Alternatively, just allocate a char array and
             | cast it as you please. Strict aliasing does not apply to
             | char arrays if I recall.
             | 
             | I could be misreading, but you seem to be implying that you
             | can trick the aliasing rules by casting Foo* to char* and
             | then cast the char* to Bar*, but that still violates the
             | rule. Even a union isn't allowed as a way of aliasing, but
             | as you say it's often allowed in practice and is heavily
             | used in the Linux kernel (and Linus has made his opinion on
             | this part of the language standard very clear).
             | 
             | In theory, the right way to access the bits of a Foo as a
             | Bar is to memcpy to a fresh Bar object, and then memcpy
             | back if you want to update the original variable. The
             | compiler is then allowed to optimise this into a direct
             | access of the bits.
        
               | ryao wrote:
               | You are misreading. I said to take a char * and then cast
               | it to whatever you want. You can cast it to struct A *.
               | Then you can cast the original char * to struct B *. The
               | compiler will be fine with this since the strict aliasing
               | rule excludes char *.
               | 
               | If you insist on doing what you described, just skip char
               | * and mark the pointer with __attribute__((may_alias))
               | and then it will be okay. That is a compiler extension
               | that lets you turn off strict aliasing rules.
        
               | quietbritishjim wrote:
               | Ah, I see. Like this:                   char
               | x[sizeof(struct Foo)];         struct Foo* f = (struct
               | Foo*)&x;         struct Bar* b = (struct Bar*)&x;
        
             | RustyRussell wrote:
             | > ryao 7 hours ago | parent | context | flag | on:
             | Hacktical C: practical hacker's guide to the C pro...
             | cast a `struct Foo*` into a `struct Bar*` and access the
             | Foo through it (in practice we teach this as the "strict
             | aliasing" rules, and that's how all(?) compilers implement
             | it, but that's not what SS6.5 paragraph 7 of the standard
             | says!)
             | 
             | Use the union type. Abusing it for aliasing violates the
             | standard too, but GCC and Clang implement an extension that
             | permits this. Alternatively, just allocate a char array and
             | cast it as you please. Strict aliasing does not apply to
             | char arrays if I recall.                 allow a signed
             | integer to overflow
             | 
             | Is this still true? I thought that the reason for this is
             | because C left the implementation to define how signed
             | arithmetic worked, meaning you could not assume two's
             | complement, but the most recent C standard was supposed to
             | mandate two's complement.
             | 
             | >> pass a NULL pointer to memcpy, even if the length is
             | zero
             | 
             | > There is a reason for this. memcpy is allowed to start
             | reading early as a performance optimization, before it does
             | a branch that checks if reading is only.
             | 
             | Where did you get this idea from? It's not possible, since
             | you can hand an address at the end of an array, and length
             | 0. The array ends at the end of a page.
             | 
             | You can't read extra bytes in this case!
        
               | ryao wrote:
               | Handing memcpy() the address at the end of an array and
               | length 0 is undefined behavior. It is often said that the
               | reason for this is to allow memcpy() to read before it
               | branches to make it fast.
               | 
               | This lead me to think of the case where you hand it the
               | address right before the end of a byte array where the
               | byte after the last byte is an unmapped page and tell it
               | to copy 1 byte. I suspect systems that have such an
               | optimization would read beyond 1 byte into invalid
               | memory. This is my criticism of the idea of having
               | memcpy(NULL, NULL, 0) be undefined to make that speed
               | trick legal. I am suggesting that an undefined number of
               | low values to copy must also be undefined, yet they are
               | not under the standard.
        
       | throwaway7894 wrote:
       | As someone who has a file with similar hacks, I will say this: I
       | am not a C++ fan, but if you find yourself writing C code where
       | you simulate methods via structs with function pointers often,
       | just use C++ as a basic "C with classes" at that point. You want
       | methods anyway, you have to go through a pointer dereference to
       | call the function, it's just not worth the code weirdness. If you
       | have the grit to use structs with function pointers everywhere,
       | you have the grit to stick to the simpler subset of C++.
        
         | uecker wrote:
         | Why? I do not find the syntactic sugar C++ adds very helpful
         | and it misses other C features.
        
           | ryao wrote:
           | Perhaps he wants to jump through hoops to avoid function
           | pointers, even when doing that in C++ for optional functions
           | like is done in the VFS requires gymnastics:
           | 
           | https://godbolt.org/z/4GWdvsz6z
           | 
           | That is the closest I can get it to implementing an optional
           | function via a C++ class member function instead of a
           | function pointer. It is not only insane, but also masochistic
           | in comparison to how it would be done via function pointers:
           | 
           | https://godbolt.org/z/qG3v5zcYc
        
         | unclad5968 wrote:
         | I'm torn. The step from C to any c++ is big. Now if you want
         | anybody to be able to use your code they need to be using c++
         | or you have to provide a C api anyway. On the other hand,
         | manually implementing vtables is annoying. Ive been sticking to
         | pure C and haven't been bothered enough to go back to any c++
         | yet (about 6 months on my current project). I mostly only miss
         | templated containers so far.
        
           | ryao wrote:
           | It is more annoying to want to implement an optional function
           | in a class and then have no simple way to check if that
           | optional function is implemented in the object without,
           | having to edit code that guards the call sites every time you
           | add a derived class that implements it, or having to
           | implement your own way of querying the object to know if it
           | is supported.
        
             | unclad5968 wrote:
             | I've never come across a situation where I wanted to do
             | this. What would be a use case for optional class
             | functions?
        
               | ryao wrote:
               | Well, it would not be a class in that case, but a use
               | case would be anything that is modular where you want to
               | support a new function, but do not require all of your
               | implementors to use it. The best example of this is the
               | VFS. Here is documentation for the Linux VFS:
               | 
               | https://www.kernel.org/doc/html/latest/filesystems/vfs.ht
               | ml
               | 
               | The vast majority of the function pointers in those
               | structures are optional (even if not explicitly stated).
               | To give a few common sense examples:                 * If
               | your filesystem does not support extended attributes, you
               | would not implement .listxattr and instead set it to
               | NULL.       * There are multiple ways of implementing
               | read and write in file_operations. You have the basic
               | read and write operations, and more efficient variants.
               | You don't need to implement the more efficient variants
               | if you don't want to implement them.       * The .bmap
               | call is used to find out how the filesystem stores a file
               | on a block device, which used to be used by the syslinux
               | (and might still be). This obviously is incompatible with
               | NFS (or any multidisk filesystem like ZFS) so it
               | absolutely must be optional.
               | 
               | Then there are other options, like not supporting mmap,
               | or not supporting creation/removal of subdirectories.
               | That sounds absurd, but some FUSE filesystems,
               | particularly those exporting a program's statistics,
               | don't bother with either of those since they are not
               | needed. I do not believe Linux sysfs allows users to make
               | directories either.
               | 
               | I could continue, but this gives a few examples of why
               | you might want to have optional functionality in a class-
               | like interface.
               | 
               | By the way, I mentioned setting things you do not
               | implement to NULL. This is done simply by not specifying
               | them when using the structure initializer syntax. The
               | compiler will zero unspecified members.
        
               | pjmlp wrote:
               | Nothing prevents implement something like that in C++, it
               | is still C++ code.
               | 
               | If you want to make something fancy, templates, _if
               | constexpr requires_ func-to-call, call func.
        
               | ryao wrote:
               | The point of the throwaway account's comment was to say
               | that you should use C++ class member functions instead of
               | C function pointers in structures, but that is impossible
               | to do in the general case in a sane way, since there is
               | no way to leave a member function unimplemented and then
               | check its status at runtime.
               | 
               | You need to use hacks to shoehorn C++ class member
               | functions into this. In particular, you need stub
               | functions. Then either, call them and have them either
               | return a special error code or throw an exception, or use
               | a custom query function that is implemented by derived
               | classes that lets you find out if a function is a stub or
               | not to allow you to skip calling it. Another idea would
               | be to use thread local storage with setjmp()/longjmp(),
               | which is probably the sanest way of doing this insane
               | idea:
               | 
               | https://godbolt.org/z/4GWdvsz6z
               | 
               | And the C way for comparison:
               | 
               | https://godbolt.org/z/qG3v5zcYc
               | 
               | The idea that the simplest way of approximating what you
               | can do with function pointers in C structures via C++
               | class member functions is to use TLS and setjmp/longjmp
               | shows what a bad idea it is to use class member functions
               | instead of function pointers for optional functions in
               | the first place.
        
               | pjmlp wrote:
               | C++ was designed as "Typescript for C" for its time,
               | because sometimes that is exactly the kind of code one
               | needs to write, even if we discourage many of the
               | classical patterns when better alternatives exist.
               | 
               | The same C example compiled in C++23 mode,
               | https://godbolt.org/z/MWa7qqrK7
               | 
               | As for possible alternatives, here is a basic one without
               | taking into consideration virtual mechanics, only to show
               | the principles.                   #include <concepts>
               | template <class T>         concept has_mmap = requires (T
               | obj)         {             { obj.mmap() } ->
               | std::convertible_to<int>;              };
               | class VFS {         public:             VFS() = default;
               | virtual ~VFS() = default;         };              class
               | ExampleFS : public VFS {             // mmap not
               | available         };              class ExampleWithMMAP :
               | public VFS {         public:                  int mmap()
               | {                 return 0;             }          };
               | int main() {             ExampleFS fs;
               | ExampleWithMMAP fsWithMMAP;                  /*
               | <source>: In function 'int main()':
               | <source>:33:19: error: 'class ExampleFS' has no member
               | named 'mmap'              40 |         return fs.mmap();
               | |                 */             if constexpr
               | (has_mmap<ExampleFS>) {                 return fs.mmap();
               | }                  // ExampleWithMMAP has mmap(), just
               | call it without issues             if constexpr
               | (has_mmap<ExampleWithMMAP>) {                 return
               | fsWithMMAP.mmap();             }                  // want
               | to use the variable name instead of the type?
               | if constexpr (has_mmap<decltype(fsWithMMAP)>) {
               | return fsWithMMAP.mmap();             }         }
               | 
               | -- https://godbolt.org/z/cjcbrzT3z
               | 
               | Naturally it is possible to be a bit even more creative,
               | and moreso with C++26 reflection.
        
         | codr7 wrote:
         | Nope, not from my experience.
         | 
         | Because in C++ the features are just there right around the
         | corner, they will seep into the code base.
         | 
         | And I don't want even classes, there's too much junk in there
         | that I don't need.
        
           | ryao wrote:
           | Can you think of anything I missed:
           | 
           | https://news.ycombinator.com/item?id=43701516
        
             | codr7 wrote:
             | offsetof/baseof springs to mind, for intrusive stuff.
             | 
             | You can sort of emulate it using pointers to member but it
             | quickly loses its appeal.
        
         | ryao wrote:
         | This is very bad advice for a few reasons:
         | 
         | 1. It is not possible to add optional member functions (which
         | would be pure virtual functions) to a C++ class base class and
         | then check at runtime if they are unimplemented in the object
         | (at least not without implementing some way to query the
         | object, which is slow). If you say to handle this by having
         | typeid checks at runtime, look at the VFS and then notice that
         | you cannot implement this typeid check in advance, since you
         | cannot add a typeid check for a derived class that did not even
         | exist when you compiled your code. Thus, you still need to use
         | structs of function pointers in C++. Maybe you can use C++
         | classes for some cases where structs of function pointers are
         | used, but you would giving up the ability to implement optional
         | functions in a sane way.
         | 
         | 2. It ignores all of the things in C that are absent from C++.
         | In particular, C++ refuses to support C's variably modified
         | types and variable length arrays, which are useful language
         | features.
         | 
         | 3. It ignores all of the things in C++ that you likely do not
         | want, such as exceptions and RTTI. The requirement to typecast
         | whenever you assign a void pointer to any other pointer is also
         | ridiculous.
        
           | humanrebar wrote:
           | You can do everything you describe in C++. Even the language
           | features are available (or diableable) as compilation flags.
        
             | ryao wrote:
             | Not everything. See the features that C++ refuses to
             | support. Beyond that, there is no compiler flag to stop
             | requiring explicit casts of void pointers before assigning
             | them.
             | 
             | Furthermore, less is more. You get faster build times with
             | C because it does not support all of the features C++ has.
             | Just because you can do it in C++ does not mean you should.
             | 
             | I used C++ for one of my first projects for a startup in
             | health care and I really wish I had not. C++ made
             | development a hellish experience as I spent most of it on
             | fighting the compiler to be able to use every C++ language
             | feature I could imagine and not enough on actual issues. It
             | easily doubled development time since I spent most of it on
             | things that only existed because C++ had overcomplicated
             | everything (e.g. reference versus pointer, public versus
             | private, shoehorning OOP into places it did not belong,
             | operator overloading, templates, etcetera). This was during
             | my initial attempt at graduate studies and after ruining a
             | semester because of it (this had been intended to be a part
             | time thing), I parted ways with the company. The C++ daemon
             | went on to be the heart of the company, despite the
             | lingering bugs.
             | 
             | I ended up fixing the remaining issues as a consultant
             | years later, but eventually, I realized that everything
             | would have been better had I not used C++ in the first
             | place. There are times when I fantasize about rewriting it
             | in C. One of these days, I might actually do that for the
             | company for free if only to put an end to a mistake of my
             | youth. Unfortunately, now that I have fixed the daemon, it
             | has the advantage of being a mature, reliable codebase, so
             | it is difficult to justify a rewrite.
             | 
             | That said, despite my complaints about the effect C++ had
             | on development, I did a number of things right when
             | architecting that daemon. The lingering bugs turned out to
             | be trivial and it has scaled with the company for 13 years
             | with no end in sight. When it finally is replaced, the
             | reason will likely be that it did not support HA, rather
             | than some inability to scale. My younger self had refrained
             | from pursuing HA since it seemed infeasible to do within
             | the spare time I had during a single semester.
        
               | humanrebar wrote:
               | I was making a narrow comment that you can turn on VLAs,
               | off exceptions, etc., with C++ flags on publicly
               | available compilers regardless of what the C++ language
               | specification and the C++ experts say.
               | 
               | The difference in build times between identical code
               | compiled with the C language or C++ language is probably
               | negligible. Or at least dwarfed by using a better build
               | system, a faster build machine, and/or some sort of build
               | caching technology.
               | 
               | > Beyond that, there is no compiler flag to stop
               | requiring explicit casts of void pointers before
               | assigning them.
               | 
               | I believe that's true. And there are probably a few other
               | ergonomic differences beyond this one. Has anyone
               | proposed that as a feature flag for Clang and/or GCC?
               | Open source C and C++ compiler devs don't have a lot of
               | free time such that they peruse social media looking for
               | things to do.
               | 
               | No comment on your anecdote other than to say I have
               | heard versions of that story before but with other
               | programs and in basically every other language. Including
               | C.
               | 
               | I'm not saying you're wrong. I think a lot of your points
               | are valid points. About taste. Which is fair and fine,
               | but it's also true that the difference between C and
               | C-style C++ are pretty minor, especially if someone knows
               | how to enforce coding standards with clang-query wired up
               | to CI or something like that.
        
               | ryao wrote:
               | You cannot use variably modified types in C++. The
               | following will not compile no matter what flags you give
               | the C++ compiler as far as I know:
               | 
               | https://godbolt.org/z/z9M55s3q6
               | 
               | What is particularly nice about that code is that a C
               | compiler will realize that it has a buffer overflow.
               | Adapting it for C++ will cause the C++ compiler to not
               | notice the buffer overflow.
               | 
               | If you are going to be writing C, there is no reason to
               | compile it as C++. Using C++ limits your ability to use
               | newer features of C and exposes you to headaches like the
               | ABI compatibility break of GCC 5.0 that was done for
               | C++11. C has never had an ABI compatibility break caused
               | by a revision of the language. Your suggestion that
               | people should use C++ even when it is not what anyone
               | wants befuddles me.
               | 
               | If you said this in a room with Linus Torvalds, I wonder
               | if he would start cursing again.
        
           | pjmlp wrote:
           | 1. There are other approaches to this with templates and
           | concepts, and as added bonus, stronger type checking.
           | 
           | Thankfully regarding 2., Google went the extra mile to pay
           | for removing them from the Linux kernel, and they were made
           | optional C11 onwards exactly because they are an attack
           | vector.
           | 
           | 3. It is called stronger type safety, ridiculous is the C
           | community still approaching computers as if writing K&R C.
        
         | jstimpfle wrote:
         | There's a big issue with C++ classes -- or rather _methods_.
         | You need to list the entire class definition to expose public
         | methods. That includes private members and private methods.
         | This breaks encapsulation, exposing implementation details and
         | their dependencies in the public header. This in turn leads to
         | looong compile times and needless recompilations.
        
           | pjmlp wrote:
           | Hence modules, and yes I know there are still some issues to
           | sort out.
        
       | steinuil wrote:
       | Some points about the introduction, but otherwise this seems like
       | an interesting collection of (slightly deranged?) patterns in C.
       | 
       |  _> The truth is that any reasonably complicated software system
       | created by humans will have bugs, regardless of what technology
       | was used to create it._
       | 
       | "Drivers wearing seatbelts still die in car accidents and in some
       | cases seatbelts prevent drivers from getting out of the wreckage
       | so we're better off without them." This is cope.
       | 
       |  _> Using a stricter language helps with reducing some classes of
       | bugs, at the cost of reduced flexibility in expressing a solution
       | and increased effort creating the software._
       | 
       | ...and a much smaller effort debugging the software. A logic
       | error is much easier to reason about than memory corruption or
       | race condition on shared memory. The time you spend designing
       | your system and handling the errors upfront pays dividends later
       | when you get the inevitable errors.
       | 
       | I'm not saying that all software should be rewritten in memory-
       | safe languages, but I'd rather those who choose to use the only
       | language where this kind of errors regularly happens be honest
       | about it.
        
         | codr7 wrote:
         | Debugging from specific classes of bugs, yes.
         | 
         | I'm not trying to hide anything, just help shift the balance
         | back to common sense.
        
       | _false wrote:
       | Honest q: after skimming through the book it's unclear how it's
       | targeted towards hackers (c.f. academics)?
        
         | codr7 wrote:
         | Defined as practical, curious problem solvers, I'm aware the
         | word has other interpretations.
        
       | talles wrote:
       | Any quick way to make a PDF out of this?
        
         | theiasson wrote:
         | You'll need pandoc and xelatex                 $ pandoc --pdf-
         | engine=xelatex --toc README.md {macro,fix,list,task,malloc1,vec
         | tor,error,set,malloc2,dynamic,stream1,slog}/README.md -o
         | book.pdf
        
       | teo_zero wrote:
       | Beware that the second section about fixed-point arithmetic is
       | actually about floating point with exponent in base 10.
       | 
       | Besides, the operations are all wrong and only work for trivial
       | values of the exponents, like 0, 1 and 2.
        
       | Calwestjobs wrote:
       | "These days; many programmers will recommend choosing a stricter
       | language, regardless of the problem being solved. Most of those
       | programmers wouldn't trust themselves with the kind of freedom C
       | offers, many haven't even bothered to learn the language
       | properly."
       | 
       | Same thing people said about other people not compiling by hand
       | lol.
        
         | hresvelgr wrote:
         | The people I've met who have made similar remarks about C have
         | authored some of the most heinous pointer tomfoolery I've ever
         | seen in my life. It's a remark indicative of a hubris that has
         | remained unchecked or worse, in denial.
        
           | codr7 wrote:
           | So you couldn't understand their code, and therefore everyone
           | who says the same things is incompetent and in denial. That's
           | one way to explain the experience you had.
        
       | siev wrote:
       | I like the sentiment, I love C. But this book seems riddled with
       | errors and baffling decisions.
       | 
       | First of all, the fixed points are LITERALLY NOT FIXED POINTS.
       | They're decimal floats. Fixed points are _just_ integers that re-
       | scale when multiplied or divided. There is no exponent field, no
       | nothing. The author seems to have confused the notion  "fixed
       | points allow for precise calculations of monetary values" to mean
       | that they're decimal. They're not. That section of the book
       | contradicts itself constantly and also the code is wrong.
       | 
       | Also an ordered vector is used to implement a map/set. Because:
       | 
       | > Most people would likely instinctively reach for hash tables,
       | and typically spend the next few months researching optimal hash
       | algorithms and table designs.
       | 
       | > A binary searched vector is as simple as it gets and performs
       | pretty well while being more predictable.
       | 
       | A basic hash table or hash set[1] is both simpler and faster than
       | this solution. And I don't see what's stopping someone from
       | spending the next few months researching optimal dynamic array
       | growth and searching algorithms instead. This line of reasoning
       | just doesn't make any sense.
       | 
       | And "Once nice advantage is that since they don't need any
       | infrastructure, they're comparably cheap to create." What? It
       | needs a dynamic array!
       | 
       | [1] https://github.com/skeeto/scratch/tree/master/set32
        
         | exceptione wrote:
         | Thanks for sparing us bad information.
         | 
         | What would you recommend as a source instead?
        
         | codr7 wrote:
         | Thanks for sharing your opinions, I don't agree.
        
           | chipsa wrote:
           | The fact you don't agree is immaterial to the fact that your
           | "fixed point" code doesn't implement fixed point arithmetic.
           | Fixed point arithmetic has a fixed scale factor for the type,
           | not a value dependent scale factor.
        
             | codr7 wrote:
             | Good luck :)
        
         | tialaramex wrote:
         | Oh wow, yeah, that's not a fixed point type, it's a bad decimal
         | type, code which uses this type is more likely to be faulty. If
         | you can afford 64-bit integers, just work in pennies (cents,
         | whatever) everywhere and don't sweat it.
         | 
         | The growable array type ("vector" following C++ parlance) lacks
         | the bifurcated reservation API meaning it has the same problem
         | as Bjarne's std::vector - but it's 2025 people, just because
         | C++ made this mistake last century doesn't mean you need to
         | copy them.
         | 
         | And finally yes you want a really good general purpose hash
         | table, this is one of the places where generics shine most
         | brightly, don't "spend the next few months researching" pick a
         | language which does a decent job of this out of the box, but
         | since you're in C, your utility library should likewise provide
         | a decent hash table out of the box.
         | 
         | Swiss Tables are literally just a single growable allocation,
         | this idea that you've somehow made your thing cheaper than a
         | hash table by using the growable array type underneath it means
         | you're _at best_ four decades behind the state of the art,
         | which is a bad sign.
         | 
         | This is a Young Discipline. David Musser's "Introspective
         | sorting" paper was written _after_ I learned sorting at
         | University. Literally the class where they taught me about
         | sorting was held before that paper was even written, let alone
         | widely disseminated. The whole terminology of  "Lock free"
         | versus "Wait free" again, that's newer than my undergraduate
         | final project on distributed systems. Because this is a Young
         | Discipline it's crucial to go check, hey, the stuff I learned
         | in class years ago, is that actually still correct, and does my
         | understanding match reality - or am I about to recite a known
         | falsehood because I forgot how time works and/or I didn't pay
         | attention in class?
        
           | badmintonbaseba wrote:
           | > The growable array type ("vector" following C++ parlance)
           | lacks the bifurcated reservation API meaning it has the same
           | problem as Bjarne's std::vector - but it's 2025 people, just
           | because C++ made this mistake last century doesn't mean you
           | need to copy them.
           | 
           | What's "the bifurcated reservation API"?
        
             | tialaramex wrote:
             | So, these types [the growable array, C++ and this library
             | call them "vector", Rust calls them Vec, a lot of the GC
             | languages call this ArrayList or even plain List] have the
             | amortized constant time append operation which they achieve
             | via a growth factor, often doubling. However, as the
             | programmer we might well know useful things about how big
             | our growable array will be, either immediately or in its
             | eventual future, this won't influence correctness but
             | responding appropriately can have a large impact on
             | performance. To use this knowledge, the type should provide
             | a reservation API - a way to _tell_ it what you know.
             | 
             | There are several ways you could arrange this, but some of
             | them can't optimize certain scenarios practically. I call
             | Rust's choice here a "bifurcated" API because it has two
             | functions named `reserve` and `reserve_exact` where many
             | provide only one (typically named `reserve` but analogous
             | to `reserve_exact`)
             | 
             | Because we know the circumstance, we can use the amortized
             | growth strategy where appropriate in `reserve` even though
             | we don't use it for `reserve_exact`.
             | 
             | Suppose I'm receiving bundles of Doodads, to form a
             | Shipment, I can see how many Doodads are in the bundle I
             | received, but I only know it's the last bundle of the
             | shipment or it's not, I don't have advance notice of the
             | full size of the Shipment.
             | 
             | If I receive bundles of 10, 15, 11, 20, 9, 14 and finally
             | 13 Doodads. Total shipment size was 92 Doodads.
             | 
             | With just naive doubling, we grow to 1, 2, 4, 8, 16, 32, 64
             | and finally 128 Doodads space, we perform 127 copies + 92
             | new writes = 219 Doodad writes, 8 heap allocations. That's
             | our base case, it's what Bjarne Stroustrup recommends and
             | what you'd get in many languages out of the box or if
             | you've never used a reservation API.
             | 
             | If we abuse Vec::reserve_exact - as might be tempting if
             | it's the only API - we grow 10, 25, 36, 56, 65, 79, 92, we
             | perform 271 copies + 92 new writes = 363 Doodad writes, 7
             | heap allocations, one fewer allocation but many more
             | copies, probably slightly worse. Ouch.
             | 
             | If we use the bifurcated API we grow 10, 25, 50, 100, we
             | perform 85 copies + 92 new writes = 177 Doodad writes, 4
             | allocations, we're doing markedly better.
        
       ___________________________________________________________________
       (page generated 2025-04-16 17:02 UTC)