はじめに

WPF アプリを配布する際、依存している DLL を 1 つの実行ファイル(exe)にまとめておくと、配布や運用が楽になります。

.NET には単一ファイル発行(PublishSingleFile)という仕組みがありますが、これを有効にしただけでは ネイティブ DLL が exe の隣に残ってしまう ことがあります。

今回の記事では、IncludeNativeLibrariesForSelfExtract を使ってネイティブ DLL も含めて完全に 1 ファイルへまとめる方法と、その仕組み・注意点について見ていきます。

開発環境

  • Windows11
  • .NET8(net8.0-windows)
  • Visual Studio 2022
  • Prism.Unity(8.1.97)

使用ライブラリ(抜粋)

  • MaterialDesignThemes(4.9.0)
  • MahApps.Metro.IconPacks.Material(5.1.0)
  • Microsoft.Data.SqlClient(5.2.2)
  • Dapper(2.1.35)
  • NLog(6.0.5)

まずは単一ファイル発行を有効にする

発行プロファイル(Properties/PublishProfiles/*.pubxml)に単一ファイル発行の設定を記述します。Visual Studio の「発行」ウィザードで「単一ファイルとして発行する」にチェックを入れても同じ内容が書き込まれます。

FolderProfile.pubxml
<?xml version="1.0" encoding="utf-8"?>
<Project>
  <PropertyGroup>
    <Configuration>Release</Configuration>
    <Platform>Any CPU</Platform>
    <PublishDir>bin\Release\net8.0-windows\publish\win-x64\</PublishDir>
    <PublishProtocol>FileSystem</PublishProtocol>
    <_TargetId>Folder</_TargetId>
    <TargetFramework>net8.0-windows</TargetFramework>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <SelfContained>true</SelfContained>
    <PublishSingleFile>true</PublishSingleFile>
    <PublishReadyToRun>false</PublishReadyToRun>
  </PropertyGroup>
</Project>

ポイントは以下の 3 つです。

  • SelfContained:.NET ランタイムごと同梱する(実行先に .NET のインストールが不要になる)
  • PublishSingleFile:マネージド DLL を 1 つの exe にまとめる
  • RuntimeIdentifier:単一ファイル発行には RID(ここでは win-x64)の指定が必須

発行してみると…ネイティブDLLが残る

この状態で発行すると、確かに Prism や MaterialDesign などの マネージド DLL は exe に取り込まれます。(参考:ネイティブライブラリ

ところが出力フォルダを見ると、次のような DLL が exe の隣に残っています。

publish/win-x64/
├── SampleApp.exe
├── D3DCompiler_47_cor3.dll
├── PenImc_cor3.dll
├── PresentationNative_cor3.dll
├── wpfgfx_cor3.dll
├── vcruntime140_cor3.dll
├── Microsoft.Data.SqlClient.SNI.dll
├── appsettings.json
└── nlog.config

これらはすべて ネイティブ DLL です。

  • *_cor3.dll:WPF のネイティブ描画まわり(Direct3D コンパイラ、ペン入力、描画エンジンなど)
  • vcruntime140_cor3.dll:Visual C++ ランタイム
  • Microsoft.Data.SqlClient.SNI.dll:SQL Server 接続のネイティブライブラリ

PublishSingleFile は既定では マネージドアセンブリだけ を exe に取り込み、ネイティブ DLL は対象外なのです。

IncludeNativeLibrariesForSelfExtract で同梱する

ネイティブ DLL も exe に取り込むには、IncludeNativeLibrariesForSelfExtracttrue にします。発行固有の設定なので、発行プロファイル側に追記するのがおすすめです。

FolderProfile.pubxml
    <PublishSingleFile>true</PublishSingleFile>
    <PublishReadyToRun>false</PublishReadyToRun>
    <!-- ネイティブ DLL (WPF / vcruntime / SqlClient SNI 等) も単一ファイルに同梱する -->
    <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>

再度発行すると、ネイティブ DLL がすべて消え、出力フォルダが exe と設定ファイルだけになります。

publish/win-x64/
├── SampleApp.exe   ← ネイティブDLLも取り込まれてサイズ増
├── appsettings.json
└── nlog.config

本当に exe に入っているのか確認する

「ネイティブ DLL がフォルダから消えた」だけだと、本当に exe に取り込まれたのか少し不安になります。

簡単な確認方法として、exe をバイナリとして開き、ネイティブ DLL のファイル名(例:wpfgfx_cor3.dll)の文字列が含まれているかを探す方法があります。

確認用スニペット
$exe  = "publish\win-x64\SampleApp.exe"
$bytes = [System.IO.File]::ReadAllBytes($exe)
$text  = [System.Text.Encoding]::ASCII.GetString($bytes)
"wpfgfx_cor3.dll を含む : " + $text.Contains("wpfgfx_cor3.dll")

特別なツールは使わず、PowerShell から .NET のクラスを直接呼んで exe の中身を覗いています。やっていることは 3 ステップです。

  • [System.IO.File]::ReadAllBytes($exe):exe ファイルをまるごとバイト配列として読み込みます。dotnet SDK も専用の解析ツールも要りません。
  • [System.Text.Encoding]::ASCII.GetString($bytes):読み込んだバイト列を ASCII 文字列として解釈します。バイナリ中に紛れている可読文字列(DLL 名など)が、ここで普通の文字列として取り出せます。
  • $text.Contains("wpfgfx_cor3.dll"):その文字列の中に目的の DLL 名が含まれるかを判定します。

この力技が成立するのは、自己展開バンドルがネイティブ DLL を 元のファイル名付きのまま exe の末尾に連結して格納しているからです。圧縮や暗号化がかかっていなければ、ファイル名はそのまま ASCII 文字列として exe 内に残ります。

True が返れば、ネイティブ DLL が exe 内のバンドルに格納されていることが確認できます。なお ASCII 全文を一度に文字列化するので、100MB クラスの exe では多少メモリを食う点だけ留意してください。

注意点

便利な反面、いくつか気をつける点があります。

  • exe のサイズが増える:当然ながらネイティブ DLL のぶんだけ大きくなります。SelfContained=true と組み合わせると 100MB を超えることも珍しくありません。
  • 初回起動でわずかに展開コストがかかる:起動時に一時フォルダへ展開するため、ごく僅かですがオーバーヘッドがあります(2 回目以降は展開済みファイルが再利用されます)。
  • アンチウイルスやサンドボックス環境:一時フォルダへの展開がブロックされる環境では動作に影響することがあります。

「とにかく 1 ファイルで配りたい」「実行先に余計なファイルを置きたくない」というケースでは非常に有効な設定です。

まとめ

  • PublishSingleFile だけではマネージド DLL しか取り込まれず、ネイティブ DLL は exe の隣に残ったままになる
  • IncludeNativeLibrariesForSelfExtract=true を足すとネイティブ DLL も 1 ファイルにまとめられます
  • 仕組みは「起動時に一時フォルダへ自己展開してロードする」方式で、見た目の 1 ファイルとロード時の実体は別物です
  • サイズ増加や初回展開コストはあるので、配布物を 1 個で渡したい場面に絞って使うのが落としどころだと感じています

参考