Hacker Newsnew | past | comments | ask | show | jobs | submit | uecker's commentslogin

I was programming in C++ before switching to C and I would say that C++ adds a huge amount of mental load compared to C. I think one can understand how much of a relieve it is to not worry about everything C++ does only after not using C++ for a quite a while.

Any time I go back to C from C++, it's only comfortable if I've got a C utility library that replaces most of std::

And that utility library (there are dozens of them) is just as subject to debate and issues as libstdc++

I am not going to implement my own C versions of 90% of the stuff std:: provides, sorry.


There is a difference between "needing a library" and "implement my own". One can just pick one. And even when implementing things yourself, one has to do this just once.

GCC was implemented in C and there are plenty of other C compilers written in C. GCC has been converted to C++ at some point, but large parts are still essentially C and I do not think the change to C++ was actually helpful (but others may disagree). In any case, the idea that one needs C++ to have C compilers is certainly simply wrong.

My original C compiler was implemented in C and then bootstrapped. Zortech C++ was implemented in Zortech C, and then bootstrapped.

D was implemented in "C with Classes" and then translated to D and bootstrapped. There isn't a line of C code in it.

Over time, we gradually replace the C-isms and C++isms and anachronisms in it to native D-isms.


Clang/LLVM with its more permissive licence sees _much_ wider use and is written purely in C++. PlayStation, automotive, platforms and runtimes like Chromium/V8, Android, etc. are all built with Clang.

Clang usage is a fraction of GCC usage.

Regards, an embedded dev.


A decade ago surely, in 2026 I doubt it.

With Android, iDevices, PlayStation, Switch, and everyone that had proprietary compilers down using downstream forks from clang, due to the more appealing license.

Who is left still using GCC, other than existing projects lacking a clang backend for a snowflake embed CPU?

In any case, if it is a modern GCC release past 2012, it was compiled with C++ as well.


The license is indeed the big reason for many proprietary forks (not so much that llvm is written in C++). This is not a good thing though.

How is this relevant? (also wider use for C I would doubt)

Naturally GCC was originally written in C, given its age, and the original GNU coding standards document.

With time, the GCC developers acknowledged the benefits of using C++ over C, and migrated the code.

GCC requires a C++ compiler to bootstrap since around 2012, and GNU coding standards has been updated to several languages beyond C, time to go up with times.


I think the fallacy of this argument is obvious.

Not really, given that GCC no longer compiles with a pure C compiler.

Unless of course, you don't have anything to do, and feel like bootstraping GCC 16, using a compiler chain all the way back to 2012 thereabouts.


The question is whether GCC is a good example of the benefits of C++ for compilers. Considering the code looks to 95% like C code and uses data structures that we originally implemented in C, I don't see this argument.

Or use a buffer abstraction in C. This is not exactly rocket science. The "this is impossible to prevent in C" nonsense does far more harm than good.

Errors are easily found and corrected for a modest one-person project in C.

But we’re combining probability of error creation (which is effectively constant) and the limits of human cognition.

Some things are impossible at one scale, become possible at another, and become inevitable at yet another.


To be fair, C is a pain to use, so it is better to improve Rust. It is annoying when for example, you have to free several allocated structures when there is an error in the middle of a functon.

I personally like to use C and find Rust annoyingly complex. I think it may be an alternative to C++, but C++ is also too complex for my taste. I do not find it annoying to free several allocated structures when there is an error, but one could also automate this with a often used extension.

There is also the question whether trading memory safety against supply chain risks is really worth it.


What convinced me that this is wishful thinking was CVE-2023-53156. Yes, it used "unsafe" but the wraparound in release defeated the manual check, and when you aim for performance comparable to C, Rust tends to be full of unsafe blocks.

IMHO better C tooling would be a far better investment than rewriting in Rust.


Incremental/quantitative improvement isn't disproven by contradiction. Anecdote is not data. Look at a bigger dataset, e.g. https://github.com/rust-fuzz/trophy-case there's a ton of overflows that result in caught panics or OOMs, and not bypasses. It works to reduce defect rate and severity.

Note that C's tools for this like Valgrind, instrumented allocators and LLVM sanitizers work with Rust too. Investment in catching these in C generally helps unsafe Rust too, but Rust also has Miri, and a much better baseline for static analysis.


Yes, use a distro with good security posture such as Debian to reduce risk.

Are the packages in the repositories of arch also affected?

No? Then it's not a problem.

Every device in this household that isn't a smartphone runs on Arch.

All my servers run on Arch.

Never had a problem, because I don't blindly install stuff from the AUR.


I guess official arch packages are also ok nowadays, my point was more that one should avoid non-curated repositories of packages such as cargo.

This has not much to do with skill. Standardization does not work like this, and I told you this before.

Yeah, I keep missing the existing practice in secure C compilers that is supposed to land on a WG14 paper.

You can tell me the times you wish for, I will keep playing my trumpet until I see something in C that resembles to Modula-2 was offering in safety, nowadays brought back by Zig.


Your trumpet does not help and is just annoying.

The support for variably modified types is excellent, if you discount MSVC which is lacking support for modern C anyway (it seems to catch up a bit though).

Real-world usage certainly remains poor. Using pointers to VM types remains annoying, and I wish the committee would settle on a solution to the ordering of VM parameters. But, yeah, the VM types are solid in GCC and clang and should be used more.

I think the risks of a rewrite - especially when using AI - are far more problematic than memory safety. In the long run those C projects will be memory safe in the next five years using memory safe C implementations.

My perspective is that it is good to have a beta in a lot of directions.

No one really knows what the endgame of software security looks like.

So some people should try the port to rust angle, some should focus on hardening the C, some should explore more exotic options like formally provable languages etc


There is nothing wrong with trying different things. But the fundamental problem here is that projects and their communities are social projects and need to be to fulfill their purposes and to ensure long term maintenance. In a free software context, rewrites just like forks (1) are fundamentally an asocial (2) activity because they fragment the community (if successful) and then increase overall maintenance burden if not able to replace the original project completely (rarely the case). Disrespect the license choice of the original authors makes this worse.

1) There may be situation were are fork makes sense (e.g. because one project can not serve different use cases well): 2) Which is why usually a "higher goal" is used to justify this, e.g. authors pretend (or lie to themselves, or may be be stupid enough to actually believe this) that some improvement in memory safety is really that important.


You won't get anywhere pressing your case here. This group has already found you guilty, and no argument will change their minds.

You've been caricatured into a blind AI-follower rust-rewriter-just-because type, and that's the surface they'll continually attack (you're wasting time, hurting the community, v2-itis, bikeshedding, premature optimization, copyright violation, moustache-twirling-evil-intent-rug-pull-later, etc etc etc).

Just continue in your work. It's good, and we need people like you.


There was no caricature as a "AI follower" etc. I gave my arguments you didn't.

They can still break stuff memory safely.

The elegance of the fork() + exec() model is that every kind of configuration can be done after the fork using all the usual APIs. Every attempt to replace it with a combined call that I have seen so far seemed fundamentally poorer because it needs to add all configuration options as parameters to the call and then do this in away that you can extend it later and does not become a mess.

I have the entirely opposite opinion. IMO a big mistake of the UNIXy model is that so much state is preserved across the creation of a process. For example, there are APIs to have a specific thing be fd number 4 so you can run a program and have it find that thing at fd 4. This is weird.

Windows, for all its many, many faults, did not use fork+exec and instead mostly has options for how one creates a process. It wasn’t done elegantly, but it was the right decision.


Well, a lot of the power of the UNIX shell comes form this and I see this as a major advantage over Windows. So no, I do not think Windows got it right.

Any kind of replacement should aim for the same conceptual simplicity and power. Sadly, I fear that people driving development nowadays are more interested in building unbreakable walled gardens for advertisement or app stores, or trying to squeeze down the some small gain when used on the cloud. I am more interested in general computing on the user side.


Nothing about the UNIX shell is reliant on the fork model. Windows processes have stdio handles as well.

A lot of features of UNIX shells are build around pipe and dup and the fork + exec model. One can certainly implement in differently, but it is - like UNIX in general - very nice and elegant.

It's an elegant hack, but it's still a hack. Not what we should be doing in 2026.

I would prefer to continue to use elegant interfaces even beyond 2026.

I would prefer to use elegant interfaces that aren't massive hacks.

Define "hack".

Something that works but is a surprising and suboptimal way to do things.

I dunno, that's the best I can do for now. Maybe you can do better?


I don't think it is hack. I think it is a nice and clean API and the hate is largely irrational. I think one could improve usability for multi-threaded programs though.

Help me out here, please. Off the top of my head, the exec command is dependent on exec, except that a spawn + wait implementation would be a mostly okay substitute.

Pipes and redirections don’t need fork + exec. Neither do subshells.


If you use pipe() you get two ends in the same process, then you fork and child and parent can communicate. This is how a unix shell setups up pipes and it is rather elegant.

Doing the same thing on Windows, I create the pipe and get two ends in the same process. Then I'd call CreateProcess and indicate I want the pipe's handle (fd) inherited to the child, and I'd use a prearranged way to tell the child what the fd value is it should use.

Possibly the most common way to tell the child the value is by setting it as a CLI arg in CreateProcess.


Yes, and CreateProcess needs special facilities to make this possible while in the UNIX model you don't.

Which special facilities are you referring to? If it's the ability to selectively inherit handles (fds) to a new process, Linux's lack of this "special facility" is nothing to be proud of.

How do you selectively pass on fds without having a global impact on your process?


As explained above, you fork and then before exec you can use all the usual APIs to close/duplicate/... file descriptors. This has no global impact on the parent process because you are already in the child, but one still has access to all the context of the parent needed to whatever configuration you want to do, including things that will be invented in 20 years and are not even conceived today.

And as noted elsewhere, what you want is a new process with a specific fd: duplicating your current process state is a path-dependent evolution, rather than a logic step.

There have been plenty of comments here about effective workarounds, multi-process architectures to keep fork cheap, zygotes... these are very specifically working around the problem, while trying to avoid admitting it's a problem.


You are changing your argument. Let's first agree that my point that being able to use standard APIs (dup,close,pipe,...) is elegant and avoids having a lot of parameter for a process creation call.

I don't think it's an elegant design at all; I think it's a workable design given the constraint of requiring fork.

It's an achievable result with less specialised parts, but there's a reason we don't write all of our logic in NAND gates.

I'm still only inferring what you meant by CreateProcess needing special facilities; I assume you meant opt-in fd inheritance. Saving a parameter call while forcing all fds to be shared/duplicated seems penny-wise but pound-foolish.


Having fd 4 mean something specific is no weirder than having fds 0,1, and 2 mean something specific, which is probably never going to change. At some point you just gotta embrace the Unix.

Heh! The Unix didn't embrace the idea of file descriptor 3 meaning something specific. (-:

* https://jdebp.uk/FGA/bernstein-on-ttys/cttys.html

Interestingly, on MS/PC/DR-DOS file descriptor 3 was stdaux. and file descriptor 4 was stdprn.


Is it weirder, that you can pass an variable precisely into argument 4? You do need to pass information to a subprocess and there needs to be some agreement on what means what. Sure, maybe you could use names instead of fds, but that sounds needlessly complicated.

A way to pass a defined list of handles to a subprocess (or a friendly other process) makes sense. Having that mechanism be direct inheritance of those handles with the same numbering as the source is obnoxious.

That’s like saying you could use positions to specify function argument access (as in assembly) instead of variable names. File descriptors being numbers that are likely array indexes in a file handle seems like a leaky abstraction. Having a namespace that a parent process share with its children seems like a much cleaner design.

Well, Cygwin and Busybox have shown me that fork-heavy activities are about 100x slower on Windows than Linux.

The Windows approach may be correct, but it suffers in performance from the POSIX perspective.

I have heard that WSL1 iimproves this.


Linux has worked pretty hard to optimize fork(). This doesn’t mean that fork() is a good idea.

Windows does not historically depend on fork(), so there was no native fork(), so Cygwin kludged it up.


Actually, there is a native fork. There had to be, as POSIX personality support was a part of the Windows NT 3.1 design. What there wasn't was a Win32 form of fork. The Native API for Windows NT allowed it quite straightforwardly.

Iirc Cygwin used to use it but iirc they moved away from it because they said that it was pretty slow

Though actually iirc werfault uses NtCreateUserProcess() to clone processes when writing out crash dumps to this day


You're simply failing to grasp the value of the simplicity, compatibility, and portability of POSIX/*nix. Inventing yet another way to create a process would be complex and break things. It's a-la-carte to enable configuration after fork of the new CoW or non-CoW process but before exec (unless vfork or similar were used). This is the model.

If you want to greenfield re-engineer the world with all new system calls and a totally different execution model, feel free to go right ahead.


"The reasonable man adapts himself to POSIX: the unreasonable one persists in trying to adapt the POSIX to himself. Therefore all progress depends on the unreasonable man."

― George Bernard Shaw, probably.


I kinda disagree, though I do see the usefulness here. While fork/exec can be useful in some cases, it'd be honestly pretty neat if the APIs took a pidfd argument (maybe with 0 meaning current process). Only program is setuid/setgid binaries I suppose but maybe this case is better handled by special casing `exec`.

For example

   pidfd_t ps = spawn(); // creates a process stopped (kernel does this anyway by default)
   setuid(ps, 33);
   capset(ps, ...);
   socket(ps, ...);
   mmap(ps, ...);
   process_vm_writev(ps, ...);
   exec(ps, ...);
   signal(ps, SIGCONT);
   // error handling elided
I guess this is a little bit me being a bit of critical of the usual syscall APIs for not thinking about "what if I want to do this to another process I have access to" but...

It also makes things like thread safety even reasonably doable with fork. I do agree though that stuff like CreateProcess which take in a gazillion parameters don't really make for the greatest of userspace APIs


Maybe, a few people proposed this. It is a lot better than a single spawn call.

But how often would one actually need this? And what are the semantics? Refer arguments (e.g. file descriptors) to the current process or the other one? How are cross-permissions handled? It seems a lot of complexity...

Someones proposed a ptrace_syscall which could achieve the same thing.


> But how often would one actually need this?

Well, the idea is that it'd probably be close to the default API for spawning processes (and could even be the bedrock for posix_spawn and friends in libc (and potentially even "simple" fork cases[1])). fork/clone would be the special case

In most cases, most programs don't need special setup. Something like `ptrace_syscall` would also work for this and would be probably the way to do it with the backwards compat limitations of nowadays

ptrace-ability seems to be generally how permissions for this sort of thing are handled in general (see also procfs, process_vm_writev, ptrace, etc). The complication is a little bit around setuid programs but either you could special case execve to imply SIGCONT for setuid or have execve also imply a SIGCONT as well

[1]: Probably would be rare for a compiler to optimize it though


Calling that elegant is a path dependence of the history of fork+exec.

In an alternative world where fork+exec never existed, a lot of those "usual APIs" would probably have had an explicit pid argument to them that let you modify process configuration from a different process. (This is how Fuschia works, e.g.). There's a lot of benefit to this world: the most obvious is that you don't have to magic up some IPC system just to report configuration errors, but there's actually a good amount of utility in being able to have a manager process that is tweaking attributes of its children (e.g., debuggers would love it).


Or you could call ptrace_syscall (that doesn't currently exist) on your child processes that you are tracing because you'd always be tracing them by default, or get an io_uring for the child process, or...

A ptrace_syscall would be interesting and would seem to be a full replacement for having the pid argument everywhere.

But frankly, I am not really seeing the value.


The value is not needing to change every other syscall and not needing to write new ones with a pid argument (besides which, what when you want to change it to a pidfd argument? then you add pidfd_syscall instead of duplicating every syscall again)

I meant the value of running syscalls in another process from the parent process in contrast to (v)forking and running them in the child directly.

The value is starting the child with a clean slate instead of a copy of its parent.

Weren't there enough parallel paths of development in this world?

> The elegance of the fork() + exec() model is that every kind of configuration can be done after the fork using all the usual APIs.

Unfortunately, the opposite is true, when the parent process is multi-threaded. In the child process, only one thread exists (the thread returning from fork()), but the memory is an exact copy of the parent's. As a result, the child may inherit locks (resident in memory) that are in acquired state, but have no owner threads -- the threads that are responsible for eventually releasing those locks in the child's copy of the process memory do not exist in the child. If the single thread in the child process (returning from fork()) attempts to take such a lock (before exec), it deadlocks. This is why POSIX says that only async-signal-safe functions may be called in a child process, between fork and exec. And then, for example, "malloc" is not such a function (at least per POSIX), so the fork-to-exec environment in the child process is an extremely uncomfortable one. You've got to preallocate everything in the parent, can't report errors to stderr, etc.

https://pubs.opengroup.org/onlinepubs/9799919799/functions/f...

https://pubs.opengroup.org/onlinepubs/9799919799/functions/V...

The fork(2) Linux manual page spells out the sam restriction.

https://man7.org/linux/man-pages/man2/fork.2.html

https://man7.org/linux/man-pages/man7/signal-safety.7.html

"pthread_atfork" exists, but is effectively unusable.

https://pubs.opengroup.org/onlinepubs/9799919799/functions/p...


Yes, threads are a complication, but this still not "the opposite".

Yeah. The right way to eliminate fork() is to make the usual APIs that modify process state take an explicit process handle, so the same APIs can be used to set up an empty process. They can also be composed in other ways, eg for IPC or debugging.

I agree with it, although still the fork is expensive like they mention. There is clone with some flags, although that does not really solve it.

I think one problem is that it is already how it is; making an entirely new operating system (that is not Linux, not GNU, and not POSIX) would solve it, but that is not the case here, so it would need to be done as it is.

One possibility would be a new function that creates a new empty child process, but the parent process specifies what system calls the child process executes, and can stop if specifying that exec or exit is (successfully) called by the child process, or if the parent process gives it the program memory to execute directly instead of using a file (since that use is also useful). The new function can still have some of the clone flags available. (I don't actually know how much better it would work.)

There are other possibilities as well.

The existing methods can also remain available for when they are helpful, but functions such as popen might be changed to use the new method.


It should be spawn, configure, exec. Configure can be done if the process starts with a ptrace attachment and no threads, so you can force it to do syscalls. Linux doesn't even have a concept of "process with no threads", so it'd probably have to have a dummy thread.

I agree. I think the current way is very nice to use (in c). I think the best way would be to have something similar to vfork() but not bound by posix rules. Then make the normal posix apis (close, setuid, etc.) act like the Rust “builder” pattern. Possibly giving them a prefix for explicitness. That way the “fill out a giant structure” people could have their wish and the people that just want a faster posix experience don’t have to learn an entirely new concept and api surface. It would be future extensible that way, too (just add more prefixed calls to the builder).

> something similar to vfork() but not bound by posix rules

POSIX says nothing much about vfork() anymore. It was a mistake removing it. Zealots failed to understand that vfork() >> fork(). https://news.ycombinator.com/item?id=30502392


The flip side of this is that you have to be aware of the entire state of the process, including everything done in libraries, in order to correctly start a new process.

Quick, what's the highest numbered open file descriptor in the your program?

This gets even worse if you have multiple threads running. Without looking it up, what is the state of all the various synchronization primitives in a forked process?


That's mostly papering over design mistake that most syscalls doesn't accept target pid. Otherwise you could just create suspended process, configure it with syscalls that explicitly take target pid, and start it.

Maybe, I am not saying fork() + exec() model couldn't be improved, but most people saying it is "terrible" and it needs to die seem to go on to propose something substantially worse.

Or have a syscall that runs any other syscall in a different process.

The new system calls described in the article have an extensible declarative command interface built into them to do things like close or duplicate file descriptors. Not opposed to it but it definitely stood out to me.

Whatever elegance fork(2) has (or doesn't) have, clone(2) has more.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: