Я часто повторяю, что .NET Core — это опенсорс и он работает «везде». MonoGame, Unity, Apple Watch, Raspberry Pi и микроконтроллеры, дюжина линуксов, Windows и так далее. Уже немало.

Но кому-то всё-таки мало. Михал Стреховски хочет запускать C# действительно везде.


C# в Windows 3.11

Он запустил код C# в двух «невозможных» системах, которые теперь дополнили наше определение «работает везде». Хотя это забавные эксперименты (не повторяйте их в продакшне), они подчёркивают как технические способности Михала, так и гибкость базовой платформы.

Запуск C# под Windows 3.11


В семи твитах Михал рассказывает, как ему удалось запустить код C# под Windows 3.11. Приложение простое, здесь только вызов функции MessageBoxA с отображением соответствующего диалогового окна, которое в Windows с первых дней. Для вызова функции и получения результата используется DllImport/PInvoke.

Я сначала показал это приложение для Windows 3.11, потому что оно классное. Но в реальности автор начал с того места, где закончился его эксперимент с DOS. Он компилирует нативный код C#, и после этого правил больше не существует.

В этом примере он работает на платформе Win16, а не Win32. Однако в 1992 году (да, я тогда жил и программировал, и использовал это в проектах!) существовал определённый технологический мост под названием Win32s: подмножество API из Windows NT, которые были портированы обратно на Windows 3.11. Поэтому с учётом некоторых ограничений можно написать 32-битный код и обращаться из Win16 к Win32.

Михал понял, что объектные файлы, созданные AOT-компилятором CoreRT в 2020 году, можно собрать компоновщиком из Visual C++ 2.0 образца 1994 года. В результате получается машинный код, скомпонованный с интерфейсами Win32s, работающими в 16-разрядной Windows 3.11. Магия. Респект Михалу.


Простое приложение Hello World C#

Запуск C# в 8 КБ под DOS


Я и раньше писал об автономных исполняемых файлах .NET Core 3.x, я большой фанат этого дела. Моё приложение ужалось до 28 мегабайт. Это совсем немного, учитывая, что оно включает в себя среду выполнения .NET и множество других ресурсов. Конечно, не следует судить о VM/рантайме по размеру минимально возможной программы, но Михал хотел посмотреть, до какого предела можно дойти — и поставил цель 8000 байт!

Программа работает в текстовом режиме, что, по-моему, здорово. Она также устраняет необходимость в сборщике мусора, поскольку здесь отсутствует выделение ресурсов. Это означает, что вы не можете нигде использовать new. Нет ссылочных типов.

Для объявления статических массивов он использует поля fixed char []: они должны жить в стеке, а стек у нас маленький.

Конечно, когда вы пытаетесь сделать какой-то автономный экзешник .NET, то изначально получаете файл 65 мегабайт, который включает приложение, среду выполнения и стандартные библиотеки.

dotnet publish -r win-x64 -c Release

Можно применить ILLinker и PublishedTrimmed для оптимизации Tree Trimming из .NET Core 3.х, но так вы уменьшите файл лишь до 25 мегабайт.

Он попытался использовать Mono и mkbundle, доведя размер до 18,2 мегабайт, но затем поймал ошибку. И среда выполнения по-прежнему никуда не делась.

Таким образом, единственным подходящим рантаймом остался CoreRT, который не включает в себя виртуальную машину, а только вспомогательные функции.

dotnet publish -r win-x64 -c Release /p:Mode=CoreRT

Так он получил 4,7 мегабайта, но это всё равно слишком много. С некоторыми настройками можно дойти до 3 мегабайт. Можно полностью вытянуть рефлексию и дойти до 1,2 мегабайта. Теперь она поместится на дискете!

dotnet publish -r win-x64 -c Release /p:Mode=CoreRT-ReflectionFree

Этот размер в один мегабайт кажется жёстким ограничением только для .NET SDK.

Вот где Михал уходит от стандартных инструментов. Он делает реимплементацию-заглушку для базовых типов System! Затем перекомпилирует с некоторыми волшебными переключателями, чтобы вышла только IL-версия экзешника.

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

Затем передаёт это в CoreRT, чтобы получить нативный код.

ilc.exe zerosnake.ilexe -o zerosnake.obj --systemmodule zerosnake --Os -g

И вот мы здесь.

«Теперь у нас zerosnake.obj — стандартный объектный файл, ничем не отличающийся от объектных файлов, создаваемых другими нативными компиляторами, такими как C или C++. Последний шаг — скомпоновать его».

Ещё несколько хитростей — и на выходе 27 КБ! Затем он убирает из компоновщика несколько переключателей, чтобы отключить и удалить различные вещи, используя те же методы, которые используют разработчики на ассемблере, и в результате остаётся 8176 байт. Эпический триллер.

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

Подпишитесь на твиттер Михала и поаплодируйте ему.