I often talk about how .NET Core is open source and runs "everywhere." MonoGame, Unity, Apple Watches, Raspberry Pi, and Microcontrollers (as well as a dozen Linuxes, Windows, etc) is a lot of places.
Michal Strehovský wants C# to run EVERYWHERE and I love him for it.
He recently got some C# code running in two "impossible" places that are now added to our definition of everywhere. While these are fun experiments (don't do this in production) it does underscore the flexibility of both Michals' technical abilities and the underlying platform.
Running C# on Windows 3.11
In this 7 tweet thread Michael talks about how he got C# running in Windows 3.11. The app is very simple, just calling MessageBoxA which has been in Windows since Day 1. He's using DllImport/PInvoke to call MessageBox and receive its result.
I'm showing this Windows 3.11 app first because it's cool, but he started where his DOS experiment left off. He's compiling C# native code, and once that's done you can break all kinds of rules.
In this example he's running Win16...not Win32. However (I was alive and coding and used this on a project!) in 1992 there was a bridge technology called Win32s that was a subset of APIs that were in Windows NT and were backported to Windows 3.11 in the form of Win32s. Given some limitations, you could write 32 bit code and thunk from Win16 to Win32.
Michal learned that the object files that CoreTR's AOT (ahead of time) compiler in 2020 can be linked with the 1994 linker from Visual C++ 2.0. The result is native code that links up with Win32s that runs in 16-bit (ish) Windows 3.11. Magical. Kudos Michal.
Running C# in 8kb on DOS
I've blogged about self-contained .NET Core 3.x executables before and I'm a huge fan. I got my app down to 28 megs. It's small by some measurements, given that it includes the .NET runtime and a lot of accoutrements. Certainly one shouldn't judge a VM/runtime by its hello world size, but Michal wanted to see how small he could go - with 8000 bytes as the goal!
He's using text-mode which I think is great. He also removes the need for the garbage collector by using a common technique - no allocations allowed. That means you can't use new anywhere. No reference types.
He uses things like "fixed char" fields to declare fixed arrays, remembering they must live on the stack and the stack is small.
Of course, when you dotnet publish something self-contained, you'll initially get a 65 meg ish EXE that includes the app, the runtime, and the standard libraries.
dotnet publish -r win-x64 -c Release
He can use ILLinker and PublishedTrimmed to use .NET Core 3.x's Tree Trimming, but that gets it down to 25 megs.
He tries using Mono and mkbundle and that gets him down to 18.2 megs but then he hits a bug. And he's still got a runtime.
So the only runtime that isn't a runtime is CoreRT which includes no virtual machine, just functions to support you.
dotnet publish -r win-x64 -c Release /p:Mode=CoreRT
And this gets him to 4.7 megs, but still too big. Some tweaks go to about 3 megs. He can pull out reflection entirely and get to 1.2 megs! It'll fit on a floppy now!
dotnet publish -r win-x64 -c Release /p:Mode=CoreRT-ReflectionFree
This one megabyte size seems to be a hardish limit with just the .NET SDK.
Here's where Michal goes off the rails. He makes a stub reimplementation of the System base types! Then recompiles with some magic switches to get an IL only version of the EXE
csc.exe /debug /O /noconfig /nostdlib /runtimemetadataversion:v4.0.30319 MiniBCL.cs Game\FrameBuffer.cs Game\Random.cs Game\Game.cs Game\Snake.cs Pal\Thread.Windows.cs Pal\Environment.Windows.cs Pal\Console.Windows.cs /out:zerosnake.ilexe /langversion:latest /unsafe
Then he feeds that to CoreIT to get the native code
ilc.exe zerosnake.ilexe -o zerosnake.obj --systemmodule zerosnake --Os -g
yada yada yada and he's now here
"Now we have zerosnake.obj — a standard object file that is no different from object files produced by other native compilers such as C or C++. The last step is linking it."
A few more tweaks at he's at 27kb! He then pulls off a few linker switches to disable and strip various things - using the same techniques that native developers use and the result is 8176 bytes. Epic.
link.exe /debug:full /subsystem:console zerosnake.obj /entry:__managed__Main kernel32.lib ucrt.lib /merge:.modules=.rdata /merge:.pdata=.rdata /incremental:no /DYNAMICBASE:NO /filealign:16 /align:16
What's the coolest and craziest place you've ever run .NET code? Go follow Michal on Twitter and give him some applause.
Sponsor: Like C#? We do too! That’s why we've developed a fast, smart, cross-platform .NET IDE which gives you even more coding power. Clever code analysis, rich code completion, instant search and navigation, an advanced debugger... With JetBrains Rider, everything you need is at your fingertips. Code C# at the speed of thought on Linux, Mac, or Windows. Try JetBrains Rider today!