C# for Systems Programming
github.com
github.com
You can really tell, though, that the language wasn't meant to be used this way. Some common annoyances are:
1) C# is unable to declare fixed-size arrays in structs. There are ways around it, but they are all more unsafe, require code generation or hit the GC. If you've ever written kernel code, you know that fixed-size data members are really common, so this is more annoying that you might think.
2) C# types are either always references (classes) or always values (everything else). There are reference types, but they are restricted so that, for example, the array subscription operator cannot return them. This means that you can't directly operate on values in an array. Once again, there are ways around this, but they involve pointers, which pull in a bunch of the "unsafe" language.
3) The standard library makes a lot of choices that, in hindsight, are extremely unfortunate. Data structures carry generation numbers that increment on various operations and invalidate iterators. This makes a lot of operations that are sublinear in C++ take linear time in C#. You pretty much can't use the C# stdlib for any low-level code.
The projects that successfully use C# in low-level code pretty much all roll their own containers, stdlib and a module that implements black-magic stuff, usually called something like UnsafeUtil. On balance, having written a bunch of lower-level C# code, I'd still slightly prefer it to C++ for (e.g.) talking to the GPU driver, but there's no denying that that's a very low bar to clear. For general kernel code I wouldn't use it.
The 4 languages I would consider for low-level code these days are, in order:
- C/C++ (footguns galore, but lots of existing literature)
- Go (surprisingly easy to control the GC and get it to do what you want)
- Rust (the Rick & Morty of programming languages - not a compliment)
- C# (See above)
Rick&Morty being a very long going (and excellent) parody version of Back To The Future franchise I'm not sure what qualities this characterization imbues on a programming language and would like to hear your views on the matter (since I found your analysis of C# very much agreeable).
My comment was referring to the impression that people who like Rust seem to think that you either also accept Rust as the best thing ever, or you don’t get it. I probably shouldn’t have mentioned it, because now the thread will be about Rust.
I don't think Rust is in any way unique in having people who think that way...
An anecdote: I recently opened a position on my team, and got two applicants on the same day who ignored the project description, and basically said we should hire them to tell us how everything we do could be better if we rewrote multiple projects in Rust.
Of course it's completely possible for a programming language to be both (A) really well-designed and (B) have annoying people among its community. I suspect that's the case here.
It's not the case of the rust dev team, which seems very balanced. But I've met a few rust enthusiasts that match the worst of the fancy FP or Typing fan boys.
The thing I hate about rust is abbreviated keywords like “fn” and “mut” because it sounds like fricken mutt. A function is a function not a fn, and is it really so hard to type “mutable”?
There’s more important things in life to worry about than rewriting codebases in language Z.
Edit: Fat arrow?
I don't read code in English, even though I'm a native speaker. So its kindof a silly question: this would only come up when walking someone else through the code. I internally read code using data-flow and control-flow graphs. The written abbreviation is just shorthand for a specific type of graph vertex (or edge).
[1] https://doc.rust-lang.org/book/ch18-01-all-the-places-for-pa...
There is Func<T> - that's a delegate type.
things.Select(thing => thing.Stuff);
I verbalise the brackets as "thing goes to thing dot stuff".I love Rust, but I think it does go over the top, more so than other languages.
Also, give it a few years and the loud and annoying people will have moved on to being loud and annoying about something else.
So it goes.
It is more a C replacement
To the extent that you should have exceptions in your software, Rust's panic picks that slack up pretty well, and to the extent you should have error handling C++ exceptions were a bad idea and sum types like Result are much better.
Note that although Rust's types don't have inheritance the traits they can implement do. Which is much more likely to be something you actually wanted than say, C++ multiple inheritance.
https://www.stroustrup.com/quotes.html#:~:text=%22There%20ar....
Rust is the good kind of smart. Actual functional solutions to difficult problems. R&M is the cringe-worthy kind of smart. Built specifically to please the viewers personal ego.
Agreed on #3, a lot of the BCL is just miserable in terms of design. There have been improvements lately but I'm still stuck using my own custom containers for various cases.
One good move the BCL and runtime teams have made over time is to just expose more unsafe primitives so you can easily build your own stuff - the Memory and Unsafe classes give you lots of useful tools, Span<T> and Memory<T> let you safely work with memory allocated in various ways, and various BCL stuff has been updated to accept Spans instead of requiring you to use managed arrays.
Hard to blame Unity here, though. They have written their own compiler backend that converts CIL to C++ and that was back in the .NET Framework days when performance wasn't that high a priority for language and framework design.
Their transpiler also had a lot in common with ones for other targets (I know for a fact that in the past it used to be very similar structurally to JSIL) which probably improves the likelihood that they would have been able to leverage community knowledge and contributions here.
It's understandable though that they want their transpiler and C#->shader compiler to be their secret sauce, though, since they see them as unique advantages over Godot, Unreal, etc.
The language itself is really cool, I'm a big fan even if I've not had many chances to write it professionally. It's a really well thought out language, I wouldn't use it in every single scenario but there's a lot of legitimate use-cases for it (especially if you need lots of performance but don't fancy the footguns of C++). The learning curve is quite steep but it's nowhere near as bad as some people make it out to be.
And of course all of them will fight over the specifics (in the case of python, I've been guilty of this when it comes to tabs vs spaces).
The only other langauages I see people this with, are VB & PHP (for somewhat different reasons).
It happens when some new band push the needle for a genre, its only when this happens that the "modern" stuff start to look old.
C is getting deprecated while C++ will need to catch up somehow (if there's a way).
This is actually a good effect of Rust, making people think more that safety is important and that its possible to have more of that in languages without a GC.
(Sorry for hijacking the question, that was not directed at me, but this is a good debate so i couldn't resist)
https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csh...
https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csh...
https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csh...
https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csh...
https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csh...
.Net 6 of makes sense because it's supposed to be an LTS version. However, if the release and EOL projections[0] I don't think it actually matters all that much: "LTS releases get free support and patches for 3 years. Current releases get free support and patches for 18 months."
That feels fairly aggressive for a library and language for business applications. I imagine many projects won't be released until the version of .Net used to build it is EOL.
[0]: https://dotnet.microsoft.com/platform/support/policy/dotnet-...
Taking Go as example, there are many pitfalls to using it in low-level situations, but the GC is, in practice, never a source of significant issues.
The real problems are:
1) The foreign function interface (CGO) requires a context switch in both directions. (Not because of GC, but because of Go's threading model.) A lot of work ends up being in trying to avoid calls between Go and C in hot loops.
2) Especially talking to the kernel, there are surprising sharp edges in the ABI and C calling convention. I once spent a week trying to populate a bit field correctly. (But other languages would have the same problems matching any C behavior that's left up to the compiler.)
3) The lack of memory fences is a big deal when trying to use kernel-managed ring buffers.
The GC, comparatively, is problem-free for two reasons:
1) A lot of embedded code uses static buffers anyway.
2) The problem of managing memory doesn't go away just because you're in ring0. The Linux VFS, for example, is heavily garbage collected, it just uses a jerry-rigged GC consisting of counting references. I'm sure if I make a categorial statement on this someone on HN will take me to school, but I think a real GC might be a net performance win in some places in the Kernel, like the VFS.
Finally, people often bring up that GC is fine for throughput, but not for latency. Fair enough, but manual reference counting is also not better: someone has to call free at some point, and if you happen to draw the short straw in a blocking system call, it still affects latency. The amortization behavior is probably different, but not in any way that I think matters to most people.
Of course that difference also tends to go away if you're using static buffers.
https://blog.discord.com/why-discord-is-switching-from-go-to...
"After digging through the Go source code, we learned that Go will force a garbage collection run every 2 minutes at minimum. In other words, if garbage collection has not run for 2 minutes, regardless of heap growth, go will still force a garbage collection."
Discussed here at https://news.ycombinator.com/item?id=22238335
1) You mention there's a workaround -- I can't recall exactly what it is but I think an attribute added to the array can be used to force this behavior -- that's a language limitation, for sure, in that there's effectively no way using language syntax to declare that (and no way for the compiler to verify that)
2) The latest version of the language blurs the lines here in a few ways -- using ref types allows one to "directly operate on values in an array" without resorting to unsafe.
I think there's really two things to consider in these cases: (1) Can C# handle the requirement at all? (2) Can C# handle it in a way that is compiler verified, or runtime verified without the use of `unsafe`?
The answer to the first question, in my years of working with the language, have always been "Yes". Ultimately, I can find an attribute or fall back to unsafe and do what I need to do without the compiler's help, but the frequency with which I've needed to do so has been "very close to never"[0]. The answer to the second question is often "Yes", too, especially in the most recent versions. The few cases where I have had to drop to `unsafe` for a performance critical piece of code, I have been able to replace with ReadOnlySpan/Span in every case I've encountered, for example.
[0] But I don't write systems software in C#
You aren't going to see C# or Go used in the Linux kernel.
"Rust (the Rick & Morty of programming languages - not a compliment)"
You mean something that is good to great but that "outsiders" or late adopters feel unexplainably threatened by and thus feel the need to attack it and its admirers? Where ironically the detractors are louder and more numerous than the advocates ever were?
Rust is particularly susceptible to this effect as it has been racking up wins and growing in influence, making people feel insecure about their current skillset/knowledge (always a weird motivator for rhetoric in this industry).
I have only dabbled in Rust a few times. It feels very unintuitive and "write-only" from my 1000 foot, ignorant perspective. But I realize that's on me, and appreciate the focus and purpose of the language.
Rust today seems like a 2013 Tesla Model S. A couple of years ago it was the GM EV1: It used to be largely promise of a future to come with a loads of downsides, where today it's expensive to get started with, has a lot of incompatibilities, but it's superior to most alternatives. Tomorrow it and those like it will be the norm.
for example `n.unbind().bind()` can convert to a different type but `inside_n = n.unbind()` will let you work with the data inside the container `n` and after you are finished you can call `inside_n.bind()` to lift it back up to the container type.
Nor C++ or Ada, so what? Linux isn't some kind of golden rule for all OSes on the galaxy.
I am more than happy to do bare metal coding in C# (Meadow) and Go (TinyGo) without having to deal with Linux.
Volumes could be written as a reply.
>Linux isn't some kind of golden rule for all OSes on the galaxy.
Just about every operating system or major piece of system software is written in C, or limited C++ that is defacto C. Rust has a universe more of a likelihood to supplant those than Go or C#. And in fact is the only modern language making any headway in those cases. We're about two decades past people deploying the trite "Oh well that's just legacy inertia..." argument.
Google didn't write Fuchsia in Go, and hasn't used an iota of it in ChromeOS or Android. Microsoft uses somewhere approximating 0% .NET/C# code in the Windows core. Microsoft research made a toy .NET OS once that, unsurprisingly, went nowhere, just as you can find irrelevant toy example projects pushing the same premise, but they're dead ends.
This is of course meaningless for the vast majority of projects that the residents of HN are working on, where the compromises of Go, C#/.NET, GCs, etc, are massively outweighed by the benefits they bring to those sorts of projects.
>I am more than happy to do bare metal coding in C# (Meadow) and Go (TinyGo) without having to deal with Linux.
Okay?
As for the rest it would be a waste of my time on Earth to prove you wrong and change your mind.
Okay?
Since IBM i, IBM z, Unisys ClearPath exist, and power the world of insurances and finance, this sentence is obviously false.
> Google didn't write Fuchsia in Go
Indeed, only the TCP/IP stack and volume management tooling, should be easy for you to find out about them.
Then there is this thing called gVisor, it is powering parts of the world that run on top of Google Kubernetes Engine.
Maybe something there for you to learn as well.
> Microsoft uses somewhere approximating 0% .NET/C# code in the Windows core
On the NT kernel, yes, yet good luck using Windows only with the kernel, without the remaining layers, many of which developed in a mix of COM and .NET.
A basic proof how much Windows systems programming experience you have.
Ah then there is this thing called IoT Edge runtime for intelligent devices, written in a mix of Rust and C#.
And before I forget, NVIDIA DRIVE AGX firmware is written in Ada/SPARK.
Bye and learn to grow beyond the Linux bubble.
"A basic proof how much Windows systems programming experience you have."
Which programming language would one write "COM" in? I am so poorly skilled that I must have missed that language. I spent a decade+ writing Windows code, including exposing it through COM/COM+/DCOM, but apparently I was doing it wrong.
And while there is loads of C++ in what we consider the Windows platform, there is vanishingly little .NET.
The "Linux bubble" comment is especially weird. I spend my day in macOS, with most of my day being in XCode slinging Swift and GoLand spinning Go. Somehow I can evaluate the world without trying to mangle it to fit whatever my current primary skills are.
The initial point is simply that Go or C# or Java in their normal form aren't "system programming" suitable (which generally means low-level kernels and system services and projects having similar needs. Notepad on Windows isn't system programming), and it's unrealistic for Go to be ranked highly on such a list, much less before Rust which is purpose built for this task.
Could someone use them for system programming? Sure they could. It would be the wrong tool but people makes loads of bad decisions.
The point about Linux and other operating system choices wasn't a rule, it was an observation that the rhetoric often plied on developer boards -- usually where developers want everything to be a nail to their skillset hammer -- has little correlation with the world. Nowhere did I exclude Ada or any other compiled, non-GCd (which Ada never uses when used for anything of any criticality) languages. Anything can make just as good of native code.
It's more correct to say that they attempted to write part of Fuchsia in Go, but the results performed so badly that they decided it would need to be rewritten in another language.
Just as Microsoft once made a good faith effort to rewrite Windows in C# (Longhorn) before that effort had to be abandoned.
Midori had no issues powering Asian Bing servers during its life in production.
I have no doubt the usual Microsoft politics of the era applied as well.
It helps when the Go guy on the team is no longer there to argue for it.
> Which programming language would one write "COM" in? ....
VB 6, Delphi, .NET, C++ Builder, C++/CX, C++ with MFC/ATL/WRL, whatever language speaks the COM ABI.
Yeah, maybe you were doing it wrong.
> The "Linux bubble" comment is especially weird. I spend my day in macOS, with most of my day being in XCode slinging Swift and GoLand spinning Go. Somehow I can evaluate the world without trying to mangle it to fit whatever my current primary skills are.
Good for you then, happy future.
I think you misread my comment which was pointing out that COM is an interfacing standard, and has little to nothing to do with language choices or this conversation. Pointing out that there are multiple languages that can build to it isn't a counterpoint, but actually is the reason I made my comment. It's like saying you "developed the program in JSON over HTTP" in a conversation about programming languages.
Though regardless of that, Microsoft uses C++ for the overwhelming majority of a Windows install (this is trivial to verify). This is meaningless, having nothing to do with the root comment, but somehow this conversation was pulled there.
Windows programs can be written in just about anything. No one ever contested that. "Low-level" code, however, is seldom done in the GC languages. Seldom doesn't mean never, as stated over and over again, and Googling for random fringe counterexamples aren't counterpoints.
My friends writing safety critical bare metal industrial control software using Ada would, I suspect, feel this was a somewhat limited perspective.
If you ever do consider writing such volumes, I would recommend getting co-authors to cover the areas you're less familiar with.
It's a big industry. There are loads of exceptions and they still comprise a tiny minority.
I was being polite in not openly pointing out that your comment was trolling of absolutely no value or purpose, predicated entirely on your own misreading, but here we are.
It's weird to crown Go as better when it is nicer simply by ignoring very low-level work then pretend that the argument of OS development is irrelevant.
A bit of an exaggeration, but its like saying HTML is a better "programming" language than javascript when creating documents, then ignore the fact HTML isn't actually programmable.
Discord switched from Go to Rust because of slowness issues, I promise if its too slow for an app its too slow for OS.
If Go wants to be a very low level "systems everywhere" then get rid of the GC. Or alternatively stop ranking it above Rust and C++, when its apples to oranges.
Oh sure, the only explanation is that people feel threatened and insecure; the language is totally perfect and its admirers have never wasted anyone's time or demanded that other people blindly drop everything and start porting to their favorite language.
> Rust today seems like a 2013 Tesla Model S. A couple of years ago it was the GM EV1: It used to be largely promise of a future to come with a loads of downsides, where today it's expensive to get started with, has a lot of incompatibilities, but it's superior to most alternatives. Tomorrow it and those like it will be the norm.
What a beautiful analogy, considering that https://en.m.wikipedia.org/wiki/Electric_car_use_by_country puts electric cars at 4.6% of all cars sold in 2020.
I've been through enough waves of asshole fan boys for different things now that I can mostly tune them out, but they're basically never the people actually getting things done so even on the days I can't tune them out I try not to let them reflect badly on the project.
Similarly, Rick and Morty is still funny to me even if I do basically have to block any vocal fan of it I encounter on twitter.
Please don't be as bad as they are.
What about Zig? https://ziglang.org
Sure, you need to be careful, but given that you'd be doing systems programming anyway, you probably would prefer to have explicit control over bounds checking, since the compiler/JIT is (unfortunately) not sophisticated enough yet to elide bounds checking in many cases.
Do OS written in C# have good performance?
GC code using Boehm would not be suitable for device drivers nor realtime code.
I can understand where making it available for higher level systems is desirable.
I could envision the kinds of alternative GC systems that could be inbuilt given the knowledge an OS may have about a process or a sub-system etc. Alternatively, Objective C settled upon reference counting (ARC) and this is superior to GC where it counts.
Where do you envision the GC bootstrapping and how do you see it interacting with virtual memory and your malloc implementation?
It's all highly dependent on the interface between the language and its runtime. Hypothetically, your C# runtime (and by runtime, I'm using it in the same sense that Rust or C++ would use it; I'm not referring to a VM) could provide a function that allocates memory (C#-esque pseudocode):
public static ulong New(ulong bytes)
{
if (gcInitialized)
{
return GcNew(bytes);
}
else
{
return BootstrapHeapNew(bytes);
}
}
Until the GC is boostrapped, all allocations are made on a heap, and either manually freed or kept around until the GC takes over deallocation.> GC code using Boehm would not be suitable for device drivers nor realtime code.
I completely agree, I just provided it as an example of a GC implemented in the same language.
> Where do you envision the GC bootstrapping and how do you see it interacting with virtual memory and your malloc implementation?
I don't see the GC interacting with userland in this case, and instead would only be used for kernel data structures. I don't really think it's a good idea to have GC in a kernel, I only wanted to point out that low-level code and garbage collection are not mutually exclusive.
ARC does nothing special other than automate [myCocoaClass retain] and [myCocoaClass release], a compromise after putting a conservative trancing GC in Objective did not work as expected, given the amount of caveats with GC unaware C code.
As such, Objective-C performance related to heap management remains unchanged.
Blogging about Midori (2015) - https://news.ycombinator.com/item?id=21988694 - Jan 2020 (27 comments)
The Error Model in Midori - https://news.ycombinator.com/item?id=11054912 - Feb 2016 (41 comments)
Midori: A Tale of Three Safeties - https://news.ycombinator.com/item?id=10871222 - Jan 2016 (7 comments)
What happened to Microsoft's Midori operating system project? - https://news.ycombinator.com/item?id=10576697 - Nov 2015 (34 comments)
Blogging about Midori - https://news.ycombinator.com/item?id=10545353 - Nov 2015 (1 comment)
More details on Microsoft's new M# language - https://news.ycombinator.com/item?id=6983649 - Dec 2013 (49 comments)
Microsoft's Midori operating-system skunkworks project soldiers on - https://news.ycombinator.com/item?id=4758071 - Nov 2012 (1 comment)
Goodbye, XP. Hello, Midori - https://news.ycombinator.com/item?id=232554 - June 2008 (1 comment)
Here's my almost decade-old open source code which works for hours allocating very little (the process gets killed by the OS if exceeds 11MB of RAM): https://github.com/Const-me/SkyFM
Here's somewhat similar modern code, also multimedia related: https://github.com/Const-me/Vrmac/tree/master/VrmacVideo
The second one is a general-purpose cross-platform graphics and multimedia library. The specific part I linked implement an equivalent of <Video> HTML element on top of Linux kernel APIs like V4L2 (video decoder) and ALSA (audio rendering). All that stuff is C# on top of .NET Core 2.1. It’s running great with JIT and GC, even consumes less memory than VLC player.
If directly consuming low-level Linux kernel APIs and implementing non-trivial APIs on top of them is not a system programming, then I don’t know what is.
Meanwhile in Delphi you just select the target OS from the list of supported OS'es and just "build and deploy". Why is that every single other IDE that I've tested it's an adventure to create cross-building, to the point where you need to give blood from your fingers to the God of that OS before you finally make a good build.
If you have a single language with a single compiler, presumably with a smaller user base, chances are that is less of a problem.
The creator of Zig is maintaining a very accessible and immediate cross compilation infrastructure for Zig and C alike (zig cc). It's a secondary thing to him, but I practically use it very often since it just works, in contrast to the other stuff. Unfortunately even zig cc has often issues with less tested platforms like 32 bit ARM
I'm imagining a something like .NET 6 SDK combined with a minimal Linux distribution, and a special dotnet publish command that outputs a vhd/bin file that can be directly booted from disk.
Over time, the .NET stack could consume bits and pieces of what it's hosting layer provides with objectives of totally replacing it, ship of theseus-style.
FWIW some maintainers have been tuning .NET for Alpine Linux, so if you just want a small container to host .NET apps you should give that a try.
For initial porting to a new system or processor architecture, the C++ part of CoreCLR (the main runtime for .NET) can be built with CMake and LLVM on the target system. The libraries can be cross-compiled on another system that already supports .NET and copied to the target system. Some Details are here:
https://github.com/dotnet/runtime/blob/main/docs/design/core...
For building from source to satisfy requirements Linux distributions, Microsoft has a system to build from source. My understanding from the last time I closely looked at it is some binary dependencies are decompiled to MSIL (the bytecode used by .NET). Since CoreCLR includes a MSIL assembler written in C++ (ilasm), it can bootstrap using these MSIL sources. But I have not looked at this project closely in a while and it evolved quite a lot while I was watching it. The system for the source build is here:
The modern compiler - Roslyn - was explicitly re-written in C#, and is currently the main compiler (and has been for about three years now).
And isn't not needing warm up one of the AOT selling points?
Of course, for command line tools such as grep, it is more realistic to measure the warmup phase as well, as that will reflect real usage.
Do OS written in C# have good performance?
Check any of XEROX PARC workstations OSes, namely Smalltalk, Interlisp-D and Mesa/Cedar.
Oberon, EthOS, Oberon-2 and A2 at ETHZ.
TOPAZ at Olivetti.
Singularity and Midori at Microsoft.
Or have some fun digging through Android sources to see outside Linux kernel and ART, what is Java and what is C++.
In practice yes, but there’s no requirement for it.