.NET Core 3.0でサポートされるWindowsデスクトップアプリケーションを追う

## はじめに

.NET Core 3.0が9月23日にリリースされ、様々な新機能が追加されています。
この中でも個人的に気になったのが**Windowsデスクトップアプリケーション**のサポートです。
今回はこれについて詳しく調べてみました。
.NET Core 3.0の新機能については以下のページが参考になります。

[.NET Core 3.0 (プレビュー 9) の新機能](https://docs.microsoft.com/ja-jp/dotnet/core/whats-new/dotnet-core-3-0)
[Microsoft、「.NET Core 3.0」を正式リリース ～オープンソース化されたWPF/WinForms開発をサポート - 窓の杜](https://forest.watch.impress.co.jp/docs/news/1208888.html)

## Windowsデスクトップアプリケーションの作成

Windowsデスクトップアプリケーションの作成は以下のコマンドで作成することができます。

```sh
# WinForms
$ dotnet new winforms
# Wpf
$ dotnet new wpf
```

作成できるプロジェクトは`dotnet new`で確認することができます。

```sh
$ dotnet new
使用法: new [options]

## 省略

Templates                                         Short Name               Language          Tags
----------------------------------------------------------------------------------------------------------------------------------
Console Application                               console                  [C#], F#, VB      Common/Console
Class library                                     classlib                 [C#], F#, VB      Common/Library
WPF Application                                   wpf                      [C#]              Common/WPF
WPF Class library                                 wpflib                   [C#]              Common/WPF
WPF Custom Control Library                        wpfcustomcontrollib      [C#]              Common/WPF
WPF User Control Library                          wpfusercontrollib        [C#]              Common/WPF
Windows Forms (WinForms) Application              winforms                 [C#]              Common/WinForms
Windows Forms (WinForms) Class library            winformslib              [C#]              Common/WinForms
Worker Service                                    worker                   [C#]              Common/Worker/Web

## 省略
```

試しに**wpfプロジェクト**を作成してみます。

```
## wpfプロジェクト作成
C:\Users\xxx\Documents\dotnet-wpf-test>dotnet new wpf
The template "WPF Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on C:\Users\xxx\Documents\dotnet-wpf-test\dotnet-wpf-test.csproj...
  C:\Users\xxx\Documents\dotnet-wpf-test\dotnet-wpf-test.csproj の復元が 49.81 ms で完了しました。

Restore succeeded.

# ファイル確認
C:\Users\xxx\Documents\dotnet-wpf-test>dir
 ドライブ C のボリューム ラベルがありません。
 ボリューム シリアル番号は 7036-8880 です

 C:\Users\xxx\Documents\dotnet-wpf-test のディレクトリ

2019/09/24  21:21    <DIR>          .
2019/09/24  21:21    <DIR>          ..
2019/09/24  21:21               384 App.xaml
2019/09/24  21:21               348 App.xaml.cs
2019/09/24  21:21               276 dotnet-wpf-test.csproj
2019/09/24  21:21               509 MainWindow.xaml
2019/09/24  21:21               667 MainWindow.xaml.cs
2019/09/24  21:21    <DIR>          obj
               5 個のファイル               2,184 バイト
               3 個のディレクトリ  22,242,934,784 バイトの空き領域
```

通常の.NET Frameworkで**wpfプロジェクト**を作成したときとほぼ同じ内容のファイルが作成されています。
このプロジェクトは通常の.NET Coreのプロジェクトと同様に`dotnet run`コマンドで実行できます。
実行すると何もないウィンドウが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42193/82f62213-2853-7687-187c-7ed980f906cd.png)

## Windowsデスクトップアプリケーションと従来のアプリケーションの違い

Windowsデスクトップアプリケーションがサポートされる前でもWin32APIを使用すればウィンドウを持ったアプリケーションを作成することはできました。
ではWindowsデスクトップアプリケーションとは**winforms**や**wpf**ことを指すのでしょうか。
従来のアプリケーションの決定的な違いは`publish`したときに現れます。

試しに先ほど作成したプロジェクトを`publish`してみます。

```
# publish
C:\Users\xxx\Documents\dotnet-wpf-test>dotnet publish
.NET Core 向け Microsoft (R) Build Engine バージョン 16.3.0+0f4c62fea
Copyright (C) Microsoft Corporation.All rights reserved.

  C:\Users\xxx\Documents\dotnet-wpf-test\dotnet-wpf-test.csproj の復元が 13.77 ms で完了しました。
  dotnet-wpf-test -> C:\Users\xxx\Documents\dotnet-wpf-test\bin\Debug\netcoreapp3.0\dotnet-wpf-test.dll
  dotnet-wpf-test -> C:\Users\xxx\Documents\dotnet-wpf-test\bin\Debug\netcoreapp3.0\publish\

# ファイル確認
C:\Users\xxx\Documents\dotnet-wpf-test>dir bin\Debug\netcoreapp3.0\publish
 ドライブ C のボリューム ラベルがありません。
 ボリューム シリアル番号は 7036-8880 です

 C:\Users\xxx\Documents\dotnet-wpf-test\bin\Debug\netcoreapp3.0\publish のディレクトリ

2019/09/24  21:30    <DIR>          .
2019/09/24  21:30    <DIR>          ..
2019/09/24  21:24               437 dotnet-wpf-test.deps.json
2019/09/24  21:30             7,168 dotnet-wpf-test.dll
2019/09/24  21:30           159,744 dotnet-wpf-test.exe
2019/09/24  21:30             1,644 dotnet-wpf-test.pdb
2019/09/24  21:24               161 dotnet-wpf-test.runtimeconfig.json
               5 個のファイル             169,154 バイト
               2 個のディレクトリ  22,245,412,864 バイトの空き領域
```

`dotnet-wpf-test.exe`をエクスプローラからダブルクリックします。
すると**`MainWindow`のみが表示されます**。
従来のプロジェクトでは必ずコンソールウィンドウが同時に表示されていましたが**Windows Desktopアプリケーションでは表示されません**。
これがWindows Desktopアプリケーションと従来のアプリケーションの違いです。

### (補足)PEフォーマットと`Subsystem`

Windowsがサポートしている`.exe`ファイルはPEフォーマットと呼ばれるフォーマットになっています。
PEフォーマットには`Subsystem`と呼ばれるフィールドがあり、Windowsはこのフィールドを見て**Windowsアプリケーション**か**コンソールアプリケーション**かを判断しています。
**Windowsデスクトップアプリケーション**を`publish`するとこのフィールドが**Windowsアプリケーション**の値になるためコンソールウィンドウが表示されなくなります。

参考:[PE(Portable Executable)ファイルフォーマットの概要](http://home.a00.itscom.net/hatada/mcc/doc/pe.html)

## csproj解読

作成したプロジェクトの`.csproj`は以下のようになっています。

```xml
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <RootNamespace>dotnet_wpf_test</RootNamespace>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>
```

注目すべきは**OutputTypeがWinExeになっている**こと、**SdkがMicrosoft.NET.Sdk.WindowsDesktop**になっていることでしょうか。
プロジェクトを展開して**WinExe**で調べてみます。

```sh
$ dotnet build -pp > project-all.txt
```

いくつか引っ掛かりますが興味深いのは以下の場所です。

```xml
  <!--
    ============================================================
                                        _CreateAppHost
    If we found a restored apphost, create the modified destination apphost
    with options from the project.
    ============================================================
     -->
  <UsingTask TaskName="CreateAppHost" AssemblyFile="$(MicrosoftNETBuildTasksAssembly)" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
  <Target Name="_CreateAppHost" Inputs="@(IntermediateAssembly);$(AppHostSourcePath)" Outputs="$(AppHostIntermediatePath)" DependsOnTargets="_GetAppHostPaths;CoreCompile" Condition="'$(ComputeNETCoreBuildOutputFiles)' == 'true' and&#xD;&#xA;                     '$(AppHostSourcePath)' != '' and&#xD;&#xA;                     Exists('@(IntermediateAssembly)') and&#xD;&#xA;                     Exists('$(AppHostSourcePath)')" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
      <!-- ★注目ポイント -->
      <_UseWindowsGraphicalUserInterface Condition="($(RuntimeIdentifier.StartsWith('win')) or $(DefaultAppHostRuntimeIdentifier.StartsWith('win'))) and '$(OutputType)'=='WinExe'">true</_UseWindowsGraphicalUserInterface>
    </PropertyGroup>
    <CreateAppHost AppHostSourcePath="$(AppHostSourcePath)" AppHostDestinationPath="$(AppHostIntermediatePath)" AppBinaryName="$(AssemblyName)$(TargetExt)" IntermediateAssembly="@(IntermediateAssembly->'%(FullPath)')" WindowsGraphicalUserInterface="$(_UseWindowsGraphicalUserInterface)" Retries="$(CopyRetryCount)" RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)" />
  </Target>
```

OutputTypeがWinExeのとき、**CreateAppHostタスクのWindowsGraphicalUserInterfaceがtrueになる**ことがわかります。

CreateAppHostのソースは以下にあります。

https://github.com/dotnet/sdk/blob/master/src/Tasks/Microsoft.NET.Build.Tasks/CreateAppHost.cs

```csharp
// CreateAppHost.csより引用

HostWriter.CreateAppHost(appHostSourceFilePath: AppHostSourcePath,
                        appHostDestinationFilePath: AppHostDestinationPath,
                        appBinaryFilePath: AppBinaryName,
                        windowsGraphicalUserInterface: isGUI,
                        assemblyToCopyResorcesFrom: resourcesAssembly);
```

HostWriterのCreateAppHost関数に`isGUI (WindowsGraphicalUserInterface)`を渡しています。

HostWriterのソースは以下にあります

https://github.com/dotnet/core-setup/blob/master/src/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs

```csharp
// HostWriter.csより引用

// Re-write the destination apphost with the proper contents.
using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostDestinationFilePath))
{
    using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor())
    {
        BinaryUtils.SearchAndReplace(accessor, AppBinaryPathPlaceholderSearchValue, bytesToWrite);

        appHostIsPEImage = BinaryUtils.IsPEImage(accessor);

        if (windowsGraphicalUserInterface)
        {
            if (!appHostIsPEImage)
            {
                throw new AppHostNotPEFileException();
            }

            BinaryUtils.SetWindowsGraphicalUserInterfaceBit(accessor);
        }
    }
}
```

BinaryUtils.SetWindowsGraphicalUserInterfaceBitがそれっぽい処理みたいです。

https://github.com/dotnet/core-setup/blob/master/src/managed/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs

```csharp
// BinaryUtils.csより引用 (★部分は追記部分)

/// <summary>
/// The value of the sybsystem field which indicates Windows GUI (Graphical UI)
/// </summary>
private const UInt16 WindowsGUISubsystem = 0x2;


/// <summary>
/// This method will attempt to set the subsystem to GUI. The apphost file should be a windows PE file.
/// </summary>
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
internal static unsafe void SetWindowsGraphicalUserInterfaceBit(MemoryMappedViewAccessor accessor)
{
    byte* pointer = null;

    try
    {
        accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
        byte* bytes = pointer + accessor.PointerOffset;

        // https://en.wikipedia.org/wiki/Portable_Executable
        UInt32 peHeaderOffset = ((UInt32*)(bytes + PEHeaderPointerOffset))[0];

        if (accessor.Capacity < peHeaderOffset + SubsystemOffset + sizeof(UInt16))
        {
            throw new AppHostNotPEFileException();
        }

        UInt16* subsystem = ((UInt16*)(bytes + peHeaderOffset + SubsystemOffset));

        // https://docs.microsoft.com/en-us/windows/desktop/Debug/pe-format#windows-subsystem
        // The subsystem of the prebuilt apphost should be set to CUI
        if (subsystem[0] != WindowsCUISubsystem)
        {
            throw new AppHostNotCUIException();
        }

        // ★WindowsGUISubsystem(2)の書き込み
        // Set the subsystem to GUI
        subsystem[0] = WindowsGUISubsystem;
    }
    finally
    {
        if (pointer != null)
        {
            accessor.SafeMemoryMappedViewHandle.ReleasePointer();
        }
    }
}
```

これでようやく**OutputType**を**WinExe**にすると**Windowsアプリケーションとして作成される**ことがわかりました。

## 最後に

今回の記事はほぼ何の役にもたたないことばかり書いてますが.NET Coreでデスクトップアプリケーションが作成できるようになったというのは感動的です。
他にもC#8.0が使えたり.NET Standard 2.1がサポートされていたりするので**.NET Core3.0をぜひ試してみましょう**😊