DLLをJSのようにimport段階でネットワークから動的ロードする
고남현 @gnh1201@hackers.pub
Windowsアプリで見られる「DLL Hell」問題
Windows(.NET)ベースでアプリを作成していると、ビルドの結果として主実行ファイルである*.exeの他に、数多くの*.dllファイルが待ち構えています。
例えば、
Program.exe # 主実行ファイル
a.dll # コンパイルされたライブラリA
b.dll # コンパイルされたライブラリB
c.dll # コンパイルされたライブラリC
...
このように*.exeファイルを補助する様々なコンパイル済みライブラリ(*.dll)ファイルが生成され、配布時にもこれらを含める必要があります。これを指す用語として「DLL Hell」という表現もあります。
これらのファイルを一つにまとめる既存の方法(Costura.Fody、ILmerge、ILRepackなど)がありますが、ここで紹介する方法はこれらとは異なります。
アセンブリを一つにまとめることは推奨事項ではない
*.exe、*.dllなどのコンパイル済み成果物はWindows環境では「アセンブリ」と呼ばれます。配布の不便さから、このアセンブリを一つのファイルにまとめる非常に多様な方法が存在します。
前述の結合ツールを使用する方法もありますが、すべての追加アセンブリを圧縮してプロジェクト内に入れておく方法など、様々な結合方法があり得ます。
しかし、これらの結合方法はエコシステムで十分に合意された方法を使用するというよりは、開発者の臨機応変な対応に大きく依存しているため、結局は開発者本人だけが知る結合方法として残り、将来的な協業の障害となる可能性が高いです。
実際、Microsoftはアセンブリを結合する例を公式に提供しておらず、以前に公式サポートしていたILmergeツールについても公式サポートを中止しました。
本当にやむを得ない状況でない限り、アセンブリはファイル単位で分離された状態を維持しながら配布することを推奨するという意味です。
データソースを変えてみたらどうだろう?
私はアセンブリを一つのファイルにまとめる作業は避けることに決めました。代わりにそれに準ずる方法を考えてみることにしました。その考えの結果として出てきたのが、ネットワークから動的にアセンブリを取得するようにすることです。
C#の場合、ライブラリをインポートするために次のような構文(using文法)を使用します。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
using WelsonJS.Esent;
.NETの場合、ライブラリのインポート過程(usingキーワードを解決(resolve)する過程)の途中に介入してプログラミングすることが可能です。
私は追加アセンブリ(*.dll)発生の最小化が要求されるアプリのusingキーワード解決過程に介入して、アセンブリを解決する部分にCDN(ここではAzure Blob Storageというオブジェクトストレージを使用しました)を接続することにしました。
*.dllファイルを取得する経路が、これからはローカルではなくネットワークになるのです。
適用した際にもし発生する可能性のある悪用事例を最小化するために、有効なコード署名を持つアセンブリのみがロードされるように検証過程を追加しました。
実際の実装は以下のリンクを参照してください。
既にアセンブリが同じディレクトリに存在する場合は、ネットワークから取得しません。
.NET ILでコンパイルされたアセンブリではなく、ネイティブ(C/C++系)アセンブリの場合も処理できるか?
ネイティブ(C/C++系)でコンパイルされたアセンブリにも対応するための実装がAssemblyLoader.cs (gnh1201/welsonjs)に含まれています。
ただし、ライブラリが.NET IL(C#など)でコンパイルされている場合は自動的に処理されますが、ネイティブライブラリを使用する場合は明示的にロードする必要があります。
上記内容が反映された例
以下のコードをMainまたはこれに準ずるエントリーポイントで実行することで、追加アセンブリのロードが開始されます。
// load external assemblies
AssemblyLoader.BaseUrl = GetAppConfig("AssemblyBaseUrl");
AssemblyLoader.Logger = _logger;
AssemblyLoader.Register();
AssemblyLoader.LoadNativeModules("MyNativeLib", new Version(1, 0, 0, 0), new[] { "MyNativeLib.dll" });
usingキーワードで要求される.NET IL系アセンブリは自動的に検索してロードする
usingキーワードで要求される.NET IL系アセンブリは自動的に検索してロードします。ネイティブ系アセンブリは明示的にアセンブリ情報を記入して、どれをロードすべきかを定義します。
アセンブリ解決中に接続するリモートネットワークアドレスの例は次のとおりです。Base URLがhttps://example.cdn.tld/packagesの場合:
- https://example.cdn.tld/packages/managed/MyManagedLib/1.0.0.0/MyManagedLib.dll (.NET IL系アセンブリ)
- https://example.cdn.tld/packages/native/MyNativeLib/1.0.0.0/MyNativeLib.dll (ネイティブ系アセンブリ)
必ずセキュア通信(https)をサポートするサーバーである必要があり、すべての追加アセンブリ(*.dll)は有効なコード署名を持っている必要があります。
ネットワークを通じたアセンブリの動的ロードを使えば配布時に追加アセンブリを含める必要がないのか?
ネットワークを利用したアセンブリの動的ロードを適用したとしても、依然として配布時に追加アセンブリ(*.dll)を一緒に配布することが推奨されます。
この方法は、ユーザーがWindowsアプリの配布プロセスについて理解が不足していて、追加アセンブリ(*.dllなど)を省略して主実行ファイル(*.exe)だけを別のコンピュータに移した際に発生するライブラリ欠落エラーを解決するのに役立つ補助的な方法として使用されるべきです。
もちろん、この方法を使えば配布時に追加アセンブリをまったく含めなくても自動的にネットワークから取得するため支障はありません。
ただし、あくまでも補助的な方法であり、配布時に無条件に追加アセンブリを含めなくてもよいという意味ではないという点に留意する必要があります。