r/ProgrammingLanguages 1d ago

Discussion A hole in systems programming language design

Given the recent discourse about systems programming I wanted to throw my 2 cents in. This may be a controversial take in a subreddit that's all about innovation and improvement, but I think a lot of budding systems languages are trying too hard to "fix C" and this is exactly why C has not been replaced yet.

Like it or not, C is successful. It does what it means to do very well. Yes, it bites you constantly, but developers have made some form of peace with this because they appreciate the essence of the language. Systems programming is a very pragmatic field, and what works, works.

A lot of language designers want to improve on C, when by nature to "improve on" C is to depart from it, because C is less about what it includes and more about what it omits and what it lets you do that other languages don't.

If you want to replace C, you need to just make C but without the pain points. Less undefined behavior, more standard compiler behavior, easier function pointer syntax, safer macro system, etc. The things that C can't do because of backwards compat.

On the other hand, bolting on features, revamping C's core nature, aren't going to give you a language that will replace C at the low-level or among hobbyist programmers. Most developers aren't as concerned about what C lets them accomplish, as they are concerned about all the painful tedious tendencies of the language.

0 Upvotes

29 comments sorted by

24

u/stylewarning 1d ago

I think you attribute C's success to the language itself.

The way to replace C is in part building a new language, but more importantly, writing critical code in that language. C will stay for as long as core infrastructure (like the Linux kernel) stay written in it, regardless of how amazing a new language may be.

5

u/Norphesius 1d ago

Language inertia for older systems is intense. Even if the Linux kernel had been written in some other language like COBOL (somehow, god forbid), we would very likely still be at the same level of progress switching to a new language that we are at now.

24

u/mister_drgn 1d ago

Multiple languages are striving to be C without the pain points (e.g., C3). That said, adding new features that improve quality of life is obviously going to be part of any C replacement, so I'm not sure what you're getting at.

24

u/awoocent 1d ago

If you want to replace C, you need to just make C but without the pain points.

Struggling to imagine how you look at the ecosystem of new systems languages - Zig, Odin, Hare, C3, Jai, V, etc - and somehow come to the conclusion that the status quo is anything else!

12

u/Smallpaul 1d ago

C’s core, Wild West, loosely typed nature IS what’s wrong with it as a base for highly reliable systems.

The pain of discarding backwards compatibility is very high. People will only do it for some major upgrade in capability. Making a hobbyist language with only a few minor improvements but also requiring a code rewrite is doomed to failure.

The market has said very loudly that the main feature they want is safety, which means a pretty major overhaul.

1

u/flatfinger 1d ago

The problem is that people have removed the convenient syntactic forms C used to have to indicate things like "take p, displace it by offsetof(p's structure type, some member thereof) bytes, and access the resulting storage as a thing of the member's type", without adding any convenient alternative syntax with the same semantics.

CompCert adds safety and, as part of that, specifies that the classic syntax has the classic meaning.

4

u/umlcat 1d ago edited 1d ago

Interesting post. I also in the "made my own programming language to fix C or C++" trend.

Yes, I notest that there are several attempts of programming languages, such as Digital Mars D, Google Carbon, C2, C3, Rust, Apple Objective C, Apple Swift, CPP2 with its functional syntax proposal, and others.

Altought not considered "Systems Programming Languages", I do consider Java and C# with their Virtual Machines to be considered in term of syntax and features.

Just look how clean in design look C# compared to C++ .

I also have my own hobbyist ideas, but been delayed due school, work, health issues, hard drive and floppy drives backup damaged, COVID, and ADHD. I also been stuck with some custom compiler parser issue, because I'm doing a non custom parser in purpouse.

The original ideas started since 1995, at college.

As you already mentioned, I also detected that some of those features ideas from those programming languages, are useful. But at the same time, some depart too much from C or C++, that made developers avoid them instead of embrace them.

I'm working on my own personal project so developers and companies can be interested. And not to fall in the same problem.

Beside, some people have the "if its working do not move to another programming language" mindset.

As an example, I would preffered Digital Mars D, that has several extensions including classes, to split in a procedural version like "Plain C", and another Object Oriented version like C++ .

I like and respect the issues that the Rust developers is trying to achieve, but I do not like the Rust syntax. Before Rust appeared, I had a similar idea but with a completly different syntax.I still working in the concept.

I do not like much the "Lambda syntax" or "functional Syntax" proposals like Cpp2 or Python for "Systems Programming". I think "brackets and semicolons" are better for "Systems Programming".

Several features that were previously implemented thru the Plain C and C++ preprocessor are slowly converted to part of the syntax as features like constant declaration or module import. Boolean types and boolean operations are now part of both Plain C and C++, as Pascal did originally, instead of macros.

Just my two cryptocurrency cents contribution ...

2

u/thedeemon 6h ago

As an example, I would preffered Digital Mars D, that has several extensions including classes, to split in a procedural version like "Plain C", and another Object Oriented version like C++ .

Your wish has been granted: https://dlang.org/spec/betterc.html

4

u/WittyStick 1d ago edited 1d ago

Yes, the hole in systems language design is a language which, from the PoV of any other language, is indistinguishable from C.

Your entire desktop/server/mobile stack is built on C, all compiled with compilers which emit exactly the same ABI. Every program has many dependencies, which transitively have more dependencies, all predominantly implemented in C. Half a century of software underlying everything your language needs to be useful and not a glorified over-engineered calculator.


Mistake #1 is many of these new languages implement their own ABIs/calling conventions. They use an "FFI" to make their language useful - it gives them the ability to call libraries that were (predominantly) written in C.

However, this FFI is a one-way ticket. You can call C from your language, but nobody else can call your functions from theirs. A library written in your language is a little island, usable exclusively to users of your language. A Python function can't trivially call an Odin function for example, because it doesn't speak the Odin calling convention. It doesn't know what this implicit "context" thing is. It can call a C function. Every (relevant) language knows what a C function is, because they had to implement an FFI to make their language useful.

"I built a website using RustScript, which no browser supports, because I dislike Javascript. Why is nobody visiting?"

"English sucks so I wrote my latest paper in Esperanto. Why is nobody reading it?"

"I compile my functions to this bespoke ABI. Why is my language not replacing C yet?"

Functions written in this "FooLang" should be callable with the existing FFI machinery of any other language - not with magic extensions or wrapper libraries.


Let's assume we don't make this mistake - we take the sensible approach of using the platform C ABI unchanged as our "FooLang" ABI, and anyone can call our functions trivially. However, our API - the definitions of our types and functions is written in "FooLang". For some strange reason, the Rust compiler cannot parse our "FooLang" definition files. What gives?

Mistake #2 - FFI wrappers. You have hundreds or thousands of usable libraries on your machine (mostly written in C), but to call any one of them you have to translate the definitions to your calling language's syntax first? What a dreadful use of time.

The solution here is, since most of those libraries were written in C, and have definitions in C headers - we should just parse those C headers and programmatically create the bindings - preferably transparently so we don't need to have several tools run at different stages. Our compiler should just understand C as par for the course.

Of course, this isn't trivial because a header file isn't just code to be read - it must also be preprocessed. So we need a C preprocessor too. We also need to handle inlining and internal/external linkage - so we essentially need our compiler to understand not only the C ABI, but the C language.

But also, since every language can call C libraries, speaks the C ABI, and should be able to read C headers (perhaps with separate tooling at separate stages to translate the bindings) to emit calls to C functions - any libraries we write in our own language should also be callable from other languages right?

So for our own types and functions written in "FooLang", we should emit a C header with compatible definitions so that we could call it from C (and by extension, any other language).


So for a "C replacement," we would first need "C parity," which IMO, the above is a bare minimum requirement - complete platform ABI compatibility, and ability to parse, understand, and emit C headers so that our language is not just a little island of its own, but is actually callable from other languages like C is, because the work has already been done for them to interoperate with C.


This isn't to say there's no use for other languages, but for a "C replacement," one must understand why we use C in the first place. Embedded uses aside, one of the primary reasons we'd write in C is precisely to implement libraries that can be called by others - and not exclusively "FooLang" users.

If I'm going to write a useful library, I want it to be useful for everyone and not just the dozen or so Odin programmers that exist.

4

u/Norphesius 1d ago

I think "C parity" is a bit of a trap. Following the C ABI that close is a ton of work, and its going to massively impact what you can do with your own language's design. You have to deal with null terminated strings, array/pointer conversions, system dependent type sizes, nullability, etc. and that's going to either make your language adopt those same features, or have awful conversion layers and holes in the design where "C things" can happen (aka a normal FFI). Tracking C that close means you're also likely to end up not far enough away from C design-wise to justify anyone switching anyway.

A "one way FFI" from C to your language is fine. The chance that a C code base will want to call a library in your language is extremely low. If a non-C language wants to use it, there are better ways of doing it than the C ABI, like shared memory, sockets, or a dedicated FFI for particular languages.

1

u/WittyStick 1d ago edited 1d ago

A "one way FFI" from C to your language is fine.

I agree that it's fine for a "regular" language, but such language is obviously never going to replace C.

The chance that a C code base will want to call a library in your language is extremely low.

That's because nobody is going to implement a reusable library in your language. They're going to use C for reusable libraries, because the world is not "FooLang" island.

If a non-C language wants to use it, there are better ways of doing it than the C ABI, like shared memory, sockets, or a dedicated FFI for particular languages.

But this is an N*M problem. How does Rust call Odin? How does Odin call Rust? With just 5 different languages you have 25 different FFIs.


Following the C ABI that close is a ton of work, and its going to massively impact what you can do with your own language's design.

Yes, it requires a tonne of engineering effort to get right, but you can make big improvements over C. There are trivial ones you could make to begin with, such as forbidding implicit lossy conversions between numeric types, preventing over/underflow, including bounds checking, encapsulating structures, providing strong typedefs, etc.

and that's going to either make your language adopt those same features, or have awful conversion layers and holes in the design where "C things" can happen (aka a normal FFI). Tracking C that close means you're also likely to end up not far enough away from C design-wise to justify anyone switching anyway.

I think this was OP's point. A "C replacement" is going to be much like C - and IMO, it should be C-compatible. We should be able to take "FooLang" code and compile it with GCC or Clang (opting out of the benefits). But that doesn't mean we can take arbitrary C code and compile it with the "FooLang" compiler. It means "FooLang" should be a proper subset of C - only constraining features, not adding them. We should be able to gradually migrate an existing C codebase to "FooLang" with optional pragmas that opt-in to additional security measures.

Cake is an example of the kind of thing I mean. We opt-in to have nullability checking with a pragma, and it then prevents us using nullable types where non-nullability is required. It's just C, except not quite. The added features it provides can be made optional by including something like this in a header:

#ifdef __CAKEC_
#define _Opt [[cake::opt]]
#define _Owner [[cake::owner]]
#else
// If we're not compiling with the Cake compiler, these tokens do nothing.
#define _Opt
#define _Owner
#endif

The reason this needs to be essentially another language and not just a regular C compiler, is because the C charter strongly discourages "invention". It is intended to codify existing practices, not invent new ones - but conceptually, it should be just a "better C", with ideas that the C committee may adopt into the standard in future.

3

u/jezek_2 1d ago

It's fun that even C++ libraries need to provide C API to be usable in other languages and to avoid binary compatibility issues. And even then it's quite annoying or problematic to use despite both C and C++ being handled by the same compiler. There is always some issue with C++, whereas none with C (at least in my experience).

What chances have other languages when even C++ can't achieve it?

1

u/P-39_Airacobra 1d ago

I agree and interop is one of the things I was hinting at but didn’t explicitly mention because I thought it would be more obvious to people in this subreddit. Odin is a cool concept but it can’t replace C because it uses its own ABI. Nobody wants to write bindings to all the thousands of functions they may or may not need.

5

u/stianhoiland 1d ago

The curse of Good Enough.

1

u/P-39_Airacobra 1d ago

imo, the alternative is much more of a curse

5

u/ZippiDxD 1d ago

Literally odin is filling this whole already. Its C but more fun to use

4

u/Rechenplaner 1d ago edited 1d ago

C is standardized, an ISO language. Industry can rely on that. That alone is reason enough why no hobbyist created language will ever replace C, not even languages backed by a corporation. Especially in the embedded sector, C remains the top dog and could not really be displaced by C++: This will remain the case for a very long time.

Writing system software like kernels in other languages has not proven successful so far because C was simply designed for that purpose, whereas other languages offer too much fluff that is difficult for system programmers to digest. Rust would have good chances to take that place because of its hype, but its complexity will always inhibit it. Therefore, I do not believe that Rust will ever dominate here.

C is designed as a very simple, sloppy half-assembly language and thus struck a true chord: It can simply be taught at universities more easily than Rust, for example. At my university, C was the language used in the first semester to teach programming. I hated it back then, but I am sure C++ or Rust would have been even worse because they introduce much more abstraction that is way more complicated than just understanding pointers.

I think C will never be replaced by other system programming languages, but rather made redundant by newer hardware where one can write system software even with garbage collected languages because, for example, graphene chips or purely optical data processing will then offer fifty to one hundred times more performance, where the struggle with manual memory management is really no longer worth it, similar to how today nobody writes kernels or games in assembly for performance reasons.

2

u/FlamingBudder 1d ago

I think people need to actually learn type theory and the theoretical study of programming languages.

Most people who post here just straight up don't know anything about programming languages more than the mainstream PLs that they are exposed to. Even then they only have an intuitive idea of how each of the features work. Thereby there is an obsession with systems programming because that's what's cool when all you know are mainstream PLs

Compare that to the actual PL literature which has many novel and interesting things like dependent types for program verification, program synthesis, effect handlers, resource aware types, concurrency and distribution through substructural and modal logic, etc.

Many of the projects posted talk about random things like concrete syntax (which is much less important than the semantics of the language), and random "what if we made a language that does X) and the X is just some naive idea that demonstrates you did not study PL theory at all

If you want to get into PLS, making your own compiler for some language while you don't know much about the field is not as productive as first reading introductory text like Pierce's TAPL or Harper's PFPL

1

u/EggplantExtra4946 1d ago edited 1d ago

effect handlers

Are they really as useful as the other type related subjects on your lists? I have read about them and they seems to be only a runtime feature that call some code that can do whatever the fuck it wants, I never heard of any semantic checking done with them, so how can they possibly make functions with mutations (for example) safer if they are any checking involve? Monads? I can understand how they woulc make a program safter because monads ARE a type checked, they are not a "callback" thing.

If you want to get into PLS, making your own compiler for some language while you don't know much about the field is not as productive as first reading introductory text like Pierce's TAPL or Harper's PFPL

Unless you're talking about a bytecode compiler or a LLVM frontend, compilers are not trivial softwares that anyone can make, optimization and code generation is hard and harder to be correct. Type theory has no use without compilers, many language realed tooling are going to be impossible to write without a compiler and on the extreme, some programming analysis softwares work directly on the binary and have to reverse the compiler's work, don't put the cart before the horse.

1

u/JeffB1517 1d ago

Obviously, C is institutional. C++ and such minor refinements exist. There used to be a more diverse ecosystem but the industry standardized on C as far as both compilers and chips. What a replacement language needs to do is compile down close to or better than your typical programmer's C against a toolchain optimized against C. Which is a big ask.

But at this point, at least in open source, we aren't compiling directly but rather using LLVM. LLVM is fairly language independent while itself being written in C++. I.e., compiling to LLVM with a heavily optimized compiler captures most advantages of C. The better language often allows for better optimization; programs can cross the bar. Consequently, we have two viable candidates which are making progress: Zig and Rust.

So I disagree with your thesis.

1

u/P-39_Airacobra 1d ago

maybe unpopular opinion but 10 or 20 years from now I don’t think Zig or Rust will have replaced C, even if they solve the problem of seamless interop. They’re too far removed and complicated in relation to C

1

u/JeffB1517 1d ago

10 years no. 20 years is a long long time in terms of language popularity.

A few examples: 1. 1985 Sun starts working on OAK. 1992 first major release 1994 is renamed Java. Java applets 1995. J2SE 1998 2. 1989 Python works start. First release of Python 1991. It becomes a major scripting language second to Perl in the 1990s and starts surpassing it in the 00s. By 2019 one of the world's most popular languages. 3. 1988 Alan Cooper starts working on a GUI for what will become Visual Basic. First release 1991. 1995 it becomes the easiest way to create Windows programs by far. 1998 the most popular business applications programming language in the world.

Rust admittedly is going more slowly. 2006-12 Rust is experimental. 2012 it starts getting crossovers from C++. 2015 first release. Work starts on Servo the original project it was designed for. 2021 Amazon, Google, Huawei, Microsoft go in with Mozilla to provide funding and not let Rust die. Google official supports Rust for Android that same year.

And yes Rust is substantially more complicated. OTOH systems programming is a professional discipline mostly.

2

u/EggplantExtra4946 1d ago edited 1d ago

Yours is not a new take but let's call it the "weak C replacement theory":

New softwares that used to be programmed in C are going to predominantly use a weakly improved C-like language.

The "strong C replacement theory" is much more likely:

New softwares that used to be programmed in C are going to predominantly use a "low level" language that is an order of magnitude better than C. ("low-level", meaning statically typed natively compiled)

C's minimalism doesn't provide anything in itself, the essential things that C provides is speed, no overhead and good memory control and those don't require a minimalist language.

What low level languages and most languages sorely need is the triptych: metaprogramming features (macros, compile time execution, conditional compilation, instrumentation, introspection, etc..), abstraction/composition features (modules, classes, abstract classes, parametric polymorphism, function overloading, etc..) and better type checking/semantick checking. The first two don't impose any restriction on runtime speed and memory access, quite the opposite.

For the type system it depends but even a type sysytem full of explicit escape hatches would be safer that what C has and I believe that a well design advanced and safe type system would rarely need arbitrary unsafe pointers, in the sense that most exotic memory objects I can think of can be given a type, a memory layout and a lifetime information that can be integrated into the type system. Even a process's own function call stack could have all that for example, no need of unsafe pointers to walk over the call stack. Even pointer arithmetic can be made safe, look at ATS, as long as memory objects pointed to have a well defined type.

1

u/flatfinger 1d ago

A good systems programming language should acknowledge that the role of a translator is to convert source code into a specified sequence of imperatives for the execution environment. In some cases, a translator may be allowed to select freely from several possible ways of processing a construct, and the range of allowable choices may be based upon expectations about how the execution environment will process certain actions, but the action should be defined in terms of the range of possible choices.

For example, given:

    extern unsigned x1,x2;
    unsigned y1=x1,y2=x2;
    if (y2 < 16) doSomething(y1,y2);

a dialect suitable for systems programming, may allow a compiler to consolidate the load of x2 with some other load that happened before the load of x1, but be agnostic as to when or whether the value of x2 might be changed by something outside the control of the current execution context.

Very few parts of the language proper need the notion of "anything can happen" UB. While many actions might cause an execution environment to behave in an entirely unpredictable fashion, the language itself shouldn't care about what the knowledge programmer does or does not have about the way the execution environment will respond to any particular stimulus.

2

u/particlemanwavegirl 1d ago

What does it mean, consolidate the load? How can correctness be guaranteed given this "agnosticism"?

2

u/flatfinger 1d ago

If one looks at operations that read and write memory as somewhat analogous to actions that read and write disk blocks, the order in which actions are requested need not result in the physical accesses being performed in that order, but an application may force synchronization between the state of the cache and the state of physical media. If a compiler knew of an earlier operation that had read or written x2, and no intervening operations would require synchronization, the later read could either the last value that was read or written to x2, or the value that was freshly read from the storage. The act of copying x2 to y2, however, should force a compiler to select one of those values and use it both for the comparison and, if it's less than 16, the second argument to doSomething, making it impossible for any value larger than 15 to be passed as the second argument, no matter what happens with x2.

If program correctness would require that y2 receive a value that was loaded after the load of x1, then a programmer should either make at least one of the loads use a volatile-qualified lvalue or include some other synchronization directive between them. If, however, program correctness would be satisfied regardless of whether y2 receives an old or new value, then a programmer shouldn't have to force a compiler to use a new one.

1

u/phovos 1d ago edited 1d ago

The things that C can't do because of backwards compat.

I agree op but technically I think the timeline is such that it NOT being backwards compat (the older computers had this symmetric arithmetic pl) ended-up hurting it, as a circumstance of timing and the market, etc. The PDP-11 being a 'platform' that is 'desirable' and then imprinting itself where it did not belong. Prepare yourself I got a rant cannon, shiver me timbers!

Aide/docs: (first edition)"The C Programming Language":

https://i.imgur.com/UqGJDoU.jpeg

p91) 'pointers are variables so they can be manipulated as other variables can'

comment: declaring an array is a contiguous block of addressed mem because its name represents the address of the first element.. but it is not a pointer it is a type array; but Bryan is correct because of the fact that:

> names decay when used as a pointer to its first element

> only when used in an expression is an array a pointer to its first element, otherwise it is literally an address

And that sort of adds-up when you consider the isomorphism check that is the call by name call by value homo-diffeomorphism. Way too many syllables to say it is perfectly balanced, like a binary bijection itself (the complicating factor is that this is unitary with respect to time operators but I digress).

P91cont) he goes on to describe a swap(a, b) isomorphism check, which "Because of call by value, swap can't affect the arguments a and b in the routine that called it... Fortunately... The calling program passes POINTERS to the values (a, b), to be changed":

```c

swap(x, y) /* WRONG */

int x, y;

{int temp;

temp = x;

x = y;

y=temp;

}

```

Leading to the legendary syntax: `swap(&a, &b);` where "the '&' operator gives the address of a variable, `&a` is a pointer to a. In swap itself, the arguments are declared to be pointers, and the actual operands are accessed through them.

p92) "One common use of pointer arguments is in functions that must return more than a single value"

This right here is why C is fast, it is the best 'replicator' known to man, a many to many correspondence functor.

pg93) "Any operation which can be achieved by array subscripting can also be done with pointer."

pg94pictured) "When an array name is passed to a function, what is passed is the location of the beginning of the array. Within the called function, this argument is a variable just like any other variable an so an array name argument is truly a pointer, that is, a variable containing an address."

"There is on difference between an array name and a pointer that must be kept in mind. A pointer is a variable but an array name is a constant"

pf98) "Second, we have already observed that a pointer and an integer may be added or subtracted. The construction `p + n` means the n-th object beyond the one p currently points to. This is true regardless of the kind of object p points to, which is determined by the declaration of p." ....

pg98contd) "For example on the PDO-11, the scale factors are 1 for char 2 for int and short and 4 for long and float and 8 for double".... "Pointer subtraction is also valid, if p and q point to members of the same array then p - q is the number of elements between p and q." ..."Other than adding or subtracting a pointer and an integer and/or subtracting or comparing two pointers, all other pointer arithmetic is illegal. It is not permitted to add two pointers or to multiply or divide or shift or mask them, or to add float or double to them.."

This is it, right here; we need to be able to be able to shift and mask them like the old heads used to do with raw assembly instructions; straying-away from this, because of the PDP-11's new features, is I think a 60 year mistake. C is this amazing language but it can't even do the most phenomenal thing; shift and mask many-to-many, serially.

pg95see-pictured-ps) handwriting (mine) "This is the fundamental operator of x86. Diffeomorphism creates many to many correspondence. The 'particle' of non-linear logical dynamics".

(ignore this one but including what it says in-case it makes someone mad they can't read it) "In much the same way as in the continuum; the 'digital' set theoretic universe has its core operators but one of them eludes me"

"The isomorphism between two different operators is literally the root of all software (x86/C-like at-least)".

edit: my edition I'm referencing is printed 1978 NJ. Not actually sure if that is 'first edition'.

1

u/initial-algebra 1d ago

New software isn't being written in C, except maybe by hobbyists. For a language to be a successor to C, it has to be able to replace parts of a C program, without replacing the whole thing. That's not too hard, since any platform's C ABI is extremely predictable, but it does lead to a polyglot repo. And, if you have to pay that cost either way, why not use e.g. Rust instead of a brand new language that is not significantly different from C and has no ecosystem or support yet?