How small is the smallest .NET Hello World binary?
blog.washi.dev
blog.washi.dev
Michal Strehovský used the framework currently known as .NET (as opposed to the Framework formerly known as .NET per OP) to create a snake game in under 8KB with no .NET [nor .NET Framework] runtime dependency. (Medium sucks and Twitter isn't useful right now, but I believe ~"hello world" supported Windows 3.11.)
https://medium.com/@MStrehovsky/building-a-self-contained-ga... [https://web.archive.org/web/20200103110836/https://medium.co... | https://archive.ph/b6qXE]
https://news.ycombinator.com/item?id=22010159 (2020)
https://news.ycombinator.com/item?id=22104734
You can see the latest work (and sponsorship!) at https://flattened.net: "bflat - C# as you know it but with Go-inspired tooling".
I wonder if this could be used to make C# webassembly more viable. If I remember, a significant part of the download size is the .NET libraries (themselves written in C#?)
On that note, is there any 2D game framework/engine that targets the browser that uses C# and produces small binaries?
Until WASM ships GC, you're stuck with having to download an entire runtime regardless.
>On that note, is there any 2D game framework/engine that targets the browser that uses C# and produces small binaries?
Godot supports C# scripting and WASM compilation, probably your best bet.
Thanks for the Godot tip. I haven't checked it out in a while. I found it very counterintuitive (and a 3D engine is definitely overkill for small 2D games... I basically just want Flash... I guess Phaser or Haxe are my best bets).
You might enjoy reading this GitHub thread [0] where the community contributed a WASM library wrapping the C# regex code so that regex101.com could have a "C# mode". Lots of nerd sniping about reducing the payload size.
(There's also another thread [1] discussing the minification of a rust version of that same regex101 wasm library to provide a "rust mode" using @burntsushi's regex crate.)
https://learn.microsoft.com/en-us/dotnet/core/extensions/cha...
Having a job class with a type parameter for its encapsulated workload saves you from having to actually write different job classes, at least. It's trivial to write a generic extension method that takes the encapsulated type as a parameter, sets up the channel with your preferred options, and registers the reader & writer for DI. Then you have a one-liner for adding this kind of channel support to your projects.
Some of the benefits of this in my view is: - Clear view of how many dependencies a controller has, though the dependencies are abstractions - Easy to Control+Click into the service in your IDE. - Easy to "Find all usages" of the service. - Easy to mock in unit tests, just mock the interface - Can be decorated if needed with Scrutor - Fast, just a method call - Leverages Task if async is required, easy to e.g. call a service get id returned to use to call another service.
How would you do these things with ChannelWriter and ChannelReader, do you have an example of this approach somewhere?
Regarding your other points:
Dependencies can be traced by the job type associated with the channel. It is somewhat indirect but no more indirect than an interface, really.
Since I am not concerned with results, mocking is even easier. I just have a service implementation that consumes from a ChannelReader and discards the result. This is really a single class since it can take a type parameter if that makes sense--no need for separate mocks.
I haven't used Scrutor, so I can't speak to that.
Reading and writing from a channel are also just method calls. There are synchronous and asynchronous options for both.
I don't have any open code to share, but I found the design very intuitive. If you read the documentation and follow your intuition, you will probably implement things exactly as I did.
Also I am not trying to convince anyone that channels are the best answer! I just found them helpful in my projects and think they have not been promoted very well in the .NET sphere.
The fact that this does not raise eyebrows is one of the (avoidable) reasons, among many, why C# gets bad rep. Go solves this by the virtue of its design that makes it painful to invent big brain solutions that should have stayed simple. This is where C# has much longer learning curve and requires judgement when applying the tools at hand.
I've been mostly using C# for GUI applications and recently have been thinking about relying on a message bus as the main communication device between services and view models. As you mentions that complicates the flow of logic and ones loses benefits of explicitly listing your dependencies you get by just injecting all your services via constructor parameters. But that makes it simpler to refactor complex view models and services without impacting the rest of the codebase (at least in theory, it's of course often messier in practice...).
Community Toolkit code is well written and often exists as "does not quite meet the bar for including in standard library but otherwise solves common use cases".
https://learn.microsoft.com/en-us/dotnet/standard/parallel-p...
Obviously, that created a direct coupling. You could set up an interface and have the controller consume the interface instead, but it was still a coupling in practice. I suppose you register the public method as a delegate or something instead and just inject the delegate, but that's a bit hacky to me, and I think my juniors would find it confusing.
That sort of "cutesy" operator overloading is generally frowned upon in C# best practices, but still possible if you don't mind some C# developers either frowning or laughing at you.
A good source of truth for an actual executable would be Native AOT binary, which can run on Windows, Linux or macOS. Naturally, it will be much larger, having to incorporate GC, possibly ThreadPool, Console and other auxiliary code.
The best example I got off the top of my head is KeePass v1 [0] and KeePass v2 [1]. v1 is written in C++ with native controls, and v2 uses Windows Forms.
If you look at the menu bar and the toolbar, you'll see a difference. Most notably the drag handle on the left, and the search box on the right, in v2. The difference is often a bit easier to spot on Windows 7.
[0]: https://keepass.info/screenshots/main_big.png [1]: https://keepass.info/screenshots/keepass_2x/main_big.png
If you are talking about the base controls, then maybe. But there are .Net cross-platform frameworks such as Avalonia that can get you a modern loooking UI with theming.
https://github.com/irihitech/Semi.Avalonia
https://github.com/AvaloniaUI/Citrus.Avalonia
etc.
It's an odd annoyance that Windows developers have to deal with.
As a user, think about how annoying it is to get a message saying you need to run Windows Update before you can start an application. Totally unnecessary own-goal for the team that decided to ship their independent component in Windows Update.
It's way easier to either 1.) go self-contained, or 2.) use the on-the-fly .NET download for a framework-dependent build. I absolutely think they made the right call removing current .NET as a Windows component. The annoyance was far greater when .NET Framework was part of Windows.
I think comparing container images would be a good start. Use the most minimal base image you can get away with, or even "FROM scratch" if possible, but compare only images with the same architecture. I'd prefer 32-bit, to take things like running 32-bit on 64-bit or 64-bit with pointer compression out of the picture.
Then compare the size of the uncompressed exported tar. Probably also not completely fair, depending on what question you want to answer but it takes the obvious variances out of the equation.
EDIT: Thinking about it some more, it might even be more fair to compare maximally compressed image size to account for compression within the container. Of course you'd have to compress with the same algorithm and parameters or just add the decompressors size to the final result like they do in compression benchmarks.
Specifically about how C leverages the “free” runtime of the OS while Lisp has to bundle is own.
This idea of container runtime sizes would be an amusing thread. A good test of something like Nix or Guix to a very fine grain. “We don’t need bash or ls so we removed it. “ “The VM only simulate a single network card, so we’ll scrap all the other driver. “
Windows has only recently been moving toward providing a public run-time for C; the "Universal C Run-Time" (UCRT) that has been introduced in Windows 10.
Stdio, no. Printf, no (unless you’re an adherent of LIBCTINY[1] or one of its descendants[2] and need only a minimal set of features). Malloc, yes—MSVC’s malloc() on Win32 has always been a thin wrapper around kernel32!HeapAlloc. Strlen or memcpy or whatnot—also possibly yes, but it’s not like minimal implementations of those are large anyway.
[1] http://bytepointer.com/resources/pietrek_libctiny_1996.htm
As a matter of OS design, this is no longer obvious:
https://lwn.net/Articles/806776/
> A new mechanism to help thwart return-oriented programming (ROP) and similar attacks has recently been added to the OpenBSD kernel. It will block system calls that are not made via the C library (libc) system-call wrappers.
MacOS doesn't implement that, sure, but it could.
You can do this for any format that stores executables, for any programming framework. .jar might be interesting, .py obviously isn't. You can do this for obscure old formats, or for something very common like the standard Linux .elf.
Although .zipapp is to Python what .jar is to .py. Probably even closer to .war.
But for purely stand alone stuff, "nuitka3 /tmp/hello.py --standalone" does output a executable that can be used without the user to manually bring in the Python run time. In that case, the hello world is about 16Mb on Ubuntu.
It would be interesting to do this with MicroPython though.
echo "hello world"Without actually checking, the result is going to be that the output size increases slightly over time.
The size definitely gets bigger with each iteration:
$ echo text >0.txt
$ for i in {0..9}; do
gzip <$i.txt >$((i + 1)).txt
done
$ ls | sort -n | xargs -n1 wc -c
5 0.txt
25 1.txt
46 2.txt
69 3.txt
82 4.txt
105 5.txt
120 6.txt
143 7.txt
161 8.txt
184 9.txt
207 10.txt cd /tmp
echo 'echo $0' > hello\ world
chmod +x hello\ world
PATH=. hello\ worldI count 18 bytes in that program. Too long!
I left a definitive answer on Quora some years ago:
https://www.quora.com/What-programming-language-has-the-shor...
I'll just observe that there is no way to compress "hello world" below a certain size (definitely not to size 0). If you think you have, you've just "moved" it into, say, the framework/os/input/algorithm/etc.
But this fun little debate we're having here is actually connected to some deep theoretical questions, like Kolmogorov complexity, its invariance theorem, and applications to the concept of data compression [0].
Hello World
What actually never happened, to nobody's surprise.
So now we have .Net that was renamed into .Net Framework that is legacy, .Net Core that is legacy but compatible with the modern version, and .Net that is current. Anyway, the platform never stopped being called .Net, because it's larger than just the runtime.
We also have 2 different number sequences starting from 1, and one starting from... some times 4, other times 6, depends on your point of view.
We also have a bunch of confused people without any reason, because all of this is as clear as water. But anyway, it's not the author fault that he didn't communicate the version in an adequate way.
This is not correct. .NET Framework was named Framework from 1.0. The only time something was renamed is .NET 5 which came after .NET Core 3.1.
> We also have a bunch of confused people without any reason, because all of this is as clear as water.
It's funny you say that. Do you consider yourself one of those confused people? :)
Yes. I have never took place in a conversation about versioning problems in .Net where each person wasn't talking about completely different things.
Anyway, I clearly remember nobody ever naming anything "framework" until the second or third stable version. And if there was such a thing, I would probably have heard, because MS was incredibly loud at the time.
It was retroactively renamed after it.
I remember back in the day - around 2001 - Microsoft thought it will center all their products around Web Services and call them .NET. Windows .NET Server was the supposed name for Server 2003. In the end a few things came out of it: Visual Studio .NET, .NET (the framework), VB.NET, ASP.NET.
Unless they provide feature parity, it will never happen.
A working WinForms designer for third-party controls (read: any control not provided by the framework itself or NDA-ed vendors) in Visual Studio would be nice, for example.
correction: without some specific version of .net.
In theory: yes. In practice: mostly. There can only be one .NET Framework 4 installed at a time; the recent changes are found at https://learn.microsoft.com/en-us/dotnet/framework/whats-new.
> Which version number should I request in my ultraportable executable?
This gets tricky if you want to support more than Microsoft does. Here is the details on recent .NET Framework versions per OS: https://learn.microsoft.com/en-us/dotnet/framework/install/g... and ancient of days: https://learn.microsoft.com/en-us/archive/blogs/astebner/mai....
For example: Microsoft currently supports Windows 10+, which first included .NET Framework 4.6. However, Microsoft only currently supports v4.6.2+, and v4.8 has been "Windows Update"'d since May 2019. I personally bumped an old open source project from v3.5 to v4.8 recently because of how hard it was for myself as a returning contributor to build these days.
If you want the most "ultra-portable" executable for .NET Framework, you could choose 1.1 or 2.0. Picking 1.1 in 2023 is about as silly as picking VB6, it also won't feel just about anything like modern .NET. 2.0 feels a lot more like modern .NET (especially because that's when Generics and Generic Collections first exist), but also not really something I'd recommend in 2023. (But in theory targeting .NET Fx 2.0 gets you ultra-portable all the way back to Windows 98.)
That's fair though. The post is titled "the smallest .NET hello world binary", not "the smallest C# hello world binary"
But anyway, this other article [0] (shared in another comment here) about creating the smallest version of Snake (the game) was done on top of .NET Core (the new version of .NET that isn't bundled with Windows) and effectively does the same thing, but actually produces a non-.NET binary, if that's more to your taste.
[0]: https://medium.com/@MStrehovsky/building-a-self-contained-ga...
except for runtime environment initialization, JIT compilation, and execution.
Display drivers and firmware of various mcus used in the computer monitor itself?
Could be a fascinating story.
I don't see it in .net meta data nor about
0x200 is 512, or 0.5K. It's been a long time since I've done PE size optimisation at this level but I remember 512 was the minimum accepted by Windows 9x but NT could accept alignments as small as 1.
Also, I didn't see any mention of the old trick of overlapping the DOS MZ and PE headers, which was state-of-the-art when I was still doing this stuff: http://www.phreedom.org/research/tinype/
...and then you realise that the demoscene has managed to do this in a 1k binary:
https://www.pouet.net/prodlist.php?type%5B%5D=1k&platform%5B...
> "A fully self-contained natively compiled C# Hello World, including GC and everything can be as small as ~440 kB."
.net classic framework is windows deprecated legacy, and really no new apps should be built on it.
In many things, .NET Core/.NET 5+ is the Python 3 of .NET ecosystem.
https://learn.microsoft.com/en-us/dotnet/core/deploying/nati...
I agree though, the minimal .NET example is still not close to a full-featured .NET platform. I kind of like the idea of opting-out of features though. Bringing the whole runtime for the party each time is a bit overkill (unless its already on every target machine). Lots of code doesn't need reflection. Some special cases actively dislike GC, etc.
thats_just_c_with_extra_steps.jpg
module Hello
begin
println("hello world")
end Hello
It uses the https://github.com/rochus-keller/Pelib assembly generator.Recently though, I think maybe the way to get off the "cycle of reincarnation" for this type of thinking is to do WASM - but I cannot abandon hardware like that. I am mentally incapable of accepting a spiritual machine that I cannot break with a hammer upon my desk; [I am] too primitive; an indestructible global computer that runs on other people's hardware for thousands of dollars of compute time and is slower than an RPi (like Ethereum) is anathema to me in some fundamental way.
I still want to get that (Wirth was right) on a shirt with his face on it.
In what respect in this context? I should have explicitly stated that "Hello.dll" is a .NET assembly, i.e. Oberon+ uses the Mono CLR to run and debug the Oberon+ code (but not the .NET framework); it can also generate C99 (as a substitute for AOT compilation), but here it is about the minimal size of a Hello World .NET binary.
WASM and especially the WAMR runtime might be a good alternative to the Mono CLR in future, but today it's too slow and only a few architectures are supported (much less than Mono).
Shouldn't Oberon+ be self-hosting? There's so much C++ ecosystem crud. It reminds me of all those projects for open source software hosting forges promoted as alternatives to GitHub and Gitlab, and then when you go to clone their code or file a bug... it's all on GitHub...
As a former Smalltalk developer, what do you think about it would actually slow you down (despite popular claims re productivity in Smalltalk)? Is it tooling for C++ static types? If so, then what about Delphi?
And is it one goal of yours, then, to mold Oberon+ so that it would eventually be at parity, productivity-wise, with C++?
I was a Smalltalk developer in the early nineties as well, but then switched to statically typed languages and the source file concept for many reasons (performance, testing effort, integratability, etc.). For the present case performance is critical (parsing and cross-referencing of large code base) and also the specific required presentation/interaction features which come with Qt out of the box; and I already have a large toolbox based on C++ which I was able to reuse.
> And is it one goal of yours, then, to mold Oberon+ so that it would eventually be at parity, productivity-wise, with C++?
It would take more than a lifetime to achieve that goal given that my https://github.com/rochus-keller/LeanQt and https://github.com/rochus-keller/LeanCreator/, which support my efficient toolbox today, have more than a million SLOC. My goal in the first place is to find out how I have to extend Oberon+ so I can use it in real-world (i.e. non-academic) projects with not too big restrictions compared to my current use of C++; reaching parity in terms of efficiency and the therefore required ecosystem is yet another challenge.
Just a fun anecdote. I don't know if this is the case anymore.
I don't know the truth of the original statement, but it isn't too unbelievable.
Production binaries of course doesn’t embed any source or debugging symbols.
It’s like erroneously shipping source maps files into your front end production build. It shouldn’t happen and it’s not necessary, but it’s just one configuration variable away so it happens a lot.
Anyway, I went ahead and compiled a debug app on .NET 1.1. The binary does not include the source code. And neither does the PDB. It includes a file path to the source though ("Visual Studio Projects\ConsoleApplication1\Class1.cs").
When compiled with VS 2008 and .NET 3.5, the binary does not include the source code either.
You might confuse readable parts in the binary with the metadata.
Yes, using a tool such as .NET Reflector, but the source was never embedded in the compiled binaries, or anywhere else if you were competent with the toolchain.
I've worked with .NET since the pre-1.0 betas back in ~2001 (I know...appeal to authority). The source code was never included in .NET debug or production builds. You could however use tools such as .NET Reflector (now owned by RedGate) to decompile the IL and reconstruct code as C#, VB etc. If you had the PDB files then you also had the symbols and could decompile to a close representation (variable names) to the original source.
This was a very useful feature because the .NET Framework managed code DLL's were and still are shipped obfuscated. This meant you could in the early days use ILDASM, and then .NET Reflector to find out what was going on inside the .NET Framework code.
Of course this created a market for obfuscators so that commercial and paid for shipping code was more difficult to reconstruct (especially since you wouldn't have the PDB files available).
You could do pretty much the same thing with Java.
Now if your binaries were NGEN'd [0], your chances of reverse engineering were reduced considerably because the IL is now gone and you're working with pre-compiled machine code rather than pre-JIT'd IL.
[0]: https://learn.microsoft.com/en-us/dotnet/framework/tools/nge...
I do recall good old Visual Basic did this though, as it ran interpreted. The executable it generated was just a small loader with the source code appended.
But your original post especially doesn't make sense because the default Debug configuration has always put the debug symbols in a separate PDB file. So even if you were arguing that early C# developers didn't understand how to use their tools, they wouldn't have had the source embedded in the EXE.
You can think of this like a source map, but only files and line numbers.
It has always been trivial to load an assembly in something like dnSpy or another IL Disassembler and generate C# code and patch .NET assemblies. At least in the versions < 5.
https://learn.microsoft.com/en-us/dotnet/standard/assembly/f...
No. It taught me quite some.
https://www.gnostice.com/nl_article.asp?id=225&t=The_Smalles...
A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux: http://www.muppetlabs.com/~breadbox/software/tiny/teensy.htm...
Tiny ELF Files: Revisited in 2021: https://nathanotterness.com/2021/10/tiny_elf_modernized.html
https://nathanotterness.com/2021/10/tiny_elf_modernized.html
has a 120 byte hello world program for x86_64 ELF.
Sorry, I don't understand how the OS decides to load your "helloworld" runtime, instead of e.g. the .NET Framework 4 runtime, to go with your 0-length binary?
The program file created in the article has a valid PE file header, including a ".NET Directory" header section that tells the OS about the runtime to load. If you don't have an equivalent, how is your new runtime being loaded?
> See "Stück" for example.
I don't understand this reference?
> I don't understand this reference?
Neither the article apparently.
[0]: https://medium.com/@MStrehovsky/building-a-self-contained-ga...
You know you don't need to use the new features, you can still build your apps the way you like and gradually adopt stuff you think might be better for you. Many of the new features aren't always aimed at line of business apps but have been added to improve "systems programming" capabilities.
And they aren't taking anything way from you (unless you're jumping from .NET Framework to .NET 5 and beyond, e.g. remoting).
Why not teach the right tool for the job and illustrate it being ported to C (or lower) if the aim is to make it smaller?
Can you imagine a mechanical engineer trying to turn a screw with a pair of pliers with the justification that it's just a simple screw and they want to see what the minimal number of turns is whilst shaving the screw to improve grip? You'd think they were nuts...
(Granted it's apples-to-oranges, given that this blog post is for C# managed mode (as opposed to AoT).)
Not true. You can get all of that in C++ if you’re careful and enable compiler flags that no one uses. The entire reason for the IL platform that .NET uses is for cross-platform executables, just like Java.
.NET does give one safety in the form of bounds checking and what-not, but that’s the runtime, not the byte code. There’s no “bounds check” opcode; array dereferences are managed by the runtime and bounds checks are elided if safety can be proven (like Rust does).