Я часто повторяю, что .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-компилятором CoreTR в 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 GameFrameBuffer.cs GameRandom.cs GameGame.cs GameSnake.cs PalThread.Windows.cs PalEnvironment.Windows.cs PalConsole.Windows.cs /out:zerosnake.ilexe /langversion:latest /unsafe
Затем передаёт это в CoreIT, чтобы получить нативный код.
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
Подпишитесь на твиттер Михала и поаплодируйте ему.
Автор: m1rko