Solod: Go can be a better C(solod.dev) |
Solod: Go can be a better C(solod.dev) |
eventually, they fell back to rust.
there's a lesson here: subset languages are fine for experiments and small projects, but they're rarely the right choice for production software.
...and C still can't be replaced. it's still the language of choice for mission-critical systems and low-level problem solving.
I personally like Zig but I'd never write a huge 200k loc project with it, I see it more fit for low level stuff like small embedded devices where you must interface with C code.
Still I won't start a new project in C++. If I wanted high-level features and zero-cost abstractions, I'd take Rust. If I wanted working really close to hardware, do bit-twiddling and knowing where every byte is allocated, I'd take Zig. If I wanted to write a small piece of code intended to run absolutely everywhere, including old and esoteric architectures, I would still have to go with C (plain, old).
Why not C++? It allows as many low-level operations as one wishes, but don't forces you to manage memory manually where it isn't necessary.
> If I wanted to write a small piece of code intended to run absolutely everywhere
GCC and Clang have support of C++ since many years. Is there any modern platform for which no GCC or Clang backend exist?
Those two languages are on the opposite ends of any complexity scale. Someone looking for a better C has a ton of options before getting to "lets use C++ and ask our devs to practice discipline".
I don't get it at all. Green threads are cool, but again, it's easy to write unsafe concurrent code in Go, so why would I choose it over e.g. Rust (where its hard to write unsafe concurrent code) or C++ (which gives me more control)?
Then, a user (like me) tries writing something like
package main
func main() {
register := 42
println(register)
}
well, oops. /tmp/solod_build3904763637/main.c: In function 'main':
/tmp/solod_build3904763637/main.c:6:21: error: expected identifier or '(' before '=' token
6 | so_int register = 42;
| ^
/tmp/solod_build3904763637/main.c:7:29: error: expected expression before 'register'
7 | so_println("%" PRIdINT, register);
| ^~~~~~~~ (exit status 1)
OK, now you grab the list of reserved words of the target language, which is not always an easy thing to do, and rename type names and variables as needed.The next bad thing is when you step on your own toes and see that the new names you invented, like `so_int` or `so_println`, will inevitably pollute the global namespace. We'll either cross our fingers and hope that no one will create a variable named `so_int`, or we'll need to add all our new kind-of-reserved words to our already big list of exceptions.
I'm sure there are multiple levels of complexity beyond this.
Not trying to say that seeing a bunch of new translators from language A to language B is bad: not at all! It really seems that this is one of the popular usages of the agents, and a rewarding one. But doing it without hidden bugs is kinda hard.
To be fair, it's not like it's a completely foreign concept; consider C's categories of reserved identifiers (e.g., no double underscores or underscore followed by a capital letter) as well as the identifier ugilification that needs to be done in the C/C++ stdlibs to avoid collisions with user code.
In that vein, the transpiler could pick some prefix which users are highly unlikely to use and document that said prefix is off limits (e.g., "You can't use identifiers starting with `_solod_id_`") or maybe it could use C's reserved IDs (idk if such a transpiler would count as an "implementation" as far as the C standard is concerned).
I've never designed a transpiler (I'm not a programmer), but surely the correct approach is to transform the source code into its type-checked AST, and then translate to the target language from there?
Yes, it should; but it also makes things much more difficult. This specific transpiler indeed builds an AST, but does not care about identifiers, just emitting them as is [1] (I hope I found the correct place in the code).
[1]: https://github.com/solod-dev/solod/blob/8485bc867ae0f0269d75...
A compiler preserves the semantics - if you compile Scheme to C, you get a real Scheme program in C, with full semantics.
A transpiler does not have to guarantee that. It is much easier to transpile Python to JavaScript than it is to truly compile Python to JavaScript. A transpiler can tolerate some amount of leakage between worlds (think 'arguments' as a variable name in Python vs JS).
It really depends on your needs. Sometimes a transpiler is okay, sometimes totally inadequate.
You critique the approach as brittle and yet the approach you propose doesn't solve the thorny problem gp described of a growing ball of reserved keywords.
Protobuf has similar problem: whatever names you use for your proto messages should be translated to the target language as is, and with multiple target languages, chances are that you'll hit a keyword in one of them; each language plugin should figure out how to resolve this.
$ cat test.proto
syntax = "proto3";
package test;
message TestMessage {
int32 register = 1;
int32 foo = 2;
}
$ protoc --cpp_out=. test.proto
$ grep 'register\|foo' test.pb.cc | head -n 2
: register__{0},
foo_{0},
(I may understand `register__`, but why `foo_`? no idea)There is a more brutal option: AI could help write a c++ to plain and simple C transpiler... then we would lose the semantics of the original program, but free ourself from the very few toxic and real-life c++ compilers out there.
Funny because the canonical C++ compiler for the first decade of the language's life did compile to C. People eventually moved on to other compilers to free themselves from having to deal with the toxicity of C output.
What's old is new again.
So updating cfront for modern C++.
So even if someone creates 'so_int' it becomes 'noso_so_int'
Which while some of the design decisions might be debatable, they actually knew what C is all about, informed by their own experience with what worked in C, Alef and Limbo, across UNIX, Plan 9 and Inferno.
I was a little worried at the start because nobody would normally consider Go for games, but I did a bunch of tests and found it's just no big deal.
(I'm focused on game play and not interested in pushing hardware to its limits.)
I started about a year ago asking the AI very beginner questions about Go and 3d math. I think it did steer me down some wrong path sometimes and of course I did some dumb things myself too. I'm not using Agents, just asking questions about features, then writing the code myself by hand.
I really like the power of the tools and the constraints of the language in Go. It's been fun so far.
[1] https://github.com/gen2brain/raylib-go
It's interesting you would mention Odin because I spend a fair amount of time playing with it as well. It was easy to get started and fun. Fast, easy to iterate. The reason I moved away was not the language itself, but I felt it was too much Ginger Bills baby. Oh, and managing memory is no fun. I want to make game play, not think about allocations :)
That hadn't really occurred to me before.
So, if you accidentally code something that is not in the subset but is in the full language, the linter would then happily accept that. Imo there will be similar issues for lsps and formatters also. You're going to probably end up in a situation where you have to fork all of those anyway.
func newPerson() *Person {
p := Person{Name: "Alice", Age: 30}
return &p
}
becomes static main_Person* newPerson(void) {
main_Person p = (main_Person){.Name = so_str("Alice"), .Age = 30};
return &p;
}
Quoting the FAQ: "So itself has few safeguards other than the default Go type checking. It will panic on out-of-bounds array access, but it won't stop you from returning a dangling pointer or forgetting to free allocated memory. Most memory-related problems can be caught with AddressSanitizer in modern compilers, so I recommend enabling it during development by adding -fsanitize=address to your CFLAGS."So saying you get the "safety of Go" is a bit of a stretch.
You can pass pointers to earlier frames in the stack, they're still active, but you can't return a pointer to an expired stack frame.
"Everything is stack-allocated by default; heap is opt-in through the standard library."
So it supports both stack and heap, and I guess static allocation too.
Wut?
Also, how do you preserve garbage collector semantics without garbage collector?
- Everything in the language is statically allocated or stack-allocated. You have to call a malloc / free function to get heap allocated things
- The language is not memory safe (you can't return slices, pointers, or interface types from a function if the thing was created inside the function, unless you used heap allocation)
- Interfaces (the only variable size struct Go has) are implemented by creating a struct of function pointers. Arrays and maps (the non-struct variable size types) are implemented as stack-only and maps are limited to 1024 keys. You can opt into heap-based arrays / maps in the standard library to bypass this.
a) none
b) http 1.1
c) ... forget about more
Maybe you need rar support ... O well, ... And then you always enter the world where you need to start linking external libs, and then your mixing not just the base language and the transcompile but also whatever interface for calling those external libs.
Ironically, with LLMs being around, your often better to just have it write whatever is missing. Until you find out then, that the language misses some feature.
O great, you needed crypto support. Ok let the LLM write it for you, its only 100x slower then whatever was written years ago and had tons of enhancements and edge cases fixed.
At that point, you can just tell a LLM to write in C from the start, and be done with it. lol.
Transcompile languages have always had tons of issues, and there is a reason why non became popular. Haxe comes to mind. How many years this exist and barely anybody knows it. Because your often just better writing in the original language and fixing the issues, or picking a better fitting language for your project.
While implementations have all died out now in favour of direct compilation, C++ the language was designed to be a transpiled language, and was for many years with C as the target. You can likely find many who agree that it has tons of issues, but unpopular it is not.
It’s perfectly fine for you to disagree with them, but it’s also perfectly fine for them to publish their solutions to their problems.
‘A better C’ is a good description for these projects. ‘The better C’ would not be.
Also, thank you for an educational comment.
Edit: I forgot to ask about your thoughts on Ada's and Zig's type punning.
It is a major problem with most really old languages, JS is notoriously prone to this kind of "remove the bad bits" thinking when in actuality most of those complaints are non-issues if you run a linter.
Maybe it's possible to force the transliteration to use a different allocator and then you could use the one you wrote in C?
Wish something like this existed instead i am stuck with flutter and react native on Mobile.
When will a time come when i can use some functional languages like Haskell or a plain boring language like Go for making apps with OTA ability for mobiles.
As vibe coding takes over, app store approval will become slowerer and OTA is really great when you need to make quick changes!
You can OTA each day and do base app release to store once each week.
i think this maybe the space where very little work is being done.
It’s not. Garbage collection made sure of it. I believe everyone agrees that a "better C" has to have manual memory management. Most even rule out Go as a systems language because of GC.
Of course, I’m pretty sure Go is much better than C at some problems. But there’s no way in hell it supersedes it.
Most of those folks would never manage to replicate something like Xerox Cedar on their own, from 1980 in their beloved 2026 computers.
Thankfully we have the likes of Apple and Google, that have a my way or the highway education system for such developers, that want to go through their magical gardens.
This seems completely wrong. Manual memory management is perhaps C's most famous issue. I'm sure _someone_ thinks manual memory management is a feature rather than a bug, but I don't think there's any broad consensus about this at all.
Moreover, Go gives you a lot of levers to control your allocations, and with some care (probably less care than writing correct C code) you can avoid allocating at all (this is how Go's own runtime works).
C is great when you need to do manual memory management. Most software written today doesn't need to do manual memory management. (And frankly, for the typical software project, manual memory management is a liability.)
But Go is a better language for many programs that were often written in C: network servers, CLI utilities, TUI utilities, etc.
Interlisp-D, Smalltalk, Cedar, Topaz, Oberon, Active Oberon, Singularity, Midori, Ironclad
Go's runtime is written in Go.
The whole compiler toolchain, GC, compiler, linker, Assembler, is written in Go.
There are Go compilers for bare metal, no OS needed, like TinyGo, the runtime, written in Go is the OS.
I love the "you cannot write an OS in a GC language" discourse.
It isn't only a mainstream thing because everyone only cares about UNIX clones.
Can you even write an OS in pure ISO standard C? I know atomic (https://en.cppreference.com/c/language/atomic) was added in C11, but is that sufficient to write an OS? How do you write a kernel-user space transition in standard C, for example?
Anything you can think C is better, it isn't ISO C, rather non standard C compiler specific extensions, which can language can also be.
Just do like in K&R C days, use Assembly for what language isn't directly capable of, and it is right there as part of a regular Go toolchain installation.
They were trying to build a faster Python, thinking that would appeal to C++ developers. The theory was that developers were using C++ at Google because they had to, not because they wanted to, and would choose something like Python instead if it were up to the performance task. Although it turned out in the end that C++ developers actually wanted to use C++.
But does Limbo have any 'fatal flaws' like that? Especially compared to Go.
No doubt Rob Pike is a prolific and capable programmer. But seems like he needed quite a few attempts before arriving at Go (looking at C's flaws -> Alef -> Limbo -> Go, Wikipedia also mentions a domain-specific language called Sawzall).
We need a little more in 2026, besides compile time execution.
Yet all major C++ compilers support it.
The next point would be gc,maybe it can be improved and its being improved the recent being mark sweep algorithm was made a bit efficient by addition of more meta data about the total object structure, not going into it more.
Thing is it breaks the fundamental promise it was suppose to keep and yes its better than rust for network applications(am talking of getting things done in default way). It's stdlib is the strongest among all languages.
That summarises my point go needs severely improve it's issue of thread safety by default provision and gc efficiency.
Do an internet search for "go garbage collector benchmark" if you're curious what the grownups are talking about.
It seems you are failing to see that this is this very syntax complexity which makes most of this toxicity.
And having a modern c++ transplier to plain and simple C, would re-open the door to alternative and 'real-life' small/medium compilers, not like those vendor/developer-locked very few options we have today.
Somebody did it for microsoft rust. Would just be a good idea for nowadays c++.
Well, we are talking about programming languages after all. That goes without saying. One has to be odd to want to do that. But we wouldn’t have it any other way!
Need? Not really, not on the desktop (embedded is a different story entirely). But there are still benefits, as well as costs.
Thing is, most programmers have no idea what the actual costs an benefits are, because they have a hopelessly incomplete view of what manual memory management actually is. To them, and this includes long time C++ programmers, manual memory management means calling the general purpose allocator for every little object. At best they’d batch allocation in arrays or vectors, but otherwise it’s RAII for most stuff, and new/malloc() for anything more complicated. This kind of memory management have high costs and low benefits. In some cases the runtime overhead is even greater than using a GC.
And then there are arenas. Much easier to deal with, much lower overhead, much safer, more predictable… They make the cost/benefit analysis completely different, to the point you could ask yourself why you would even bother depending on a GC and the heavy runtime that goes with it.
https://www.dgtlgrove.com/p/untangling-lifetimes-the-arena-a...
Is it still, today? There are other manual memory languages today with better safety records and ergonomics. Plus a host of new and better features.
I think C is really great if you need C. For libraries, for knowledge, for size, for compatibility, for "simplicity", for fun, for whatever.
But other than having a strong reason to want C, on a purely language feature comparison, it's not great.
> resolving identifiers as symbols, and then choose legal names for them in the target language
The responsibility for avoiding collision with the target language is in the transpiler; it should mangle identifiers appropriately to ensure you can't accidentally hit a keyword in the target language.
Lots of reasons. If you can't come up with one then you're not the intended audience, and that's fine.
I made the comment for several reasons. Here's two: (1) I had high confidence that my understanding is correct and wanted to share my knowledge with others. (2) In case my understanding wasn't correct I was hoping someone would correct me, resulting in both me and others learning some interesting nuance.
As for why you in particular should read my comment: Curiosity. There are few traits that are as important to strategic thinking, creativity and success in reaching ones goals as curiosity. When I see something that doesn't map to my understanding of the world my reaction is to wonder whether I'm missing something. It has served me incredibly well so far.
"I don't even write code, yet here is my technical assessment of how a tricky software problem should work".
You can come up with an algorithm or system without knowing programming. Presumably we can agree the opposite, that being a programmer doesn't give you mystical insight into every programming problem.
Ultimately this isn't even a programming problem.
Timmy writes homework, Jimmy writes homework. They can name the homework however they want. Both go in the same pigeon hole. How can the teacher tell who did which homework?
It is. I too find my competence profile a bit weird.
I've been interested in security since I was 12 yet never worked full time as a software developer. I have done some coding, but that was over a decade ago, and even then it wasn't very much.
On the other hand I understand the theoretical foundations of formal languages quite well, as well as the engineering required to implement them. I'm comfortable talking about any stage of the compiler pipeline, I can recite the Lambda cube and which logics the interesting corners correspond to, and I'm designing a family of programming languages in my free time.
I understand digital design fairly well too for that matter.
edit: I suppose you don't get segfaults or buffer overflows and the sort in go for accessing memory in a parallel context, you get recoverable panics. But that is still not really thread safety in my opinion, it is memory safety.
(I think the original author meant type safety when they made that statement. Though it still doesn't make all that much sense to me)
Dynamic memory allocation on a microcontroller can be treacherous full stop, but if you have accepted the tradeoffs of using dynamic memory allocation then why not GC? You wouldn't want gc[1]-style GC, but there are other GC algorithms that can work well enough on micros. The Go spec calls for garbage collection, but it doesn't say how you have to collect garbage.
> but it does not replace all use cases
It could replace all use cases. There are arguably much better tools for many jobs[2], but if you had to the language isn't going to stop you.
[1] gc the compiler, not GC garbage collection. Naming is the hardest problem in CS.
[2] Some would say all jobs, but for the sake of discussion let's agree that Go can be a reasonable choice at least sometimes.
That’s the thing: many do not use dynamic allocation at all. Did you know for instance that you could write an entire modern cipher suite using only a small (<300 bytes), strictly bounded stack allocation?
As for those who do use the "heap", many can do so with a strict stack discipline, so it’s not really a heap. Reason being, the environment has an extremely low call stack budget, so all bigger allocations happens in another stack, located in the heap.
Then there are arenas, which are much simpler to implement than a general allocator, tend to have less overhead (both space and runtime) than malloc() or a GC, though I reckon they can still run out of space. Still, less treacherous than uncontrolled use of malloc().
Most of all though, even if the compiler can do mighty static region analysis to minimise heap usage and GC overhead, heck, even perhaps reduce it to absolutely zero in some cases, there’s still the problem of the programming interface: how does the programmer know about stuff like max heap usage? I guess the compiler could tell them, but then how do they fix it if usage exceeds the limit, or is unbounded? The wouldn’t be any clear link from heap usage to the source code, or it would be so diffuse it would be hard to determine where to begin.
And of course, a GC’ed program would likely rely on many more pointers under the hood than one that manages its memory manually, which means significant space overhead even if the GC implementation itself is perfect.
You want me to write something for an ESP-32, it’s gonna be in something like C, Rust, or Zig.
What are you talking about
Huh?
You should reassess how your current understanding of language popularity compares to what's actually going on outside your environment.
Typescript has already surpassed C++ in popularity. [1] [2] [3] [4]
As a primarily C++ dev myself I wish that wasn't the case, but it is.
> aside from a couple of legacy features that don't seem to be commonly used, it can be erased without needing transpilation
"If you ignore the parts that don't need transpilation, it doesn't need transpilation". Well yeah, sure.
But even there, erasing types is still a transpilation, just an error-prone version of it. It's still being run as Javascript at the end, whether you actually compile to Javascript or use a frankentranspilation instead.
[1] https://www.libhunt.com/index [2] https://www.itransition.com/developers/in-demand-programming... [3] https://eu.36kr.com/en/p/3549523189739394 [4] https://survey.stackoverflow.co/2025/technology
He says as he then tries to prove it by showing rates of use and mentions inside programming bubbles, forgetting that we're talking about popularity. Time to step outside into the real world, away from screens. Many non-programmers who make up the vast majority of the population have heard of C++. Typescript is largely unknown.
> "If you ignore the parts that don't need transpilation, it doesn't need transpilation". Well yeah, sure.
The earlier claim was that transpiled languages never become popular because of the impedance mismatch between the source language and the target language, which introduces a number of practical problems. If your language is identical except for a couple of features nobody uses then you will never hit the impedance mismatch spoken of. You are technically right in a vacuum, but what about the rest of the universe? It is an example, but it remains unclear how it is a better example.
> But even there, erasing types is still a transpilation
If we are ignoring the couple of exceptions, then that is debated. Some definitions of transpilation declare that there must be a translation to a different language. Typescript is the same language as the target with extensions.
Since the larger discussion is about Go, did you know that there is also a Go with extensions language that the gc compiler[1] implements? It is especially apt because it also only needs erasure. Do you consider it to be transpiled? Or is it just plain old Go, with extensions? The sentiment I see on HN is that it's just plain old Go, not a different language that needs to be transpiled to Go. Chances are you aren't even aware that this language exists because everyone just calls it Go as well. It isn't thought of as something else even though it technically is.
But we don't have to worry about semantics. What remains clear is that simple erasure doesn't run into the impedance mismatch problem, and thus doesn't run into what was originally claimed. Hell, even the exceptions, where Typescript truly requires transpilation by any definition, map fairly cleanly to Javascript so there still isn't any major impedance mismatch to run up against like there generally is when the languages have larger gaps in functionality. It is an example, but what makes it a better example?
[1] In case you are aren't aware, the gc compiler is the canonical Go compiler implementation provided by the Go project.
The definition is wrong, then.
I wrote C++ for most of my career. And as of late, I found myself avoiding more and more features from it. The STL is mostly trash, not worth the increase in compilation times. Templates are good for containers, but that’s about it. Inheritance and polymorphism are circumstantial enough that I’m not sure they’re worth adding to the language: in the rare cases I do need them, I can always write my v-table by hand.
The more I write C++, the more I find C is not that far from that "much smaller and cleaner language struggling to get out". And I say that fully aware of the many egregious faults still present in C.
There are two features from C++ I would really miss in a big C project: generics (templates), and destructors. But then we can always write a lightweight pre-processor to add those generics and a `defer` statement. Even if it requires a full parser, C parsers are pretty easy to write.
Started with the excellent MIR compiler, and I wrote much of the class/string/json/dict, exceptions and AI made the generics,ownership tracking, and safety checks/traps. Added some go
The memory model is mixed, I ended up using arenas for dictionaries and adding a full ownership checking / safety checking compiler stage.
It's purely for the joy of making something interesting.
Maybe you're interested in collaborating? :)
The way C++ was first implemented was a good idea. It’s everything else I disagree with.
"I was asked a few weeks ago, "What was the biggest surprise you encountered rolling out Go?" I knew the answer instantly: Although we expected C++ programmers to see Go as an alternative, instead most Go programmers come from languages like Python and Ruby. Very few come from C++.
We—Ken, Robert and myself—were C++ programmers when we designed a new language to solve the problems that we thought needed to be solved for the kind of software we wrote. It seems almost paradoxical that other C++ programmers don't seem to care."
https://commandcenter.blogspot.com/2012/06/less-is-exponenti...
I'm kind of surprised that the Go creators were surprised by this.
Like I know they didn't like C++, but Go is by no means a replacement for C++ despite whatever was said about "building a better C++/Java".
It took so long to pick up generics that Rust was already around and the logical next step for C++ developers by the time that happened. And that's only scratching the surface of why people choose C++.
On the other hand, having a more performant "simple" language that directly supports concurrency and lets you compile to native code without a ton of ceremony could be very helpful indeed if you're a Python dev who need to take an app to the next level without having to learn intricacies of C FFI or the GIL.
What Googlers don't have a grasp of is how resource constrained most other businesses are. They cannot afford to hire C++ experts and were using Python because Python developers were the only developers they could get their hands on, even where Python wasn't providing sufficient performance.
Indeed they were. Your quote even says so — seeking out to solve the problems C++ was thought to have, which is that it didn't have the ergonomics of languages like Python, as explained in the original public Go announcement. To "feel like a dynamically-typed language" was a primary motivation. That was clearly told.
You can certainly hold the view that a better C++ and a fast Python are one and the same, but I suspect the HN crowd will vehemently disagree with you. Conflating the two would not serve to communicate much here.
> were very surprised by the adoption from Python folks.
Quite naturally. Python users were already using Python. One would be inclined to think that they didn't need another Python. Except it turns out they did, because it was actually they who needed a faster Python.
Oof, I don't think so. It's just ridiculous how many things Google uses C++ for where something like Go or Java would be a better fit, and everyone I talked to agreed. No one liked C++ for search-like things.
I think they just underestimated how powerful the network effects were. People wanted to use Go, but then it didn't have Flume support forever, so what can you actually do with it? And all the NLP libraries were in C++. And the vector-search library was C++, and graph clustering, and so on. One of the basic data formats was super-clunky for ages as well, but I forget if it was SSTable / RecordIO / Capacitor or what.
Or perhaps the Go creators were just writing very different code in very different domains from what I saw as the bread-and-butter C++/flume data-crunching pipelines of Search and Maps.
Survivorship bias. You're only looking at the people who remained C++ developers. A bit like a politician bragging that they have 100% approval rating among their supporters even as their supporter count dwindles.
There are obviously exceptions. There always are. But the reality is that Go was picked up by the Python crowd instead. Which was noted as being surprising at the time as it was assumed that Python users didn't need another Python. What was hard for the Googlers to fathom was that anyone would use Python in a performance-sensitive area to begin with.
C's most famous issue is that it doesn't have a dynamic memory management concept at all. You can implement manual memory management on top of C, like you can in any language, but it is not a core feature. If C had manual memory management then it could be reasoned about, which would avoid many of the pitfalls associated with memory management being bolted on top.
Whether that is a feature or a bug probably depends on what kind of software you are building. If you are targeting a PC, a dynamic memory management concept in the language can be useful. If you are targeting a small microcontroller where you don't want dynamic allocation then things can get a bit weird having language features that need to be disabled or having to rely on outside processes (code review, linters, etc.) to control use.
But the general consensus these days does seem to be that a language should include a dynamic memory management concept, even if it is sometimes disabled. So a "better C" likely would gain memory management, but you make a fair point that it doesn't have to be manual in nature.
It seems like you’re conflating dynamic and manual memory management? Or maybe you are using these terms in a way I’m unfamiliar with. C definitely has manual memory management via malloc and free. It can be reasoned about, but doing so correctly is very difficult (this is what Rust’s ownership system formalizes, after all). I don’t think this is controversial?
> If you are targeting a small microcontroller where you don't want dynamic allocation then things can get a bit weird having language features that need to be disabled or having to rely on outside processes (code review, linters, etc.) to control use.
C has dynamic memory (again, malloc and free) and thus it is in the same boat as other languages that have to take care to avoid using dynamic memory when writing embedded code (a decade and a half ago I was an embedded engineer using C and C++).
Perhaps I should have said heap memory to be more clear, but the intent was to recognize that C does have memory management (the stack), but it manages it automatically. However, I don't think the intent was actually lost as you specifically called out malloc, which was directionally correct. So I guess there was no need to be more clear as the message was delivered just fine.
> C definitely has manual memory management via malloc
No. malloc isn't part of the C language, it is a library function and one that isn't guaranteed to be available at that.
> It can be reasoned about, but doing so correctly is very difficult
To the best of our knowledge it is impossible. That is why Rust's claim to fame is adding a model necessary to make reasoning about memory possible.
> C has dynamic memory (again, malloc and free)
Again, malloc and free are not language features. They are bolted on top. You can bolt malloc and free onto every language under the sun. You can even bolt malloc and free onto Rust, but you will then break the ability to reason about its memory if you use it.
It’s actually much easier than 95% of programmers think. The trick is to stop using fine grained allocations all the time, which are very difficult without tricks like RAII or a borrow checker (not to mention the high runtime overhead), and start using arenas instead. https://www.rfleury.com/p/untangling-lifetimes-the-arena-all...
I reckon mandatory manual memory management is not ideal. But the ability to manage some memory manually, including in cases that require something more flexible than a stack, is definitely a feature.
I believe every willing user of Zig, Odin, and Rust would agree with me on this one.
> […] with some care […] you can avoid allocating at all […]
As long as it’s crystal clear from the source code, or I have a tool that tells me which line of mine is responsible for the allocation.
The tool is the escape analyzer, but I don’t think anyone has made it ergonomic yet.
I'll definitely check it out and see what I could do. I added two compiler stages - a decent memory ptr ownership tracker and midopt compiler optimizer you could likely use without many changes that improved performance 35% over c2mir.
As simple as that, not everyone of us is a Linus.
I should also point out that if you are using a Mac, changes are its iBoot Safe C might already been replaced by Embedded Swift, a GC enabled systems language.
Chapter 5 of The Garbage Collection Handbook, or A Unified Theory of Garbage Collection paper for the incoming replies related to RC.
People care about UNIX clones because they are lazy, UNIX has the source code available, and an existing ecosystem that they don't want to replicate, so it always ends up being yet another clone, thus throwing away all the possible innovations.
We see this happening even with Haiku, Genode, Redox OS, or Windows now shipping alongside Linux on top of Hyper-V.
Unless one is an Apple or Google, with the money and will power to push something out the door, using Objective-C, Swift, Java, Kotlin, with plain C and C++ standard libraries, and even then people will bend backwards to put UNIX into those systems, even when the platform owners went to great effort to hide it under the official userspace APIs.
I’m pretty sure Linus could not have written Linux today. Too much hardware to support, too many drivers to write, before it’s remotely useful. Well, except perhaps on some specialised servers.
Yes, not everyone is Linus. Otherwise we’d be using GNU Hurd. But he also came at the right time.
Which is a real shame, because people thinking outside the UNIX clone box have come up with some incredibly innovative operating systems like Plan 9 and OS/400.
Because of the 30 million lines problem. Writing a serious OS kernel nowadays is flat out impossible: the hardware is just too diverse, it requires too many drivers. The best we can do right now is just add to an existing kernel. Kind of a vicious cycle: the more drivers we accrete to any given OS, the more impossible it is to get feature parity from the ground up.
The only solution to that is for hardware vendor to standardise their interfaces (at the hardware level), and actually give us the fucking manual. And I mean the real manual, the one that tells us voltages and pins and baud rates and the actual wire formats required to talk to it. A proprietary Windows driver is not a manual, even if it comes with an SDK. Heck I’m not even sure the source code of a Linux driver would count.
But they won’t do it. They just won’t. So unless we do something drastic like forbidding hardware vendor to ever ship software (to force them to standardise and document their actual interfaces), writing another OS that can actually compete with the existing ones will remain flat out impossible.
There's some nuance to this. The runtime code uses specific compiler support and restrictions that are not ordinary Go - possible (or even done, like this case) ≠ ergonomic. No doubt of course that for the Go project, this is still a win.
I love how language extensions, and subsets are always valid for C and C++, but used as an attack against other languages, as if to prove they are not good enough for a specific use case.
Well, neither are C and C++, as they either have to rely on hand written Assembly code, language extensions, or be gutted down into subsets outside the official language standard.
Ultimately, it comes down to the (subjective, sure) ergonomic limitations of a language. To me, Go without garbage collection and implemented using specific compiler support and significant amount of unsafe operations, does not really represent Go, just as, so to speak, a Rust project containing 90% unsafe code does not really represent Rust. Both are possible of course, and there are even use cases in which they may be desirable in the broader context (ie. the Go runtime).
Therefore, this is not an attack on the language; it is simply an assessment of its ergonomics for particular use cases.
Exactly. You don’t need GC if you don’t produce garbage. And since Go uses stack-based memory like C you can avoid producing garbage just the same.
And if you do need some kind of special memory allocation then you can do the same tricks you do with C. You still don’t have to use the built-in allocator.
All the bellyaching about GC is silly because you don’t need it in the first place. The cries are like saying C isn’t suitable for microcontrollers because its standard library includes malloc… Just don’t use it.
> You want me to write something for an ESP-32, it’s gonna be in something like C, Rust, or Zig.
Sure. The Go creators were explicit that Go is designed for building network systems. That doesn’t mean it is impossible to use for anything else, but it was optimized for a particular niche. You will feel more comfortable using languages designed for microcontrollers when programming for microcontrollers.
But it’s not GC you aren’t using that gets in the way. You don’t have to use every language feature just because it is there. Rust doesn’t become unusable for programming because it has a string type and you only need integers. Just don’t use it.
In some cases, avoiding heap allocation is just impossible. Take OCaml: last time I checked (over 10 years ago) there are two kinds of values: integers, and pointers to heap allocated objects. That’s basically it. Even floats are allocated on the heap (except when inside an array that’s the only optimisation). So unless you only do integer operations and avoid constructing any kind of data structure, you can’t avoid heap allocation.
Other languages are much more conservative than OCaml in this regard. That with value types and all. If they’re clear enough which constructs don’t trigger heap allocation, and the set of thereof is rich enough to allow significant work, then yes "just don’t use the GC" is a valid option. Not all languages (I mean implementations, but we can conflate the two in most cases) fulfil those two conditions. Maybe Go does.
Perhaps there is something to be said about that within the space of all programming languages, but why would you conflate them in our discussion about Go given that having multiple implementations was established as a language design requirement? For many years the core Go team maintained two different Go compilers themselves to ensure that requirement was met, although these days it has turned its focus to one, relying on the community of other compilers to fulfill the other implementation requirements. It is impossible to have that conflation because there isn't just one implementation in which to conflate it with.
In practice, if you were going use Go on a microcontroller you would not use the same compiler as you would use if you writing a web server. There are different tradeoffs for different computing environments and so different compilers can focus on different machines. As we already discussed, one such tradeoff is in GC implementations. While it is fair to say that dynamic memory allocation is not ideal on microcontrollers to begin with, if you chose to go down that road and accepted GC to go with it you'd want a GC designed for microcontrollers, not a GC designed for handling web requests. Those needs are very different. Conflating languages and implementations straight up doesn't make any sense in the context of this particular discussion.
I think the real impediment isn't related to garbage collection, but it's just ecosystem inertia and industry knowledge inertia--the same forces that inhibit Rust adoption or C++ adoption or Ada adoption. Does that seem reasonable, or am I misunderstanding? What do you think?
C and C++ start out so unsafe, I can hardly use them on substantial projects without sanitisers now. Sanitisers need to be made standard so I can use them everywhere.
Garbage collected languages start out with unpredictable memory usage, making them unusable on constrained environments without the relevant memory analysers (static or dynamic). Such tools need to be made standard so garbage collection may be an option on more platforms.
malloc/free, not being a language feature, cannot be reasoned about. Since the larger discussion is about Go, consider using malloc/free in Go. Of course you can do it. There is nothing stopping you. Since it is not part of a language it can be bolted on to any language, not just C. But if you do, it should be obvious you can no longer reason about the program in terms of how memory is used. If there was a way to reason about it, C would already have all the same memory guarantees that Rust does.
They’re defined in the standard all the same. Of course they can be reasoned with. The defined parts at least. You may argue that the undefined parts cannot, but those are explicitly outside the scope of the C standard, no need to discuss them any further.
Is that a fact? Are you telling me that today, there's a Go compiler that's best suited for web servers, and a different Go compiler best suited for micro-controllers? Could you name the micro-controller one?
> As we already discussed, one such tradeoff is in GC implementations.
The internals of the GC are less important than how they affect the language itself. When memory is very limited, I need predictable memory usage, so memory usage must be part of the language's semantics. If they're not, I can't use that language in a constrained environment, full stop.
At the very least, I need to know which subset of Go's language and standard library have predictable memory usage. If there is no such clear subset, then Go cannot reliably be used in a constrained environment.
No, I would have no reason to tell you that because, given what we're talking about, you would already know that. You couldn't possibly partipcate in good faith if weren't familiar with the Go ecosystem. This is such a strange comment.
My question was genuine. What triggered it is when you wrote "in practice". I myself draw a sharp distinction between a language and its implementation, but the moment "in practice" gets thrown in, I have to assume we’re talking about existing implementations only. And I have to say, as unfamiliar as I am with the Go ecosystem, the existence of a compiler (somewhat?) tailored to embedded use, is deeply surprising to me.