В процессе миграции с .NET Framework на .NET Core могут всплыть некоторые неприятные моменты. Например, если ваше приложение использует домены — логику придется переписывать. Аналогичная ситуация с Thread.Abort(): Microsoft настолько не любит эту практику (и справедливо), что сначала они объявили этот метод deprecated, а затем полностью выпилили его из фреймворка и теперь он вероломно выбрасывает PlatformNotSupportedException.
Но что делать, если ваше приложение использует Thread.Abort(), а вы очень хотите перевести его на .NET Core, ничего не переписывая? Ну, мы-то прекрасно знаем, что платформа очень даже поддерживает этот функционал, так что могу вас обрадовать: выход есть, нужно всего лишь собрать свою собственную версию CLR.
Disclaimer: Это сугубо практическая статья с минимумом теории, призванная только продемонстрировать новые варианты взаимодействия разработчика и .NET среды. Никогда не делайте так в продакшене. Но если очень хочется...
Сделать это стало возможным благодаря двум вещам: стремлению Microsoft к кроссплатформенности .NET Core и проделанной разработчиками работе по переносу исходников фреймворка в открытый доступ. Давайте используем это в своих интересах.
Теоретический минимум:
Прежде чем переходить к нашей основной цели — возвращению Thread.Abort() — давайте для разминки поменяем что-нибудь фундаментальное в CoreFX чтобы проверить работоспособность всех инструментов. Например, по следам моей предыдущей статьи про dynamic, полностью запретим его использование в приложении. Зачем? Потому что мы можем.
Прежде всего установим все необходимое для сборки:
.NET desktop development
Desktop development with C++
.NET Core cross-platform development
Клонируем corefx:
Теперь запретим dynamic. Для этого откроем (здесь и далее я буду указывать относительные от корня репозитория пути)
И в конец функции CallSite<T>.Create вставляем незамысловатый код:
Возвращаемся в corefx и выполняем build.cmd. После окончания сборки, создаем в Visual Studio новый .NET Core проект со следующим содержимым:
Компилируем наш проект. Теперь идем в
и находим там пакет выглядящий примерно так: Microsoft.Private.CoreFx.NETCoreApp.5.0.0-dev.19465.1.nupkg. Открываем .csproj нашего проекта и вставляем туда следующие строки:
Версия должна быть такая же, как в названии собранного пакета. В моем случае 5.0.0-dev.19465.1.
Переходим в настройки nuget для нашего проекта и добавляем туда два новых пути:
И снимаем галочки у всех остальных.
Переходим в папку с проектом и выполняем
Готово! Осталось только запустить:
Работает! Библиотеки берутся не из GAC, dynamic не работает.
Теперь перейдем ко второй части, возвращению Thread.Abort(). Здесь нас ждет неприятный сюрприз: имплементация Thread лежит в CoreCLR, который не является частью CoreFX и предустанавливается на машину отдельно. Сперва создадим демонстрационный проект:
Выкачиваем coreclr. Находим файл
И заменяем Abort() на
Теперь нам нужно вернуть атрибуты и с++ имплементацию. Я собрал её по кусочкам из различных открытых репозиториев .NET Framework и для удобства оформил все изменения в виде пулл-реквеста.
Note: при сборке возникали проблемы с ресурсами в «новых» атрибутах, которые я
«исправил» заглушками. Учитывайте это, если захотите использовать этот код где-либо, кроме домашних экспериментов
После интеграции этих изменений в код, запускаем build.cmd из coreclr. Сборка на поздних этапах может начать сыпать ошибками, но это не страшно, нам главное чтобы смог собраться CoreCLR. Он будут лежать в:
Выполняем
Файлы из Windows_NT.x64.Debug скидываем в папку с опубликованным приложением.
Как только библиотеки собрались, переходим в
и клонируем папку 3.0.0. Назовем её, например, 5.0.1. Скопируем туда все, что лежит в Windows_NT.x64.Debug, кроме папок. Теперь наша версия CoreCLR будет доступна через runtimeconfig.
Собираем наш проект. Добавляем в .csproj:
Повторяем манипуляции с nuget из предыдущей части статьи. Публикуем
В runtimeconfig.json впишем следующую конфигурацию:
Запускаем!
Магия произошла. Теперь в наших .NET Core приложениях снова работает Thread.Abort(). Но, разумеется, только на Windows.
Но что делать, если ваше приложение использует Thread.Abort(), а вы очень хотите перевести его на .NET Core, ничего не переписывая? Ну, мы-то прекрасно знаем, что платформа очень даже поддерживает этот функционал, так что могу вас обрадовать: выход есть, нужно всего лишь собрать свою собственную версию CLR.
Disclaimer: Это сугубо практическая статья с минимумом теории, призванная только продемонстрировать новые варианты взаимодействия разработчика и .NET среды. Никогда не делайте так в продакшене. Но если очень хочется...
Сделать это стало возможным благодаря двум вещам: стремлению Microsoft к кроссплатформенности .NET Core и проделанной разработчиками работе по переносу исходников фреймворка в открытый доступ. Давайте используем это в своих интересах.
Теоретический минимум:
- dotnet publish возволяет нам публиковать standalone приложение: фреймворк будет поставляться вместе с ним, а не искаться где-то в GAC
- Версию CoreCLR, на которой будет запускаться приложение, при некоторых условиях можно задать при помощи runtimeconfig.json
- Мы можем собрать свой собственный CoreFX: github.com/dotnet/corefx
- Мы можем собрать свой собственный CoreCLR: github.com/dotnet/coreclr
Кастомизируем CoreFX
Прежде чем переходить к нашей основной цели — возвращению Thread.Abort() — давайте для разминки поменяем что-нибудь фундаментальное в CoreFX чтобы проверить работоспособность всех инструментов. Например, по следам моей предыдущей статьи про dynamic, полностью запретим его использование в приложении. Зачем? Потому что мы можем.
Prerequisites
Прежде всего установим все необходимое для сборки:
- CMake
- Visual Studio 2019 Preview
- Latest .NET Core SDK (.NET Core 3.0 Preview)
Visual Studio 2019 — Workloads
.NET desktop development
- All Required Components
- .NET Framework 4.7.2 Development Tools
Desktop development with C++
- All Required Components
- VC++ 2019 v142 Toolset (x86, x64)
- Windows 8.1 SDK and UCRT SDK
- VC++ 2017 v141 Toolset (x86, x64)
.NET Core cross-platform development
- All Required Components
Visual Studio 2019 — Individual components
- C# and Visual Basic Roslyn Compilers
- Static Analysis Tools
- .NET Portable Library Targeting Pack
- Windows 10 SDK or Windows 8.1 SDK
- Visual Studio C++ Core Features
- VC++ 2019 v142 Toolset (x86, x64)
- VC++ 2017 v141 Toolset (x86, x64)
- MSBuild
- .NET Framework 4.7.2 Targeting Pack
- Windows Universal CRT SDK
Клонируем corefx:
git clone https://github.com/dotnet/corefx.git
Теперь запретим dynamic. Для этого откроем (здесь и далее я буду указывать относительные от корня репозитория пути)
corefx\src\System.Linq.Expressions\src\System\Runtime\CompilerServices\CallSite.cs
И в конец функции CallSite<T>.Create вставляем незамысловатый код:
throw new PlatformNotSupportedException("No way");
Возвращаемся в corefx и выполняем build.cmd. После окончания сборки, создаем в Visual Studio новый .NET Core проект со следующим содержимым:
public int Test { get; set; }
public static void Main(string[] args)
{
try
{
dynamic a = new Program();
a.Test = 120;
}
catch (Exception e)
{
Console.WriteLine(e);
}
//Узнаем, откуда берутся сборки
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
Console.WriteLine(asm.Location);
}
}
Компилируем наш проект. Теперь идем в
corefx\artifacts\packages\Debug\NonShipping
и находим там пакет выглядящий примерно так: Microsoft.Private.CoreFx.NETCoreApp.5.0.0-dev.19465.1.nupkg. Открываем .csproj нашего проекта и вставляем туда следующие строки:
<PropertyGroup>
<PackageConflictPreferredPackages>Microsoft.Private.CoreFx.NETCoreApp;runtime.$(RuntimeIdentifiers).Microsoft.Private.CoreFx.NETCoreApp;$(PackageConflictPreferredPackages)</PackageConflictPreferredPackages>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Private.CoreFx.NETCoreApp" Version="5.0.0-dev.19465.1" />
</ItemGroup>
Версия должна быть такая же, как в названии собранного пакета. В моем случае 5.0.0-dev.19465.1.
Переходим в настройки nuget для нашего проекта и добавляем туда два новых пути:
corefx\artifacts\packages\Debug\NonShipping
corefx\artifacts\packages\Debug\Shipping
И снимаем галочки у всех остальных.
Переходим в папку с проектом и выполняем
dotnet publish --runtime win-x64 --self-contained
Готово! Осталось только запустить:
Работает! Библиотеки берутся не из GAC, dynamic не работает.
Make CoreCLR Great Again
Теперь перейдем ко второй части, возвращению Thread.Abort(). Здесь нас ждет неприятный сюрприз: имплементация Thread лежит в CoreCLR, который не является частью CoreFX и предустанавливается на машину отдельно. Сперва создадим демонстрационный проект:
var runtimeInformation = RuntimeInformation.FrameworkDescription;
Console.WriteLine(runtimeInformation);
var thr = new Thread(() =>
{
try
{
while (true)
{
Console.WriteLine(".");
Thread.Sleep(500);
}
}
catch (ThreadAbortException)
{
Console.WriteLine("Thread aborted!");
}
});
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
Console.WriteLine(asm.Location);
}
thr.Start();
Thread.Sleep(2000);
thr.Abort();
Выкачиваем coreclr. Находим файл
coreclr\src\System.Private.CoreLib\shared\System\Threading\Thread.cs
И заменяем Abort() на
[SecuritySafeCritical]
[SecurityPermissionAttribute(SecurityAction.Demand, ControlThread = true)]
public void Abort()
{
AbortInternal();
}
[System.Security.SecurityCritical] // auto-generated
[ResourceExposure(ResourceScope.None)]
[MethodImplAttribute(MethodImplOptions.InternalCall)]
private extern void AbortInternal();
Теперь нам нужно вернуть атрибуты и с++ имплементацию. Я собрал её по кусочкам из различных открытых репозиториев .NET Framework и для удобства оформил все изменения в виде пулл-реквеста.
Note: при сборке возникали проблемы с ресурсами в «новых» атрибутах, которые я
«исправил» заглушками. Учитывайте это, если захотите использовать этот код где-либо, кроме домашних экспериментов
После интеграции этих изменений в код, запускаем build.cmd из coreclr. Сборка на поздних этапах может начать сыпать ошибками, но это не страшно, нам главное чтобы смог собраться CoreCLR. Он будут лежать в:
coreclr\bin\Product\Windows_NT.x64.Debug
Простой путь
Выполняем
dotnet publish --runtime win-x64 --self-contained
Файлы из Windows_NT.x64.Debug скидываем в папку с опубликованным приложением.
Сложный путь
Как только библиотеки собрались, переходим в
C:\Program Files\dotnet\shared\Microsoft.NETCore.App
и клонируем папку 3.0.0. Назовем её, например, 5.0.1. Скопируем туда все, что лежит в Windows_NT.x64.Debug, кроме папок. Теперь наша версия CoreCLR будет доступна через runtimeconfig.
Собираем наш проект. Добавляем в .csproj:
<PropertyGroup>
<DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences> <PackageConflictPreferredPackages>Microsoft.Private.CoreFx.NETCoreApp;runtime.$(RuntimeIdentifiers).Microsoft.Private.CoreFx.NETCoreApp;$(PackageConflictPreferredPackages)</PackageConflictPreferredPackages>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Private.CoreFx.NETCoreApp" Version="5.0.0-dev.19465.1" />
</ItemGroup>
Повторяем манипуляции с nuget из предыдущей части статьи. Публикуем
dotnet publish --runtime win-x64 --self-contained
В runtimeconfig.json впишем следующую конфигурацию:
{
"runtimeOptions": {
"tfm": "netcoreapp3.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "5.0.1"
}
}
}
Результат
Запускаем!
Магия произошла. Теперь в наших .NET Core приложениях снова работает Thread.Abort(). Но, разумеется, только на Windows.